feat: per-tag display preferences (#1868)

This commit is contained in:
Mo
2022-10-25 07:27:26 -05:00
committed by GitHub
parent 9248d0ff16
commit ee7f11c933
32 changed files with 783 additions and 413 deletions

View File

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

View File

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

View File

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

View File

@@ -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[] {

View File

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

View File

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

View File

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

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

View File

@@ -1,3 +1,4 @@
export * from './Tag'
export * from './TagMutator'
export * from './TagContent'
export * from './TagPreferences'

View File

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