feat: note types

This commit is contained in:
Mo
2022-05-03 10:50:47 -05:00
parent e4fbb2515e
commit f5a90060ea
10 changed files with 327 additions and 386 deletions

View File

@@ -72,7 +72,7 @@ export const ChangeEditorButton: FunctionComponent<Props> = observer(
ref={buttonRef} ref={buttonRef}
className="sn-icon-button border-contrast" className="sn-icon-button border-contrast"
> >
<VisuallyHidden>Change editor</VisuallyHidden> <VisuallyHidden>Change note type</VisuallyHidden>
<Icon type="dashboard" className="block" /> <Icon type="dashboard" className="block" />
</DisclosureButton> </DisclosureButton>
<DisclosurePanel <DisclosurePanel

View File

@@ -21,7 +21,8 @@ import {
import { Fragment, FunctionComponent } from 'preact' import { Fragment, FunctionComponent } from 'preact'
import { useCallback, useEffect, useState } from 'preact/hooks' import { useCallback, useEffect, useState } from 'preact/hooks'
import { EditorMenuItem, EditorMenuGroup } from '@/Components/NotesOptions/ChangeEditorOption' import { EditorMenuItem, EditorMenuGroup } from '@/Components/NotesOptions/ChangeEditorOption'
import { createEditorMenuGroups, PLAIN_EDITOR_NAME } from './createEditorMenuGroups' import { createEditorMenuGroups } from './createEditorMenuGroups'
import { PLAIN_EDITOR_NAME } from '@/Constants'
type ChangeEditorMenuProps = { type ChangeEditorMenuProps = {
application: WebApplication application: WebApplication
@@ -169,7 +170,7 @@ export const ChangeEditorMenu: FunctionComponent<ChangeEditorMenuProps> = ({
} }
return ( return (
<Menu className="pt-0.5 pb-1" a11yLabel="Change editor menu" isOpen={isVisible}> <Menu className="pt-0.5 pb-1" a11yLabel="Change note type menu" isOpen={isVisible}>
{groups {groups
.filter((group) => group.items && group.items.length) .filter((group) => group.items && group.items.length)
.map((group, index) => { .map((group, index) => {
@@ -194,9 +195,7 @@ export const ChangeEditorMenu: FunctionComponent<ChangeEditorMenuProps> = ({
<MenuItem <MenuItem
type={MenuItemType.RadioButton} type={MenuItemType.RadioButton}
onClick={onClickEditorItem} onClick={onClickEditorItem}
className={ className={'sn-dropdown-item py-2 text-input focus:bg-info-backdrop focus:shadow-none'}
'sn-dropdown-item py-2 text-input focus:bg-info-backdrop focus:shadow-none'
}
onBlur={closeOnBlur} onBlur={closeOnBlur}
checked={isSelectedEditor(item)} checked={isSelectedEditor(item)}
> >

View File

@@ -9,8 +9,7 @@ import {
NoteType, NoteType,
} from '@standardnotes/snjs' } from '@standardnotes/snjs'
import { EditorMenuItem, EditorMenuGroup } from '@/Components/NotesOptions/ChangeEditorOption' import { EditorMenuItem, EditorMenuGroup } from '@/Components/NotesOptions/ChangeEditorOption'
import { PLAIN_EDITOR_NAME } from '@/Constants'
export const PLAIN_EDITOR_NAME = 'Plain Editor'
type EditorGroup = NoteType | 'plain' | 'others' type EditorGroup = NoteType | 'plain' | 'others'
@@ -50,10 +49,7 @@ export const createEditorMenuGroups = (application: WebApplication, editors: SNC
} }
GetFeatures() GetFeatures()
.filter( .filter((feature) => feature.content_type === ContentType.Component && feature.area === ComponentArea.Editor)
(feature) =>
feature.content_type === ContentType.Component && feature.area === ComponentArea.Editor,
)
.forEach((editorFeature) => { .forEach((editorFeature) => {
const notInstalled = !editors.find((editor) => editor.identifier === editorFeature.identifier) const notInstalled = !editors.find((editor) => editor.identifier === editorFeature.identifier)
const isExperimental = application.features.isExperimentalFeature(editorFeature.identifier) const isExperimental = application.features.isExperimentalFeature(editorFeature.identifier)
@@ -69,8 +65,7 @@ export const createEditorMenuGroups = (application: WebApplication, editors: SNC
const editorItem: EditorMenuItem = { const editorItem: EditorMenuItem = {
name: editor.name, name: editor.name,
component: editor, component: editor,
isEntitled: isEntitled: application.features.getFeatureStatus(editor.identifier) === FeatureStatus.Entitled,
application.features.getFeatureStatus(editor.identifier) === FeatureStatus.Entitled,
} }
editorItems[getEditorGroup(editor.package_info)].push(editorItem) editorItems[getEditorGroup(editor.package_info)].push(editorItem)

View File

@@ -1,12 +1,8 @@
import { WebApplication } from '@/UIModels/Application' import { WebApplication } from '@/UIModels/Application'
import { import { CollectionSort, CollectionSortProperty, sanitizeHtmlString, SNNote } from '@standardnotes/snjs'
CollectionSort,
CollectionSortProperty,
sanitizeHtmlString,
SNNote,
} from '@standardnotes/snjs'
import { FunctionComponent } from 'preact' import { FunctionComponent } from 'preact'
import { Icon } from '@/Components/Icon' import { Icon } from '@/Components/Icon'
import { PLAIN_EDITOR_NAME } from '@/Constants'
type Props = { type Props = {
application: WebApplication application: WebApplication
@@ -55,10 +51,8 @@ export const NotesListItem: FunctionComponent<Props> = ({
const flags = flagsForNote(note) const flags = flagsForNote(note)
const showModifiedDate = sortedBy === CollectionSort.UpdatedAt const showModifiedDate = sortedBy === CollectionSort.UpdatedAt
const editorForNote = application.componentManager.editorForNote(note) const editorForNote = application.componentManager.editorForNote(note)
const editorName = editorForNote?.name ?? 'Plain editor' const editorName = editorForNote?.name ?? PLAIN_EDITOR_NAME
const [icon, tint] = application.iconsController.getIconAndTintForEditor( const [icon, tint] = application.iconsController.getIconAndTintForEditor(editorForNote?.identifier)
editorForNote?.identifier,
)
return ( return (
<div <div
@@ -69,17 +63,11 @@ export const NotesListItem: FunctionComponent<Props> = ({
> >
{!hideEditorIcon && ( {!hideEditorIcon && (
<div className="icon"> <div className="icon">
<Icon <Icon ariaLabel={`Icon for ${editorName}`} type={icon} className={`color-accessory-tint-${tint}`} />
ariaLabel={`Icon for ${editorName}`}
type={icon}
className={`color-accessory-tint-${tint}`}
/>
</div> </div>
)} )}
<div className={`meta ${hideEditorIcon ? 'icon-hidden' : ''}`}> <div className={`meta ${hideEditorIcon ? 'icon-hidden' : ''}`}>
<div className="name-container"> <div className="name-container">{note.title.length ? <div className="name">{note.title}</div> : null}</div>
{note.title.length ? <div className="name">{note.title}</div> : null}
</div>
{!hidePreview && !note.hidePreview && !note.protected && ( {!hidePreview && !note.hidePreview && !note.protected && (
<div className="note-preview"> <div className="note-preview">
{note.preview_html && ( {note.preview_html && (
@@ -90,9 +78,7 @@ export const NotesListItem: FunctionComponent<Props> = ({
}} }}
></div> ></div>
)} )}
{!note.preview_html && note.preview_plain && ( {!note.preview_html && note.preview_plain && <div className="plain-preview">{note.preview_plain}</div>}
<div className="plain-preview">{note.preview_plain}</div>
)}
{!note.preview_html && !note.preview_plain && note.text && ( {!note.preview_html && !note.preview_plain && note.text && (
<div className="default-preview">{note.text}</div> <div className="default-preview">{note.text}</div>
)} )}
@@ -128,11 +114,7 @@ export const NotesListItem: FunctionComponent<Props> = ({
<div className="flag-icons"> <div className="flag-icons">
{note.locked && ( {note.locked && (
<span title="Editing Disabled"> <span title="Editing Disabled">
<Icon <Icon ariaLabel="Editing Disabled" type="pencil-off" className="sn-icon--small color-info" />
ariaLabel="Editing Disabled"
type="pencil-off"
className="sn-icon--small color-info"
/>
</span> </span>
)} )}
{note.trashed && ( {note.trashed && (
@@ -142,11 +124,7 @@ export const NotesListItem: FunctionComponent<Props> = ({
)} )}
{note.archived && ( {note.archived && (
<span title="Archived"> <span title="Archived">
<Icon <Icon ariaLabel="Archived" type="archive" className="sn-icon--mid color-accessory-tint-3" />
ariaLabel="Archived"
type="archive"
className="sn-icon--mid color-accessory-tint-3"
/>
</span> </span>
)} )}
{note.pinned && ( {note.pinned && (

View File

@@ -31,10 +31,7 @@ export type EditorMenuItem = {
export type EditorMenuGroup = AccordionMenuGroup<EditorMenuItem> export type EditorMenuGroup = AccordionMenuGroup<EditorMenuItem>
export const ChangeEditorOption: FunctionComponent<ChangeEditorOptionProps> = ({ export const ChangeEditorOption: FunctionComponent<ChangeEditorOptionProps> = ({ application, note }) => {
application,
note,
}) => {
const [isOpen, setIsOpen] = useState(false) const [isOpen, setIsOpen] = useState(false)
const [isVisible, setIsVisible] = useState(false) const [isVisible, setIsVisible] = useState(false)
const [menuStyle, setMenuStyle] = useState<SubmenuStyle>({ const [menuStyle, setMenuStyle] = useState<SubmenuStyle>({
@@ -90,7 +87,7 @@ export const ChangeEditorOption: FunctionComponent<ChangeEditorOptionProps> = ({
> >
<div className="flex items-center"> <div className="flex items-center">
<Icon type="dashboard" className="color-neutral mr-2" /> <Icon type="dashboard" className="color-neutral mr-2" />
Change editor Change note type
</div> </div>
<Icon type="chevron-right" className="color-neutral" /> <Icon type="chevron-right" className="color-neutral" />
</DisclosureButton> </DisclosureButton>

View File

@@ -32,6 +32,8 @@ const DeletePermanentlyButton = ({ closeOnBlur, onClick }: DeletePermanentlyButt
) )
const iconClass = 'color-neutral mr-2' const iconClass = 'color-neutral mr-2'
const iconClassDanger = 'color-danger mr-2'
const iconClassWarning = 'color-warning mr-2'
const getWordCount = (text: string) => { const getWordCount = (text: string) => {
if (text.trim().length === 0) { if (text.trim().length === 0) {
@@ -88,15 +90,9 @@ const NoteAttributes: FunctionComponent<{
application: SNApplication application: SNApplication
note: SNNote note: SNNote
}> = ({ application, note }) => { }> = ({ application, note }) => {
const { words, characters, paragraphs } = useMemo( const { words, characters, paragraphs } = useMemo(() => countNoteAttributes(note.text), [note.text])
() => countNoteAttributes(note.text),
[note.text],
)
const readTime = useMemo( const readTime = useMemo(() => (typeof words === 'number' ? calculateReadTime(words) : 'N/A'), [words])
() => (typeof words === 'number' ? calculateReadTime(words) : 'N/A'),
[words],
)
const dateLastModified = useMemo(() => formatDate(note.userModifiedDate), [note.userModifiedDate]) const dateLastModified = useMemo(() => formatDate(note.userModifiedDate), [note.userModifiedDate])
@@ -168,283 +164,276 @@ const NOTE_SIZE_WARNING_THRESHOLD = 0.5 * BYTES_IN_ONE_MEGABYTE
const NoteSizeWarning: FunctionComponent<{ const NoteSizeWarning: FunctionComponent<{
note: SNNote note: SNNote
}> = ({ note }) => }> = ({ note }) => {
(new Blob([note.text]).size > NOTE_SIZE_WARNING_THRESHOLD ? ( return new Blob([note.text]).size > NOTE_SIZE_WARNING_THRESHOLD ? (
<div className="flex items-center px-3 py-3.5 relative bg-note-size-warning"> <div className="flex items-center px-3 py-3.5 relative bg-note-size-warning">
<Icon type="warning" className="color-accessory-tint-3 flex-shrink-0 mr-3" /> <Icon type="warning" className="color-accessory-tint-3 flex-shrink-0 mr-3" />
<div className="color-grey-0 select-none leading-140% max-w-80%"> <div className="color-grey-0 select-none leading-140% max-w-80%">
This note may have trouble syncing to the mobile application due to its size. This note may have trouble syncing to the mobile application due to its size.
</div> </div>
</div> </div>
) : null) ) : null
}
export const NotesOptions = observer( export const NotesOptions = observer(({ application, appState, closeOnBlur }: NotesOptionsProps) => {
({ application, appState, closeOnBlur }: NotesOptionsProps) => { const [altKeyDown, setAltKeyDown] = useState(false)
const [altKeyDown, setAltKeyDown] = useState(false)
const toggleOn = (condition: (note: SNNote) => boolean) => { const toggleOn = (condition: (note: SNNote) => boolean) => {
const notesMatchingAttribute = notes.filter(condition) const notesMatchingAttribute = notes.filter(condition)
const notesNotMatchingAttribute = notes.filter((note) => !condition(note)) const notesNotMatchingAttribute = notes.filter((note) => !condition(note))
return notesMatchingAttribute.length > notesNotMatchingAttribute.length return notesMatchingAttribute.length > notesNotMatchingAttribute.length
}
const notes = Object.values(appState.notes.selectedNotes)
const hidePreviews = toggleOn((note) => note.hidePreview)
const locked = toggleOn((note) => note.locked)
const protect = toggleOn((note) => note.protected)
const archived = notes.some((note) => note.archived)
const unarchived = notes.some((note) => !note.archived)
const trashed = notes.some((note) => note.trashed)
const notTrashed = notes.some((note) => !note.trashed)
const pinned = notes.some((note) => note.pinned)
const unpinned = notes.some((note) => !note.pinned)
useEffect(() => {
const removeAltKeyObserver = application.io.addKeyObserver({
modifiers: [KeyboardModifier.Alt],
onKeyDown: () => {
setAltKeyDown(true)
},
onKeyUp: () => {
setAltKeyDown(false)
},
})
return () => {
removeAltKeyObserver()
}
}, [application])
const getNoteFileName = (note: SNNote): string => {
const editor = application.componentManager.editorForNote(note)
const format = editor?.package_info?.file_type || 'txt'
return `${note.title}.${format}`
}
const downloadSelectedItems = async () => {
if (notes.length === 1) {
application.getArchiveService().downloadData(new Blob([notes[0].text]), getNoteFileName(notes[0]))
return
} }
const notes = Object.values(appState.notes.selectedNotes) if (notes.length > 1) {
const hidePreviews = toggleOn((note) => note.hidePreview) const loadingToastId = addToast({
const locked = toggleOn((note) => note.locked) type: ToastType.Loading,
const protect = toggleOn((note) => note.protected) message: `Exporting ${notes.length} notes...`,
const archived = notes.some((note) => note.archived)
const unarchived = notes.some((note) => !note.archived)
const trashed = notes.some((note) => note.trashed)
const notTrashed = notes.some((note) => !note.trashed)
const pinned = notes.some((note) => note.pinned)
const unpinned = notes.some((note) => !note.pinned)
useEffect(() => {
const removeAltKeyObserver = application.io.addKeyObserver({
modifiers: [KeyboardModifier.Alt],
onKeyDown: () => {
setAltKeyDown(true)
},
onKeyUp: () => {
setAltKeyDown(false)
},
}) })
await application.getArchiveService().downloadDataAsZip(
return () => { notes.map((note) => {
removeAltKeyObserver() return {
} filename: getNoteFileName(note),
}, [application]) content: new Blob([note.text]),
}
const getNoteFileName = (note: SNNote): string => { }),
const editor = application.componentManager.editorForNote(note) )
const format = editor?.package_info?.file_type || 'txt' dismissToast(loadingToastId)
return `${note.title}.${format}` addToast({
} type: ToastType.Success,
message: `Exported ${notes.length} notes`,
const downloadSelectedItems = async () => {
if (notes.length === 1) {
application
.getArchiveService()
.downloadData(new Blob([notes[0].text]), getNoteFileName(notes[0]))
return
}
if (notes.length > 1) {
const loadingToastId = addToast({
type: ToastType.Loading,
message: `Exporting ${notes.length} notes...`,
})
await application.getArchiveService().downloadDataAsZip(
notes.map((note) => {
return {
filename: getNoteFileName(note),
content: new Blob([note.text]),
}
}),
)
dismissToast(loadingToastId)
addToast({
type: ToastType.Success,
message: `Exported ${notes.length} notes`,
})
}
}
const duplicateSelectedItems = () => {
notes.forEach((note) => {
application.mutator.duplicateItem(note).catch(console.error)
}) })
} }
}
const openRevisionHistoryModal = () => { const duplicateSelectedItems = () => {
appState.notes.setShowRevisionHistoryModal(true) notes.forEach((note) => {
} application.mutator.duplicateItem(note).catch(console.error)
})
}
return ( const openRevisionHistoryModal = () => {
<> appState.notes.setShowRevisionHistoryModal(true)
{notes.length === 1 && ( }
<>
<button return (
onBlur={closeOnBlur} <>
className="sn-dropdown-item" {notes.length === 1 && (
onClick={openRevisionHistoryModal} <>
> <button onBlur={closeOnBlur} className="sn-dropdown-item" onClick={openRevisionHistoryModal}>
<Icon type="history" className={iconClass} /> <Icon type="history" className={iconClass} />
Note history Note history
</button> </button>
<div className="min-h-1px my-2 bg-border"></div> <div className="min-h-1px my-2 bg-border"></div>
</> </>
)} )}
<button
className="sn-dropdown-item justify-between"
onClick={() => {
appState.notes.setLockSelectedNotes(!locked)
}}
onBlur={closeOnBlur}
>
<span className="flex items-center">
<Icon type="pencil-off" className={iconClass} />
Prevent editing
</span>
<Switch className="px-0" checked={locked} />
</button>
<button
className="sn-dropdown-item justify-between"
onClick={() => {
appState.notes.setHideSelectedNotePreviews(!hidePreviews)
}}
onBlur={closeOnBlur}
>
<span className="flex items-center">
<Icon type="rich-text" className={iconClass} />
Show preview
</span>
<Switch className="px-0" checked={!hidePreviews} />
</button>
<button
className="sn-dropdown-item justify-between"
onClick={() => {
appState.notes.setProtectSelectedNotes(!protect).catch(console.error)
}}
onBlur={closeOnBlur}
>
<span className="flex items-center">
<Icon type="password" className={iconClass} />
Password protect
</span>
<Switch className="px-0" checked={protect} />
</button>
{notes.length === 1 && (
<>
<div className="min-h-1px my-2 bg-border"></div>
<ChangeEditorOption appState={appState} application={application} note={notes[0]} />
</>
)}
<div className="min-h-1px my-2 bg-border"></div>
{appState.tags.tagsCount > 0 && <AddTagOption appState={appState} />}
{unpinned && (
<button <button
className="sn-dropdown-item justify-between"
onClick={() => {
appState.notes.setLockSelectedNotes(!locked)
}}
onBlur={closeOnBlur} onBlur={closeOnBlur}
className="sn-dropdown-item"
onClick={() => {
appState.notes.setPinSelectedNotes(true)
}}
> >
<span className="flex items-center"> <Icon type="pin" className={iconClass} />
<Icon type="pencil-off" className={iconClass} /> Pin to top
Prevent editing
</span>
<Switch className="px-0" checked={locked} />
</button> </button>
)}
{pinned && (
<button <button
className="sn-dropdown-item justify-between"
onClick={() => {
appState.notes.setHideSelectedNotePreviews(!hidePreviews)
}}
onBlur={closeOnBlur} onBlur={closeOnBlur}
className="sn-dropdown-item"
onClick={() => {
appState.notes.setPinSelectedNotes(false)
}}
> >
<span className="flex items-center"> <Icon type="unpin" className={iconClass} />
<Icon type="rich-text" className={iconClass} /> Unpin
Show preview
</span>
<Switch className="px-0" checked={!hidePreviews} />
</button> </button>
)}
<button onBlur={closeOnBlur} className="sn-dropdown-item" onClick={downloadSelectedItems}>
<Icon type="download" className={iconClass} />
Export
</button>
<button onBlur={closeOnBlur} className="sn-dropdown-item" onClick={duplicateSelectedItems}>
<Icon type="copy" className={iconClass} />
Duplicate
</button>
{unarchived && (
<button <button
className="sn-dropdown-item justify-between"
onClick={() => {
appState.notes.setProtectSelectedNotes(!protect).catch(console.error)
}}
onBlur={closeOnBlur} onBlur={closeOnBlur}
className="sn-dropdown-item"
onClick={() => {
appState.notes.setArchiveSelectedNotes(true).catch(console.error)
}}
> >
<span className="flex items-center"> <Icon type="archive" className={iconClassWarning} />
<Icon type="password" className={iconClass} /> <span className="color-warning">Archive</span>
Protect
</span>
<Switch className="px-0" checked={protect} />
</button> </button>
{notes.length === 1 && ( )}
<> {archived && (
<div className="min-h-1px my-2 bg-border"></div> <button
<ChangeEditorOption appState={appState} application={application} note={notes[0]} /> onBlur={closeOnBlur}
</> className="sn-dropdown-item"
)} onClick={() => {
<div className="min-h-1px my-2 bg-border"></div> appState.notes.setArchiveSelectedNotes(false).catch(console.error)
{appState.tags.tagsCount > 0 && <AddTagOption appState={appState} />} }}
{unpinned && ( >
<button <Icon type="unarchive" className={iconClassWarning} />
onBlur={closeOnBlur} <span className="color-warning">Unarchive</span>
className="sn-dropdown-item"
onClick={() => {
appState.notes.setPinSelectedNotes(true)
}}
>
<Icon type="pin" className={iconClass} />
Pin to top
</button>
)}
{pinned && (
<button
onBlur={closeOnBlur}
className="sn-dropdown-item"
onClick={() => {
appState.notes.setPinSelectedNotes(false)
}}
>
<Icon type="unpin" className={iconClass} />
Unpin
</button>
)}
<button onBlur={closeOnBlur} className="sn-dropdown-item" onClick={downloadSelectedItems}>
<Icon type="download" className={iconClass} />
Export
</button> </button>
<button onBlur={closeOnBlur} className="sn-dropdown-item" onClick={duplicateSelectedItems}> )}
<Icon type="copy" className={iconClass} /> {notTrashed &&
Duplicate (altKeyDown ? (
</button> <DeletePermanentlyButton
{unarchived && ( closeOnBlur={closeOnBlur}
onClick={async () => {
await appState.notes.deleteNotesPermanently()
}}
/>
) : (
<button <button
onBlur={closeOnBlur} onBlur={closeOnBlur}
className="sn-dropdown-item" className="sn-dropdown-item"
onClick={() => { onClick={async () => {
appState.notes.setArchiveSelectedNotes(true).catch(console.error) await appState.notes.setTrashSelectedNotes(true)
}} }}
> >
<Icon type="archive" className={iconClass} /> <Icon type="trash" className={iconClassDanger} />
Archive <span className="color-danger">Move to trash</span>
</button> </button>
)} ))}
{archived && ( {trashed && (
<>
<button <button
onBlur={closeOnBlur} onBlur={closeOnBlur}
className="sn-dropdown-item" className="sn-dropdown-item"
onClick={() => { onClick={async () => {
appState.notes.setArchiveSelectedNotes(false).catch(console.error) await appState.notes.setTrashSelectedNotes(false)
}} }}
> >
<Icon type="unarchive" className={iconClass} /> <Icon type="restore" className={iconClass} />
Unarchive Restore
</button> </button>
)} <DeletePermanentlyButton
{notTrashed && closeOnBlur={closeOnBlur}
(altKeyDown ? ( onClick={async () => {
<DeletePermanentlyButton await appState.notes.deleteNotesPermanently()
closeOnBlur={closeOnBlur} }}
onClick={async () => { />
await appState.notes.deleteNotesPermanently() <button
}} onBlur={closeOnBlur}
/> className="sn-dropdown-item"
) : ( onClick={async () => {
<button await appState.notes.emptyTrash()
onBlur={closeOnBlur} }}
className="sn-dropdown-item" >
onClick={async () => { <div className="flex items-start">
await appState.notes.setTrashSelectedNotes(true) <Icon type="trash-sweep" className="color-danger mr-2" />
}} <div className="flex-row">
> <div className="color-danger">Empty Trash</div>
<Icon type="trash" className={iconClass} /> <div className="text-xs">{appState.notes.trashedNotesCount} notes in Trash</div>
Move to trash
</button>
))}
{trashed && (
<>
<button
onBlur={closeOnBlur}
className="sn-dropdown-item"
onClick={async () => {
await appState.notes.setTrashSelectedNotes(false)
}}
>
<Icon type="restore" className={iconClass} />
Restore
</button>
<DeletePermanentlyButton
closeOnBlur={closeOnBlur}
onClick={async () => {
await appState.notes.deleteNotesPermanently()
}}
/>
<button
onBlur={closeOnBlur}
className="sn-dropdown-item"
onClick={async () => {
await appState.notes.emptyTrash()
}}
>
<div className="flex items-start">
<Icon type="trash-sweep" className="color-danger mr-2" />
<div className="flex-row">
<div className="color-danger">Empty Trash</div>
<div className="text-xs">{appState.notes.trashedNotesCount} notes in Trash</div>
</div>
</div> </div>
</button> </div>
</> </button>
)} </>
{notes.length === 1 ? ( )}
<> {notes.length === 1 ? (
<div className="min-h-1px my-2 bg-border"></div> <>
<ListedActionsOption application={application} note={notes[0]} /> <div className="min-h-1px my-2 bg-border"></div>
<div className="min-h-1px my-2 bg-border"></div> <ListedActionsOption application={application} note={notes[0]} />
<SpellcheckOptions appState={appState} note={notes[0]} /> <div className="min-h-1px my-2 bg-border"></div>
<div className="min-h-1px my-2 bg-border"></div> <SpellcheckOptions appState={appState} note={notes[0]} />
<NoteAttributes application={application} note={notes[0]} /> <div className="min-h-1px my-2 bg-border"></div>
<NoteSizeWarning note={notes[0]} /> <NoteAttributes application={application} note={notes[0]} />
</> <NoteSizeWarning note={notes[0]} />
) : null} </>
</> ) : null}
) </>
}, )
) })

View File

@@ -1,11 +1,5 @@
import { Dropdown, DropdownItem } from '@/Components/Dropdown' import { Dropdown, DropdownItem } from '@/Components/Dropdown'
import { import { FeatureIdentifier, PrefKey, ComponentArea, ComponentMutator, SNComponent } from '@standardnotes/snjs'
FeatureIdentifier,
PrefKey,
ComponentArea,
ComponentMutator,
SNComponent,
} from '@standardnotes/snjs'
import { import {
PreferencesGroup, PreferencesGroup,
PreferencesSegment, PreferencesSegment,
@@ -18,6 +12,7 @@ import { FunctionComponent } from 'preact'
import { useEffect, useState } from 'preact/hooks' import { useEffect, useState } from 'preact/hooks'
import { HorizontalSeparator } from '@/Components/Shared/HorizontalSeparator' import { HorizontalSeparator } from '@/Components/Shared/HorizontalSeparator'
import { Switch } from '@/Components/Switch' import { Switch } from '@/Components/Switch'
import { PLAIN_EDITOR_NAME } from '@/Constants'
type Props = { type Props = {
application: WebApplication application: WebApplication
@@ -27,11 +22,7 @@ type EditorOption = DropdownItem & {
value: FeatureIdentifier | 'plain-editor' value: FeatureIdentifier | 'plain-editor'
} }
const makeEditorDefault = ( const makeEditorDefault = (application: WebApplication, component: SNComponent, currentDefault: SNComponent) => {
application: WebApplication,
component: SNComponent,
currentDefault: SNComponent,
) => {
if (currentDefault) { if (currentDefault) {
removeEditorDefault(application, currentDefault) removeEditorDefault(application, currentDefault)
} }
@@ -53,9 +44,7 @@ const removeEditorDefault = (application: WebApplication, component: SNComponent
} }
const getDefaultEditor = (application: WebApplication) => { const getDefaultEditor = (application: WebApplication) => {
return application.componentManager return application.componentManager.componentsForArea(ComponentArea.Editor).filter((e) => e.isDefaultEditor())[0]
.componentsForArea(ComponentArea.Editor)
.filter((e) => e.isDefaultEditor())[0]
} }
export const Defaults: FunctionComponent<Props> = ({ application }) => { export const Defaults: FunctionComponent<Props> = ({ application }) => {
@@ -64,9 +53,7 @@ export const Defaults: FunctionComponent<Props> = ({ application }) => {
() => getDefaultEditor(application)?.package_info?.identifier || 'plain-editor', () => getDefaultEditor(application)?.package_info?.identifier || 'plain-editor',
) )
const [spellcheck, setSpellcheck] = useState(() => const [spellcheck, setSpellcheck] = useState(() => application.getPreference(PrefKey.EditorSpellcheck, true))
application.getPreference(PrefKey.EditorSpellcheck, true),
)
const [addNoteToParentFolders, setAddNoteToParentFolders] = useState(() => const [addNoteToParentFolders, setAddNoteToParentFolders] = useState(() =>
application.getPreference(PrefKey.NoteAddToParentFolders, true), application.getPreference(PrefKey.NoteAddToParentFolders, true),
@@ -95,7 +82,7 @@ export const Defaults: FunctionComponent<Props> = ({ application }) => {
{ {
icon: 'plain-text', icon: 'plain-text',
iconClassName: 'color-accessory-tint-1', iconClassName: 'color-accessory-tint-1',
label: 'Plain Editor', label: PLAIN_EDITOR_NAME,
value: 'plain-editor', value: 'plain-editor',
}, },
]) ])
@@ -124,12 +111,12 @@ export const Defaults: FunctionComponent<Props> = ({ application }) => {
<PreferencesSegment> <PreferencesSegment>
<Title>Defaults</Title> <Title>Defaults</Title>
<div> <div>
<Subtitle>Default Editor</Subtitle> <Subtitle>Default Note Type</Subtitle>
<Text>New notes will be created using this editor.</Text> <Text>New notes will be created using this type.</Text>
<div className="mt-2"> <div className="mt-2">
<Dropdown <Dropdown
id="def-editor-dropdown" id="def-editor-dropdown"
label="Select the default editor" label="Select the default note type"
items={editorItems} items={editorItems}
value={defaultEditorValue} value={defaultEditorValue}
onChange={setDefaultEditor} onChange={setDefaultEditor}
@@ -141,9 +128,8 @@ export const Defaults: FunctionComponent<Props> = ({ application }) => {
<div className="flex flex-col"> <div className="flex flex-col">
<Subtitle>Spellcheck</Subtitle> <Subtitle>Spellcheck</Subtitle>
<Text> <Text>
The default spellcheck value for new notes. Spellcheck can be configured per note from The default spellcheck value for new notes. Spellcheck can be configured per note from the note context
the note context menu. Spellcheck may degrade overall typing performance with long menu. Spellcheck may degrade overall typing performance with long notes.
notes.
</Text> </Text>
</div> </div>
<Switch onChange={toggleSpellcheck} checked={spellcheck} /> <Switch onChange={toggleSpellcheck} checked={spellcheck} />
@@ -152,16 +138,11 @@ export const Defaults: FunctionComponent<Props> = ({ application }) => {
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="flex flex-col"> <div className="flex flex-col">
<Subtitle>Add all parent tags when adding a nested tag to a note</Subtitle> <Subtitle>Add all parent tags when adding a nested tag to a note</Subtitle>
<Text> <Text>When enabled, adding a nested tag to a note will automatically add all associated parent tags.</Text>
When enabled, adding a nested tag to a note will automatically add all associated
parent tags.
</Text>
</div> </div>
<Switch <Switch
onChange={() => { onChange={() => {
application application.setPreference(PrefKey.NoteAddToParentFolders, !addNoteToParentFolders).catch(console.error)
.setPreference(PrefKey.NoteAddToParentFolders, !addNoteToParentFolders)
.catch(console.error)
setAddNoteToParentFolders(!addNoteToParentFolders) setAddNoteToParentFolders(!addNoteToParentFolders)
}} }}
checked={addNoteToParentFolders} checked={addNoteToParentFolders}

View File

@@ -20,3 +20,5 @@ export const BYTES_IN_ONE_MEGABYTE = 1_000_000
export const TAG_FOLDERS_FEATURE_NAME = 'Tag folders' export const TAG_FOLDERS_FEATURE_NAME = 'Tag folders'
export const TAG_FOLDERS_FEATURE_TOOLTIP = 'A Plus or Pro plan is required to enable Tag folders.' export const TAG_FOLDERS_FEATURE_TOOLTIP = 'A Plus or Pro plan is required to enable Tag folders.'
export const SMART_TAGS_FEATURE_NAME = 'Smart Tags' export const SMART_TAGS_FEATURE_NAME = 'Smart Tags'
export const PLAIN_EDITOR_NAME = 'Plain Text'

View File

@@ -69,10 +69,10 @@
"@reach/listbox": "^0.16.2", "@reach/listbox": "^0.16.2",
"@reach/tooltip": "^0.16.2", "@reach/tooltip": "^0.16.2",
"@reach/visually-hidden": "^0.16.0", "@reach/visually-hidden": "^0.16.0",
"@standardnotes/components": "1.7.15", "@standardnotes/components": "1.8.0",
"@standardnotes/filepicker": "1.13.4", "@standardnotes/filepicker": "1.13.4",
"@standardnotes/sncrypto-web": "1.9.0", "@standardnotes/sncrypto-web": "1.9.0",
"@standardnotes/snjs": "2.105.3", "@standardnotes/snjs": "2.106.0",
"@standardnotes/stylekit": "5.25.0", "@standardnotes/stylekit": "5.25.0",
"@zip.js/zip.js": "^2.4.10", "@zip.js/zip.js": "^2.4.10",
"mobx": "^6.5.0", "mobx": "^6.5.0",

110
yarn.lock
View File

@@ -2437,10 +2437,10 @@
resolved "https://registry.yarnpkg.com/@standardnotes/common/-/common-1.19.6.tgz#6c672f4f10d5ed123a2c392dfb35dbe3c698b3b9" resolved "https://registry.yarnpkg.com/@standardnotes/common/-/common-1.19.6.tgz#6c672f4f10d5ed123a2c392dfb35dbe3c698b3b9"
integrity sha512-HxPrV9JhLp//S7h+QjOjlMPy0lgrjLT4iBKzr8JG8lUeWSS1mY3p1I+w0D9+yiraS2SPq2L14Z0VKds0zs9HXw== integrity sha512-HxPrV9JhLp//S7h+QjOjlMPy0lgrjLT4iBKzr8JG8lUeWSS1mY3p1I+w0D9+yiraS2SPq2L14Z0VKds0zs9HXw==
"@standardnotes/components@1.7.15": "@standardnotes/components@1.8.0":
version "1.7.15" version "1.8.0"
resolved "https://registry.yarnpkg.com/@standardnotes/components/-/components-1.7.15.tgz#3d57dd21cd4bf1f95eea7521fce7dd62e8f99455" resolved "https://registry.yarnpkg.com/@standardnotes/components/-/components-1.8.0.tgz#eaeadd6c0eb36c034cd2c9d6b625f463a0188482"
integrity sha512-fZOaqi62BDhVkqH6Bd9NQhVPyPR3KARob8xm/JhMLWt1d8G1ZFEQ/IOLTMFpYQCt9IroLG/pl6GD0Xj46ISgkw== integrity sha512-mgbekII0aMVkVuNZNXspLUoTP/fTf74Mz6ic91/8ca7S/e/4TcFg9OgTBV5Fqy/ShA42fhHEfJUG4QgRR+KdOw==
"@standardnotes/config@^2.4.1": "@standardnotes/config@^2.4.1":
version "2.4.1" version "2.4.1"
@@ -2450,27 +2450,27 @@
"@typescript-eslint/eslint-plugin" "^5.12.1" "@typescript-eslint/eslint-plugin" "^5.12.1"
"@typescript-eslint/parser" "^5.12.1" "@typescript-eslint/parser" "^5.12.1"
"@standardnotes/domain-events@^2.27.13": "@standardnotes/domain-events@^2.27.14":
version "2.27.13" version "2.27.14"
resolved "https://registry.yarnpkg.com/@standardnotes/domain-events/-/domain-events-2.27.13.tgz#ebe9d06b154fa8f40cbda6e4da507856f052f8d0" resolved "https://registry.yarnpkg.com/@standardnotes/domain-events/-/domain-events-2.27.14.tgz#3d3829f51d4d88b8c64a260b719e6edbc3bfb5f3"
integrity sha512-Cc+bDNtHFqLnNyr3freB9DmGWDVl1zrWPHHaEahfQyA8RnBkGrQ28xz5+0Q5lKLTsBjGppi7Py/VPD+QMvdNmg== integrity sha512-AsuNYvYs9L6Ib+FpLpp86OpP0p9wB0LNfwtJm4aXJtbHxi63vRO7xLZvfiFrGr9Fj8OB6aSZ6lH0TpUSk18H7w==
dependencies: dependencies:
"@standardnotes/auth" "^3.18.11" "@standardnotes/auth" "^3.18.11"
"@standardnotes/features" "^1.39.0" "@standardnotes/features" "^1.40.0"
"@standardnotes/encryption@^1.6.1": "@standardnotes/encryption@^1.6.2":
version "1.6.1" version "1.6.2"
resolved "https://registry.yarnpkg.com/@standardnotes/encryption/-/encryption-1.6.1.tgz#51d1d69a1c45e95d5486adfa624af4766fbb94a8" resolved "https://registry.yarnpkg.com/@standardnotes/encryption/-/encryption-1.6.2.tgz#936a1cc7c80fff06b4d960afb9e954569ef953ff"
integrity sha512-qKg5An1ZHY+gDHb96T8zEIQl5TcRaZNPeCP2zULQBEyLzhFDlyoz8MMRiZPa6yjiCDEODfwG87gQwlaFGkFR3Q== integrity sha512-KqM3ht58VDjQzNAlStFhUquURAHWZs3HH2agQFRGJZZop0KPQqzgs6LIXRgw0Yyo8MrnMreMobRxuINWj8ddGQ==
dependencies: dependencies:
"@standardnotes/models" "^1.6.2" "@standardnotes/models" "^1.6.3"
"@standardnotes/responses" "^1.6.13" "@standardnotes/responses" "^1.6.14"
"@standardnotes/services" "^1.10.1" "@standardnotes/services" "^1.10.2"
"@standardnotes/features@^1.39.0": "@standardnotes/features@^1.40.0":
version "1.39.0" version "1.40.0"
resolved "https://registry.yarnpkg.com/@standardnotes/features/-/features-1.39.0.tgz#d2234eec2664fbe8fc3946e2349d6d2c7348c2f1" resolved "https://registry.yarnpkg.com/@standardnotes/features/-/features-1.40.0.tgz#362f9656b6125cab61752d038981d67f12689fb6"
integrity sha512-r5vuJoDsi6W/ZomOYMMe0DC3YJLbhYDn9NkYglsi0/F5aQUciIg9hLUb0aRrQm/q9zQ5+zEJ8ukN6f5sJGYYcw== integrity sha512-3C/fj7/HQ6S3HZJujB92QW6ydc0oOaCrUPtYkCMHnjBV0qpGDtpzbR3GLYqmHpmiM5H87s9BppEQleiMu1+dyA==
dependencies: dependencies:
"@standardnotes/auth" "^3.18.11" "@standardnotes/auth" "^3.18.11"
"@standardnotes/common" "^1.19.6" "@standardnotes/common" "^1.19.6"
@@ -2483,43 +2483,43 @@
"@standardnotes/common" "^1.19.6" "@standardnotes/common" "^1.19.6"
"@standardnotes/utils" "^1.6.3" "@standardnotes/utils" "^1.6.3"
"@standardnotes/files@^1.0.5": "@standardnotes/files@^1.0.6":
version "1.0.5" version "1.0.6"
resolved "https://registry.yarnpkg.com/@standardnotes/files/-/files-1.0.5.tgz#ed293082167555ca89ce1c2a879eb7307064db61" resolved "https://registry.yarnpkg.com/@standardnotes/files/-/files-1.0.6.tgz#6fafd7114a014c32c320ecc328f1bfdf9f8856f1"
integrity sha512-ACsl9emP7S9OpCC8FcGZZxdxGJjMjZ3wvshevsjHqGtKcKQX/2/xU8FcUNg05VzvGMicV8Smekp+/6BvcNpjEA== integrity sha512-8/+bLEQP91JQKEh0du8lRRIuHV+jB62s+jTcK+nuyFkg72JR7Qgeu+tUpV3O3vassj9ZaZhO5s1+pgOiJU+QCg==
dependencies: dependencies:
"@standardnotes/models" "^1.6.2" "@standardnotes/models" "^1.6.3"
"@standardnotes/responses" "^1.6.13" "@standardnotes/responses" "^1.6.14"
"@standardnotes/services" "^1.10.1" "@standardnotes/services" "^1.10.2"
"@standardnotes/utils" "^1.6.3" "@standardnotes/utils" "^1.6.3"
"@standardnotes/models@^1.6.2": "@standardnotes/models@^1.6.3":
version "1.6.2" version "1.6.3"
resolved "https://registry.yarnpkg.com/@standardnotes/models/-/models-1.6.2.tgz#45ae19246ba1ac476ed26bbe3f9fa6e3ce6c9271" resolved "https://registry.yarnpkg.com/@standardnotes/models/-/models-1.6.3.tgz#d3d344cc877fd484f87171a3465d732f15f49199"
integrity sha512-WJtlDi2tj+HkG10VQygFbzUfon76N7gV73wuryocJ1LE5LiJT7O+LlQvrVM96QWxDdbjUXxA4fz++W0v4c6m7w== integrity sha512-qmOJsxgpov+VTzhJYc9uqTDP1+JvtFXjJSkhiHiA4E/ZRrejxvTl6iPu6ZF5gVHWf+93KsGhw449pc/kjj/5bA==
dependencies: dependencies:
"@standardnotes/features" "^1.39.0" "@standardnotes/features" "^1.40.0"
"@standardnotes/responses" "^1.6.13" "@standardnotes/responses" "^1.6.14"
"@standardnotes/utils" "^1.6.3" "@standardnotes/utils" "^1.6.3"
"@standardnotes/responses@^1.6.13": "@standardnotes/responses@^1.6.14":
version "1.6.13" version "1.6.14"
resolved "https://registry.yarnpkg.com/@standardnotes/responses/-/responses-1.6.13.tgz#aac7da9c9f158150d23e41238eade5fa6fe98b76" resolved "https://registry.yarnpkg.com/@standardnotes/responses/-/responses-1.6.14.tgz#5c9cc1b2dbf36c335350c31c4dc5ef2a971365e3"
integrity sha512-ponGQwqcZlt/5h8QzXnG5gJLiLG7uq6cYmsyQKVigLSSllv4eN/I7WLp1HWdZGcPPyh/+HCYk5z6YgSJM2Rulg== integrity sha512-iEPt0iaYyb1SjicAaKbH3jHgrExDz3R9fzCUlB0wh61B35g/8czi0thSzTmJI2DP3/hMqWOIUDj1M1piN+Rqog==
dependencies: dependencies:
"@standardnotes/auth" "^3.18.11" "@standardnotes/auth" "^3.18.11"
"@standardnotes/common" "^1.19.6" "@standardnotes/common" "^1.19.6"
"@standardnotes/features" "^1.39.0" "@standardnotes/features" "^1.40.0"
"@standardnotes/services@^1.10.1": "@standardnotes/services@^1.10.2":
version "1.10.1" version "1.10.2"
resolved "https://registry.yarnpkg.com/@standardnotes/services/-/services-1.10.1.tgz#ef166ead223d91b7124b9e0f134ae46e4479159c" resolved "https://registry.yarnpkg.com/@standardnotes/services/-/services-1.10.2.tgz#b21ac3596b1b9ea326dc7e99b0ac01e487c0a0db"
integrity sha512-Q55OMJXJS/HZpQ5Iwl20u3594kkE7VF9M/yap0H9kFQJcKlyYlsSF1W7QdyFCx5keHT0JLRzjY88muBcb9o9fA== integrity sha512-LGDa9OMjhSB/nu6uAIo7qG6D0cZ9jg/3dZzspBFx8Jz/eE/ZgjufNXm6iwu5FxhMtybLOw2fUkSCmA8mdCBNkA==
dependencies: dependencies:
"@standardnotes/auth" "^3.18.11" "@standardnotes/auth" "^3.18.11"
"@standardnotes/common" "^1.19.6" "@standardnotes/common" "^1.19.6"
"@standardnotes/models" "^1.6.2" "@standardnotes/models" "^1.6.3"
"@standardnotes/responses" "^1.6.13" "@standardnotes/responses" "^1.6.14"
"@standardnotes/utils" "^1.6.3" "@standardnotes/utils" "^1.6.3"
"@standardnotes/settings@^1.14.1": "@standardnotes/settings@^1.14.1":
@@ -2541,21 +2541,21 @@
buffer "^6.0.3" buffer "^6.0.3"
libsodium-wrappers "^0.7.9" libsodium-wrappers "^0.7.9"
"@standardnotes/snjs@2.105.3": "@standardnotes/snjs@2.106.0":
version "2.105.3" version "2.106.0"
resolved "https://registry.yarnpkg.com/@standardnotes/snjs/-/snjs-2.105.3.tgz#4eebce30c8cc6469ba6aada28ceb9825b47827c5" resolved "https://registry.yarnpkg.com/@standardnotes/snjs/-/snjs-2.106.0.tgz#2eecf2e1bfcbcd913ed993346c140fd857d93645"
integrity sha512-dYzGCbljJ6x88+kJX2iMS+8hyKGSvLljJqlmeWYVnoVPs9ZR2CkfZfjugaVkNIMS9mteuAO36t0YUyhQA6yBPQ== integrity sha512-EB1FXqVfe2f/Bgcl8xzi18wWkq9RWVxVDJVNJqYorALEY0yR/3ERjA8ZcV5trY4l0oLiG55rzYYTlgqDHj99/g==
dependencies: dependencies:
"@standardnotes/auth" "^3.18.11" "@standardnotes/auth" "^3.18.11"
"@standardnotes/common" "^1.19.6" "@standardnotes/common" "^1.19.6"
"@standardnotes/domain-events" "^2.27.13" "@standardnotes/domain-events" "^2.27.14"
"@standardnotes/encryption" "^1.6.1" "@standardnotes/encryption" "^1.6.2"
"@standardnotes/features" "^1.39.0" "@standardnotes/features" "^1.40.0"
"@standardnotes/filepicker" "^1.13.4" "@standardnotes/filepicker" "^1.13.4"
"@standardnotes/files" "^1.0.5" "@standardnotes/files" "^1.0.6"
"@standardnotes/models" "^1.6.2" "@standardnotes/models" "^1.6.3"
"@standardnotes/responses" "^1.6.13" "@standardnotes/responses" "^1.6.14"
"@standardnotes/services" "^1.10.1" "@standardnotes/services" "^1.10.2"
"@standardnotes/settings" "^1.14.1" "@standardnotes/settings" "^1.14.1"
"@standardnotes/sncrypto-common" "^1.8.0" "@standardnotes/sncrypto-common" "^1.8.0"
"@standardnotes/utils" "^1.6.3" "@standardnotes/utils" "^1.6.3"