feat: GUI to create smart views (#1997)
This commit is contained in:
@@ -10,6 +10,7 @@ import Advanced from '@/Components/Preferences/Panes/General/Advanced/AdvancedSe
|
||||
import PreferencesPane from '../../PreferencesComponents/PreferencesPane'
|
||||
import PlaintextDefaults from './PlaintextDefaults'
|
||||
import Persistence from './Persistence'
|
||||
import SmartViews from './SmartViews/SmartViews'
|
||||
|
||||
type Props = {
|
||||
viewControllerManager: ViewControllerManager
|
||||
@@ -22,6 +23,11 @@ const General: FunctionComponent<Props> = ({ viewControllerManager, application,
|
||||
<Persistence application={application} />
|
||||
<PlaintextDefaults application={application} />
|
||||
<Defaults application={application} />
|
||||
<SmartViews
|
||||
application={application}
|
||||
navigationController={viewControllerManager.navigationController}
|
||||
featuresController={viewControllerManager.featuresController}
|
||||
/>
|
||||
<Tools application={application} />
|
||||
<LabsPane application={application} />
|
||||
<Advanced
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
import { WebApplication } from '@/Application/Application'
|
||||
import Button from '@/Components/Button/Button'
|
||||
import Icon from '@/Components/Icon/Icon'
|
||||
import IconPicker from '@/Components/Icon/IconPicker'
|
||||
import Popover from '@/Components/Popover/Popover'
|
||||
import ModalDialog from '@/Components/Shared/ModalDialog'
|
||||
import ModalDialogButtons from '@/Components/Shared/ModalDialogButtons'
|
||||
import ModalDialogDescription from '@/Components/Shared/ModalDialogDescription'
|
||||
import ModalDialogLabel from '@/Components/Shared/ModalDialogLabel'
|
||||
import Spinner from '@/Components/Spinner/Spinner'
|
||||
import { NavigationController } from '@/Controllers/Navigation/NavigationController'
|
||||
import { SmartView, TagMutator } from '@standardnotes/snjs'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { useCallback, useRef, useState } from 'react'
|
||||
|
||||
type Props = {
|
||||
application: WebApplication
|
||||
navigationController: NavigationController
|
||||
view: SmartView
|
||||
closeDialog: () => void
|
||||
}
|
||||
|
||||
const EditSmartViewModal = ({ application, navigationController, view, closeDialog }: Props) => {
|
||||
const [title, setTitle] = useState(view.title)
|
||||
const titleInputRef = useRef<HTMLInputElement>(null)
|
||||
|
||||
const [selectedIcon, setSelectedIcon] = useState<string | undefined>(view.iconString)
|
||||
|
||||
const [isSaving, setIsSaving] = useState(false)
|
||||
|
||||
const [shouldShowIconPicker, setShouldShowIconPicker] = useState(false)
|
||||
const iconPickerButtonRef = useRef<HTMLButtonElement>(null)
|
||||
|
||||
const toggleIconPicker = useCallback(() => {
|
||||
setShouldShowIconPicker((shouldShow) => !shouldShow)
|
||||
}, [])
|
||||
|
||||
const saveSmartView = useCallback(async () => {
|
||||
if (!title.length) {
|
||||
titleInputRef.current?.focus()
|
||||
return
|
||||
}
|
||||
|
||||
setIsSaving(true)
|
||||
|
||||
await application.mutator.changeAndSaveItem<TagMutator>(view, (mutator) => {
|
||||
mutator.title = title
|
||||
mutator.iconString = selectedIcon || 'restore'
|
||||
})
|
||||
|
||||
setIsSaving(false)
|
||||
closeDialog()
|
||||
}, [application.mutator, closeDialog, selectedIcon, title, view])
|
||||
|
||||
const deleteSmartView = useCallback(async () => {
|
||||
void navigationController.remove(view, true)
|
||||
closeDialog()
|
||||
}, [closeDialog, navigationController, view])
|
||||
|
||||
const close = useCallback(() => {
|
||||
closeDialog()
|
||||
}, [closeDialog])
|
||||
|
||||
return (
|
||||
<ModalDialog>
|
||||
<ModalDialogLabel closeDialog={close}>Edit Smart View "{view.title}"</ModalDialogLabel>
|
||||
<ModalDialogDescription>
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex items-center gap-2.5">
|
||||
<div className="text-sm font-semibold">Title:</div>
|
||||
<input
|
||||
className="rounded border border-border bg-default py-1 px-2"
|
||||
value={title}
|
||||
onChange={(event) => {
|
||||
setTitle(event.target.value)
|
||||
}}
|
||||
ref={titleInputRef}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-2.5">
|
||||
<div className="text-sm font-semibold">Icon:</div>
|
||||
<button
|
||||
className="rounded border border-border p-2"
|
||||
aria-label="Change icon"
|
||||
onClick={toggleIconPicker}
|
||||
ref={iconPickerButtonRef}
|
||||
>
|
||||
<Icon type={selectedIcon || 'restore'} />
|
||||
</button>
|
||||
<Popover
|
||||
open={shouldShowIconPicker}
|
||||
anchorElement={iconPickerButtonRef.current}
|
||||
togglePopover={toggleIconPicker}
|
||||
align="start"
|
||||
overrideZIndex="z-modal"
|
||||
>
|
||||
<div className="p-2">
|
||||
<IconPicker
|
||||
selectedValue={selectedIcon || 'restore'}
|
||||
onIconChange={(value?: string | undefined) => {
|
||||
setSelectedIcon(value)
|
||||
toggleIconPicker()
|
||||
}}
|
||||
platform={application.platform}
|
||||
useIconGrid={true}
|
||||
portalDropdown={false}
|
||||
/>
|
||||
</div>
|
||||
</Popover>
|
||||
</div>
|
||||
</div>
|
||||
</ModalDialogDescription>
|
||||
<ModalDialogButtons>
|
||||
<Button className="mr-auto" disabled={isSaving} onClick={deleteSmartView} colorStyle="danger">
|
||||
Delete
|
||||
</Button>
|
||||
<Button disabled={isSaving} onClick={saveSmartView}>
|
||||
{isSaving ? <Spinner className="h-4.5 w-4.5" /> : 'Save'}
|
||||
</Button>
|
||||
<Button disabled={isSaving} onClick={close}>
|
||||
Cancel
|
||||
</Button>
|
||||
</ModalDialogButtons>
|
||||
</ModalDialog>
|
||||
)
|
||||
}
|
||||
|
||||
export default observer(EditSmartViewModal)
|
||||
@@ -0,0 +1,26 @@
|
||||
import Button from '@/Components/Button/Button'
|
||||
import Icon from '@/Components/Icon/Icon'
|
||||
import { SmartView } from '@standardnotes/snjs'
|
||||
|
||||
type Props = {
|
||||
view: SmartView
|
||||
onEdit: () => void
|
||||
onDelete: () => void
|
||||
}
|
||||
|
||||
const SmartViewItem = ({ view, onEdit, onDelete }: Props) => {
|
||||
return (
|
||||
<div className="flex items-center gap-2 py-1.5">
|
||||
<Icon type={view.iconString} size="custom" className="h-5.5 w-5.5" />
|
||||
<span className="mr-auto text-sm">{view.title}</span>
|
||||
<Button small onClick={onEdit}>
|
||||
Edit
|
||||
</Button>
|
||||
<Button small onClick={onDelete}>
|
||||
Delete
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default SmartViewItem
|
||||
@@ -0,0 +1,85 @@
|
||||
import { WebApplication } from '@/Application/Application'
|
||||
import Button from '@/Components/Button/Button'
|
||||
import { NavigationController } from '@/Controllers/Navigation/NavigationController'
|
||||
import { isSystemView, SmartView } from '@standardnotes/snjs'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { useMemo, useState } from 'react'
|
||||
import { Title } from '../../../PreferencesComponents/Content'
|
||||
import PreferencesGroup from '../../../PreferencesComponents/PreferencesGroup'
|
||||
import PreferencesSegment from '../../../PreferencesComponents/PreferencesSegment'
|
||||
import AddSmartViewModal from '@/Components/SmartViewBuilder/AddSmartViewModal'
|
||||
import { AddSmartViewModalController } from '@/Components/SmartViewBuilder/AddSmartViewModalController'
|
||||
import EditSmartViewModal from './EditSmartViewModal'
|
||||
import SmartViewItem from './SmartViewItem'
|
||||
import { FeaturesController } from '@/Controllers/FeaturesController'
|
||||
import NoSubscriptionBanner from '@/Components/NoSubscriptionBanner/NoSubscriptionBanner'
|
||||
|
||||
type NewType = {
|
||||
application: WebApplication
|
||||
navigationController: NavigationController
|
||||
featuresController: FeaturesController
|
||||
}
|
||||
|
||||
type Props = NewType
|
||||
|
||||
const SmartViews = ({ application, navigationController, featuresController }: Props) => {
|
||||
const [editingSmartView, setEditingSmartView] = useState<SmartView | undefined>(undefined)
|
||||
|
||||
const addSmartViewModalController = useMemo(() => new AddSmartViewModalController(application), [application])
|
||||
|
||||
const nonSystemSmartViews = navigationController.smartViews.filter((view) => !isSystemView(view))
|
||||
|
||||
return (
|
||||
<>
|
||||
<PreferencesGroup>
|
||||
<PreferencesSegment>
|
||||
<Title>Smart Views</Title>
|
||||
{!featuresController.hasSmartViews && (
|
||||
<NoSubscriptionBanner
|
||||
className="mt-2"
|
||||
application={application}
|
||||
title="Upgrade for smart views"
|
||||
message="Create smart views to organize your notes according to conditions you define."
|
||||
/>
|
||||
)}
|
||||
{featuresController.hasSmartViews && (
|
||||
<>
|
||||
<div className="my-2 flex flex-col">
|
||||
{nonSystemSmartViews.map((view) => (
|
||||
<SmartViewItem
|
||||
key={view.uuid}
|
||||
view={view}
|
||||
onEdit={() => setEditingSmartView(view)}
|
||||
onDelete={() => navigationController.remove(view, true)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<Button
|
||||
onClick={() => {
|
||||
addSmartViewModalController.setIsAddingSmartView(true)
|
||||
}}
|
||||
>
|
||||
Create Smart View
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</PreferencesSegment>
|
||||
</PreferencesGroup>
|
||||
{!!editingSmartView && (
|
||||
<EditSmartViewModal
|
||||
application={application}
|
||||
navigationController={navigationController}
|
||||
view={editingSmartView}
|
||||
closeDialog={() => {
|
||||
setEditingSmartView(undefined)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{addSmartViewModalController.isAddingSmartView && (
|
||||
<AddSmartViewModal controller={addSmartViewModalController} platform={application.platform} />
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default observer(SmartViews)
|
||||
Reference in New Issue
Block a user