feat: per-tag display preferences (#1868)
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { CreateItemDelta } from './../Index/ItemDelta'
|
||||
import { DeletedPayload } from './../../Abstract/Payload/Implementations/DeletedPayload'
|
||||
import { createFile, createNote, createTag, mockUuid, pinnedContent } from './../../Utilities/Test/SpecUtils'
|
||||
import { createFile, createNote, createTagWithTitle, mockUuid, pinnedContent } from './../../Utilities/Test/SpecUtils'
|
||||
import { ContentType } from '@standardnotes/common'
|
||||
import { DeletedItem, EncryptedItem } from '../../Abstract/Item'
|
||||
import { EncryptedPayload, PayloadTimestampDefaults } from '../../Abstract/Payload'
|
||||
@@ -174,7 +174,7 @@ describe('item display controller', () => {
|
||||
it('should ignore items not matching content type on construction', () => {
|
||||
const collection = new ItemCollection()
|
||||
const note = createNoteWithContent({ title: 'a' })
|
||||
const tag = createTag()
|
||||
const tag = createTagWithTitle()
|
||||
collection.set([note, tag])
|
||||
|
||||
const controller = new ItemDisplayController(collection, [ContentType.Note], {
|
||||
@@ -187,7 +187,7 @@ describe('item display controller', () => {
|
||||
it('should ignore items not matching content type on sort change', () => {
|
||||
const collection = new ItemCollection()
|
||||
const note = createNoteWithContent({ title: 'a' })
|
||||
const tag = createTag()
|
||||
const tag = createTagWithTitle()
|
||||
collection.set([note, tag])
|
||||
|
||||
const controller = new ItemDisplayController(collection, [ContentType.Note], {
|
||||
@@ -207,7 +207,7 @@ describe('item display controller', () => {
|
||||
sortBy: 'title',
|
||||
sortDirection: 'asc',
|
||||
})
|
||||
const tag = createTag()
|
||||
const tag = createTagWithTitle()
|
||||
|
||||
const delta = CreateItemDelta({ inserted: [tag], changed: [note] })
|
||||
collection.onChange(delta)
|
||||
|
||||
@@ -6,6 +6,9 @@ import { SystemViewId } from './SystemViewId'
|
||||
import { EmojiString, IconType } from '../../Utilities/Icon/IconType'
|
||||
import { SmartViewDefaultIconName, systemViewIcon } from './SmartViewIcons'
|
||||
import { SmartViewContent } from './SmartViewContent'
|
||||
import { TagPreferences } from '../Tag/TagPreferences'
|
||||
import { ItemInterface } from '../../Abstract/Item'
|
||||
import { ContentType } from '@standardnotes/common'
|
||||
|
||||
export const SMART_TAG_DSL_PREFIX = '!['
|
||||
|
||||
@@ -13,10 +16,13 @@ export function isSystemView(view: SmartView): boolean {
|
||||
return Object.values(SystemViewId).includes(view.uuid as SystemViewId)
|
||||
}
|
||||
|
||||
export const isSmartView = (x: ItemInterface): x is SmartView => x.content_type === ContentType.SmartView
|
||||
|
||||
export class SmartView extends DecryptedItem<SmartViewContent> {
|
||||
public readonly predicate!: PredicateInterface<DecryptedItem>
|
||||
public readonly title: string
|
||||
public readonly iconString: IconType | EmojiString
|
||||
public readonly preferences?: TagPreferences
|
||||
|
||||
constructor(payload: DecryptedPayloadInterface<SmartViewContent>) {
|
||||
super(payload)
|
||||
@@ -28,6 +34,8 @@ export class SmartView extends DecryptedItem<SmartViewContent> {
|
||||
this.iconString = this.payload.content.iconString || SmartViewDefaultIconName
|
||||
}
|
||||
|
||||
this.preferences = this.payload.content.preferences
|
||||
|
||||
try {
|
||||
this.predicate = this.content.predicate && predicateFromJson(this.content.predicate)
|
||||
} catch (error) {
|
||||
|
||||
@@ -38,4 +38,10 @@ describe('SNTag Tests', () => {
|
||||
|
||||
expect(tag.noteCount).toEqual(2)
|
||||
})
|
||||
|
||||
it('preferences should be undefined if not specified', () => {
|
||||
const tag = create('helloworld', [])
|
||||
|
||||
expect(tag.preferences).toBeFalsy()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -6,6 +6,7 @@ import { ContentReference } from '../../Abstract/Reference/ContentReference'
|
||||
import { isTagToParentTagReference } from '../../Abstract/Reference/Functions'
|
||||
import { DecryptedPayloadInterface } from '../../Abstract/Payload/Interfaces/DecryptedPayload'
|
||||
import { TagContent, TagContentSpecialized } from './TagContent'
|
||||
import { TagPreferences } from './TagPreferences'
|
||||
|
||||
export const TagFolderDelimitter = '.'
|
||||
|
||||
@@ -17,12 +18,14 @@ export class SNTag extends DecryptedItem<TagContent> implements TagContentSpecia
|
||||
public readonly title: string
|
||||
public readonly iconString: VectorIconNameOrEmoji
|
||||
public readonly expanded: boolean
|
||||
public readonly preferences?: TagPreferences
|
||||
|
||||
constructor(payload: DecryptedPayloadInterface<TagContent>) {
|
||||
super(payload)
|
||||
this.title = this.payload.content.title || ''
|
||||
this.expanded = this.payload.content.expanded != undefined ? this.payload.content.expanded : true
|
||||
this.iconString = this.payload.content.iconString || DefaultTagIconName
|
||||
this.preferences = this.payload.content.preferences
|
||||
}
|
||||
|
||||
get noteReferences(): ContentReference[] {
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import { IconType } from './../../Utilities/Icon/IconType'
|
||||
import { ItemContent } from '../../Abstract/Content/ItemContent'
|
||||
import { EmojiString } from '../../Utilities/Icon/IconType'
|
||||
import { TagPreferences } from './TagPreferences'
|
||||
|
||||
export interface TagContentSpecialized {
|
||||
title: string
|
||||
expanded: boolean
|
||||
iconString: IconType | EmojiString
|
||||
preferences?: TagPreferences
|
||||
}
|
||||
|
||||
export type TagContent = TagContentSpecialized & ItemContent
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ContentType } from '@standardnotes/common'
|
||||
import { ContentReferenceType, MutationType } from '../../Abstract/Item'
|
||||
import { createFile, createTag } from '../../Utilities/Test/SpecUtils'
|
||||
import { createFile, createTagWithContent, createTagWithTitle } from '../../Utilities/Test/SpecUtils'
|
||||
import { SNTag } from './Tag'
|
||||
import { TagMutator } from './TagMutator'
|
||||
|
||||
@@ -8,7 +8,7 @@ describe('tag mutator', () => {
|
||||
it('should add file to tag', () => {
|
||||
const file = createFile()
|
||||
|
||||
const tag = createTag()
|
||||
const tag = createTagWithTitle()
|
||||
const mutator = new TagMutator(tag, MutationType.UpdateUserTimestamps)
|
||||
mutator.addFile(file)
|
||||
const result = mutator.getResult()
|
||||
@@ -23,7 +23,7 @@ describe('tag mutator', () => {
|
||||
it('should remove file from tag', () => {
|
||||
const file = createFile()
|
||||
|
||||
const tag = createTag()
|
||||
const tag = createTagWithTitle()
|
||||
const addMutator = new TagMutator(tag, MutationType.UpdateUserTimestamps)
|
||||
addMutator.addFile(file)
|
||||
const addResult = addMutator.getResult()
|
||||
@@ -35,4 +35,36 @@ describe('tag mutator', () => {
|
||||
|
||||
expect(removeResult.content.references).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('preferences should be undefined if previously undefined', () => {
|
||||
const tag = createTagWithTitle()
|
||||
const mutator = new TagMutator(tag, MutationType.UpdateUserTimestamps)
|
||||
const result = mutator.getResult()
|
||||
|
||||
expect(result.content.preferences).toBeFalsy()
|
||||
})
|
||||
|
||||
it('preferences should be lazy-created if attempting to set a property', () => {
|
||||
const tag = createTagWithTitle()
|
||||
const mutator = new TagMutator(tag, 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 tag = createTagWithContent({
|
||||
title: 'foo',
|
||||
preferences: {
|
||||
sortBy: 'content_type',
|
||||
},
|
||||
})
|
||||
|
||||
const mutator = new TagMutator(tag, MutationType.UpdateUserTimestamps)
|
||||
mutator.preferences = undefined
|
||||
const result = mutator.getResult()
|
||||
|
||||
expect(result.content.preferences).toBeFalsy()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -8,8 +8,18 @@ import { TagToParentTagReference } from '../../Abstract/Reference/TagToParentTag
|
||||
import { ContentReferenceType } from '../../Abstract/Reference/ContenteReferenceType'
|
||||
import { DecryptedItemMutator } from '../../Abstract/Item/Mutator/DecryptedItemMutator'
|
||||
import { TagToFileReference } from '../../Abstract/Reference/TagToFileReference'
|
||||
import { TagPreferences } from './TagPreferences'
|
||||
import { DecryptedItemInterface, MutationType } from '../../Abstract/Item'
|
||||
|
||||
export class TagMutator extends DecryptedItemMutator<TagContent> {
|
||||
private mutablePreferences?: TagPreferences
|
||||
|
||||
constructor(item: DecryptedItemInterface<TagContent>, type: MutationType) {
|
||||
super(item, type)
|
||||
|
||||
this.mutablePreferences = this.mutableContent.preferences
|
||||
}
|
||||
|
||||
set title(title: string) {
|
||||
this.mutableContent.title = title
|
||||
}
|
||||
@@ -22,6 +32,20 @@ export class TagMutator extends DecryptedItemMutator<TagContent> {
|
||||
this.mutableContent.iconString = iconString
|
||||
}
|
||||
|
||||
get preferences(): TagPreferences {
|
||||
if (!this.mutablePreferences) {
|
||||
this.mutableContent.preferences = {}
|
||||
this.mutablePreferences = this.mutableContent.preferences
|
||||
}
|
||||
|
||||
return this.mutablePreferences
|
||||
}
|
||||
|
||||
set preferences(preferences: TagPreferences | undefined) {
|
||||
this.mutablePreferences = preferences
|
||||
this.mutableContent.preferences = this.mutablePreferences
|
||||
}
|
||||
|
||||
public makeChildOf(tag: SNTag): void {
|
||||
const references = this.immutableItem.references.filter((ref) => !isTagToParentTagReference(ref))
|
||||
|
||||
|
||||
19
packages/models/src/Domain/Syncable/Tag/TagPreferences.ts
Normal file
19
packages/models/src/Domain/Syncable/Tag/TagPreferences.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { FeatureIdentifier } from '@standardnotes/features'
|
||||
import { NewNoteTitleFormat } from '../UserPrefs'
|
||||
import { CollectionSortProperty } from './../../Runtime/Collection/CollectionSort'
|
||||
|
||||
export interface TagPreferences {
|
||||
sortBy?: CollectionSortProperty
|
||||
sortReverse?: boolean
|
||||
showArchived?: boolean
|
||||
showTrashed?: boolean
|
||||
hideProtected?: boolean
|
||||
hidePinned?: boolean
|
||||
hideNotePreview?: boolean
|
||||
hideDate?: boolean
|
||||
hideTags?: boolean
|
||||
hideEditorIcon?: boolean
|
||||
newNoteTitleFormat?: NewNoteTitleFormat
|
||||
customNoteTitleFormat?: string
|
||||
editorIdentifier?: FeatureIdentifier | string
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from './Tag'
|
||||
export * from './TagMutator'
|
||||
export * from './TagContent'
|
||||
export * from './TagPreferences'
|
||||
|
||||
@@ -41,7 +41,21 @@ export const createNoteWithContent = (content: Partial<NoteContent>, createdAt?:
|
||||
)
|
||||
}
|
||||
|
||||
export const createTag = (title = 'photos') => {
|
||||
export const createTagWithContent = (content: Partial<TagContent>): SNTag => {
|
||||
return new SNTag(
|
||||
new DecryptedPayload(
|
||||
{
|
||||
uuid: mockUuid(),
|
||||
content_type: ContentType.Tag,
|
||||
content: FillItemContent<TagContent>(content),
|
||||
...PayloadTimestampDefaults(),
|
||||
},
|
||||
PayloadSource.Constructor,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
export const createTagWithTitle = (title = 'photos') => {
|
||||
return new SNTag(
|
||||
new DecryptedPayload(
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user