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

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1 @@
export type PreferenceMode = 'global' | 'tag'