diff --git a/packages/models/src/Domain/Syncable/SmartView/SmartViewMutator.spec.ts b/packages/models/src/Domain/Syncable/SmartView/SmartViewMutator.spec.ts new file mode 100644 index 000000000..10eb756ab --- /dev/null +++ b/packages/models/src/Domain/Syncable/SmartView/SmartViewMutator.spec.ts @@ -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() + }) +}) diff --git a/packages/models/src/Domain/Syncable/SmartView/SmartViewMutator.ts b/packages/models/src/Domain/Syncable/SmartView/SmartViewMutator.ts new file mode 100644 index 000000000..639267438 --- /dev/null +++ b/packages/models/src/Domain/Syncable/SmartView/SmartViewMutator.ts @@ -0,0 +1,13 @@ +import { DecryptedItemInterface, MutationType } from '../../Abstract/Item' +import { TagMutator } from '../Tag' +import { SmartViewContent } from './SmartViewContent' + +export class SmartViewMutator extends TagMutator { + constructor(item: DecryptedItemInterface, type: MutationType) { + super(item, type) + } + + set predicate(predicate: SmartViewContent['predicate']) { + this.mutableContent.predicate = predicate + } +} diff --git a/packages/models/src/Domain/Syncable/SmartView/index.ts b/packages/models/src/Domain/Syncable/SmartView/index.ts index bf1dfdd5f..e00e0f702 100644 --- a/packages/models/src/Domain/Syncable/SmartView/index.ts +++ b/packages/models/src/Domain/Syncable/SmartView/index.ts @@ -2,3 +2,5 @@ export * from './SmartView' export * from './SmartViewBuilder' export * from './SystemViewId' export * from './SmartViewContent' +export * from './SmartViewMutator' +export * from './SmartViewIcons' diff --git a/packages/models/src/Domain/Syncable/Tag/TagMutator.ts b/packages/models/src/Domain/Syncable/Tag/TagMutator.ts index 67420c4ad..0f3854d7f 100644 --- a/packages/models/src/Domain/Syncable/Tag/TagMutator.ts +++ b/packages/models/src/Domain/Syncable/Tag/TagMutator.ts @@ -11,10 +11,10 @@ import { TagToFileReference } from '../../Abstract/Reference/TagToFileReference' import { TagPreferences } from './TagPreferences' import { DecryptedItemInterface, MutationType } from '../../Abstract/Item' -export class TagMutator extends DecryptedItemMutator { +export class TagMutator extends DecryptedItemMutator { private mutablePreferences?: TagPreferences - constructor(item: DecryptedItemInterface, type: MutationType) { + constructor(item: DecryptedItemInterface, type: MutationType) { super(item, type) this.mutablePreferences = this.mutableContent.preferences diff --git a/packages/models/src/Domain/Utilities/Item/ItemGenerator.ts b/packages/models/src/Domain/Utilities/Item/ItemGenerator.ts index 5e5793c02..24eaa20a0 100644 --- a/packages/models/src/Domain/Utilities/Item/ItemGenerator.ts +++ b/packages/models/src/Domain/Utilities/Item/ItemGenerator.ts @@ -33,6 +33,7 @@ import { import { DeletedItem } from '../../Abstract/Item/Implementations/DeletedItem' import { EncryptedItemInterface } from '../../Abstract/Item/Interfaces/EncryptedItem' import { DeletedItemInterface } from '../../Abstract/Item/Interfaces/DeletedItem' +import { SmartViewMutator } from '../../Syncable/SmartView' type ItemClass = new (payload: DecryptedPayloadInterface) => DecryptedItem @@ -56,7 +57,7 @@ const ContentTypeClassMapping: Partial> = { [ContentType.ExtensionRepo]: { itemClass: SNFeatureRepo }, [ContentType.File]: { itemClass: FileItem, mutatorClass: FileMutator }, [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.Theme]: { itemClass: SNTheme, mutatorClass: ThemeMutator }, [ContentType.UserPrefs]: { itemClass: SNUserPrefs, mutatorClass: UserPrefsMutator }, diff --git a/packages/models/src/Domain/Utilities/Test/SpecUtils.ts b/packages/models/src/Domain/Utilities/Test/SpecUtils.ts index ecffe7cb5..02694c94a 100644 --- a/packages/models/src/Domain/Utilities/Test/SpecUtils.ts +++ b/packages/models/src/Domain/Utilities/Test/SpecUtils.ts @@ -5,6 +5,7 @@ import { DecryptedPayload, PayloadSource, PayloadTimestampDefaults } from '../.. import { FileContent, FileItem } from '../../Syncable/File' import { NoteContent, SNNote } from '../../Syncable/Note' import { SNTag } from '../../Syncable/Tag' +import { SmartView, SmartViewContent } from '../../Syncable/SmartView' let currentId = 0 @@ -55,6 +56,20 @@ export const createTagWithContent = (content: Partial): SNTag => { ) } +export const createSmartViewWithContent = (content: Partial): SmartView => { + return new SmartView( + new DecryptedPayload( + { + uuid: mockUuid(), + content_type: ContentType.SmartView, + content: FillItemContent(content), + ...PayloadTimestampDefaults(), + }, + PayloadSource.Constructor, + ), + ) +} + export const createTagWithTitle = (title = 'photos') => { return new SNTag( 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({ title }), + ...PayloadTimestampDefaults(), + }, + PayloadSource.Constructor, + ), + ) +} + export const createFile = (name = 'screenshot.png') => { return new FileItem( new DecryptedPayload( diff --git a/packages/snjs/lib/Services/Items/ItemManager.ts b/packages/snjs/lib/Services/Items/ItemManager.ts index dee97373f..35fd1a69d 100644 --- a/packages/snjs/lib/Services/Items/ItemManager.ts +++ b/packages/snjs/lib/Services/Items/ItemManager.ts @@ -9,7 +9,7 @@ import * as Services from '@standardnotes/services' import { PayloadManagerChangeData } from '../Payloads' import { DiagnosticInfo, ItemsClientInterface, ItemRelationshipDirection } from '@standardnotes/services' import { ApplicationDisplayOptions } from '@Lib/Application/Options/OptionalOptions' -import { CollectionSort, DecryptedItemInterface, ItemContent } from '@standardnotes/models' +import { CollectionSort, DecryptedItemInterface, ItemContent, SmartViewDefaultIconName } from '@standardnotes/models' type ItemsChangeObserver = { contentType: ContentType[] @@ -1229,7 +1229,7 @@ export class ItemManager Models.FillItemContent({ title, predicate: predicate.toJson(), - iconString: iconString || 'restore', + iconString: iconString || SmartViewDefaultIconName, } as Models.SmartViewContent), true, ) as Promise diff --git a/packages/web/src/javascripts/Components/Preferences/Panes/General/General.tsx b/packages/web/src/javascripts/Components/Preferences/Panes/General/General.tsx index ec99f28f5..fceb4a812 100644 --- a/packages/web/src/javascripts/Components/Preferences/Panes/General/General.tsx +++ b/packages/web/src/javascripts/Components/Preferences/Panes/General/General.tsx @@ -23,11 +23,7 @@ const General: FunctionComponent = ({ viewControllerManager, application, - + void + controller: EditSmartViewModalController + platform: Platform } -const EditSmartViewModal = ({ application, navigationController, view, closeDialog }: Props) => { - const [title, setTitle] = useState(view.title) +const EditSmartViewModal = ({ controller, platform }: Props) => { + const { + view, + title, + setTitle, + predicateJson, + setPredicateJson, + isPredicateJsonValid, + setIsPredicateJsonValid, + icon, + setIcon, + save, + isSaving, + closeDialog, + deleteView, + } = controller + const titleInputRef = useRef(null) - - const [selectedIcon, setSelectedIcon] = useState(view.iconString) - - const [isSaving, setIsSaving] = useState(false) + const predicateJsonInputRef = useRef(null) const [shouldShowIconPicker, setShouldShowIconPicker] = useState(false) const iconPickerButtonRef = useRef(null) @@ -41,29 +50,26 @@ const EditSmartViewModal = ({ application, navigationController, view, closeDial return } - setIsSaving(true) + void save() + }, [save, title.length]) - await application.mutator.changeAndSaveItem(view, (mutator) => { - mutator.title = title - mutator.iconString = selectedIcon || 'restore' - }) + useEffect(() => { + if (!predicateJsonInputRef.current) { + return + } - setIsSaving(false) - closeDialog() - }, [application.mutator, closeDialog, selectedIcon, title, view]) + if (isPredicateJsonValid === false) { + predicateJsonInputRef.current.focus() + } + }, [isPredicateJsonValid]) - const deleteSmartView = useCallback(async () => { - void navigationController.remove(view, true) - closeDialog() - }, [closeDialog, navigationController, view]) - - const close = useCallback(() => { - closeDialog() - }, [closeDialog]) + if (!view) { + return null + } return ( - Edit Smart View "{view.title}" + Edit Smart View "{view.title}"
@@ -85,7 +91,7 @@ const EditSmartViewModal = ({ application, navigationController, view, closeDial onClick={toggleIconPicker} ref={iconPickerButtonRef} > - +
{ - setSelectedIcon(value) + setIcon(value || SmartViewDefaultIconName) toggleIconPicker() }} - platform={application.platform} + platform={platform} useIconGrid={true} portalDropdown={false} />
+
+
Predicate:
+
+