feat: edit smart view predicate as json (#2012)
This commit is contained in:
@@ -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()
|
||||
})
|
||||
})
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -2,3 +2,5 @@ export * from './SmartView'
|
||||
export * from './SmartViewBuilder'
|
||||
export * from './SystemViewId'
|
||||
export * from './SmartViewContent'
|
||||
export * from './SmartViewMutator'
|
||||
export * from './SmartViewIcons'
|
||||
|
||||
@@ -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<TagContent> {
|
||||
export class TagMutator<Content extends TagContent = TagContent> extends DecryptedItemMutator<Content> {
|
||||
private mutablePreferences?: TagPreferences
|
||||
|
||||
constructor(item: DecryptedItemInterface<TagContent>, type: MutationType) {
|
||||
constructor(item: DecryptedItemInterface<Content>, type: MutationType) {
|
||||
super(item, type)
|
||||
|
||||
this.mutablePreferences = this.mutableContent.preferences
|
||||
|
||||
@@ -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<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.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 },
|
||||
|
||||
@@ -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<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') => {
|
||||
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<SmartViewContent>({ title }),
|
||||
...PayloadTimestampDefaults(),
|
||||
},
|
||||
PayloadSource.Constructor,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
export const createFile = (name = 'screenshot.png') => {
|
||||
return new FileItem(
|
||||
new DecryptedPayload(
|
||||
|
||||
@@ -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<I extends Models.DecryptedItemInterface = Models.DecryptedItemInterface> = {
|
||||
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<Models.SmartView>
|
||||
|
||||
@@ -23,11 +23,7 @@ const General: FunctionComponent<Props> = ({ viewControllerManager, application,
|
||||
<Persistence application={application} />
|
||||
<PlaintextDefaults application={application} />
|
||||
<Defaults application={application} />
|
||||
<SmartViews
|
||||
application={application}
|
||||
navigationController={viewControllerManager.navigationController}
|
||||
featuresController={viewControllerManager.featuresController}
|
||||
/>
|
||||
<SmartViews application={application} featuresController={viewControllerManager.featuresController} />
|
||||
<Tools application={application} />
|
||||
<LabsPane application={application} />
|
||||
<Advanced
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { WebApplication } from '@/Application/Application'
|
||||
import Button from '@/Components/Button/Button'
|
||||
import Icon from '@/Components/Icon/Icon'
|
||||
import IconPicker from '@/Components/Icon/IconPicker'
|
||||
@@ -8,25 +7,35 @@ 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 { Platform, SmartViewDefaultIconName } from '@standardnotes/snjs'
|
||||
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 = {
|
||||
application: WebApplication
|
||||
navigationController: NavigationController
|
||||
view: SmartView
|
||||
closeDialog: () => 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<HTMLInputElement>(null)
|
||||
|
||||
const [selectedIcon, setSelectedIcon] = useState<string | undefined>(view.iconString)
|
||||
|
||||
const [isSaving, setIsSaving] = useState(false)
|
||||
const predicateJsonInputRef = useRef<HTMLTextAreaElement>(null)
|
||||
|
||||
const [shouldShowIconPicker, setShouldShowIconPicker] = useState(false)
|
||||
const iconPickerButtonRef = useRef<HTMLButtonElement>(null)
|
||||
@@ -41,29 +50,26 @@ const EditSmartViewModal = ({ application, navigationController, view, closeDial
|
||||
return
|
||||
}
|
||||
|
||||
setIsSaving(true)
|
||||
void save()
|
||||
}, [save, title.length])
|
||||
|
||||
await application.mutator.changeAndSaveItem<TagMutator>(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 (
|
||||
<ModalDialog>
|
||||
<ModalDialogLabel closeDialog={close}>Edit Smart View "{view.title}"</ModalDialogLabel>
|
||||
<ModalDialogLabel closeDialog={closeDialog}>Edit Smart View "{view.title}"</ModalDialogLabel>
|
||||
<ModalDialogDescription>
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex items-center gap-2.5">
|
||||
@@ -85,7 +91,7 @@ const EditSmartViewModal = ({ application, navigationController, view, closeDial
|
||||
onClick={toggleIconPicker}
|
||||
ref={iconPickerButtonRef}
|
||||
>
|
||||
<Icon type={selectedIcon || 'restore'} />
|
||||
<Icon type={icon || SmartViewDefaultIconName} />
|
||||
</button>
|
||||
<Popover
|
||||
open={shouldShowIconPicker}
|
||||
@@ -96,28 +102,48 @@ const EditSmartViewModal = ({ application, navigationController, view, closeDial
|
||||
>
|
||||
<div className="p-2">
|
||||
<IconPicker
|
||||
selectedValue={selectedIcon || 'restore'}
|
||||
selectedValue={icon || SmartViewDefaultIconName}
|
||||
onIconChange={(value?: string | undefined) => {
|
||||
setSelectedIcon(value)
|
||||
setIcon(value || SmartViewDefaultIconName)
|
||||
toggleIconPicker()
|
||||
}}
|
||||
platform={application.platform}
|
||||
platform={platform}
|
||||
useIconGrid={true}
|
||||
portalDropdown={false}
|
||||
/>
|
||||
</div>
|
||||
</Popover>
|
||||
</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>
|
||||
</ModalDialogDescription>
|
||||
<ModalDialogButtons>
|
||||
<Button className="mr-auto" disabled={isSaving} onClick={deleteSmartView} colorStyle="danger">
|
||||
<Button className="mr-auto" disabled={isSaving} onClick={deleteView} colorStyle="danger">
|
||||
Delete
|
||||
</Button>
|
||||
<Button disabled={isSaving} onClick={saveSmartView}>
|
||||
<Button disabled={isSaving} onClick={saveSmartView} primary colorStyle="info">
|
||||
{isSaving ? <Spinner className="h-4.5 w-4.5" /> : 'Save'}
|
||||
</Button>
|
||||
<Button disabled={isSaving} onClick={close}>
|
||||
<Button disabled={isSaving} onClick={closeDialog}>
|
||||
Cancel
|
||||
</Button>
|
||||
</ModalDialogButtons>
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,17 @@
|
||||
import Button from '@/Components/Button/Button'
|
||||
import Icon from '@/Components/Icon/Icon'
|
||||
import { SmartView } from '@standardnotes/snjs'
|
||||
import { useCallback } from 'react'
|
||||
|
||||
type Props = {
|
||||
view: SmartView
|
||||
onEdit: () => void
|
||||
onDelete: () => void
|
||||
onDelete: (view: SmartView) => void
|
||||
}
|
||||
|
||||
const SmartViewItem = ({ view, onEdit, onDelete }: Props) => {
|
||||
const onClickDelete = useCallback(() => onDelete(view), [onDelete, view])
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2 py-1.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}>
|
||||
Edit
|
||||
</Button>
|
||||
<Button small onClick={onDelete}>
|
||||
<Button small onClick={onClickDelete}>
|
||||
Delete
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { WebApplication } from '@/Application/Application'
|
||||
import Button from '@/Components/Button/Button'
|
||||
import { NavigationController } from '@/Controllers/Navigation/NavigationController'
|
||||
import { isSystemView, SmartView } from '@standardnotes/snjs'
|
||||
import { ContentType, isSystemView, SmartView } from '@standardnotes/snjs'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { useMemo, useState } from 'react'
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { Title } from '../../../PreferencesComponents/Content'
|
||||
import PreferencesGroup from '../../../PreferencesComponents/PreferencesGroup'
|
||||
import PreferencesSegment from '../../../PreferencesComponents/PreferencesSegment'
|
||||
@@ -13,21 +12,45 @@ import EditSmartViewModal from './EditSmartViewModal'
|
||||
import SmartViewItem from './SmartViewItem'
|
||||
import { FeaturesController } from '@/Controllers/FeaturesController'
|
||||
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 = {
|
||||
application: WebApplication
|
||||
navigationController: NavigationController
|
||||
featuresController: FeaturesController
|
||||
}
|
||||
|
||||
type Props = NewType
|
||||
|
||||
const SmartViews = ({ application, navigationController, featuresController }: Props) => {
|
||||
const [editingSmartView, setEditingSmartView] = useState<SmartView | undefined>(undefined)
|
||||
|
||||
const SmartViews = ({ application, featuresController }: Props) => {
|
||||
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 (
|
||||
<>
|
||||
@@ -45,12 +68,12 @@ const SmartViews = ({ application, navigationController, featuresController }: P
|
||||
{featuresController.hasSmartViews && (
|
||||
<>
|
||||
<div className="my-2 flex flex-col">
|
||||
{nonSystemSmartViews.map((view) => (
|
||||
{smartViews.map((view) => (
|
||||
<SmartViewItem
|
||||
key={view.uuid}
|
||||
view={view}
|
||||
onEdit={() => setEditingSmartView(view)}
|
||||
onDelete={() => navigationController.remove(view, true)}
|
||||
onEdit={() => editSmartViewModalController.setView(view)}
|
||||
onDelete={deleteItem}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
@@ -65,15 +88,8 @@ const SmartViews = ({ application, navigationController, featuresController }: P
|
||||
)}
|
||||
</PreferencesSegment>
|
||||
</PreferencesGroup>
|
||||
{!!editingSmartView && (
|
||||
<EditSmartViewModal
|
||||
application={application}
|
||||
navigationController={navigationController}
|
||||
view={editingSmartView}
|
||||
closeDialog={() => {
|
||||
setEditingSmartView(undefined)
|
||||
}}
|
||||
/>
|
||||
{!!editSmartViewModalController.view && (
|
||||
<EditSmartViewModal controller={editSmartViewModalController} platform={application.platform} />
|
||||
)}
|
||||
{addSmartViewModalController.isAddingSmartView && (
|
||||
<AddSmartViewModal controller={addSmartViewModalController} platform={application.platform} />
|
||||
|
||||
@@ -8,7 +8,7 @@ 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 { Platform } from '@standardnotes/snjs'
|
||||
import { Platform, SmartViewDefaultIconName } from '@standardnotes/snjs'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { AddSmartViewModalController } from './AddSmartViewModalController'
|
||||
@@ -141,7 +141,7 @@ const AddSmartViewModal = ({ controller, platform }: Props) => {
|
||||
onClick={toggleIconPicker}
|
||||
ref={iconPickerButtonRef}
|
||||
>
|
||||
<Icon type={icon || 'restore'} />
|
||||
<Icon type={icon || SmartViewDefaultIconName} />
|
||||
</button>
|
||||
<Popover
|
||||
open={shouldShowIconPicker}
|
||||
@@ -152,9 +152,9 @@ const AddSmartViewModal = ({ controller, platform }: Props) => {
|
||||
>
|
||||
<div className="p-2">
|
||||
<IconPicker
|
||||
selectedValue={icon || 'restore'}
|
||||
selectedValue={icon || SmartViewDefaultIconName}
|
||||
onIconChange={(value?: string | undefined) => {
|
||||
setIcon(value ?? 'restore')
|
||||
setIcon(value ?? SmartViewDefaultIconName)
|
||||
toggleIconPicker()
|
||||
}}
|
||||
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">
|
||||
<CompoundPredicateBuilder controller={predicateController} />
|
||||
</TabPanel>
|
||||
<TabPanel state={tabState} id="custom">
|
||||
<TabPanel state={tabState} id="custom" className="flex flex-col">
|
||||
<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}
|
||||
onChange={(event) => {
|
||||
setCustomPredicateJson(event.target.value)
|
||||
@@ -194,7 +194,7 @@ const AddSmartViewModal = ({ controller, platform }: Props) => {
|
||||
ref={customJsonInputRef}
|
||||
/>
|
||||
{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.
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { WebApplication } from '@/Application/Application'
|
||||
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'
|
||||
|
||||
export class AddSmartViewModalController {
|
||||
@@ -9,7 +9,7 @@ export class AddSmartViewModalController {
|
||||
|
||||
title = ''
|
||||
|
||||
icon = 'restore'
|
||||
icon: string = SmartViewDefaultIconName
|
||||
|
||||
predicateController = new CompoundPredicateBuilderController()
|
||||
|
||||
|
||||
@@ -135,7 +135,11 @@ const Navigation: FunctionComponent<Props> = ({ application }) => {
|
||||
'md:hover:[overflow-y:_overlay]',
|
||||
)}
|
||||
>
|
||||
<SmartViewsSection application={application} viewControllerManager={viewControllerManager} />
|
||||
<SmartViewsSection
|
||||
application={application}
|
||||
featuresController={viewControllerManager.featuresController}
|
||||
navigationController={viewControllerManager.navigationController}
|
||||
/>
|
||||
<TagsSection viewControllerManager={viewControllerManager} />
|
||||
</div>
|
||||
{NavigationFooter}
|
||||
|
||||
@@ -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 { observer } from 'mobx-react-lite'
|
||||
import { FunctionComponent } from 'react'
|
||||
import SmartViewsListItem from './SmartViewsListItem'
|
||||
|
||||
type Props = {
|
||||
viewControllerManager: ViewControllerManager
|
||||
navigationController: NavigationController
|
||||
featuresController: FeaturesController
|
||||
setEditingSmartView: (smartView: SmartView) => void
|
||||
}
|
||||
|
||||
const SmartViewsList: FunctionComponent<Props> = ({ viewControllerManager, setEditingSmartView }: Props) => {
|
||||
const allViews = viewControllerManager.navigationController.smartViews
|
||||
const SmartViewsList: FunctionComponent<Props> = ({
|
||||
navigationController,
|
||||
featuresController,
|
||||
setEditingSmartView,
|
||||
}: Props) => {
|
||||
const allViews = navigationController.smartViews
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -19,8 +25,8 @@ const SmartViewsList: FunctionComponent<Props> = ({ viewControllerManager, setEd
|
||||
<SmartViewsListItem
|
||||
key={view.uuid}
|
||||
view={view}
|
||||
tagsState={viewControllerManager.navigationController}
|
||||
features={viewControllerManager.featuresController}
|
||||
tagsState={navigationController}
|
||||
features={featuresController}
|
||||
setEditingSmartView={setEditingSmartView}
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -1,35 +1,36 @@
|
||||
import { WebApplication } from '@/Application/Application'
|
||||
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 { SmartView } from '@standardnotes/snjs'
|
||||
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 EditSmartViewModal from '../Preferences/Panes/General/SmartViews/EditSmartViewModal'
|
||||
import { EditSmartViewModalController } from '../Preferences/Panes/General/SmartViews/EditSmartViewModalController'
|
||||
import AddSmartViewModal from '../SmartViewBuilder/AddSmartViewModal'
|
||||
import { AddSmartViewModalController } from '../SmartViewBuilder/AddSmartViewModalController'
|
||||
import SmartViewsList from './SmartViewsList'
|
||||
|
||||
type Props = {
|
||||
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 addSmartViewModalController = useMemo(() => new AddSmartViewModalController(application), [application])
|
||||
|
||||
const [editingSmartView, setEditingSmartView] = useState<SmartView | undefined>(undefined)
|
||||
const editSmartViewModalController = useMemo(() => new EditSmartViewModalController(application), [application])
|
||||
|
||||
const createNewSmartView = useCallback(() => {
|
||||
if (!viewControllerManager.featuresController.hasSmartViews) {
|
||||
if (!featuresController.hasSmartViews) {
|
||||
premiumModal.activate(SMART_TAGS_FEATURE_NAME)
|
||||
return
|
||||
}
|
||||
|
||||
addSmartViewModalController.setIsAddingSmartView(true)
|
||||
}, [addSmartViewModalController, premiumModal, viewControllerManager.featuresController.hasSmartViews])
|
||||
}, [addSmartViewModalController, premiumModal, featuresController.hasSmartViews])
|
||||
|
||||
return (
|
||||
<section>
|
||||
@@ -47,16 +48,13 @@ const SmartViewsSection: FunctionComponent<Props> = ({ application, viewControll
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<SmartViewsList viewControllerManager={viewControllerManager} setEditingSmartView={setEditingSmartView} />
|
||||
{!!editingSmartView && (
|
||||
<EditSmartViewModal
|
||||
application={application}
|
||||
navigationController={viewControllerManager.navigationController}
|
||||
view={editingSmartView}
|
||||
closeDialog={() => {
|
||||
setEditingSmartView(undefined)
|
||||
}}
|
||||
/>
|
||||
<SmartViewsList
|
||||
navigationController={navigationController}
|
||||
featuresController={featuresController}
|
||||
setEditingSmartView={editSmartViewModalController.setView}
|
||||
/>
|
||||
{!!editSmartViewModalController.view && (
|
||||
<EditSmartViewModal controller={editSmartViewModalController} platform={application.platform} />
|
||||
)}
|
||||
{addSmartViewModalController.isAddingSmartView && (
|
||||
<AddSmartViewModal controller={addSmartViewModalController} platform={application.platform} />
|
||||
|
||||
@@ -119,7 +119,7 @@ export class ItemListController extends AbstractViewController implements Intern
|
||||
)
|
||||
|
||||
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 { didReloadItems } = await this.reloadDisplayPreferences()
|
||||
|
||||
Reference in New Issue
Block a user