feat: GUI to create smart views (#1997)

This commit is contained in:
Aman Harwara
2022-11-14 19:40:00 +05:30
committed by GitHub
parent 1c23bc1747
commit f656185c16
28 changed files with 1032 additions and 78 deletions

View File

@@ -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

View File

@@ -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)

View File

@@ -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

View File

@@ -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)