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'
|
||||
@@ -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'
|
||||
|
||||
Reference in New Issue
Block a user