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(
|
||||
{
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Uuid } from '@standardnotes/common'
|
||||
import { ComponentArea } from '@standardnotes/features'
|
||||
import { ComponentArea, FeatureIdentifier } from '@standardnotes/features'
|
||||
import { ActionObserver, PermissionDialog, SNComponent, SNNote } from '@standardnotes/models'
|
||||
|
||||
import { DesktopManagerInterface } from '../Device/DesktopManagerInterface'
|
||||
@@ -21,4 +21,5 @@ export interface ComponentManagerInterface {
|
||||
): ComponentViewerInterface
|
||||
presentPermissionsDialog(_dialog: PermissionDialog): void
|
||||
getDefaultEditor(): SNComponent | undefined
|
||||
componentWithIdentifier(identifier: FeatureIdentifier | string): SNComponent | undefined
|
||||
}
|
||||
|
||||
@@ -35,7 +35,8 @@ export class NoteViewController implements ItemViewControllerInterface {
|
||||
public isTemplateNote = false
|
||||
private saveTimeout?: ReturnType<typeof setTimeout>
|
||||
private defaultTitle: string | undefined
|
||||
private defaultTag: UuidString | undefined
|
||||
private defaultTagUuid: UuidString | undefined
|
||||
private defaultTag?: SNTag
|
||||
public runtimeId = `${Math.random()}`
|
||||
|
||||
constructor(
|
||||
@@ -49,7 +50,11 @@ export class NoteViewController implements ItemViewControllerInterface {
|
||||
|
||||
if (templateNoteOptions) {
|
||||
this.defaultTitle = templateNoteOptions.title
|
||||
this.defaultTag = templateNoteOptions.tag
|
||||
this.defaultTagUuid = templateNoteOptions.tag
|
||||
}
|
||||
|
||||
if (this.defaultTagUuid) {
|
||||
this.defaultTag = this.application.items.findItem(this.defaultTagUuid) as SNTag
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,20 +72,27 @@ export class NoteViewController implements ItemViewControllerInterface {
|
||||
|
||||
async initialize(addTagHierarchy: boolean): Promise<void> {
|
||||
if (!this.item) {
|
||||
const editor = this.application.componentManager.getDefaultEditor()
|
||||
const editorIdentifier =
|
||||
this.defaultTag?.preferences?.editorIdentifier ||
|
||||
this.application.componentManager.getDefaultEditor()?.identifier
|
||||
|
||||
const defaultEditor = editorIdentifier
|
||||
? this.application.componentManager.componentWithIdentifier(editorIdentifier)
|
||||
: undefined
|
||||
|
||||
const note = this.application.mutator.createTemplateItem<NoteContent, SNNote>(ContentType.Note, {
|
||||
text: '',
|
||||
title: this.defaultTitle || '',
|
||||
noteType: editor?.noteType || NoteType.Plain,
|
||||
editorIdentifier: editor?.identifier,
|
||||
noteType: defaultEditor?.noteType || NoteType.Plain,
|
||||
editorIdentifier: editorIdentifier,
|
||||
references: [],
|
||||
})
|
||||
|
||||
this.isTemplateNote = true
|
||||
this.item = note
|
||||
|
||||
if (this.defaultTag) {
|
||||
const tag = this.application.items.findItem(this.defaultTag) as SNTag
|
||||
if (this.defaultTagUuid) {
|
||||
const tag = this.application.items.findItem(this.defaultTagUuid) as SNTag
|
||||
await this.application.items.addTagToNote(note, tag, addTagHierarchy)
|
||||
}
|
||||
|
||||
|
||||
@@ -318,4 +318,16 @@ export class WebApplication extends SNApplication implements WebApplicationInter
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
entitledToPerTagPreferences(): boolean {
|
||||
return this.hasValidSubscription()
|
||||
}
|
||||
|
||||
hasValidSubscription(): boolean {
|
||||
return this.getViewControllerManager().subscriptionController.hasValidSubscription()
|
||||
}
|
||||
|
||||
openPurchaseFlow(): void {
|
||||
this.getViewControllerManager().purchaseFlowController.openPurchaseFlow()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -260,15 +260,18 @@ const ContentListView: FunctionComponent<Props> = ({
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<ContentListHeader
|
||||
application={application}
|
||||
panelTitle={panelTitle}
|
||||
icon={icon}
|
||||
addButtonLabel={addButtonLabel}
|
||||
addNewItem={addNewItem}
|
||||
isFilesSmartView={isFilesSmartView}
|
||||
optionsSubtitle={optionsSubtitle}
|
||||
/>
|
||||
{selectedTag && (
|
||||
<ContentListHeader
|
||||
application={application}
|
||||
panelTitle={panelTitle}
|
||||
icon={icon}
|
||||
addButtonLabel={addButtonLabel}
|
||||
addNewItem={addNewItem}
|
||||
isFilesSmartView={isFilesSmartView}
|
||||
optionsSubtitle={optionsSubtitle}
|
||||
selectedTag={selectedTag}
|
||||
/>
|
||||
)}
|
||||
<SearchBar itemListController={itemListController} searchOptionsController={searchOptionsController} />
|
||||
<NoAccountWarning
|
||||
accountMenuController={accountMenuController}
|
||||
|
||||
@@ -7,19 +7,17 @@ import DisplayOptionsMenu from './DisplayOptionsMenu'
|
||||
import { NavigationMenuButton } from '@/Components/NavigationMenu/NavigationMenu'
|
||||
import { IconType } from '@standardnotes/snjs'
|
||||
import RoundIconButton from '@/Components/Button/RoundIconButton'
|
||||
import { AnyTag } from '@/Controllers/Navigation/AnyTagType'
|
||||
|
||||
type Props = {
|
||||
application: {
|
||||
getPreference: WebApplication['getPreference']
|
||||
setPreference: WebApplication['setPreference']
|
||||
isNativeMobileWeb: WebApplication['isNativeMobileWeb']
|
||||
}
|
||||
application: WebApplication
|
||||
panelTitle: string
|
||||
icon?: IconType | string
|
||||
addButtonLabel: string
|
||||
addNewItem: () => void
|
||||
isFilesSmartView: boolean
|
||||
optionsSubtitle?: string
|
||||
selectedTag: AnyTag
|
||||
}
|
||||
|
||||
const ContentListHeader = ({
|
||||
@@ -30,6 +28,7 @@ const ContentListHeader = ({
|
||||
addNewItem,
|
||||
isFilesSmartView,
|
||||
optionsSubtitle,
|
||||
selectedTag,
|
||||
}: Props) => {
|
||||
const displayOptionsContainerRef = useRef<HTMLDivElement>(null)
|
||||
const displayOptionsButtonRef = useRef<HTMLButtonElement>(null)
|
||||
@@ -79,6 +78,7 @@ const ContentListHeader = ({
|
||||
closeDisplayOptionsMenu={toggleDisplayOptionsMenu}
|
||||
isFilesSmartView={isFilesSmartView}
|
||||
isOpen={showDisplayOptionsMenu}
|
||||
selectedTag={selectedTag}
|
||||
/>
|
||||
</Popover>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,16 @@
|
||||
import { CollectionSort, CollectionSortProperty, PrefKey } from '@standardnotes/snjs'
|
||||
import {
|
||||
CollectionSort,
|
||||
CollectionSortProperty,
|
||||
IconType,
|
||||
isSmartView,
|
||||
isSystemView,
|
||||
PrefKey,
|
||||
TagMutator,
|
||||
TagPreferences,
|
||||
VectorIconNameOrEmoji,
|
||||
} from '@standardnotes/snjs'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { FunctionComponent, useCallback, useState } from 'react'
|
||||
import { FunctionComponent, useCallback, useEffect, useState } from 'react'
|
||||
import Icon from '@/Components/Icon/Icon'
|
||||
import Menu from '@/Components/Menu/Menu'
|
||||
import MenuItem from '@/Components/Menu/MenuItem'
|
||||
@@ -8,59 +18,104 @@ import MenuItemSeparator from '@/Components/Menu/MenuItemSeparator'
|
||||
import { MenuItemType } from '@/Components/Menu/MenuItemType'
|
||||
import { DisplayOptionsMenuProps } from './DisplayOptionsMenuProps'
|
||||
import { PrefDefaults } from '@/Constants/PrefDefaults'
|
||||
import NewNotePreferences from './NewNotePreferences'
|
||||
import { PreferenceMode } from './PreferenceMode'
|
||||
import { PremiumFeatureIconClass, PremiumFeatureIconName } from '@/Components/Icon/PremiumFeatureIcon'
|
||||
import Button from '@/Components/Button/Button'
|
||||
import { classNames } from '@/Utils/ConcatenateClassNames'
|
||||
|
||||
const DisplayOptionsMenu: FunctionComponent<DisplayOptionsMenuProps> = ({
|
||||
closeDisplayOptionsMenu,
|
||||
application,
|
||||
isOpen,
|
||||
isFilesSmartView,
|
||||
selectedTag,
|
||||
}) => {
|
||||
const [sortBy, setSortBy] = useState(() =>
|
||||
application.getPreference(PrefKey.SortNotesBy, PrefDefaults[PrefKey.SortNotesBy]),
|
||||
)
|
||||
const [sortReverse, setSortReverse] = useState(() =>
|
||||
application.getPreference(PrefKey.SortNotesReverse, PrefDefaults[PrefKey.SortNotesReverse]),
|
||||
)
|
||||
const [hidePreview, setHidePreview] = useState(() =>
|
||||
application.getPreference(PrefKey.NotesHideNotePreview, PrefDefaults[PrefKey.NotesHideNotePreview]),
|
||||
)
|
||||
const [hideDate, setHideDate] = useState(() =>
|
||||
application.getPreference(PrefKey.NotesHideDate, PrefDefaults[PrefKey.NotesHideDate]),
|
||||
)
|
||||
const [hideTags, setHideTags] = useState(() =>
|
||||
application.getPreference(PrefKey.NotesHideTags, PrefDefaults[PrefKey.NotesHideTags]),
|
||||
)
|
||||
const [hidePinned, setHidePinned] = useState(() =>
|
||||
application.getPreference(PrefKey.NotesHidePinned, PrefDefaults[PrefKey.NotesHidePinned]),
|
||||
)
|
||||
const [showArchived, setShowArchived] = useState(() =>
|
||||
application.getPreference(PrefKey.NotesShowArchived, PrefDefaults[PrefKey.NotesShowArchived]),
|
||||
)
|
||||
const [showTrashed, setShowTrashed] = useState(() =>
|
||||
application.getPreference(PrefKey.NotesShowTrashed, PrefDefaults[PrefKey.NotesShowTrashed]),
|
||||
)
|
||||
const [hideProtected, setHideProtected] = useState(() =>
|
||||
application.getPreference(PrefKey.NotesHideProtected, PrefDefaults[PrefKey.NotesHideProtected]),
|
||||
)
|
||||
const [hideEditorIcon, setHideEditorIcon] = useState(() =>
|
||||
application.getPreference(PrefKey.NotesHideEditorIcon, PrefDefaults[PrefKey.NotesHideEditorIcon]),
|
||||
const isSystemTag = isSmartView(selectedTag) && isSystemView(selectedTag)
|
||||
const [currentMode, setCurrentMode] = useState<PreferenceMode>(selectedTag.preferences ? 'tag' : 'global')
|
||||
const [preferences, setPreferences] = useState<TagPreferences>({})
|
||||
const hasSubscription = application.hasValidSubscription()
|
||||
const controlsDisabled = currentMode === 'tag' && !hasSubscription
|
||||
|
||||
const reloadPreferences = useCallback(() => {
|
||||
const globalValues: TagPreferences = {
|
||||
sortBy: application.getPreference(PrefKey.SortNotesBy, PrefDefaults[PrefKey.SortNotesBy]),
|
||||
sortReverse: application.getPreference(PrefKey.SortNotesReverse, PrefDefaults[PrefKey.SortNotesReverse]),
|
||||
showArchived: application.getPreference(PrefKey.NotesShowArchived, PrefDefaults[PrefKey.NotesShowArchived]),
|
||||
showTrashed: application.getPreference(PrefKey.NotesShowTrashed, PrefDefaults[PrefKey.NotesShowTrashed]),
|
||||
hideProtected: application.getPreference(PrefKey.NotesHideProtected, PrefDefaults[PrefKey.NotesHideProtected]),
|
||||
hidePinned: application.getPreference(PrefKey.NotesHidePinned, PrefDefaults[PrefKey.NotesHidePinned]),
|
||||
hideNotePreview: application.getPreference(
|
||||
PrefKey.NotesHideNotePreview,
|
||||
PrefDefaults[PrefKey.NotesHideNotePreview],
|
||||
),
|
||||
hideDate: application.getPreference(PrefKey.NotesHideDate, PrefDefaults[PrefKey.NotesHideDate]),
|
||||
hideTags: application.getPreference(PrefKey.NotesHideTags, PrefDefaults[PrefKey.NotesHideTags]),
|
||||
hideEditorIcon: application.getPreference(PrefKey.NotesHideEditorIcon, PrefDefaults[PrefKey.NotesHideEditorIcon]),
|
||||
newNoteTitleFormat: application.getPreference(
|
||||
PrefKey.NewNoteTitleFormat,
|
||||
PrefDefaults[PrefKey.NewNoteTitleFormat],
|
||||
),
|
||||
customNoteTitleFormat: application.getPreference(
|
||||
PrefKey.CustomNoteTitleFormat,
|
||||
PrefDefaults[PrefKey.CustomNoteTitleFormat],
|
||||
),
|
||||
}
|
||||
|
||||
if (currentMode === 'global') {
|
||||
setPreferences(globalValues)
|
||||
} else {
|
||||
setPreferences({
|
||||
...globalValues,
|
||||
...selectedTag.preferences,
|
||||
})
|
||||
}
|
||||
}, [currentMode, setPreferences, selectedTag, application])
|
||||
|
||||
useEffect(() => {
|
||||
reloadPreferences()
|
||||
}, [reloadPreferences])
|
||||
|
||||
const changePreferences = useCallback(
|
||||
async (properties: Partial<TagPreferences>) => {
|
||||
if (currentMode === 'global') {
|
||||
for (const key of Object.keys(properties)) {
|
||||
const value = properties[key as keyof TagPreferences]
|
||||
await application.setPreference(key as PrefKey, value).catch(console.error)
|
||||
|
||||
reloadPreferences()
|
||||
}
|
||||
} else {
|
||||
await application.mutator.changeAndSaveItem<TagMutator>(selectedTag, (mutator) => {
|
||||
mutator.preferences = {
|
||||
...mutator.preferences,
|
||||
...properties,
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
[reloadPreferences, application, currentMode, selectedTag],
|
||||
)
|
||||
|
||||
const resetTagPreferences = useCallback(() => {
|
||||
application.mutator.changeAndSaveItem<TagMutator>(selectedTag, (mutator) => {
|
||||
mutator.preferences = undefined
|
||||
})
|
||||
}, [application, selectedTag])
|
||||
|
||||
const toggleSortReverse = useCallback(() => {
|
||||
application.setPreference(PrefKey.SortNotesReverse, !sortReverse).catch(console.error)
|
||||
setSortReverse(!sortReverse)
|
||||
}, [application, sortReverse])
|
||||
void changePreferences({ sortReverse: !preferences.sortReverse })
|
||||
}, [preferences, changePreferences])
|
||||
|
||||
const toggleSortBy = useCallback(
|
||||
(sort: CollectionSortProperty) => {
|
||||
if (sortBy === sort) {
|
||||
if (preferences.sortBy === sort) {
|
||||
toggleSortReverse()
|
||||
} else {
|
||||
setSortBy(sort)
|
||||
application.setPreference(PrefKey.SortNotesBy, sort).catch(console.error)
|
||||
void changePreferences({ sortBy: sort })
|
||||
}
|
||||
},
|
||||
[application, sortBy, toggleSortReverse],
|
||||
[preferences, changePreferences, toggleSortReverse],
|
||||
)
|
||||
|
||||
const toggleSortByDateModified = useCallback(() => {
|
||||
@@ -76,58 +131,115 @@ const DisplayOptionsMenu: FunctionComponent<DisplayOptionsMenuProps> = ({
|
||||
}, [toggleSortBy])
|
||||
|
||||
const toggleHidePreview = useCallback(() => {
|
||||
setHidePreview(!hidePreview)
|
||||
application.setPreference(PrefKey.NotesHideNotePreview, !hidePreview).catch(console.error)
|
||||
}, [application, hidePreview])
|
||||
void changePreferences({ hideNotePreview: !preferences.hideNotePreview })
|
||||
}, [preferences, changePreferences])
|
||||
|
||||
const toggleHideDate = useCallback(() => {
|
||||
setHideDate(!hideDate)
|
||||
application.setPreference(PrefKey.NotesHideDate, !hideDate).catch(console.error)
|
||||
}, [application, hideDate])
|
||||
void changePreferences({ hideDate: !preferences.hideDate })
|
||||
}, [preferences, changePreferences])
|
||||
|
||||
const toggleHideTags = useCallback(() => {
|
||||
setHideTags(!hideTags)
|
||||
application.setPreference(PrefKey.NotesHideTags, !hideTags).catch(console.error)
|
||||
}, [application, hideTags])
|
||||
void changePreferences({ hideTags: !preferences.hideTags })
|
||||
}, [preferences, changePreferences])
|
||||
|
||||
const toggleHidePinned = useCallback(() => {
|
||||
setHidePinned(!hidePinned)
|
||||
application.setPreference(PrefKey.NotesHidePinned, !hidePinned).catch(console.error)
|
||||
}, [application, hidePinned])
|
||||
void changePreferences({ hidePinned: !preferences.hidePinned })
|
||||
}, [preferences, changePreferences])
|
||||
|
||||
const toggleShowArchived = useCallback(() => {
|
||||
setShowArchived(!showArchived)
|
||||
application.setPreference(PrefKey.NotesShowArchived, !showArchived).catch(console.error)
|
||||
}, [application, showArchived])
|
||||
void changePreferences({ showArchived: !preferences.showArchived })
|
||||
}, [preferences, changePreferences])
|
||||
|
||||
const toggleShowTrashed = useCallback(() => {
|
||||
setShowTrashed(!showTrashed)
|
||||
application.setPreference(PrefKey.NotesShowTrashed, !showTrashed).catch(console.error)
|
||||
}, [application, showTrashed])
|
||||
void changePreferences({ showTrashed: !preferences.showTrashed })
|
||||
}, [preferences, changePreferences])
|
||||
|
||||
const toggleHideProtected = useCallback(() => {
|
||||
setHideProtected(!hideProtected)
|
||||
application.setPreference(PrefKey.NotesHideProtected, !hideProtected).catch(console.error)
|
||||
}, [application, hideProtected])
|
||||
void changePreferences({ hideProtected: !preferences.hideProtected })
|
||||
}, [preferences, changePreferences])
|
||||
|
||||
const toggleEditorIcon = useCallback(() => {
|
||||
setHideEditorIcon(!hideEditorIcon)
|
||||
application.setPreference(PrefKey.NotesHideEditorIcon, !hideEditorIcon).catch(console.error)
|
||||
}, [application, hideEditorIcon])
|
||||
void changePreferences({ hideEditorIcon: !preferences.hideEditorIcon })
|
||||
}, [preferences, changePreferences])
|
||||
|
||||
const TabButton: FunctionComponent<{
|
||||
label: string
|
||||
mode: PreferenceMode
|
||||
icon?: VectorIconNameOrEmoji
|
||||
}> = ({ mode, label, icon }) => {
|
||||
const isSelected = currentMode === mode
|
||||
|
||||
return (
|
||||
<button
|
||||
className={classNames(
|
||||
'relative cursor-pointer rounded-full border-2 border-solid border-transparent px-2 text-sm focus:shadow-none',
|
||||
isSelected ? 'bg-info text-info-contrast' : 'bg-transparent text-text hover:bg-info-backdrop',
|
||||
)}
|
||||
onClick={() => {
|
||||
setCurrentMode(mode)
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center justify-center">
|
||||
{icon && (
|
||||
<Icon
|
||||
size="small"
|
||||
type={icon as IconType}
|
||||
className={classNames('mr-1 cursor-pointer', isSelected ? 'text-info-contrast' : 'text-neutral')}
|
||||
/>
|
||||
)}
|
||||
<div>{label}</div>
|
||||
</div>
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
const NoSubscriptionBanner = () => (
|
||||
<div className="m-2 mt-2 mb-3 grid grid-cols-1 rounded-md border border-border p-4">
|
||||
<div className="flex items-center">
|
||||
<Icon className={classNames('mr-1 -ml-1 h-5 w-5', PremiumFeatureIconClass)} type={PremiumFeatureIconName} />
|
||||
<h1 className="sk-h3 m-0 text-sm font-semibold">Upgrade for per-tag preferences</h1>
|
||||
</div>
|
||||
<p className="col-start-1 col-end-3 m-0 mt-1 text-sm">
|
||||
Create powerful workflows and organizational layouts with per-tag display preferences.
|
||||
</p>
|
||||
<Button
|
||||
primary
|
||||
small
|
||||
className="col-start-1 col-end-3 mt-3 justify-self-start uppercase"
|
||||
onClick={() => application.openPurchaseFlow()}
|
||||
>
|
||||
Upgrade Features
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
|
||||
return (
|
||||
<Menu className="text-sm" a11yLabel="Notes list options menu" closeMenu={closeDisplayOptionsMenu} isOpen={isOpen}>
|
||||
<div className="my-1 px-3 text-xs font-semibold uppercase text-text">Preferences for</div>
|
||||
<div className={classNames('mt-1.5 flex w-full justify-between px-3', !controlsDisabled && 'mb-3')}>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<TabButton label="Global" mode="global" />
|
||||
{!isSystemTag && <TabButton label={selectedTag.title} icon={selectedTag.iconString} mode="tag" />}
|
||||
</div>
|
||||
{currentMode === 'tag' && <button onClick={resetTagPreferences}>Reset</button>}
|
||||
</div>
|
||||
|
||||
{controlsDisabled && <NoSubscriptionBanner />}
|
||||
|
||||
<MenuItemSeparator />
|
||||
|
||||
<div className="my-1 px-3 text-xs font-semibold uppercase text-text">Sort by</div>
|
||||
<MenuItem
|
||||
disabled={controlsDisabled}
|
||||
className="py-2"
|
||||
type={MenuItemType.RadioButton}
|
||||
onClick={toggleSortByDateModified}
|
||||
checked={sortBy === CollectionSort.UpdatedAt}
|
||||
checked={preferences.sortBy === CollectionSort.UpdatedAt}
|
||||
>
|
||||
<div className="ml-2 flex flex-grow items-center justify-between">
|
||||
<span>Date modified</span>
|
||||
{sortBy === CollectionSort.UpdatedAt ? (
|
||||
sortReverse ? (
|
||||
{preferences.sortBy === CollectionSort.UpdatedAt ? (
|
||||
preferences.sortReverse ? (
|
||||
<Icon type="arrows-sort-up" className="text-neutral" />
|
||||
) : (
|
||||
<Icon type="arrows-sort-down" className="text-neutral" />
|
||||
@@ -136,15 +248,16 @@ const DisplayOptionsMenu: FunctionComponent<DisplayOptionsMenuProps> = ({
|
||||
</div>
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
disabled={controlsDisabled}
|
||||
className="py-2"
|
||||
type={MenuItemType.RadioButton}
|
||||
onClick={toggleSortByCreationDate}
|
||||
checked={sortBy === CollectionSort.CreatedAt}
|
||||
checked={preferences.sortBy === CollectionSort.CreatedAt}
|
||||
>
|
||||
<div className="ml-2 flex flex-grow items-center justify-between">
|
||||
<span>Creation date</span>
|
||||
{sortBy === CollectionSort.CreatedAt ? (
|
||||
sortReverse ? (
|
||||
{preferences.sortBy === CollectionSort.CreatedAt ? (
|
||||
preferences.sortReverse ? (
|
||||
<Icon type="arrows-sort-up" className="text-neutral" />
|
||||
) : (
|
||||
<Icon type="arrows-sort-down" className="text-neutral" />
|
||||
@@ -153,15 +266,16 @@ const DisplayOptionsMenu: FunctionComponent<DisplayOptionsMenuProps> = ({
|
||||
</div>
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
disabled={controlsDisabled}
|
||||
className="py-2"
|
||||
type={MenuItemType.RadioButton}
|
||||
onClick={toggleSortByTitle}
|
||||
checked={sortBy === CollectionSort.Title}
|
||||
checked={preferences.sortBy === CollectionSort.Title}
|
||||
>
|
||||
<div className="ml-2 flex flex-grow items-center justify-between">
|
||||
<span>Title</span>
|
||||
{sortBy === CollectionSort.Title ? (
|
||||
sortReverse ? (
|
||||
{preferences.sortBy === CollectionSort.Title ? (
|
||||
preferences.sortReverse ? (
|
||||
<Icon type="arrows-sort-up" className="text-neutral" />
|
||||
) : (
|
||||
<Icon type="arrows-sort-down" className="text-neutral" />
|
||||
@@ -173,34 +287,38 @@ const DisplayOptionsMenu: FunctionComponent<DisplayOptionsMenuProps> = ({
|
||||
<div className="px-3 py-1 text-xs font-semibold uppercase text-text">View</div>
|
||||
{!isFilesSmartView && (
|
||||
<MenuItem
|
||||
disabled={controlsDisabled}
|
||||
type={MenuItemType.SwitchButton}
|
||||
className="py-1 hover:bg-contrast focus:bg-info-backdrop"
|
||||
checked={!hidePreview}
|
||||
checked={!preferences.hideNotePreview}
|
||||
onChange={toggleHidePreview}
|
||||
>
|
||||
<div className="max-w-3/4 flex flex-col">Show note preview</div>
|
||||
</MenuItem>
|
||||
)}
|
||||
<MenuItem
|
||||
disabled={controlsDisabled}
|
||||
type={MenuItemType.SwitchButton}
|
||||
className="py-1 hover:bg-contrast focus:bg-info-backdrop"
|
||||
checked={!hideDate}
|
||||
checked={!preferences.hideDate}
|
||||
onChange={toggleHideDate}
|
||||
>
|
||||
Show date
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
disabled={controlsDisabled}
|
||||
type={MenuItemType.SwitchButton}
|
||||
className="py-1 hover:bg-contrast focus:bg-info-backdrop"
|
||||
checked={!hideTags}
|
||||
checked={!preferences.hideTags}
|
||||
onChange={toggleHideTags}
|
||||
>
|
||||
Show tags
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
disabled={controlsDisabled}
|
||||
type={MenuItemType.SwitchButton}
|
||||
className="py-1 hover:bg-contrast focus:bg-info-backdrop"
|
||||
checked={!hideEditorIcon}
|
||||
checked={!preferences.hideEditorIcon}
|
||||
onChange={toggleEditorIcon}
|
||||
>
|
||||
Show icon
|
||||
@@ -208,37 +326,51 @@ const DisplayOptionsMenu: FunctionComponent<DisplayOptionsMenuProps> = ({
|
||||
<MenuItemSeparator />
|
||||
<div className="px-3 py-1 text-xs font-semibold uppercase text-text">Other</div>
|
||||
<MenuItem
|
||||
disabled={controlsDisabled}
|
||||
type={MenuItemType.SwitchButton}
|
||||
className="py-1 hover:bg-contrast focus:bg-info-backdrop"
|
||||
checked={!hidePinned}
|
||||
checked={!preferences.hidePinned}
|
||||
onChange={toggleHidePinned}
|
||||
>
|
||||
Show pinned
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
disabled={controlsDisabled}
|
||||
type={MenuItemType.SwitchButton}
|
||||
className="py-1 hover:bg-contrast focus:bg-info-backdrop"
|
||||
checked={!hideProtected}
|
||||
checked={!preferences.hideProtected}
|
||||
onChange={toggleHideProtected}
|
||||
>
|
||||
Show protected
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
disabled={controlsDisabled}
|
||||
type={MenuItemType.SwitchButton}
|
||||
className="py-1 hover:bg-contrast focus:bg-info-backdrop"
|
||||
checked={showArchived}
|
||||
checked={preferences.showArchived}
|
||||
onChange={toggleShowArchived}
|
||||
>
|
||||
Show archived
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
disabled={controlsDisabled}
|
||||
type={MenuItemType.SwitchButton}
|
||||
className="py-1 hover:bg-contrast focus:bg-info-backdrop"
|
||||
checked={showTrashed}
|
||||
checked={preferences.showTrashed}
|
||||
onChange={toggleShowTrashed}
|
||||
>
|
||||
Show trashed
|
||||
</MenuItem>
|
||||
|
||||
<MenuItemSeparator />
|
||||
|
||||
<NewNotePreferences
|
||||
disabled={controlsDisabled}
|
||||
application={application}
|
||||
selectedTag={selectedTag}
|
||||
mode={currentMode}
|
||||
changePreferencesCallback={changePreferences}
|
||||
/>
|
||||
</Menu>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { WebApplication } from '@/Application/Application'
|
||||
import { AnyTag } from '@/Controllers/Navigation/AnyTagType'
|
||||
|
||||
export type DisplayOptionsMenuPositionProps = {
|
||||
top: number
|
||||
@@ -6,10 +7,8 @@ export type DisplayOptionsMenuPositionProps = {
|
||||
}
|
||||
|
||||
export type DisplayOptionsMenuProps = {
|
||||
application: {
|
||||
getPreference: WebApplication['getPreference']
|
||||
setPreference: WebApplication['setPreference']
|
||||
}
|
||||
application: WebApplication
|
||||
selectedTag: AnyTag
|
||||
closeDisplayOptionsMenu: () => void
|
||||
isOpen: boolean
|
||||
isFilesSmartView: boolean
|
||||
|
||||
@@ -0,0 +1,278 @@
|
||||
import {
|
||||
ComponentArea,
|
||||
ComponentMutator,
|
||||
FeatureIdentifier,
|
||||
NewNoteTitleFormat,
|
||||
PrefKey,
|
||||
SNComponent,
|
||||
TagPreferences,
|
||||
} from '@standardnotes/snjs'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { ChangeEventHandler, FunctionComponent, useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { PrefDefaults } from '@/Constants/PrefDefaults'
|
||||
import Dropdown from '@/Components/Dropdown/Dropdown'
|
||||
import { DropdownItem } from '@/Components/Dropdown/DropdownItem'
|
||||
import { WebApplication } from '@/Application/Application'
|
||||
import { PLAIN_EDITOR_NAME } from '@/Constants/Constants'
|
||||
import { AnyTag } from '@/Controllers/Navigation/AnyTagType'
|
||||
import { PreferenceMode } from './PreferenceMode'
|
||||
import dayjs from 'dayjs'
|
||||
|
||||
const PlainEditorType = 'plain-editor'
|
||||
|
||||
type EditorOption = DropdownItem & {
|
||||
value: FeatureIdentifier | typeof PlainEditorType
|
||||
}
|
||||
|
||||
const PrefChangeDebounceTimeInMs = 25
|
||||
|
||||
const HelpPageUrl = 'https://day.js.org/docs/en/display/format#list-of-all-available-formats'
|
||||
|
||||
const NoteTitleFormatOptions = [
|
||||
{
|
||||
label: 'Current date and time',
|
||||
value: NewNoteTitleFormat.CurrentDateAndTime,
|
||||
},
|
||||
{
|
||||
label: 'Current note count',
|
||||
value: NewNoteTitleFormat.CurrentNoteCount,
|
||||
},
|
||||
{
|
||||
label: 'Custom format',
|
||||
value: NewNoteTitleFormat.CustomFormat,
|
||||
},
|
||||
{
|
||||
label: 'Empty',
|
||||
value: NewNoteTitleFormat.Empty,
|
||||
},
|
||||
]
|
||||
|
||||
type Props = {
|
||||
application: WebApplication
|
||||
selectedTag: AnyTag
|
||||
mode: PreferenceMode
|
||||
changePreferencesCallback: (properties: Partial<TagPreferences>) => Promise<void>
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
const NewNotePreferences: FunctionComponent<Props> = ({
|
||||
application,
|
||||
selectedTag,
|
||||
mode,
|
||||
changePreferencesCallback,
|
||||
disabled,
|
||||
}: Props) => {
|
||||
const [editorItems, setEditorItems] = useState<DropdownItem[]>([])
|
||||
const [defaultEditorIdentifier, setDefaultEditorIdentifier] = useState<string>(PlainEditorType)
|
||||
const [newNoteTitleFormat, setNewNoteTitleFormat] = useState<NewNoteTitleFormat>(
|
||||
NewNoteTitleFormat.CurrentDateAndTime,
|
||||
)
|
||||
const [customNoteTitleFormat, setCustomNoteTitleFormat] = useState('')
|
||||
|
||||
const getGlobalEditorDefault = useCallback((): SNComponent | undefined => {
|
||||
return application.componentManager.componentsForArea(ComponentArea.Editor).filter((e) => e.isDefaultEditor())[0]
|
||||
}, [application])
|
||||
|
||||
const reloadPreferences = useCallback(() => {
|
||||
if (mode === 'tag' && selectedTag.preferences?.editorIdentifier) {
|
||||
setDefaultEditorIdentifier(selectedTag.preferences?.editorIdentifier)
|
||||
} else {
|
||||
const globalDefault = getGlobalEditorDefault()
|
||||
setDefaultEditorIdentifier(globalDefault?.identifier || PlainEditorType)
|
||||
}
|
||||
|
||||
if (mode === 'tag' && selectedTag.preferences?.newNoteTitleFormat) {
|
||||
setNewNoteTitleFormat(selectedTag.preferences?.newNoteTitleFormat)
|
||||
} else {
|
||||
setNewNoteTitleFormat(
|
||||
application.getPreference(PrefKey.NewNoteTitleFormat, PrefDefaults[PrefKey.NewNoteTitleFormat]),
|
||||
)
|
||||
}
|
||||
}, [mode, selectedTag, application, getGlobalEditorDefault, setDefaultEditorIdentifier, setNewNoteTitleFormat])
|
||||
|
||||
useEffect(() => {
|
||||
if (mode === 'tag' && selectedTag.preferences?.customNoteTitleFormat) {
|
||||
setCustomNoteTitleFormat(selectedTag.preferences?.customNoteTitleFormat)
|
||||
} else {
|
||||
setCustomNoteTitleFormat(
|
||||
application.getPreference(PrefKey.CustomNoteTitleFormat, PrefDefaults[PrefKey.CustomNoteTitleFormat]),
|
||||
)
|
||||
}
|
||||
}, [application, mode, selectedTag])
|
||||
|
||||
useEffect(() => {
|
||||
void reloadPreferences()
|
||||
}, [reloadPreferences])
|
||||
|
||||
const setNewNoteTitleFormatChange = (value: string) => {
|
||||
setNewNoteTitleFormat(value as NewNoteTitleFormat)
|
||||
if (mode === 'global') {
|
||||
application.setPreference(PrefKey.NewNoteTitleFormat, value as NewNoteTitleFormat)
|
||||
} else {
|
||||
void changePreferencesCallback({ newNoteTitleFormat: value as NewNoteTitleFormat })
|
||||
}
|
||||
}
|
||||
|
||||
const removeEditorGlobalDefault = (application: WebApplication, component: SNComponent) => {
|
||||
application.mutator
|
||||
.changeAndSaveItem(component, (m) => {
|
||||
const mutator = m as ComponentMutator
|
||||
mutator.defaultEditor = false
|
||||
})
|
||||
.catch(console.error)
|
||||
}
|
||||
|
||||
const makeEditorGlobalDefault = (
|
||||
application: WebApplication,
|
||||
component: SNComponent,
|
||||
currentDefault?: SNComponent,
|
||||
) => {
|
||||
if (currentDefault) {
|
||||
removeEditorGlobalDefault(application, currentDefault)
|
||||
}
|
||||
application.mutator
|
||||
.changeAndSaveItem(component, (m) => {
|
||||
const mutator = m as ComponentMutator
|
||||
mutator.defaultEditor = true
|
||||
})
|
||||
.catch(console.error)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const editors = application.componentManager
|
||||
.componentsForArea(ComponentArea.Editor)
|
||||
.map((editor): EditorOption => {
|
||||
const identifier = editor.package_info.identifier
|
||||
const [iconType, tint] = application.iconsController.getIconAndTintForNoteType(editor.package_info.note_type)
|
||||
|
||||
return {
|
||||
label: editor.displayName,
|
||||
value: identifier,
|
||||
...(iconType ? { icon: iconType } : null),
|
||||
...(tint ? { iconClassName: `text-accessory-tint-${tint}` } : null),
|
||||
}
|
||||
})
|
||||
.concat([
|
||||
{
|
||||
icon: 'plain-text',
|
||||
iconClassName: 'text-accessory-tint-1',
|
||||
label: PLAIN_EDITOR_NAME,
|
||||
value: PlainEditorType,
|
||||
},
|
||||
])
|
||||
.sort((a, b) => {
|
||||
return a.label.toLowerCase() < b.label.toLowerCase() ? -1 : 1
|
||||
})
|
||||
|
||||
setEditorItems(editors)
|
||||
}, [application])
|
||||
|
||||
const setDefaultEditor = (value: string) => {
|
||||
setDefaultEditorIdentifier(value as FeatureIdentifier)
|
||||
|
||||
if (mode === 'global') {
|
||||
const editors = application.componentManager.componentsForArea(ComponentArea.Editor)
|
||||
const currentDefault = getGlobalEditorDefault()
|
||||
|
||||
if (value !== PlainEditorType) {
|
||||
const editorComponent = editors.filter((e) => e.package_info.identifier === value)[0]
|
||||
makeEditorGlobalDefault(application, editorComponent, currentDefault)
|
||||
} else if (currentDefault) {
|
||||
removeEditorGlobalDefault(application, currentDefault)
|
||||
}
|
||||
} else {
|
||||
void changePreferencesCallback({ editorIdentifier: value })
|
||||
}
|
||||
}
|
||||
|
||||
const debounceTimeoutRef = useRef<number>()
|
||||
|
||||
const handleCustomFormatInputChange: ChangeEventHandler<HTMLInputElement> = (event) => {
|
||||
const newFormat = event.currentTarget.value
|
||||
setCustomNoteTitleFormat(newFormat)
|
||||
|
||||
if (debounceTimeoutRef.current) {
|
||||
clearTimeout(debounceTimeoutRef.current)
|
||||
}
|
||||
|
||||
debounceTimeoutRef.current = window.setTimeout(async () => {
|
||||
if (mode === 'tag') {
|
||||
void changePreferencesCallback({ customNoteTitleFormat: newFormat })
|
||||
} else {
|
||||
application.setPreference(PrefKey.CustomNoteTitleFormat, newFormat)
|
||||
}
|
||||
}, PrefChangeDebounceTimeInMs)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="my-1 px-3 pb-2 pt-1">
|
||||
<div className="text-xs font-semibold uppercase text-text">New Note Defaults</div>
|
||||
<div>
|
||||
<div className="mt-3">Note Type</div>
|
||||
<div className="mt-2">
|
||||
<Dropdown
|
||||
portal={false}
|
||||
disabled={disabled}
|
||||
fullWidth={true}
|
||||
id="def-editor-dropdown"
|
||||
label="Select the default note type"
|
||||
items={editorItems}
|
||||
value={defaultEditorIdentifier}
|
||||
onChange={setDefaultEditor}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="mt-3">Title Format</div>
|
||||
<div className="mt-2">
|
||||
<Dropdown
|
||||
portal={false}
|
||||
disabled={disabled}
|
||||
fullWidth={true}
|
||||
id="def-new-note-title-format"
|
||||
label="Select the default note type"
|
||||
items={NoteTitleFormatOptions}
|
||||
value={newNoteTitleFormat}
|
||||
onChange={setNewNoteTitleFormatChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{newNoteTitleFormat === NewNoteTitleFormat.CustomFormat && (
|
||||
<div className="mt-2">
|
||||
<div className="mt-2">
|
||||
<input
|
||||
disabled={disabled}
|
||||
className="w-full min-w-55 rounded border border-solid border-passive-3 bg-default px-2 py-1.5 text-sm focus-within:ring-2 focus-within:ring-info"
|
||||
placeholder="e.g. YYYY-MM-DD"
|
||||
value={customNoteTitleFormat}
|
||||
onChange={handleCustomFormatInputChange}
|
||||
spellCheck={false}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-2">
|
||||
<span className="font-bold">Preview:</span> {dayjs().format(customNoteTitleFormat)}
|
||||
</div>
|
||||
<div className="mt-1">
|
||||
<a
|
||||
className="underline"
|
||||
href={HelpPageUrl}
|
||||
rel="noreferrer"
|
||||
target="_blank"
|
||||
onClick={(event) => {
|
||||
if (application.isNativeMobileWeb()) {
|
||||
event.preventDefault()
|
||||
application.mobileDevice().openUrl(HelpPageUrl)
|
||||
}
|
||||
}}
|
||||
>
|
||||
Options
|
||||
</a>
|
||||
. Use <code>[]</code> to escape date-time formatting.
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default observer(NewNotePreferences)
|
||||
@@ -0,0 +1 @@
|
||||
export type PreferenceMode = 'global' | 'tag'
|
||||
@@ -15,6 +15,8 @@ type DropdownProps = {
|
||||
onChange: (value: string, item: DropdownItem) => void
|
||||
disabled?: boolean
|
||||
className?: string
|
||||
fullWidth?: boolean
|
||||
portal?: boolean
|
||||
}
|
||||
|
||||
type ListboxButtonProps = DropdownItem & {
|
||||
@@ -42,7 +44,17 @@ const CustomDropdownButton: FunctionComponent<ListboxButtonProps> = ({
|
||||
</>
|
||||
)
|
||||
|
||||
const Dropdown: FunctionComponent<DropdownProps> = ({ id, label, items, value, onChange, disabled, className }) => {
|
||||
const Dropdown: FunctionComponent<DropdownProps> = ({
|
||||
id,
|
||||
label,
|
||||
items,
|
||||
value,
|
||||
onChange,
|
||||
disabled,
|
||||
className,
|
||||
fullWidth,
|
||||
portal = true,
|
||||
}) => {
|
||||
const labelId = `${id}-label`
|
||||
|
||||
const handleChange = (value: string) => {
|
||||
@@ -56,6 +68,7 @@ const Dropdown: FunctionComponent<DropdownProps> = ({ id, label, items, value, o
|
||||
<VisuallyHidden id={labelId}>{label}</VisuallyHidden>
|
||||
<ListboxInput value={value} onChange={handleChange} aria-labelledby={labelId} disabled={disabled}>
|
||||
<StyledListboxButton
|
||||
className={`w-full ${!fullWidth ? 'md:w-fit' : ''}`}
|
||||
children={({ value, label, isExpanded }) => {
|
||||
const current = items.find((item) => item.value === value)
|
||||
const icon = current ? current?.icon : null
|
||||
@@ -69,7 +82,7 @@ const Dropdown: FunctionComponent<DropdownProps> = ({ id, label, items, value, o
|
||||
})
|
||||
}}
|
||||
/>
|
||||
<ListboxPopover className="sn-dropdown sn-dropdown-popover">
|
||||
<ListboxPopover portal={portal} className="sn-dropdown sn-dropdown-popover">
|
||||
<div className="sn-component">
|
||||
<ListboxList>
|
||||
{items.map((item) => (
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import { ListboxButton } from '@reach/listbox'
|
||||
import styled from 'styled-components'
|
||||
|
||||
const StyledListboxButton = styled(ListboxButton).attrs(() => ({
|
||||
className: 'w-full md:w-fit',
|
||||
}))`
|
||||
const StyledListboxButton = styled(ListboxButton)`
|
||||
&[data-reach-listbox-button] {
|
||||
background-color: var(--sn-stylekit-background-color);
|
||||
border-radius: 0.25rem;
|
||||
|
||||
@@ -93,6 +93,7 @@ const IconPicker = ({ selectedValue, onIconChange, platform, className }: Props)
|
||||
<div className={'mt-2 h-full min-h-0 overflow-auto'}>
|
||||
{currentType === 'icon' && (
|
||||
<Dropdown
|
||||
fullWidth={true}
|
||||
id="change-tag-icon-dropdown"
|
||||
label="Change the icon for a tag"
|
||||
items={iconOptions}
|
||||
|
||||
@@ -19,6 +19,7 @@ type MenuItemProps = {
|
||||
icon?: IconType
|
||||
iconClassName?: string
|
||||
tabIndex?: number
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
const MenuItem = forwardRef(
|
||||
@@ -34,12 +35,14 @@ const MenuItem = forwardRef(
|
||||
icon,
|
||||
iconClassName,
|
||||
tabIndex,
|
||||
disabled,
|
||||
}: MenuItemProps,
|
||||
ref: Ref<HTMLButtonElement>,
|
||||
) => {
|
||||
return type === MenuItemType.SwitchButton && typeof onChange === 'function' ? (
|
||||
<li className="list-none" role="none">
|
||||
<button
|
||||
disabled={disabled}
|
||||
ref={ref}
|
||||
className={classNames(
|
||||
'flex w-full cursor-pointer items-center justify-between border-0 bg-transparent px-3 py-1.5',
|
||||
@@ -54,12 +57,13 @@ const MenuItem = forwardRef(
|
||||
aria-checked={checked}
|
||||
>
|
||||
<span className="flex flex-grow items-center">{children}</span>
|
||||
<Switch className="px-0" checked={checked} />
|
||||
<Switch disabled={disabled} className="px-0" checked={checked} />
|
||||
</button>
|
||||
</li>
|
||||
) : (
|
||||
<li className="list-none" role="none">
|
||||
<button
|
||||
disabled={disabled}
|
||||
ref={ref}
|
||||
role={type === MenuItemType.RadioButton ? 'menuitemradio' : 'menuitem'}
|
||||
tabIndex={typeof tabIndex === 'number' ? tabIndex : FOCUSABLE_BUT_NOT_TABBABLE}
|
||||
@@ -75,7 +79,7 @@ const MenuItem = forwardRef(
|
||||
>
|
||||
{type === MenuItemType.IconButton && icon ? <Icon type={icon} className={iconClassName} /> : null}
|
||||
{type === MenuItemType.RadioButton && typeof checked === 'boolean' ? (
|
||||
<RadioIndicator checked={checked} className="flex-shrink-0" />
|
||||
<RadioIndicator disabled={disabled} checked={checked} className="flex-shrink-0" />
|
||||
) : null}
|
||||
{children}
|
||||
</button>
|
||||
|
||||
@@ -29,7 +29,12 @@ const NoAccountWarningContent = ({ accountMenuController, noAccountWarningContro
|
||||
<p className="col-start-1 col-end-3 m-0 mt-1 text-sm">
|
||||
Sign in or register to sync your notes to your other devices with end-to-end encryption.
|
||||
</p>
|
||||
<Button primary small className="col-start-1 col-end-3 mt-3 justify-self-start" onClick={showAccountMenu}>
|
||||
<Button
|
||||
primary
|
||||
small
|
||||
className="col-start-1 col-end-3 mt-3 justify-self-start uppercase"
|
||||
onClick={showAccountMenu}
|
||||
>
|
||||
Open Account menu
|
||||
</Button>
|
||||
<button
|
||||
|
||||
@@ -1,58 +1,17 @@
|
||||
import Dropdown from '@/Components/Dropdown/Dropdown'
|
||||
import { DropdownItem } from '@/Components/Dropdown/DropdownItem'
|
||||
import {
|
||||
FeatureIdentifier,
|
||||
PrefKey,
|
||||
ComponentArea,
|
||||
ComponentMutator,
|
||||
SNComponent,
|
||||
NewNoteTitleFormat,
|
||||
Platform,
|
||||
} from '@standardnotes/snjs'
|
||||
import { PrefKey, Platform } from '@standardnotes/snjs'
|
||||
import { Subtitle, Text, Title } from '@/Components/Preferences/PreferencesComponents/Content'
|
||||
import { WebApplication } from '@/Application/Application'
|
||||
import { FunctionComponent, useEffect, useMemo, useState } from 'react'
|
||||
import { FunctionComponent, useState } from 'react'
|
||||
import HorizontalSeparator from '@/Components/Shared/HorizontalSeparator'
|
||||
import Switch from '@/Components/Switch/Switch'
|
||||
import { PLAIN_EDITOR_NAME } from '@/Constants/Constants'
|
||||
import PreferencesGroup from '../../PreferencesComponents/PreferencesGroup'
|
||||
import PreferencesSegment from '../../PreferencesComponents/PreferencesSegment'
|
||||
import CustomNoteTitleFormat from './Defaults/CustomNoteTitleFormat'
|
||||
import { PrefDefaults } from '@/Constants/PrefDefaults'
|
||||
|
||||
type Props = {
|
||||
application: WebApplication
|
||||
}
|
||||
|
||||
type EditorOption = DropdownItem & {
|
||||
value: FeatureIdentifier | 'plain-editor'
|
||||
}
|
||||
|
||||
const makeEditorDefault = (application: WebApplication, component: SNComponent, currentDefault: SNComponent) => {
|
||||
if (currentDefault) {
|
||||
removeEditorDefault(application, currentDefault)
|
||||
}
|
||||
application.mutator
|
||||
.changeAndSaveItem(component, (m) => {
|
||||
const mutator = m as ComponentMutator
|
||||
mutator.defaultEditor = true
|
||||
})
|
||||
.catch(console.error)
|
||||
}
|
||||
|
||||
const removeEditorDefault = (application: WebApplication, component: SNComponent) => {
|
||||
application.mutator
|
||||
.changeAndSaveItem(component, (m) => {
|
||||
const mutator = m as ComponentMutator
|
||||
mutator.defaultEditor = false
|
||||
})
|
||||
.catch(console.error)
|
||||
}
|
||||
|
||||
const getDefaultEditor = (application: WebApplication) => {
|
||||
return application.componentManager.componentsForArea(ComponentArea.Editor).filter((e) => e.isDefaultEditor())[0]
|
||||
}
|
||||
|
||||
export const AndroidConfirmBeforeExitKey = 'ConfirmBeforeExit'
|
||||
|
||||
const Defaults: FunctionComponent<Props> = ({ application }) => {
|
||||
@@ -60,23 +19,10 @@ const Defaults: FunctionComponent<Props> = ({ application }) => {
|
||||
() => (application.getValue(AndroidConfirmBeforeExitKey) as boolean) ?? true,
|
||||
)
|
||||
|
||||
const [editorItems, setEditorItems] = useState<DropdownItem[]>([])
|
||||
const [defaultEditorValue, setDefaultEditorValue] = useState(
|
||||
() => getDefaultEditor(application)?.package_info?.identifier || 'plain-editor',
|
||||
)
|
||||
|
||||
const [spellcheck, setSpellcheck] = useState(() =>
|
||||
application.getPreference(PrefKey.EditorSpellcheck, PrefDefaults[PrefKey.EditorSpellcheck]),
|
||||
)
|
||||
|
||||
const [newNoteTitleFormat, setNewNoteTitleFormat] = useState(() =>
|
||||
application.getPreference(PrefKey.NewNoteTitleFormat, PrefDefaults[PrefKey.NewNoteTitleFormat]),
|
||||
)
|
||||
const handleNewNoteTitleFormatChange = (value: string) => {
|
||||
setNewNoteTitleFormat(value as NewNoteTitleFormat)
|
||||
application.setPreference(PrefKey.NewNoteTitleFormat, value as NewNoteTitleFormat)
|
||||
}
|
||||
|
||||
const [addNoteToParentFolders, setAddNoteToParentFolders] = useState(() =>
|
||||
application.getPreference(PrefKey.NoteAddToParentFolders, PrefDefaults[PrefKey.NoteAddToParentFolders]),
|
||||
)
|
||||
@@ -86,70 +32,6 @@ const Defaults: FunctionComponent<Props> = ({ application }) => {
|
||||
application.toggleGlobalSpellcheck().catch(console.error)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const editors = application.componentManager
|
||||
.componentsForArea(ComponentArea.Editor)
|
||||
.map((editor): EditorOption => {
|
||||
const identifier = editor.package_info.identifier
|
||||
const [iconType, tint] = application.iconsController.getIconAndTintForNoteType(editor.package_info.note_type)
|
||||
|
||||
return {
|
||||
label: editor.displayName,
|
||||
value: identifier,
|
||||
...(iconType ? { icon: iconType } : null),
|
||||
...(tint ? { iconClassName: `text-accessory-tint-${tint}` } : null),
|
||||
}
|
||||
})
|
||||
.concat([
|
||||
{
|
||||
icon: 'plain-text',
|
||||
iconClassName: 'text-accessory-tint-1',
|
||||
label: PLAIN_EDITOR_NAME,
|
||||
value: 'plain-editor',
|
||||
},
|
||||
])
|
||||
.sort((a, b) => {
|
||||
return a.label.toLowerCase() < b.label.toLowerCase() ? -1 : 1
|
||||
})
|
||||
|
||||
setEditorItems(editors)
|
||||
}, [application])
|
||||
|
||||
const setDefaultEditor = (value: string) => {
|
||||
setDefaultEditorValue(value as FeatureIdentifier)
|
||||
const editors = application.componentManager.componentsForArea(ComponentArea.Editor)
|
||||
const currentDefault = getDefaultEditor(application)
|
||||
|
||||
if (value !== 'plain-editor') {
|
||||
const editorComponent = editors.filter((e) => e.package_info.identifier === value)[0]
|
||||
makeEditorDefault(application, editorComponent, currentDefault)
|
||||
} else {
|
||||
removeEditorDefault(application, currentDefault)
|
||||
}
|
||||
}
|
||||
|
||||
const noteTitleFormatOptions = useMemo(
|
||||
() => [
|
||||
{
|
||||
label: 'Current date and time',
|
||||
value: NewNoteTitleFormat.CurrentDateAndTime,
|
||||
},
|
||||
{
|
||||
label: 'Current note count',
|
||||
value: NewNoteTitleFormat.CurrentNoteCount,
|
||||
},
|
||||
{
|
||||
label: 'Custom format',
|
||||
value: NewNoteTitleFormat.CustomFormat,
|
||||
},
|
||||
{
|
||||
label: 'Empty',
|
||||
value: NewNoteTitleFormat.Empty,
|
||||
},
|
||||
],
|
||||
[],
|
||||
)
|
||||
|
||||
const toggleAndroidConfirmBeforeExit = () => {
|
||||
const newValue = !androidConfirmBeforeExit
|
||||
setAndroidConfirmBeforeExit(newValue)
|
||||
@@ -161,44 +43,17 @@ const Defaults: FunctionComponent<Props> = ({ application }) => {
|
||||
<PreferencesSegment>
|
||||
<Title>Defaults</Title>
|
||||
{application.platform === Platform.Android && (
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex flex-col">
|
||||
<Subtitle>Always ask before closing app</Subtitle>
|
||||
<Text>Whether a confirmation dialog should be shown before closing the app.</Text>
|
||||
<>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex flex-col">
|
||||
<Subtitle>Always ask before closing app (Android)</Subtitle>
|
||||
<Text>Whether a confirmation dialog should be shown before closing the app.</Text>
|
||||
</div>
|
||||
<Switch onChange={toggleAndroidConfirmBeforeExit} checked={androidConfirmBeforeExit} />
|
||||
</div>
|
||||
<Switch onChange={toggleAndroidConfirmBeforeExit} checked={androidConfirmBeforeExit} />
|
||||
</div>
|
||||
<HorizontalSeparator classes="my-4" />
|
||||
</>
|
||||
)}
|
||||
<HorizontalSeparator classes="my-4" />
|
||||
<div>
|
||||
<Subtitle>Default Note Type</Subtitle>
|
||||
<Text>New notes will be created using this type</Text>
|
||||
<div className="mt-2">
|
||||
<Dropdown
|
||||
id="def-editor-dropdown"
|
||||
label="Select the default note type"
|
||||
items={editorItems}
|
||||
value={defaultEditorValue}
|
||||
onChange={setDefaultEditor}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<HorizontalSeparator classes="my-4" />
|
||||
<div>
|
||||
<Subtitle>Default Note Title Format</Subtitle>
|
||||
<Text>New notes will be created with a title in this format</Text>
|
||||
<div className="mt-2">
|
||||
<Dropdown
|
||||
id="def-new-note-title-format"
|
||||
label="Select the default note type"
|
||||
items={noteTitleFormatOptions}
|
||||
value={newNoteTitleFormat}
|
||||
onChange={handleNewNoteTitleFormatChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{newNoteTitleFormat === NewNoteTitleFormat.CustomFormat && <CustomNoteTitleFormat application={application} />}
|
||||
<HorizontalSeparator classes="my-4" />
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex flex-col">
|
||||
<Subtitle>Spellcheck</Subtitle>
|
||||
|
||||
@@ -1,80 +0,0 @@
|
||||
import { WebApplication } from '@/Application/Application'
|
||||
import { Text, Subtitle } from '@/Components/Preferences/PreferencesComponents/Content'
|
||||
import HorizontalSeparator from '@/Components/Shared/HorizontalSeparator'
|
||||
import { PrefDefaults } from '@/Constants/PrefDefaults'
|
||||
import { PrefKey } from '@standardnotes/snjs'
|
||||
import { ChangeEventHandler, useRef, useState } from 'react'
|
||||
import dayjs from 'dayjs'
|
||||
|
||||
type Props = {
|
||||
application: WebApplication
|
||||
}
|
||||
|
||||
const PrefChangeDebounceTimeInMs = 25
|
||||
|
||||
const HelpPageUrl = 'https://day.js.org/docs/en/display/format#list-of-all-available-formats'
|
||||
|
||||
const CustomNoteTitleFormat = ({ application }: Props) => {
|
||||
const [customNoteTitleFormat, setCustomNoteTitleFormat] = useState(() =>
|
||||
application.getPreference(PrefKey.CustomNoteTitleFormat, PrefDefaults[PrefKey.CustomNoteTitleFormat]),
|
||||
)
|
||||
|
||||
const setCustomNoteTitleFormatPreference = () => {
|
||||
application.setPreference(PrefKey.CustomNoteTitleFormat, customNoteTitleFormat)
|
||||
}
|
||||
|
||||
const debounceTimeoutRef = useRef<number>()
|
||||
|
||||
const handleInputChange: ChangeEventHandler<HTMLInputElement> = (event) => {
|
||||
setCustomNoteTitleFormat(event.currentTarget.value)
|
||||
|
||||
if (debounceTimeoutRef.current) {
|
||||
clearTimeout(debounceTimeoutRef.current)
|
||||
}
|
||||
|
||||
debounceTimeoutRef.current = window.setTimeout(async () => {
|
||||
setCustomNoteTitleFormatPreference()
|
||||
}, PrefChangeDebounceTimeInMs)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<HorizontalSeparator classes="my-4" />
|
||||
<div>
|
||||
<Subtitle>Custom Note Title Format</Subtitle>
|
||||
<Text>
|
||||
All available date-time formatting options can be found{' '}
|
||||
<a
|
||||
className="underline"
|
||||
href={HelpPageUrl}
|
||||
target="_blank"
|
||||
onClick={(event) => {
|
||||
if (application.isNativeMobileWeb()) {
|
||||
event.preventDefault()
|
||||
application.mobileDevice().openUrl(HelpPageUrl)
|
||||
}
|
||||
}}
|
||||
>
|
||||
here
|
||||
</a>
|
||||
. Use square brackets (<code>[]</code>) to escape date-time formatting.
|
||||
</Text>
|
||||
<div className="mt-2">
|
||||
<input
|
||||
className="min-w-55 rounded border border-solid border-passive-3 bg-default px-2 py-1.5 text-sm focus-within:ring-2 focus-within:ring-info"
|
||||
placeholder="e.g. YYYY-MM-DD"
|
||||
value={customNoteTitleFormat}
|
||||
onChange={handleInputChange}
|
||||
onBlur={setCustomNoteTitleFormatPreference}
|
||||
spellCheck={false}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-2">
|
||||
<span className="font-bold">Preview:</span> {dayjs().format(customNoteTitleFormat)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default CustomNoteTitleFormat
|
||||
@@ -1,11 +1,12 @@
|
||||
type Props = {
|
||||
checked: boolean
|
||||
className?: string
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
const RadioIndicator = ({ checked, className }: Props) => (
|
||||
const RadioIndicator = ({ checked, className, disabled }: Props) => (
|
||||
<div
|
||||
className={`relative h-4 w-4 rounded-full border-2 border-solid ${
|
||||
className={`relative h-4 w-4 rounded-full border-2 border-solid ${disabled ? 'opacity-50' : ''} ${
|
||||
checked
|
||||
? 'border-info after:absolute after:top-1/2 after:left-1/2 after:h-2 after:w-2 after:-translate-x-1/2 after:-translate-y-1/2 after:rounded-full after:bg-info'
|
||||
: 'border-passive-1'
|
||||
|
||||
@@ -19,7 +19,7 @@ export const PrefDefaults = {
|
||||
[PrefKey.NotesHideProtected]: false,
|
||||
[PrefKey.NotesHideNotePreview]: false,
|
||||
[PrefKey.NotesHideDate]: false,
|
||||
[PrefKey.NotesHideTags]: true,
|
||||
[PrefKey.NotesHideTags]: false,
|
||||
[PrefKey.NotesHideEditorIcon]: false,
|
||||
[PrefKey.UseSystemColorScheme]: false,
|
||||
[PrefKey.AutoLightThemeIdentifier]: 'Default',
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
FileItem,
|
||||
WebAppEvent,
|
||||
NewNoteTitleFormat,
|
||||
useBoolean,
|
||||
} from '@standardnotes/snjs'
|
||||
import { action, computed, makeObservable, observable, reaction, runInAction } from 'mobx'
|
||||
import { WebApplication } from '../../Application/Application'
|
||||
@@ -121,12 +122,23 @@ export class ItemListController
|
||||
}),
|
||||
)
|
||||
|
||||
this.disposers.push(
|
||||
reaction(
|
||||
() => [this.navigationController.selected],
|
||||
() => {
|
||||
void this.reloadDisplayPreferences()
|
||||
},
|
||||
),
|
||||
)
|
||||
this.disposers.push(
|
||||
application.streamItems<SNTag>([ContentType.Tag], async ({ changed, inserted }) => {
|
||||
const tags = [...changed, ...inserted]
|
||||
|
||||
/** A tag could have changed its relationships, so we need to reload the filter */
|
||||
this.reloadNotesDisplayOptions()
|
||||
const { didReloadItems } = await this.reloadDisplayPreferences()
|
||||
if (!didReloadItems) {
|
||||
/** A tag could have changed its relationships, so we need to reload the filter */
|
||||
this.reloadNotesDisplayOptions()
|
||||
}
|
||||
|
||||
void this.reloadItems(ItemsReloadSource.ItemStream)
|
||||
|
||||
@@ -139,7 +151,7 @@ export class ItemListController
|
||||
|
||||
this.disposers.push(
|
||||
application.addEventObserver(async () => {
|
||||
void this.reloadPreferences()
|
||||
void this.reloadDisplayPreferences()
|
||||
}, ApplicationEvent.PreferencesChanged),
|
||||
)
|
||||
|
||||
@@ -204,7 +216,7 @@ export class ItemListController
|
||||
|
||||
reloadItems: action,
|
||||
reloadPanelTitle: action,
|
||||
reloadPreferences: action,
|
||||
reloadDisplayPreferences: action,
|
||||
resetPagination: action,
|
||||
setCompletedFullSync: action,
|
||||
setNoteFilterText: action,
|
||||
@@ -447,54 +459,66 @@ export class ItemListController
|
||||
this.application.items.setPrimaryItemDisplayOptions(criteria)
|
||||
}
|
||||
|
||||
reloadPreferences = async () => {
|
||||
reloadDisplayPreferences = async (): Promise<{ didReloadItems: boolean }> => {
|
||||
const newDisplayOptions = {} as DisplayOptions
|
||||
const newWebDisplayOptions = {} as WebDisplayOptions
|
||||
const selectedTag = this.navigationController.selected
|
||||
|
||||
const currentSortBy = this.displayOptions.sortBy
|
||||
|
||||
let sortBy = this.application.getPreference(PrefKey.SortNotesBy, PrefDefaults[PrefKey.SortNotesBy])
|
||||
let sortBy =
|
||||
selectedTag?.preferences?.sortBy ||
|
||||
this.application.getPreference(PrefKey.SortNotesBy, PrefDefaults[PrefKey.SortNotesBy])
|
||||
if (sortBy === CollectionSort.UpdatedAt || (sortBy as string) === 'client_updated_at') {
|
||||
sortBy = CollectionSort.UpdatedAt
|
||||
}
|
||||
|
||||
newDisplayOptions.sortBy = sortBy
|
||||
|
||||
newDisplayOptions.sortDirection =
|
||||
this.application.getPreference(PrefKey.SortNotesReverse, PrefDefaults[PrefKey.SortNotesReverse]) === false
|
||||
useBoolean(
|
||||
selectedTag?.preferences?.sortReverse,
|
||||
this.application.getPreference(PrefKey.SortNotesReverse, PrefDefaults[PrefKey.SortNotesReverse]),
|
||||
) === false
|
||||
? 'dsc'
|
||||
: 'asc'
|
||||
newDisplayOptions.includeArchived = this.application.getPreference(
|
||||
PrefKey.NotesShowArchived,
|
||||
PrefDefaults[PrefKey.NotesShowArchived],
|
||||
)
|
||||
newDisplayOptions.includeTrashed = this.application.getPreference(
|
||||
PrefKey.NotesShowTrashed,
|
||||
PrefDefaults[PrefKey.NotesShowTrashed],
|
||||
) as boolean
|
||||
newDisplayOptions.includePinned = !this.application.getPreference(
|
||||
PrefKey.NotesHidePinned,
|
||||
PrefDefaults[PrefKey.NotesHidePinned],
|
||||
)
|
||||
newDisplayOptions.includeProtected = !this.application.getPreference(
|
||||
PrefKey.NotesHideProtected,
|
||||
PrefDefaults[PrefKey.NotesHideProtected],
|
||||
|
||||
newDisplayOptions.includeArchived = useBoolean(
|
||||
selectedTag?.preferences?.showArchived,
|
||||
this.application.getPreference(PrefKey.NotesShowArchived, PrefDefaults[PrefKey.NotesShowArchived]),
|
||||
)
|
||||
|
||||
newWebDisplayOptions.hideNotePreview = this.application.getPreference(
|
||||
PrefKey.NotesHideNotePreview,
|
||||
PrefDefaults[PrefKey.NotesHideNotePreview],
|
||||
newDisplayOptions.includeTrashed = useBoolean(
|
||||
selectedTag?.preferences?.showTrashed,
|
||||
this.application.getPreference(PrefKey.NotesShowTrashed, PrefDefaults[PrefKey.NotesShowTrashed]),
|
||||
)
|
||||
newWebDisplayOptions.hideDate = this.application.getPreference(
|
||||
PrefKey.NotesHideDate,
|
||||
PrefDefaults[PrefKey.NotesHideDate],
|
||||
|
||||
newDisplayOptions.includePinned = useBoolean(
|
||||
!selectedTag?.preferences?.hidePinned,
|
||||
!this.application.getPreference(PrefKey.NotesHidePinned, PrefDefaults[PrefKey.NotesHidePinned]),
|
||||
)
|
||||
newWebDisplayOptions.hideTags = this.application.getPreference(
|
||||
PrefKey.NotesHideTags,
|
||||
PrefDefaults[PrefKey.NotesHideTags],
|
||||
|
||||
newDisplayOptions.includeProtected = useBoolean(
|
||||
!selectedTag?.preferences?.hideProtected,
|
||||
!this.application.getPreference(PrefKey.NotesHideProtected, PrefDefaults[PrefKey.NotesHideProtected]),
|
||||
)
|
||||
newWebDisplayOptions.hideEditorIcon = this.application.getPreference(
|
||||
PrefKey.NotesHideEditorIcon,
|
||||
PrefDefaults[PrefKey.NotesHideEditorIcon],
|
||||
|
||||
newWebDisplayOptions.hideNotePreview = useBoolean(
|
||||
selectedTag?.preferences?.hideNotePreview,
|
||||
this.application.getPreference(PrefKey.NotesHideNotePreview, PrefDefaults[PrefKey.NotesHideNotePreview]),
|
||||
)
|
||||
|
||||
newWebDisplayOptions.hideDate = useBoolean(
|
||||
selectedTag?.preferences?.hideDate,
|
||||
this.application.getPreference(PrefKey.NotesHideDate, PrefDefaults[PrefKey.NotesHideDate]),
|
||||
)
|
||||
|
||||
newWebDisplayOptions.hideTags = useBoolean(
|
||||
selectedTag?.preferences?.hideTags,
|
||||
this.application.getPreference(PrefKey.NotesHideTags, PrefDefaults[PrefKey.NotesHideTags]),
|
||||
)
|
||||
|
||||
newWebDisplayOptions.hideEditorIcon = useBoolean(
|
||||
selectedTag?.preferences?.hideEditorIcon,
|
||||
this.application.getPreference(PrefKey.NotesHideEditorIcon, PrefDefaults[PrefKey.NotesHideEditorIcon]),
|
||||
)
|
||||
|
||||
const displayOptionsChanged =
|
||||
@@ -518,12 +542,10 @@ export class ItemListController
|
||||
}
|
||||
|
||||
if (!displayOptionsChanged) {
|
||||
return
|
||||
return { didReloadItems: false }
|
||||
}
|
||||
|
||||
if (displayOptionsChanged) {
|
||||
this.reloadNotesDisplayOptions()
|
||||
}
|
||||
this.reloadNotesDisplayOptions()
|
||||
|
||||
await this.reloadItems(ItemsReloadSource.DisplayOptionsChange)
|
||||
|
||||
@@ -535,6 +557,8 @@ export class ItemListController
|
||||
type: CrossControllerEvent.RequestValuePersistence,
|
||||
payload: undefined,
|
||||
})
|
||||
|
||||
return { didReloadItems: true }
|
||||
}
|
||||
|
||||
async createNewNoteController(title?: string) {
|
||||
@@ -555,20 +579,18 @@ export class ItemListController
|
||||
await this.navigationController.selectHomeNavigationView()
|
||||
}
|
||||
|
||||
const titleFormat = this.application.getPreference(
|
||||
PrefKey.NewNoteTitleFormat,
|
||||
PrefDefaults[PrefKey.NewNoteTitleFormat],
|
||||
)
|
||||
const titleFormat =
|
||||
this.navigationController.selected?.preferences?.newNoteTitleFormat ||
|
||||
this.application.getPreference(PrefKey.NewNoteTitleFormat, PrefDefaults[PrefKey.NewNoteTitleFormat])
|
||||
|
||||
let title = formatDateAndTimeForNote(new Date())
|
||||
|
||||
if (titleFormat === NewNoteTitleFormat.CurrentNoteCount) {
|
||||
title = `Note ${this.notes.length + 1}`
|
||||
} else if (titleFormat === NewNoteTitleFormat.CustomFormat) {
|
||||
const customFormat = this.application.getPreference(
|
||||
PrefKey.CustomNoteTitleFormat,
|
||||
PrefDefaults[PrefKey.CustomNoteTitleFormat],
|
||||
)
|
||||
const customFormat =
|
||||
this.navigationController.selected?.preferences?.customNoteTitleFormat ||
|
||||
this.application.getPreference(PrefKey.CustomNoteTitleFormat, PrefDefaults[PrefKey.CustomNoteTitleFormat])
|
||||
title = dayjs().format(customFormat)
|
||||
} else if (titleFormat === NewNoteTitleFormat.Empty) {
|
||||
title = ''
|
||||
|
||||
@@ -479,6 +479,10 @@ export class NavigationController
|
||||
}
|
||||
|
||||
public setExpanded(tag: SNTag, expanded: boolean) {
|
||||
if (tag.expanded === expanded) {
|
||||
return
|
||||
}
|
||||
|
||||
this.application.mutator
|
||||
.changeAndSaveItem<TagMutator>(tag, (mutator) => {
|
||||
mutator.expanded = expanded
|
||||
|
||||
@@ -110,6 +110,10 @@ export class SubscriptionController extends AbstractViewController {
|
||||
return Boolean(this.userSubscription?.cancelled)
|
||||
}
|
||||
|
||||
hasValidSubscription(): boolean {
|
||||
return this.userSubscription != undefined && !this.isUserSubscriptionExpired && !this.isUserSubscriptionCanceled
|
||||
}
|
||||
|
||||
get usedInvitationsCount(): number {
|
||||
return (
|
||||
this.subscriptionInvitations?.filter((invitation) =>
|
||||
|
||||
@@ -34,11 +34,7 @@ const PremiumModalProvider: FunctionComponent<Props> = observer(
|
||||
|
||||
const showModal = !!featureName
|
||||
|
||||
const hasSubscription = Boolean(
|
||||
viewControllerManager.subscriptionController.userSubscription &&
|
||||
!viewControllerManager.subscriptionController.isUserSubscriptionExpired &&
|
||||
!viewControllerManager.subscriptionController.isUserSubscriptionCanceled,
|
||||
)
|
||||
const hasSubscription = application.hasValidSubscription()
|
||||
|
||||
const hasAccount = application.hasAccount()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user