feat: per-tag display preferences (#1868)
This commit is contained in:
@@ -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'
|
||||
Reference in New Issue
Block a user