feat: edit smart view predicate as json (#2012)

This commit is contained in:
Aman Harwara
2022-11-16 18:15:26 +05:30
committed by GitHub
parent 943698d506
commit f3e4ba8779
18 changed files with 391 additions and 106 deletions

View File

@@ -0,0 +1,58 @@
import { MutationType } from '../../Abstract/Item'
import { createSmartViewWithContent, createSmartViewWithTitle } from '../../Utilities/Test/SpecUtils'
import { SmartViewMutator } from './SmartViewMutator'
describe('smart view mutator', () => {
it('should set predicate', () => {
const smartView = createSmartViewWithContent({
title: 'foo',
predicate: {
keypath: 'title',
operator: '=',
value: 'foo',
},
})
const mutator = new SmartViewMutator(smartView, MutationType.UpdateUserTimestamps)
mutator.predicate = {
keypath: 'title',
operator: '=',
value: 'bar',
}
const result = mutator.getResult()
expect(result.content.predicate.value).toBe('bar')
})
it('preferences should be undefined if previously undefined', () => {
const smartView = createSmartViewWithTitle()
const mutator = new SmartViewMutator(smartView, MutationType.UpdateUserTimestamps)
const result = mutator.getResult()
expect(result.content.preferences).toBeFalsy()
})
it('preferences should be lazy-created if attempting to set a property', () => {
const smartView = createSmartViewWithTitle()
const mutator = new SmartViewMutator(smartView, MutationType.UpdateUserTimestamps)
mutator.preferences.sortBy = 'content_type'
const result = mutator.getResult()
expect(result.content.preferences?.sortBy).toEqual('content_type')
})
it('preferences should be nulled if client is reseting', () => {
const smartView = createSmartViewWithContent({
title: 'foo',
preferences: {
sortBy: 'content_type',
},
})
const mutator = new SmartViewMutator(smartView, MutationType.UpdateUserTimestamps)
mutator.preferences = undefined
const result = mutator.getResult()
expect(result.content.preferences).toBeFalsy()
})
})

View File

@@ -0,0 +1,13 @@
import { DecryptedItemInterface, MutationType } from '../../Abstract/Item'
import { TagMutator } from '../Tag'
import { SmartViewContent } from './SmartViewContent'
export class SmartViewMutator extends TagMutator<SmartViewContent> {
constructor(item: DecryptedItemInterface<SmartViewContent>, type: MutationType) {
super(item, type)
}
set predicate(predicate: SmartViewContent['predicate']) {
this.mutableContent.predicate = predicate
}
}

View File

@@ -2,3 +2,5 @@ export * from './SmartView'
export * from './SmartViewBuilder' export * from './SmartViewBuilder'
export * from './SystemViewId' export * from './SystemViewId'
export * from './SmartViewContent' export * from './SmartViewContent'
export * from './SmartViewMutator'
export * from './SmartViewIcons'

View File

@@ -11,10 +11,10 @@ import { TagToFileReference } from '../../Abstract/Reference/TagToFileReference'
import { TagPreferences } from './TagPreferences' import { TagPreferences } from './TagPreferences'
import { DecryptedItemInterface, MutationType } from '../../Abstract/Item' import { DecryptedItemInterface, MutationType } from '../../Abstract/Item'
export class TagMutator extends DecryptedItemMutator<TagContent> { export class TagMutator<Content extends TagContent = TagContent> extends DecryptedItemMutator<Content> {
private mutablePreferences?: TagPreferences private mutablePreferences?: TagPreferences
constructor(item: DecryptedItemInterface<TagContent>, type: MutationType) { constructor(item: DecryptedItemInterface<Content>, type: MutationType) {
super(item, type) super(item, type)
this.mutablePreferences = this.mutableContent.preferences this.mutablePreferences = this.mutableContent.preferences

View File

@@ -33,6 +33,7 @@ import {
import { DeletedItem } from '../../Abstract/Item/Implementations/DeletedItem' import { DeletedItem } from '../../Abstract/Item/Implementations/DeletedItem'
import { EncryptedItemInterface } from '../../Abstract/Item/Interfaces/EncryptedItem' import { EncryptedItemInterface } from '../../Abstract/Item/Interfaces/EncryptedItem'
import { DeletedItemInterface } from '../../Abstract/Item/Interfaces/DeletedItem' import { DeletedItemInterface } from '../../Abstract/Item/Interfaces/DeletedItem'
import { SmartViewMutator } from '../../Syncable/SmartView'
type ItemClass<C extends ItemContent = ItemContent> = new (payload: DecryptedPayloadInterface<C>) => DecryptedItem<C> type ItemClass<C extends ItemContent = ItemContent> = new (payload: DecryptedPayloadInterface<C>) => DecryptedItem<C>
@@ -56,7 +57,7 @@ const ContentTypeClassMapping: Partial<Record<ContentType, MappingEntry>> = {
[ContentType.ExtensionRepo]: { itemClass: SNFeatureRepo }, [ContentType.ExtensionRepo]: { itemClass: SNFeatureRepo },
[ContentType.File]: { itemClass: FileItem, mutatorClass: FileMutator }, [ContentType.File]: { itemClass: FileItem, mutatorClass: FileMutator },
[ContentType.Note]: { itemClass: SNNote, mutatorClass: NoteMutator }, [ContentType.Note]: { itemClass: SNNote, mutatorClass: NoteMutator },
[ContentType.SmartView]: { itemClass: SmartView, mutatorClass: TagMutator }, [ContentType.SmartView]: { itemClass: SmartView, mutatorClass: SmartViewMutator },
[ContentType.Tag]: { itemClass: SNTag, mutatorClass: TagMutator }, [ContentType.Tag]: { itemClass: SNTag, mutatorClass: TagMutator },
[ContentType.Theme]: { itemClass: SNTheme, mutatorClass: ThemeMutator }, [ContentType.Theme]: { itemClass: SNTheme, mutatorClass: ThemeMutator },
[ContentType.UserPrefs]: { itemClass: SNUserPrefs, mutatorClass: UserPrefsMutator }, [ContentType.UserPrefs]: { itemClass: SNUserPrefs, mutatorClass: UserPrefsMutator },

View File

@@ -5,6 +5,7 @@ import { DecryptedPayload, PayloadSource, PayloadTimestampDefaults } from '../..
import { FileContent, FileItem } from '../../Syncable/File' import { FileContent, FileItem } from '../../Syncable/File'
import { NoteContent, SNNote } from '../../Syncable/Note' import { NoteContent, SNNote } from '../../Syncable/Note'
import { SNTag } from '../../Syncable/Tag' import { SNTag } from '../../Syncable/Tag'
import { SmartView, SmartViewContent } from '../../Syncable/SmartView'
let currentId = 0 let currentId = 0
@@ -55,6 +56,20 @@ export const createTagWithContent = (content: Partial<TagContent>): SNTag => {
) )
} }
export const createSmartViewWithContent = (content: Partial<SmartViewContent>): SmartView => {
return new SmartView(
new DecryptedPayload(
{
uuid: mockUuid(),
content_type: ContentType.SmartView,
content: FillItemContent<SmartViewContent>(content),
...PayloadTimestampDefaults(),
},
PayloadSource.Constructor,
),
)
}
export const createTagWithTitle = (title = 'photos') => { export const createTagWithTitle = (title = 'photos') => {
return new SNTag( return new SNTag(
new DecryptedPayload( new DecryptedPayload(
@@ -69,6 +84,20 @@ export const createTagWithTitle = (title = 'photos') => {
) )
} }
export const createSmartViewWithTitle = (title = 'photos') => {
return new SmartView(
new DecryptedPayload(
{
uuid: mockUuid(),
content_type: ContentType.SmartView,
content: FillItemContent<SmartViewContent>({ title }),
...PayloadTimestampDefaults(),
},
PayloadSource.Constructor,
),
)
}
export const createFile = (name = 'screenshot.png') => { export const createFile = (name = 'screenshot.png') => {
return new FileItem( return new FileItem(
new DecryptedPayload( new DecryptedPayload(

View File

@@ -9,7 +9,7 @@ import * as Services from '@standardnotes/services'
import { PayloadManagerChangeData } from '../Payloads' import { PayloadManagerChangeData } from '../Payloads'
import { DiagnosticInfo, ItemsClientInterface, ItemRelationshipDirection } from '@standardnotes/services' import { DiagnosticInfo, ItemsClientInterface, ItemRelationshipDirection } from '@standardnotes/services'
import { ApplicationDisplayOptions } from '@Lib/Application/Options/OptionalOptions' import { ApplicationDisplayOptions } from '@Lib/Application/Options/OptionalOptions'
import { CollectionSort, DecryptedItemInterface, ItemContent } from '@standardnotes/models' import { CollectionSort, DecryptedItemInterface, ItemContent, SmartViewDefaultIconName } from '@standardnotes/models'
type ItemsChangeObserver<I extends Models.DecryptedItemInterface = Models.DecryptedItemInterface> = { type ItemsChangeObserver<I extends Models.DecryptedItemInterface = Models.DecryptedItemInterface> = {
contentType: ContentType[] contentType: ContentType[]
@@ -1229,7 +1229,7 @@ export class ItemManager
Models.FillItemContent({ Models.FillItemContent({
title, title,
predicate: predicate.toJson(), predicate: predicate.toJson(),
iconString: iconString || 'restore', iconString: iconString || SmartViewDefaultIconName,
} as Models.SmartViewContent), } as Models.SmartViewContent),
true, true,
) as Promise<Models.SmartView> ) as Promise<Models.SmartView>

View File

@@ -23,11 +23,7 @@ const General: FunctionComponent<Props> = ({ viewControllerManager, application,
<Persistence application={application} /> <Persistence application={application} />
<PlaintextDefaults application={application} /> <PlaintextDefaults application={application} />
<Defaults application={application} /> <Defaults application={application} />
<SmartViews <SmartViews application={application} featuresController={viewControllerManager.featuresController} />
application={application}
navigationController={viewControllerManager.navigationController}
featuresController={viewControllerManager.featuresController}
/>
<Tools application={application} /> <Tools application={application} />
<LabsPane application={application} /> <LabsPane application={application} />
<Advanced <Advanced

View File

@@ -1,4 +1,3 @@
import { WebApplication } from '@/Application/Application'
import Button from '@/Components/Button/Button' import Button from '@/Components/Button/Button'
import Icon from '@/Components/Icon/Icon' import Icon from '@/Components/Icon/Icon'
import IconPicker from '@/Components/Icon/IconPicker' import IconPicker from '@/Components/Icon/IconPicker'
@@ -8,25 +7,35 @@ import ModalDialogButtons from '@/Components/Shared/ModalDialogButtons'
import ModalDialogDescription from '@/Components/Shared/ModalDialogDescription' import ModalDialogDescription from '@/Components/Shared/ModalDialogDescription'
import ModalDialogLabel from '@/Components/Shared/ModalDialogLabel' import ModalDialogLabel from '@/Components/Shared/ModalDialogLabel'
import Spinner from '@/Components/Spinner/Spinner' import Spinner from '@/Components/Spinner/Spinner'
import { NavigationController } from '@/Controllers/Navigation/NavigationController' import { Platform, SmartViewDefaultIconName } from '@standardnotes/snjs'
import { SmartView, TagMutator } from '@standardnotes/snjs'
import { observer } from 'mobx-react-lite' import { observer } from 'mobx-react-lite'
import { useCallback, useRef, useState } from 'react' import { useCallback, useEffect, useRef, useState } from 'react'
import { EditSmartViewModalController } from './EditSmartViewModalController'
type Props = { type Props = {
application: WebApplication controller: EditSmartViewModalController
navigationController: NavigationController platform: Platform
view: SmartView
closeDialog: () => void
} }
const EditSmartViewModal = ({ application, navigationController, view, closeDialog }: Props) => { const EditSmartViewModal = ({ controller, platform }: Props) => {
const [title, setTitle] = useState(view.title) const {
view,
title,
setTitle,
predicateJson,
setPredicateJson,
isPredicateJsonValid,
setIsPredicateJsonValid,
icon,
setIcon,
save,
isSaving,
closeDialog,
deleteView,
} = controller
const titleInputRef = useRef<HTMLInputElement>(null) const titleInputRef = useRef<HTMLInputElement>(null)
const predicateJsonInputRef = useRef<HTMLTextAreaElement>(null)
const [selectedIcon, setSelectedIcon] = useState<string | undefined>(view.iconString)
const [isSaving, setIsSaving] = useState(false)
const [shouldShowIconPicker, setShouldShowIconPicker] = useState(false) const [shouldShowIconPicker, setShouldShowIconPicker] = useState(false)
const iconPickerButtonRef = useRef<HTMLButtonElement>(null) const iconPickerButtonRef = useRef<HTMLButtonElement>(null)
@@ -41,29 +50,26 @@ const EditSmartViewModal = ({ application, navigationController, view, closeDial
return return
} }
setIsSaving(true) void save()
}, [save, title.length])
await application.mutator.changeAndSaveItem<TagMutator>(view, (mutator) => { useEffect(() => {
mutator.title = title if (!predicateJsonInputRef.current) {
mutator.iconString = selectedIcon || 'restore' return
}) }
setIsSaving(false) if (isPredicateJsonValid === false) {
closeDialog() predicateJsonInputRef.current.focus()
}, [application.mutator, closeDialog, selectedIcon, title, view]) }
}, [isPredicateJsonValid])
const deleteSmartView = useCallback(async () => { if (!view) {
void navigationController.remove(view, true) return null
closeDialog() }
}, [closeDialog, navigationController, view])
const close = useCallback(() => {
closeDialog()
}, [closeDialog])
return ( return (
<ModalDialog> <ModalDialog>
<ModalDialogLabel closeDialog={close}>Edit Smart View "{view.title}"</ModalDialogLabel> <ModalDialogLabel closeDialog={closeDialog}>Edit Smart View "{view.title}"</ModalDialogLabel>
<ModalDialogDescription> <ModalDialogDescription>
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
<div className="flex items-center gap-2.5"> <div className="flex items-center gap-2.5">
@@ -85,7 +91,7 @@ const EditSmartViewModal = ({ application, navigationController, view, closeDial
onClick={toggleIconPicker} onClick={toggleIconPicker}
ref={iconPickerButtonRef} ref={iconPickerButtonRef}
> >
<Icon type={selectedIcon || 'restore'} /> <Icon type={icon || SmartViewDefaultIconName} />
</button> </button>
<Popover <Popover
open={shouldShowIconPicker} open={shouldShowIconPicker}
@@ -96,28 +102,48 @@ const EditSmartViewModal = ({ application, navigationController, view, closeDial
> >
<div className="p-2"> <div className="p-2">
<IconPicker <IconPicker
selectedValue={selectedIcon || 'restore'} selectedValue={icon || SmartViewDefaultIconName}
onIconChange={(value?: string | undefined) => { onIconChange={(value?: string | undefined) => {
setSelectedIcon(value) setIcon(value || SmartViewDefaultIconName)
toggleIconPicker() toggleIconPicker()
}} }}
platform={application.platform} platform={platform}
useIconGrid={true} useIconGrid={true}
portalDropdown={false} portalDropdown={false}
/> />
</div> </div>
</Popover> </Popover>
</div> </div>
<div className="flex flex-col gap-2.5">
<div className="text-sm font-semibold">Predicate:</div>
<div className="flex flex-col overflow-hidden rounded-md border border-border">
<textarea
className="h-full min-h-[10rem] w-full flex-grow resize-none bg-default py-1.5 px-2.5 font-mono text-sm"
value={predicateJson}
onChange={(event) => {
setPredicateJson(event.target.value)
setIsPredicateJsonValid(true)
}}
spellCheck={false}
ref={predicateJsonInputRef}
/>
{!isPredicateJsonValid && (
<div className="border-t border-border px-2.5 py-1.5 text-sm text-danger">
Invalid JSON. Double check your entry and try again.
</div>
)}
</div>
</div>
</div> </div>
</ModalDialogDescription> </ModalDialogDescription>
<ModalDialogButtons> <ModalDialogButtons>
<Button className="mr-auto" disabled={isSaving} onClick={deleteSmartView} colorStyle="danger"> <Button className="mr-auto" disabled={isSaving} onClick={deleteView} colorStyle="danger">
Delete Delete
</Button> </Button>
<Button disabled={isSaving} onClick={saveSmartView}> <Button disabled={isSaving} onClick={saveSmartView} primary colorStyle="info">
{isSaving ? <Spinner className="h-4.5 w-4.5" /> : 'Save'} {isSaving ? <Spinner className="h-4.5 w-4.5" /> : 'Save'}
</Button> </Button>
<Button disabled={isSaving} onClick={close}> <Button disabled={isSaving} onClick={closeDialog}>
Cancel Cancel
</Button> </Button>
</ModalDialogButtons> </ModalDialogButtons>

View File

@@ -0,0 +1,133 @@
import { WebApplication } from '@/Application/Application'
import { STRING_DELETE_TAG } from '@/Constants/Strings'
import {
predicateFromJson,
PredicateJsonForm,
SmartView,
SmartViewDefaultIconName,
SmartViewMutator,
} from '@standardnotes/snjs'
import { confirmDialog } from '@standardnotes/ui-services'
import { action, makeObservable, observable } from 'mobx'
export class EditSmartViewModalController {
title = ''
icon: string = SmartViewDefaultIconName
predicateJson = ''
isPredicateJsonValid = false
isSaving = false
view: SmartView | undefined = undefined
constructor(private application: WebApplication) {
makeObservable(this, {
title: observable,
icon: observable,
predicateJson: observable,
isPredicateJsonValid: observable,
isSaving: observable,
view: observable,
setTitle: action,
setIcon: action,
setPredicateJson: action,
setIsPredicateJsonValid: action,
setIsSaving: action,
setView: action,
})
}
setTitle = (title: string) => {
this.title = title
}
setIcon = (icon: string) => {
this.icon = icon
}
setPredicateJson = (json: string) => {
this.predicateJson = json
}
setIsPredicateJsonValid = (isValid: boolean) => {
this.isPredicateJsonValid = isValid
}
setView = (view: SmartView | undefined) => {
this.view = view
if (view) {
this.setTitle(view.title)
this.setIcon(view.iconString)
this.setPredicateJson(JSON.stringify(view.predicate.toJson(), null, 2))
this.setIsPredicateJsonValid(true)
}
}
setIsSaving = (isSaving: boolean) => {
this.isSaving = isSaving
}
closeDialog = () => {
this.setView(undefined)
this.setTitle('')
this.setIcon(SmartViewDefaultIconName)
this.setPredicateJson('')
}
save = async () => {
if (!this.view) {
return
}
this.validateAndPrettifyCustomPredicate()
if (!this.isPredicateJsonValid) {
return
}
this.setIsSaving(true)
await this.application.mutator.changeAndSaveItem<SmartViewMutator>(this.view, (mutator) => {
mutator.title = this.title
mutator.iconString = this.icon || SmartViewDefaultIconName
mutator.predicate = JSON.parse(this.predicateJson) as PredicateJsonForm
})
this.setIsSaving(false)
this.closeDialog()
}
deleteView = async () => {
if (!this.view) {
return
}
const view = this.view
this.closeDialog()
const shouldDelete = await confirmDialog({
text: STRING_DELETE_TAG,
confirmButtonStyle: 'danger',
})
if (shouldDelete) {
this.application.mutator.deleteItem(view).catch(console.error)
}
}
validateAndPrettifyCustomPredicate = () => {
try {
const parsedPredicate: PredicateJsonForm = JSON.parse(this.predicateJson)
const predicate = predicateFromJson(parsedPredicate)
if (predicate) {
this.setPredicateJson(JSON.stringify(parsedPredicate, null, 2))
this.setIsPredicateJsonValid(true)
} else {
this.setIsPredicateJsonValid(false)
}
} catch (error) {
this.setIsPredicateJsonValid(false)
return
}
}
}

View File

@@ -1,14 +1,17 @@
import Button from '@/Components/Button/Button' import Button from '@/Components/Button/Button'
import Icon from '@/Components/Icon/Icon' import Icon from '@/Components/Icon/Icon'
import { SmartView } from '@standardnotes/snjs' import { SmartView } from '@standardnotes/snjs'
import { useCallback } from 'react'
type Props = { type Props = {
view: SmartView view: SmartView
onEdit: () => void onEdit: () => void
onDelete: () => void onDelete: (view: SmartView) => void
} }
const SmartViewItem = ({ view, onEdit, onDelete }: Props) => { const SmartViewItem = ({ view, onEdit, onDelete }: Props) => {
const onClickDelete = useCallback(() => onDelete(view), [onDelete, view])
return ( return (
<div className="flex items-center gap-2 py-1.5"> <div className="flex items-center gap-2 py-1.5">
<Icon type={view.iconString} size="custom" className="h-5.5 w-5.5" /> <Icon type={view.iconString} size="custom" className="h-5.5 w-5.5" />
@@ -16,7 +19,7 @@ const SmartViewItem = ({ view, onEdit, onDelete }: Props) => {
<Button small onClick={onEdit}> <Button small onClick={onEdit}>
Edit Edit
</Button> </Button>
<Button small onClick={onDelete}> <Button small onClick={onClickDelete}>
Delete Delete
</Button> </Button>
</div> </div>

View File

@@ -1,9 +1,8 @@
import { WebApplication } from '@/Application/Application' import { WebApplication } from '@/Application/Application'
import Button from '@/Components/Button/Button' import Button from '@/Components/Button/Button'
import { NavigationController } from '@/Controllers/Navigation/NavigationController' import { ContentType, isSystemView, SmartView } from '@standardnotes/snjs'
import { isSystemView, SmartView } from '@standardnotes/snjs'
import { observer } from 'mobx-react-lite' import { observer } from 'mobx-react-lite'
import { useMemo, useState } from 'react' import { useCallback, useEffect, useMemo, useState } from 'react'
import { Title } from '../../../PreferencesComponents/Content' import { Title } from '../../../PreferencesComponents/Content'
import PreferencesGroup from '../../../PreferencesComponents/PreferencesGroup' import PreferencesGroup from '../../../PreferencesComponents/PreferencesGroup'
import PreferencesSegment from '../../../PreferencesComponents/PreferencesSegment' import PreferencesSegment from '../../../PreferencesComponents/PreferencesSegment'
@@ -13,21 +12,45 @@ import EditSmartViewModal from './EditSmartViewModal'
import SmartViewItem from './SmartViewItem' import SmartViewItem from './SmartViewItem'
import { FeaturesController } from '@/Controllers/FeaturesController' import { FeaturesController } from '@/Controllers/FeaturesController'
import NoSubscriptionBanner from '@/Components/NoSubscriptionBanner/NoSubscriptionBanner' import NoSubscriptionBanner from '@/Components/NoSubscriptionBanner/NoSubscriptionBanner'
import { EditSmartViewModalController } from './EditSmartViewModalController'
import { STRING_DELETE_TAG } from '@/Constants/Strings'
import { confirmDialog } from '@standardnotes/ui-services'
type NewType = { type NewType = {
application: WebApplication application: WebApplication
navigationController: NavigationController
featuresController: FeaturesController featuresController: FeaturesController
} }
type Props = NewType type Props = NewType
const SmartViews = ({ application, navigationController, featuresController }: Props) => { const SmartViews = ({ application, featuresController }: Props) => {
const [editingSmartView, setEditingSmartView] = useState<SmartView | undefined>(undefined)
const addSmartViewModalController = useMemo(() => new AddSmartViewModalController(application), [application]) const addSmartViewModalController = useMemo(() => new AddSmartViewModalController(application), [application])
const editSmartViewModalController = useMemo(() => new EditSmartViewModalController(application), [application])
const nonSystemSmartViews = navigationController.smartViews.filter((view) => !isSystemView(view)) const [smartViews, setSmartViews] = useState(() =>
application.items.getSmartViews().filter((view) => !isSystemView(view)),
)
useEffect(() => {
const disposeItemStream = application.streamItems([ContentType.SmartView], () => {
setSmartViews(application.items.getSmartViews().filter((view) => !isSystemView(view)))
})
return disposeItemStream
}, [application])
const deleteItem = useCallback(
async (view: SmartView) => {
const shouldDelete = await confirmDialog({
text: STRING_DELETE_TAG,
confirmButtonStyle: 'danger',
})
if (shouldDelete) {
application.mutator.deleteItem(view).catch(console.error)
}
},
[application.mutator],
)
return ( return (
<> <>
@@ -45,12 +68,12 @@ const SmartViews = ({ application, navigationController, featuresController }: P
{featuresController.hasSmartViews && ( {featuresController.hasSmartViews && (
<> <>
<div className="my-2 flex flex-col"> <div className="my-2 flex flex-col">
{nonSystemSmartViews.map((view) => ( {smartViews.map((view) => (
<SmartViewItem <SmartViewItem
key={view.uuid} key={view.uuid}
view={view} view={view}
onEdit={() => setEditingSmartView(view)} onEdit={() => editSmartViewModalController.setView(view)}
onDelete={() => navigationController.remove(view, true)} onDelete={deleteItem}
/> />
))} ))}
</div> </div>
@@ -65,15 +88,8 @@ const SmartViews = ({ application, navigationController, featuresController }: P
)} )}
</PreferencesSegment> </PreferencesSegment>
</PreferencesGroup> </PreferencesGroup>
{!!editingSmartView && ( {!!editSmartViewModalController.view && (
<EditSmartViewModal <EditSmartViewModal controller={editSmartViewModalController} platform={application.platform} />
application={application}
navigationController={navigationController}
view={editingSmartView}
closeDialog={() => {
setEditingSmartView(undefined)
}}
/>
)} )}
{addSmartViewModalController.isAddingSmartView && ( {addSmartViewModalController.isAddingSmartView && (
<AddSmartViewModal controller={addSmartViewModalController} platform={application.platform} /> <AddSmartViewModal controller={addSmartViewModalController} platform={application.platform} />

View File

@@ -8,7 +8,7 @@ import ModalDialogButtons from '@/Components/Shared/ModalDialogButtons'
import ModalDialogDescription from '@/Components/Shared/ModalDialogDescription' import ModalDialogDescription from '@/Components/Shared/ModalDialogDescription'
import ModalDialogLabel from '@/Components/Shared/ModalDialogLabel' import ModalDialogLabel from '@/Components/Shared/ModalDialogLabel'
import Spinner from '@/Components/Spinner/Spinner' import Spinner from '@/Components/Spinner/Spinner'
import { Platform } from '@standardnotes/snjs' import { Platform, SmartViewDefaultIconName } from '@standardnotes/snjs'
import { observer } from 'mobx-react-lite' import { observer } from 'mobx-react-lite'
import { useEffect, useRef, useState } from 'react' import { useEffect, useRef, useState } from 'react'
import { AddSmartViewModalController } from './AddSmartViewModalController' import { AddSmartViewModalController } from './AddSmartViewModalController'
@@ -141,7 +141,7 @@ const AddSmartViewModal = ({ controller, platform }: Props) => {
onClick={toggleIconPicker} onClick={toggleIconPicker}
ref={iconPickerButtonRef} ref={iconPickerButtonRef}
> >
<Icon type={icon || 'restore'} /> <Icon type={icon || SmartViewDefaultIconName} />
</button> </button>
<Popover <Popover
open={shouldShowIconPicker} open={shouldShowIconPicker}
@@ -152,9 +152,9 @@ const AddSmartViewModal = ({ controller, platform }: Props) => {
> >
<div className="p-2"> <div className="p-2">
<IconPicker <IconPicker
selectedValue={icon || 'restore'} selectedValue={icon || SmartViewDefaultIconName}
onIconChange={(value?: string | undefined) => { onIconChange={(value?: string | undefined) => {
setIcon(value ?? 'restore') setIcon(value ?? SmartViewDefaultIconName)
toggleIconPicker() toggleIconPicker()
}} }}
platform={platform} platform={platform}
@@ -182,9 +182,9 @@ const AddSmartViewModal = ({ controller, platform }: Props) => {
<TabPanel state={tabState} id="builder" className="flex flex-col gap-2.5 p-4"> <TabPanel state={tabState} id="builder" className="flex flex-col gap-2.5 p-4">
<CompoundPredicateBuilder controller={predicateController} /> <CompoundPredicateBuilder controller={predicateController} />
</TabPanel> </TabPanel>
<TabPanel state={tabState} id="custom"> <TabPanel state={tabState} id="custom" className="flex flex-col">
<textarea <textarea
className="h-full min-h-[10rem] w-full resize-none bg-default py-1.5 px-2.5 font-mono text-sm" className="h-full min-h-[10rem] w-full flex-grow resize-none bg-default py-1.5 px-2.5 font-mono text-sm"
value={customPredicateJson} value={customPredicateJson}
onChange={(event) => { onChange={(event) => {
setCustomPredicateJson(event.target.value) setCustomPredicateJson(event.target.value)
@@ -194,7 +194,7 @@ const AddSmartViewModal = ({ controller, platform }: Props) => {
ref={customJsonInputRef} ref={customJsonInputRef}
/> />
{customPredicateJson && isCustomJsonValidPredicate === false && ( {customPredicateJson && isCustomJsonValidPredicate === false && (
<div className="mt-2 border-t border-border px-2.5 py-1.5 text-sm text-danger"> <div className="border-t border-border px-2.5 py-1.5 text-sm text-danger">
Invalid JSON. Double check your entry and try again. Invalid JSON. Double check your entry and try again.
</div> </div>
)} )}

View File

@@ -1,6 +1,6 @@
import { WebApplication } from '@/Application/Application' import { WebApplication } from '@/Application/Application'
import { CompoundPredicateBuilderController } from '@/Components/SmartViewBuilder/CompoundPredicateBuilderController' import { CompoundPredicateBuilderController } from '@/Components/SmartViewBuilder/CompoundPredicateBuilderController'
import { predicateFromJson, PredicateJsonForm } from '@standardnotes/snjs' import { predicateFromJson, PredicateJsonForm, SmartViewDefaultIconName } from '@standardnotes/snjs'
import { action, makeObservable, observable } from 'mobx' import { action, makeObservable, observable } from 'mobx'
export class AddSmartViewModalController { export class AddSmartViewModalController {
@@ -9,7 +9,7 @@ export class AddSmartViewModalController {
title = '' title = ''
icon = 'restore' icon: string = SmartViewDefaultIconName
predicateController = new CompoundPredicateBuilderController() predicateController = new CompoundPredicateBuilderController()

View File

@@ -135,7 +135,11 @@ const Navigation: FunctionComponent<Props> = ({ application }) => {
'md:hover:[overflow-y:_overlay]', 'md:hover:[overflow-y:_overlay]',
)} )}
> >
<SmartViewsSection application={application} viewControllerManager={viewControllerManager} /> <SmartViewsSection
application={application}
featuresController={viewControllerManager.featuresController}
navigationController={viewControllerManager.navigationController}
/>
<TagsSection viewControllerManager={viewControllerManager} /> <TagsSection viewControllerManager={viewControllerManager} />
</div> </div>
{NavigationFooter} {NavigationFooter}

View File

@@ -1,16 +1,22 @@
import { ViewControllerManager } from '@/Controllers/ViewControllerManager' import { FeaturesController } from '@/Controllers/FeaturesController'
import { NavigationController } from '@/Controllers/Navigation/NavigationController'
import { SmartView } from '@standardnotes/snjs' import { SmartView } from '@standardnotes/snjs'
import { observer } from 'mobx-react-lite' import { observer } from 'mobx-react-lite'
import { FunctionComponent } from 'react' import { FunctionComponent } from 'react'
import SmartViewsListItem from './SmartViewsListItem' import SmartViewsListItem from './SmartViewsListItem'
type Props = { type Props = {
viewControllerManager: ViewControllerManager navigationController: NavigationController
featuresController: FeaturesController
setEditingSmartView: (smartView: SmartView) => void setEditingSmartView: (smartView: SmartView) => void
} }
const SmartViewsList: FunctionComponent<Props> = ({ viewControllerManager, setEditingSmartView }: Props) => { const SmartViewsList: FunctionComponent<Props> = ({
const allViews = viewControllerManager.navigationController.smartViews navigationController,
featuresController,
setEditingSmartView,
}: Props) => {
const allViews = navigationController.smartViews
return ( return (
<> <>
@@ -19,8 +25,8 @@ const SmartViewsList: FunctionComponent<Props> = ({ viewControllerManager, setEd
<SmartViewsListItem <SmartViewsListItem
key={view.uuid} key={view.uuid}
view={view} view={view}
tagsState={viewControllerManager.navigationController} tagsState={navigationController}
features={viewControllerManager.featuresController} features={featuresController}
setEditingSmartView={setEditingSmartView} setEditingSmartView={setEditingSmartView}
/> />
) )

View File

@@ -1,35 +1,36 @@
import { WebApplication } from '@/Application/Application' import { WebApplication } from '@/Application/Application'
import { SMART_TAGS_FEATURE_NAME } from '@/Constants/Constants' import { SMART_TAGS_FEATURE_NAME } from '@/Constants/Constants'
import { ViewControllerManager } from '@/Controllers/ViewControllerManager' import { FeaturesController } from '@/Controllers/FeaturesController'
import { NavigationController } from '@/Controllers/Navigation/NavigationController'
import { usePremiumModal } from '@/Hooks/usePremiumModal' import { usePremiumModal } from '@/Hooks/usePremiumModal'
import { SmartView } from '@standardnotes/snjs'
import { observer } from 'mobx-react-lite' import { observer } from 'mobx-react-lite'
import { FunctionComponent, useCallback, useMemo, useState } from 'react' import { FunctionComponent, useCallback, useMemo } from 'react'
import IconButton from '../Button/IconButton' import IconButton from '../Button/IconButton'
import EditSmartViewModal from '../Preferences/Panes/General/SmartViews/EditSmartViewModal' import EditSmartViewModal from '../Preferences/Panes/General/SmartViews/EditSmartViewModal'
import { EditSmartViewModalController } from '../Preferences/Panes/General/SmartViews/EditSmartViewModalController'
import AddSmartViewModal from '../SmartViewBuilder/AddSmartViewModal' import AddSmartViewModal from '../SmartViewBuilder/AddSmartViewModal'
import { AddSmartViewModalController } from '../SmartViewBuilder/AddSmartViewModalController' import { AddSmartViewModalController } from '../SmartViewBuilder/AddSmartViewModalController'
import SmartViewsList from './SmartViewsList' import SmartViewsList from './SmartViewsList'
type Props = { type Props = {
application: WebApplication application: WebApplication
viewControllerManager: ViewControllerManager navigationController: NavigationController
featuresController: FeaturesController
} }
const SmartViewsSection: FunctionComponent<Props> = ({ application, viewControllerManager }) => { const SmartViewsSection: FunctionComponent<Props> = ({ application, navigationController, featuresController }) => {
const premiumModal = usePremiumModal() const premiumModal = usePremiumModal()
const addSmartViewModalController = useMemo(() => new AddSmartViewModalController(application), [application]) const addSmartViewModalController = useMemo(() => new AddSmartViewModalController(application), [application])
const editSmartViewModalController = useMemo(() => new EditSmartViewModalController(application), [application])
const [editingSmartView, setEditingSmartView] = useState<SmartView | undefined>(undefined)
const createNewSmartView = useCallback(() => { const createNewSmartView = useCallback(() => {
if (!viewControllerManager.featuresController.hasSmartViews) { if (!featuresController.hasSmartViews) {
premiumModal.activate(SMART_TAGS_FEATURE_NAME) premiumModal.activate(SMART_TAGS_FEATURE_NAME)
return return
} }
addSmartViewModalController.setIsAddingSmartView(true) addSmartViewModalController.setIsAddingSmartView(true)
}, [addSmartViewModalController, premiumModal, viewControllerManager.featuresController.hasSmartViews]) }, [addSmartViewModalController, premiumModal, featuresController.hasSmartViews])
return ( return (
<section> <section>
@@ -47,16 +48,13 @@ const SmartViewsSection: FunctionComponent<Props> = ({ application, viewControll
/> />
</div> </div>
</div> </div>
<SmartViewsList viewControllerManager={viewControllerManager} setEditingSmartView={setEditingSmartView} /> <SmartViewsList
{!!editingSmartView && ( navigationController={navigationController}
<EditSmartViewModal featuresController={featuresController}
application={application} setEditingSmartView={editSmartViewModalController.setView}
navigationController={viewControllerManager.navigationController} />
view={editingSmartView} {!!editSmartViewModalController.view && (
closeDialog={() => { <EditSmartViewModal controller={editSmartViewModalController} platform={application.platform} />
setEditingSmartView(undefined)
}}
/>
)} )}
{addSmartViewModalController.isAddingSmartView && ( {addSmartViewModalController.isAddingSmartView && (
<AddSmartViewModal controller={addSmartViewModalController} platform={application.platform} /> <AddSmartViewModal controller={addSmartViewModalController} platform={application.platform} />

View File

@@ -119,7 +119,7 @@ export class ItemListController extends AbstractViewController implements Intern
) )
this.disposers.push( this.disposers.push(
application.streamItems<SNTag>([ContentType.Tag], async ({ changed, inserted }) => { application.streamItems<SNTag>([ContentType.Tag, ContentType.SmartView], async ({ changed, inserted }) => {
const tags = [...changed, ...inserted] const tags = [...changed, ...inserted]
const { didReloadItems } = await this.reloadDisplayPreferences() const { didReloadItems } = await this.reloadDisplayPreferences()