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 './SystemViewId'
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 { 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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

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 { 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}
/>
)

View File

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

View File

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