refactor: blocks plugins (#1956)

This commit is contained in:
Mo
2022-11-08 13:31:48 -06:00
committed by GitHub
parent bfca244061
commit 70a9dbcea6
73 changed files with 1448 additions and 1049 deletions

View File

@@ -0,0 +1,36 @@
import { ReactNode, createContext, useContext, memo } from 'react'
import { observer } from 'mobx-react-lite'
import { WebApplication } from '@/Application/Application'
const ApplicationContext = createContext<WebApplication | undefined>(undefined)
export const useApplication = () => {
const value = useContext(ApplicationContext)
if (!value) {
throw new Error('Component must be a child of <ApplicationProvider />')
}
return value
}
type ChildrenProps = {
children: ReactNode
}
type ProviderProps = {
application: WebApplication
} & ChildrenProps
const MemoizedChildren = memo(({ children }: ChildrenProps) => <>{children}</>)
const ApplicationProvider = ({ application, children }: ProviderProps) => {
return (
<ApplicationContext.Provider value={application}>
<MemoizedChildren children={children} />
</ApplicationContext.Provider>
)
}
export default observer(ApplicationProvider)

View File

@@ -28,6 +28,7 @@ import ResponsivePaneProvider from '../ResponsivePane/ResponsivePaneProvider'
import AndroidBackHandlerProvider from '@/NativeMobileWeb/useAndroidBackHandler'
import ConfirmDeleteAccountContainer from '@/Components/ConfirmDeleteAccountModal/ConfirmDeleteAccountModal'
import DarkModeHandler from '../DarkModeHandler/DarkModeHandler'
import ApplicationProvider from './ApplicationProvider'
type Props = {
application: WebApplication
@@ -193,80 +194,85 @@ const ApplicationView: FunctionComponent<Props> = ({ application, mainApplicatio
}
return (
<AndroidBackHandlerProvider application={application}>
<DarkModeHandler application={application} />
<ResponsivePaneProvider paneController={application.getViewControllerManager().paneController}>
<PremiumModalProvider application={application} viewControllerManager={viewControllerManager}>
<div className={platformString + ' main-ui-view sn-component h-full'}>
<div id="app" className="app app-column-container" ref={appColumnContainerRef}>
<FileDragNDropProvider
application={application}
featuresController={viewControllerManager.featuresController}
filesController={viewControllerManager.filesController}
>
<Navigation application={application} />
<ContentListView
<ApplicationProvider application={application}>
<AndroidBackHandlerProvider application={application}>
<DarkModeHandler application={application} />
<ResponsivePaneProvider paneController={application.getViewControllerManager().paneController}>
<PremiumModalProvider application={application} viewControllerManager={viewControllerManager}>
<div className={platformString + ' main-ui-view sn-component h-full'}>
<div id="app" className="app app-column-container" ref={appColumnContainerRef}>
<FileDragNDropProvider
application={application}
accountMenuController={viewControllerManager.accountMenuController}
featuresController={viewControllerManager.featuresController}
filesController={viewControllerManager.filesController}
itemListController={viewControllerManager.itemListController}
navigationController={viewControllerManager.navigationController}
noAccountWarningController={viewControllerManager.noAccountWarningController}
>
<Navigation application={application} />
<ContentListView
application={application}
accountMenuController={viewControllerManager.accountMenuController}
filesController={viewControllerManager.filesController}
itemListController={viewControllerManager.itemListController}
navigationController={viewControllerManager.navigationController}
noAccountWarningController={viewControllerManager.noAccountWarningController}
notesController={viewControllerManager.notesController}
selectionController={viewControllerManager.selectionController}
searchOptionsController={viewControllerManager.searchOptionsController}
linkingController={viewControllerManager.linkingController}
/>
<NoteGroupView application={application} />
</FileDragNDropProvider>
</div>
<>
<Footer application={application} applicationGroup={mainApplicationGroup} />
<SessionsModal application={application} viewControllerManager={viewControllerManager} />
<PreferencesViewWrapper viewControllerManager={viewControllerManager} application={application} />
<RevisionHistoryModal
application={application}
historyModalController={viewControllerManager.historyModalController}
notesController={viewControllerManager.notesController}
selectionController={viewControllerManager.selectionController}
searchOptionsController={viewControllerManager.searchOptionsController}
linkingController={viewControllerManager.linkingController}
subscriptionController={viewControllerManager.subscriptionController}
/>
<NoteGroupView application={application} />
</FileDragNDropProvider>
</>
{renderChallenges()}
<>
<NotesContextMenu
application={application}
navigationController={viewControllerManager.navigationController}
notesController={viewControllerManager.notesController}
linkingController={viewControllerManager.linkingController}
historyModalController={viewControllerManager.historyModalController}
/>
<TagContextMenuWrapper
navigationController={viewControllerManager.navigationController}
featuresController={viewControllerManager.featuresController}
/>
<FileContextMenuWrapper
filesController={viewControllerManager.filesController}
selectionController={viewControllerManager.selectionController}
/>
<PurchaseFlowWrapper application={application} viewControllerManager={viewControllerManager} />
<ConfirmSignoutContainer
applicationGroup={mainApplicationGroup}
viewControllerManager={viewControllerManager}
application={application}
/>
<ToastContainer />
<FilePreviewModalWrapper application={application} viewControllerManager={viewControllerManager} />
<PermissionsModalWrapper application={application} />
<ConfirmDeleteAccountContainer
application={application}
viewControllerManager={viewControllerManager}
/>
</>
</div>
<>
<Footer application={application} applicationGroup={mainApplicationGroup} />
<SessionsModal application={application} viewControllerManager={viewControllerManager} />
<PreferencesViewWrapper viewControllerManager={viewControllerManager} application={application} />
<RevisionHistoryModal
application={application}
historyModalController={viewControllerManager.historyModalController}
notesController={viewControllerManager.notesController}
selectionController={viewControllerManager.selectionController}
subscriptionController={viewControllerManager.subscriptionController}
/>
</>
{renderChallenges()}
<>
<NotesContextMenu
application={application}
navigationController={viewControllerManager.navigationController}
notesController={viewControllerManager.notesController}
linkingController={viewControllerManager.linkingController}
historyModalController={viewControllerManager.historyModalController}
/>
<TagContextMenuWrapper
navigationController={viewControllerManager.navigationController}
featuresController={viewControllerManager.featuresController}
/>
<FileContextMenuWrapper
filesController={viewControllerManager.filesController}
selectionController={viewControllerManager.selectionController}
/>
<PurchaseFlowWrapper application={application} viewControllerManager={viewControllerManager} />
<ConfirmSignoutContainer
applicationGroup={mainApplicationGroup}
viewControllerManager={viewControllerManager}
application={application}
/>
<ToastContainer />
<FilePreviewModalWrapper application={application} viewControllerManager={viewControllerManager} />
<PermissionsModalWrapper application={application} />
<ConfirmDeleteAccountContainer application={application} viewControllerManager={viewControllerManager} />
</>
</div>
</PremiumModalProvider>
</ResponsivePaneProvider>
</AndroidBackHandlerProvider>
</PremiumModalProvider>
</ResponsivePaneProvider>
</AndroidBackHandlerProvider>
</ApplicationProvider>
)
}

View File

@@ -2,7 +2,11 @@ import { WebApplication } from '@/Application/Application'
import { SNNote } from '@standardnotes/snjs'
import { FunctionComponent, useCallback, useRef } from 'react'
import { BlockEditorController } from './BlockEditorController'
import { BlocksEditor } from '@standardnotes/blocks-editor'
import { BlocksEditor, BlocksEditorComposer } from '@standardnotes/blocks-editor'
import { ItemSelectionPlugin } from './Plugins/ItemSelectionPlugin/ItemSelectionPlugin'
import { FileNode } from './Plugins/EncryptedFilePlugin/Nodes/FileNode'
import FilePlugin from './Plugins/EncryptedFilePlugin/FilePlugin'
import BlockPickerMenuPlugin from './Plugins/BlockPickerPlugin/BlockPickerPlugin'
import { ErrorBoundary } from '@/Utils/ErrorBoundary'
const StringEllipses = '...'
@@ -30,11 +34,16 @@ export const BlockEditor: FunctionComponent<Props> = ({ note, application }) =>
return (
<div className="relative h-full w-full p-5">
<ErrorBoundary>
<BlocksEditor
onChange={handleChange}
initialValue={note.content.text}
className="relative relative resize-none text-base focus:shadow-none focus:outline-none"
/>
<BlocksEditorComposer initialValue={note.text} nodes={[FileNode]}>
<BlocksEditor
onChange={handleChange}
className="relative relative resize-none text-base focus:shadow-none focus:outline-none"
>
<ItemSelectionPlugin currentNote={note} />
<FilePlugin />
<BlockPickerMenuPlugin />
</BlocksEditor>
</BlocksEditorComposer>
</ErrorBoundary>
</div>
)

View File

@@ -0,0 +1,33 @@
import { PopoverItemClassNames, PopoverItemSelectedClassNames } from '@standardnotes/blocks-editor'
import { BlockPickerOption } from './BlockPickerOption'
export function BlockPickerMenuItem({
index,
isSelected,
onClick,
onMouseEnter,
option,
}: {
index: number
isSelected: boolean
onClick: () => void
onMouseEnter: () => void
option: BlockPickerOption
}) {
return (
<li
key={option.key}
tabIndex={-1}
className={`${PopoverItemClassNames} ${isSelected ? PopoverItemSelectedClassNames : ''}`}
ref={option.setRefElement}
role="option"
aria-selected={isSelected}
id={'typeahead-item-' + index}
onMouseEnter={onMouseEnter}
onClick={onClick}
>
<i className={`icon ${option.iconName} mr-[8px] flex h-5 w-5 bg-contain fill-current text-center`} />
<div className="">{option.title}</div>
</li>
)
}

View File

@@ -0,0 +1,31 @@
import { TypeaheadOption } from '@lexical/react/LexicalTypeaheadMenuPlugin'
export class BlockPickerOption extends TypeaheadOption {
// What shows up in the editor
title: string
// Icon for display
iconName?: string
// For extra searching.
keywords: Array<string>
// TBD
keyboardShortcut?: string
// What happens when you select this option?
onSelect: (queryString: string) => void
constructor(
title: string,
options: {
iconName?: string
keywords?: Array<string>
keyboardShortcut?: string
onSelect: (queryString: string) => void
},
) {
super(title)
this.title = title
this.keywords = options.keywords || []
this.iconName = options.iconName
this.keyboardShortcut = options.keyboardShortcut
this.onSelect = options.onSelect.bind(this)
}
}

View File

@@ -0,0 +1,143 @@
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
import { LexicalTypeaheadMenuPlugin, useBasicTypeaheadTriggerMatch } from '@lexical/react/LexicalTypeaheadMenuPlugin'
import { TextNode } from 'lexical'
import { useCallback, useMemo, useState } from 'react'
import { PopoverClassNames } from '@standardnotes/blocks-editor'
import useModal from '@standardnotes/blocks-editor/src/Lexical/Hooks/useModal'
import { InsertTableDialog } from '@standardnotes/blocks-editor/src/Lexical/Plugins/TablePlugin'
import { BlockPickerOption } from './BlockPickerOption'
import { BlockPickerMenuItem } from './BlockPickerMenuItem'
import { GetNumberedListBlock } from './Blocks/NumberedList'
import { GetBulletedListBlock } from './Blocks/BulletedList'
import { GetChecklistBlock } from './Blocks/Checklist'
import { GetDividerBlock } from './Blocks/Divider'
import { GetCollapsibleBlock } from './Blocks/Collapsible'
import { GetParagraphBlock } from './Blocks/Paragraph'
import { GetHeadingsBlocks } from './Blocks/Headings'
import { GetQuoteBlock } from './Blocks/Quote'
import { GetAlignmentBlocks } from './Blocks/Alignment'
import { GetCodeBlock } from './Blocks/Code'
import { GetEmbedsBlocks } from './Blocks/Embeds'
import { GetDynamicTableBlocks, GetTableBlock } from './Blocks/Table'
import Popover from '@/Components/Popover/Popover'
export default function BlockPickerMenuPlugin(): JSX.Element {
const [editor] = useLexicalComposerContext()
const [modal, showModal] = useModal()
const [queryString, setQueryString] = useState<string | null>(null)
const checkForTriggerMatch = useBasicTypeaheadTriggerMatch('/', {
minLength: 0,
})
const [popoverOpen, setPopoverOpen] = useState(true)
const options = useMemo(() => {
const baseOptions = [
GetParagraphBlock(editor),
...GetHeadingsBlocks(editor),
GetTableBlock(() =>
showModal('Insert Table', (onClose) => <InsertTableDialog activeEditor={editor} onClose={onClose} />),
),
GetNumberedListBlock(editor),
GetBulletedListBlock(editor),
GetChecklistBlock(editor),
GetQuoteBlock(editor),
GetCodeBlock(editor),
GetDividerBlock(editor),
...GetAlignmentBlocks(editor),
GetCollapsibleBlock(editor),
...GetEmbedsBlocks(editor),
]
const dynamicOptions = GetDynamicTableBlocks(editor, queryString || '')
return queryString
? [
...dynamicOptions,
...baseOptions.filter((option) => {
return new RegExp(queryString, 'gi').exec(option.title) || option.keywords != null
? option.keywords.some((keyword) => new RegExp(queryString, 'gi').exec(keyword))
: false
}),
]
: baseOptions
}, [editor, queryString, showModal])
const onSelectOption = useCallback(
(
selectedOption: BlockPickerOption,
nodeToRemove: TextNode | null,
closeMenu: () => void,
matchingString: string,
) => {
editor.update(() => {
if (nodeToRemove) {
nodeToRemove.remove()
}
selectedOption.onSelect(matchingString)
setPopoverOpen(false)
closeMenu()
})
},
[editor],
)
return (
<>
{modal}
<LexicalTypeaheadMenuPlugin<BlockPickerOption>
onQueryChange={setQueryString}
onSelectOption={onSelectOption}
triggerFn={checkForTriggerMatch}
options={options}
onClose={() => {
setPopoverOpen(false)
}}
onOpen={() => {
setPopoverOpen(true)
}}
menuRenderFn={(anchorElementRef, { selectedIndex, selectOptionAndCleanUp, setHighlightedIndex }) => {
if (!anchorElementRef.current || !options.length) {
return null
}
return (
<Popover
align="start"
anchorPoint={{
x: anchorElementRef.current.offsetLeft,
y: anchorElementRef.current.offsetTop + anchorElementRef.current.offsetHeight,
}}
className={'min-h-80 h-80'}
open={popoverOpen}
togglePopover={() => {
setPopoverOpen((prevValue) => !prevValue)
}}
>
<div className={PopoverClassNames}>
<ul>
{options.map((option, i: number) => (
<BlockPickerMenuItem
index={i}
isSelected={selectedIndex === i}
onClick={() => {
setHighlightedIndex(i)
selectOptionAndCleanUp(option)
}}
onMouseEnter={() => {
setHighlightedIndex(i)
}}
key={option.key}
option={option}
/>
))}
</ul>
</div>
</Popover>
)
}}
/>
</>
)
}

View File

@@ -0,0 +1,13 @@
import { BlockPickerOption } from '../BlockPickerOption'
import { FORMAT_ELEMENT_COMMAND, LexicalEditor, ElementFormatType } from 'lexical'
export function GetAlignmentBlocks(editor: LexicalEditor) {
return ['left', 'center', 'right', 'justify'].map(
(alignment) =>
new BlockPickerOption(`Align ${alignment}`, {
iconName: `${alignment}-align`,
keywords: ['align', 'justify', alignment],
onSelect: () => editor.dispatchCommand(FORMAT_ELEMENT_COMMAND, alignment as ElementFormatType),
}),
)
}

View File

@@ -0,0 +1,11 @@
import { BlockPickerOption } from '../BlockPickerOption'
import { LexicalEditor } from 'lexical'
import { INSERT_UNORDERED_LIST_COMMAND } from '@lexical/list'
export function GetBulletedListBlock(editor: LexicalEditor) {
return new BlockPickerOption('Bulleted List', {
iconName: 'bullet',
keywords: ['bulleted list', 'unordered list', 'ul'],
onSelect: () => editor.dispatchCommand(INSERT_UNORDERED_LIST_COMMAND, undefined),
})
}

View File

@@ -0,0 +1,11 @@
import { BlockPickerOption } from '../BlockPickerOption'
import { LexicalEditor } from 'lexical'
import { INSERT_CHECK_LIST_COMMAND } from '@lexical/list'
export function GetChecklistBlock(editor: LexicalEditor) {
return new BlockPickerOption('Check List', {
iconName: 'check',
keywords: ['check list', 'todo list'],
onSelect: () => editor.dispatchCommand(INSERT_CHECK_LIST_COMMAND, undefined),
})
}

View File

@@ -0,0 +1,25 @@
import { BlockPickerOption } from '../BlockPickerOption'
import { $wrapNodes } from '@lexical/selection'
import { $getSelection, $isRangeSelection, LexicalEditor } from 'lexical'
import { $createCodeNode } from '@lexical/code'
export function GetCodeBlock(editor: LexicalEditor) {
return new BlockPickerOption('Code', {
iconName: 'code',
keywords: ['javascript', 'python', 'js', 'codeblock'],
onSelect: () =>
editor.update(() => {
const selection = $getSelection()
if ($isRangeSelection(selection)) {
if (selection.isCollapsed()) {
$wrapNodes(selection, () => $createCodeNode())
} else {
const textContent = selection.getTextContent()
const codeNode = $createCodeNode()
selection.insertNodes([codeNode])
selection.insertRawText(textContent)
}
}
}),
})
}

View File

@@ -0,0 +1,11 @@
import { BlockPickerOption } from '../BlockPickerOption'
import { LexicalEditor } from 'lexical'
import { INSERT_COLLAPSIBLE_COMMAND } from '@standardnotes/blocks-editor/src/Lexical/Plugins/CollapsiblePlugin'
export function GetCollapsibleBlock(editor: LexicalEditor) {
return new BlockPickerOption('Collapsible', {
iconName: 'caret-right',
keywords: ['collapse', 'collapsible', 'toggle'],
onSelect: () => editor.dispatchCommand(INSERT_COLLAPSIBLE_COMMAND, undefined),
})
}

View File

@@ -0,0 +1,11 @@
import { BlockPickerOption } from '../BlockPickerOption'
import { LexicalEditor } from 'lexical'
import { INSERT_HORIZONTAL_RULE_COMMAND } from '@lexical/react/LexicalHorizontalRuleNode'
export function GetDividerBlock(editor: LexicalEditor) {
return new BlockPickerOption('Divider', {
iconName: 'horizontal-rule',
keywords: ['horizontal rule', 'divider', 'hr'],
onSelect: () => editor.dispatchCommand(INSERT_HORIZONTAL_RULE_COMMAND, undefined),
})
}

View File

@@ -0,0 +1,15 @@
import { BlockPickerOption } from '../BlockPickerOption'
import { LexicalEditor } from 'lexical'
import { INSERT_EMBED_COMMAND } from '@lexical/react/LexicalAutoEmbedPlugin'
import { EmbedConfigs } from '@standardnotes/blocks-editor/src/Lexical/Plugins/AutoEmbedPlugin'
export function GetEmbedsBlocks(editor: LexicalEditor) {
return EmbedConfigs.map(
(embedConfig) =>
new BlockPickerOption(`Embed ${embedConfig.contentName}`, {
iconName: embedConfig.iconName,
keywords: [...embedConfig.keywords, 'embed'],
onSelect: () => editor.dispatchCommand(INSERT_EMBED_COMMAND, embedConfig.type),
}),
)
}

View File

@@ -0,0 +1,21 @@
import { BlockPickerOption } from '../BlockPickerOption'
import { $wrapNodes } from '@lexical/selection'
import { $getSelection, $isRangeSelection, LexicalEditor } from 'lexical'
import { $createHeadingNode, HeadingTagType } from '@lexical/rich-text'
export function GetHeadingsBlocks(editor: LexicalEditor) {
return Array.from({ length: 3 }, (_, i) => i + 1).map(
(n) =>
new BlockPickerOption(`Heading ${n}`, {
iconName: `icon h${n}`,
keywords: ['heading', 'header', `h${n}`],
onSelect: () =>
editor.update(() => {
const selection = $getSelection()
if ($isRangeSelection(selection)) {
$wrapNodes(selection, () => $createHeadingNode(`h${n}` as HeadingTagType))
}
}),
}),
)
}

View File

@@ -0,0 +1,11 @@
import { BlockPickerOption } from '../BlockPickerOption'
import { LexicalEditor } from 'lexical'
import { INSERT_ORDERED_LIST_COMMAND } from '@lexical/list'
export function GetNumberedListBlock(editor: LexicalEditor) {
return new BlockPickerOption('Numbered List', {
iconName: 'number',
keywords: ['numbered list', 'ordered list', 'ol'],
onSelect: () => editor.dispatchCommand(INSERT_ORDERED_LIST_COMMAND, undefined),
})
}

View File

@@ -0,0 +1,17 @@
import { BlockPickerOption } from '../BlockPickerOption'
import { $wrapNodes } from '@lexical/selection'
import { $createParagraphNode, $getSelection, $isRangeSelection, LexicalEditor } from 'lexical'
export function GetParagraphBlock(editor: LexicalEditor) {
return new BlockPickerOption('Paragraph', {
iconName: 'paragraph',
keywords: ['normal', 'paragraph', 'p', 'text'],
onSelect: () =>
editor.update(() => {
const selection = $getSelection()
if ($isRangeSelection(selection)) {
$wrapNodes(selection, () => $createParagraphNode())
}
}),
})
}

View File

@@ -0,0 +1,18 @@
import { BlockPickerOption } from '../BlockPickerOption'
import { $wrapNodes } from '@lexical/selection'
import { $getSelection, $isRangeSelection, LexicalEditor } from 'lexical'
import { $createQuoteNode } from '@lexical/rich-text'
export function GetQuoteBlock(editor: LexicalEditor) {
return new BlockPickerOption('Quote', {
iconName: 'quote',
keywords: ['block quote'],
onSelect: () =>
editor.update(() => {
const selection = $getSelection()
if ($isRangeSelection(selection)) {
$wrapNodes(selection, () => $createQuoteNode())
}
}),
})
}

View File

@@ -0,0 +1,53 @@
import { BlockPickerOption } from '../BlockPickerOption'
import { LexicalEditor } from 'lexical'
import { INSERT_TABLE_COMMAND } from '@lexical/table'
export function GetTableBlock(onSelect: () => void) {
return new BlockPickerOption('Table', {
iconName: 'table',
keywords: ['table', 'grid', 'spreadsheet', 'rows', 'columns'],
onSelect,
})
}
export function GetDynamicTableBlocks(editor: LexicalEditor, queryString: string) {
const options: Array<BlockPickerOption> = []
if (queryString == null) {
return options
}
const fullTableRegex = new RegExp(/^([1-9]|10)x([1-9]|10)$/)
const partialTableRegex = new RegExp(/^([1-9]|10)x?$/)
const fullTableMatch = fullTableRegex.exec(queryString)
const partialTableMatch = partialTableRegex.exec(queryString)
if (fullTableMatch) {
const [rows, columns] = fullTableMatch[0].split('x').map((n: string) => parseInt(n, 10))
options.push(
new BlockPickerOption(`${rows}x${columns} Table`, {
iconName: 'table',
keywords: ['table'],
onSelect: () => editor.dispatchCommand(INSERT_TABLE_COMMAND, { columns: String(columns), rows: String(rows) }),
}),
)
} else if (partialTableMatch) {
const rows = parseInt(partialTableMatch[0], 10)
options.push(
...Array.from({ length: 5 }, (_, i) => i + 1).map(
(columns) =>
new BlockPickerOption(`${rows}x${columns} Table`, {
iconName: 'table',
keywords: ['table'],
onSelect: () =>
editor.dispatchCommand(INSERT_TABLE_COMMAND, { columns: String(columns), rows: String(rows) }),
}),
),
)
}
return options
}

View File

@@ -0,0 +1,30 @@
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
import { $insertNodeToNearestRoot } from '@lexical/utils'
import { COMMAND_PRIORITY_EDITOR } from 'lexical'
import { useEffect } from 'react'
import { FileNode } from './Nodes/FileNode'
import { $createFileNode } from './Nodes/FileUtils'
import { INSERT_FILE_COMMAND } from '@standardnotes/blocks-editor'
export default function FilePlugin(): JSX.Element | null {
const [editor] = useLexicalComposerContext()
useEffect(() => {
if (!editor.hasNodes([FileNode])) {
throw new Error('FilePlugin: FileNode not registered on editor')
}
return editor.registerCommand<string>(
INSERT_FILE_COMMAND,
(payload) => {
const fileNode = $createFileNode(payload)
$insertNodeToNearestRoot(fileNode)
return true
},
COMMAND_PRIORITY_EDITOR,
)
}, [editor])
return null
}

View File

@@ -0,0 +1,31 @@
import { BlockWithAlignableContents } from '@lexical/react/LexicalBlockWithAlignableContents'
import { useMemo } from 'react'
import { ElementFormatType, NodeKey } from 'lexical'
import { useApplication } from '@/Components/ApplicationView/ApplicationProvider'
import FilePreview from '@/Components/FilePreview/FilePreview'
import { FileItem } from '@standardnotes/snjs'
export type FileComponentProps = Readonly<{
className: Readonly<{
base: string
focus: string
}>
format: ElementFormatType | null
nodeKey: NodeKey
fileUuid: string
}>
export function FileComponent({ className, format, nodeKey, fileUuid }: FileComponentProps) {
const application = useApplication()
const file = useMemo(() => application.items.findItem<FileItem>(fileUuid), [application, fileUuid])
if (!file) {
return <div>Unable to find file {fileUuid}</div>
}
return (
<BlockWithAlignableContents className={className} format={format} nodeKey={nodeKey}>
<FilePreview file={file} application={application} />
</BlockWithAlignableContents>
)
}

View File

@@ -0,0 +1,81 @@
import { DOMConversionMap, DOMExportOutput, EditorConfig, ElementFormatType, LexicalEditor, NodeKey } from 'lexical'
import { DecoratorBlockNode } from '@lexical/react/LexicalDecoratorBlockNode'
import { $createFileNode, convertToFileElement } from './FileUtils'
import { FileComponent } from './FileComponent'
import { SerializedFileNode } from './SerializedFileNode'
export class FileNode extends DecoratorBlockNode {
__id: string
static getType(): string {
return 'snfile'
}
static clone(node: FileNode): FileNode {
return new FileNode(node.__id, node.__format, node.__key)
}
static importJSON(serializedNode: SerializedFileNode): FileNode {
const node = $createFileNode(serializedNode.fileUuid)
node.setFormat(serializedNode.format)
return node
}
exportJSON(): SerializedFileNode {
return {
...super.exportJSON(),
fileUuid: this.getId(),
version: 1,
type: 'snfile',
}
}
static importDOM(): DOMConversionMap<HTMLDivElement> | null {
return {
div: (domNode: HTMLDivElement) => {
if (!domNode.hasAttribute('data-lexical-file-uuid')) {
return null
}
return {
conversion: convertToFileElement,
priority: 2,
}
},
}
}
exportDOM(): DOMExportOutput {
const element = document.createElement('div')
element.setAttribute('data-lexical-file-uuid', this.__id)
const text = document.createTextNode(this.getTextContent())
element.append(text)
return { element }
}
constructor(id: string, format?: ElementFormatType, key?: NodeKey) {
super(format, key)
this.__id = id
}
getId(): string {
return this.__id
}
getTextContent(_includeInert?: boolean | undefined, _includeDirectionless?: false | undefined): string {
return `[File: ${this.__id}]`
}
decorate(_editor: LexicalEditor, config: EditorConfig): JSX.Element {
const embedBlockTheme = config.theme.embedBlock || {}
const className = {
base: embedBlockTheme.base || '',
focus: embedBlockTheme.focus || '',
}
return <FileComponent className={className} format={this.__format} nodeKey={this.getKey()} fileUuid={this.__id} />
}
isInline(): false {
return false
}
}

View File

@@ -0,0 +1,20 @@
import type { DOMConversionOutput, LexicalNode } from 'lexical'
import { FileNode } from './FileNode'
export function convertToFileElement(domNode: HTMLDivElement): DOMConversionOutput | null {
const fileUuid = domNode.getAttribute('data-lexical-file-uuid')
if (fileUuid) {
const node = $createFileNode(fileUuid)
return { node }
}
return null
}
export function $createFileNode(fileUuid: string): FileNode {
return new FileNode(fileUuid)
}
export function $isFileNode(node: FileNode | LexicalNode | null | undefined): node is FileNode {
return node instanceof FileNode
}

View File

@@ -0,0 +1,11 @@
import { Spread } from 'lexical'
import { SerializedDecoratorBlockNode } from '@lexical/react/LexicalDecoratorBlockNode'
export type SerializedFileNode = Spread<
{
fileUuid: string
version: 1
type: 'snfile'
},
SerializedDecoratorBlockNode
>

View File

@@ -0,0 +1,18 @@
import { FileItem } from '@standardnotes/snjs'
import { TypeaheadOption } from '@lexical/react/LexicalTypeaheadMenuPlugin'
export class ItemOption extends TypeaheadOption {
icon?: JSX.Element
constructor(
public item: FileItem,
public options: {
keywords?: Array<string>
keyboardShortcut?: string
onSelect: (queryString: string) => void
},
) {
super(item.title)
this.key = item.uuid
}
}

View File

@@ -0,0 +1,30 @@
import LinkedItemMeta from '@/Components/LinkedItems/LinkedItemMeta'
import { PopoverItemClassNames, PopoverItemSelectedClassNames } from '@standardnotes/blocks-editor'
import { ItemOption } from './ItemOption'
type Props = {
index: number
isSelected: boolean
onClick: () => void
onMouseEnter: () => void
option: ItemOption
searchQuery: string
}
export function ItemSelectionItemComponent({ index, isSelected, onClick, onMouseEnter, option, searchQuery }: Props) {
return (
<li
key={option.key}
tabIndex={-1}
className={`${PopoverItemClassNames} ${isSelected ? PopoverItemSelectedClassNames : ''}`}
ref={option.setRefElement}
role="option"
aria-selected={isSelected}
id={'typeahead-item-' + index}
onMouseEnter={onMouseEnter}
onClick={onClick}
>
<LinkedItemMeta item={option.item} searchQuery={searchQuery} />
</li>
)
}

View File

@@ -0,0 +1,114 @@
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
import { LexicalTypeaheadMenuPlugin, useBasicTypeaheadTriggerMatch } from '@lexical/react/LexicalTypeaheadMenuPlugin'
import { INSERT_FILE_COMMAND, PopoverClassNames } from '@standardnotes/blocks-editor'
import { TextNode } from 'lexical'
import { FunctionComponent, useCallback, useMemo, useState } from 'react'
import { ItemSelectionItemComponent } from './ItemSelectionItemComponent'
import { ItemOption } from './ItemOption'
import { useApplication } from '@/Components/ApplicationView/ApplicationProvider'
import { ContentType, FileItem, SNNote } from '@standardnotes/snjs'
import { getLinkingSearchResults } from '@/Utils/Items/Search/getSearchResults'
import Popover from '@/Components/Popover/Popover'
type Props = {
currentNote: SNNote
}
export const ItemSelectionPlugin: FunctionComponent<Props> = ({ currentNote }) => {
const application = useApplication()
const [editor] = useLexicalComposerContext()
const [queryString, setQueryString] = useState<string | null>('')
const checkForTriggerMatch = useBasicTypeaheadTriggerMatch('@', {
minLength: 0,
})
const [popoverOpen, setPopoverOpen] = useState(true)
const onSelectOption = useCallback(
(selectedOption: ItemOption, nodeToRemove: TextNode | null, closeMenu: () => void, matchingString: string) => {
editor.update(() => {
if (nodeToRemove) {
nodeToRemove.remove()
}
selectedOption.options.onSelect(matchingString)
setPopoverOpen(false)
closeMenu()
})
},
[editor],
)
const options = useMemo(() => {
const results = getLinkingSearchResults(queryString || '', application, currentNote, {
contentType: ContentType.File,
returnEmptyIfQueryEmpty: false,
})
const files = [...results.linkedItems, ...results.unlinkedItems] as FileItem[]
return files.map((file) => {
return new ItemOption(file, {
onSelect: (_queryString: string) => {
editor.dispatchCommand(INSERT_FILE_COMMAND, file.uuid)
},
})
})
}, [application, editor, currentNote, queryString])
return (
<LexicalTypeaheadMenuPlugin<ItemOption>
onQueryChange={setQueryString}
onSelectOption={onSelectOption}
triggerFn={checkForTriggerMatch}
options={options}
onClose={() => {
setPopoverOpen(false)
}}
onOpen={() => {
setPopoverOpen(true)
}}
menuRenderFn={(anchorElementRef, { selectedIndex, selectOptionAndCleanUp, setHighlightedIndex }) => {
if (!anchorElementRef.current || !options.length) {
return null
}
return (
<Popover
align="start"
anchorPoint={{
x: anchorElementRef.current.offsetLeft,
y: anchorElementRef.current.offsetTop + anchorElementRef.current.offsetHeight,
}}
className={'min-h-80 h-80'}
open={popoverOpen}
togglePopover={() => {
setPopoverOpen((prevValue) => !prevValue)
}}
>
<div className={PopoverClassNames}>
<ul>
{options.map((option, i: number) => (
<ItemSelectionItemComponent
searchQuery={queryString || ''}
index={i}
isSelected={selectedIndex === i}
onClick={() => {
setHighlightedIndex(i)
selectOptionAndCleanUp(option)
}}
onMouseEnter={() => {
setHighlightedIndex(i)
}}
key={option.key}
option={option}
/>
))}
</ul>
</div>
</Popover>
)
}}
/>
)
}

View File

@@ -5,6 +5,7 @@ import { FunctionComponent, useCallback, useRef, useState } from 'react'
import ChangeEditorMenu from './ChangeEditorMenu'
import Popover from '../Popover/Popover'
import RoundIconButton from '../Button/RoundIconButton'
import { getIconAndTintForNoteType } from '@/Utils/Items/Icons/getIconAndTintForNoteType'
type Props = {
application: WebApplication
@@ -24,9 +25,7 @@ const ChangeEditorButton: FunctionComponent<Props> = ({
const [selectedEditor, setSelectedEditor] = useState(() => {
return note ? application.componentManager.editorForNote(note) : undefined
})
const [selectedEditorIcon, selectedEditorIconTint] = application.iconsController.getIconAndTintForNoteType(
selectedEditor?.package_info.note_type,
)
const [selectedEditorIcon, selectedEditorIconTint] = getIconAndTintForNoteType(selectedEditor?.package_info.note_type)
const toggleMenu = useCallback(async () => {
const willMenuOpen = !isOpen

View File

@@ -11,9 +11,9 @@ import { AppPaneId } from '../ResponsivePane/AppPaneMetadata'
import { useContextMenuEvent } from '@/Hooks/useContextMenuEvent'
import { classNames } from '@/Utils/ConcatenateClassNames'
import { formatSizeToReadableString } from '@standardnotes/filepicker'
import { getIconForFileType } from '@/Utils/Items/Icons/getIconForFileType'
const FileListItem: FunctionComponent<DisplayableListItemProps<FileItem>> = ({
application,
filesController,
hideDate,
hideIcon,
@@ -66,10 +66,7 @@ const FileListItem: FunctionComponent<DisplayableListItemProps<FileItem>> = ({
}, [item, onSelect, toggleAppPane])
const IconComponent = () =>
getFileIconComponent(
application.iconsController.getIconForFileType((item as FileItem).mimeType),
'w-10 h-10 flex-shrink-0',
)
getFileIconComponent(getIconForFileType((item as FileItem).mimeType), 'w-10 h-10 flex-shrink-0')
useContextMenuEvent(listItemRef, openContextMenu)

View File

@@ -15,6 +15,7 @@ import ListItemNotePreviewText from './ListItemNotePreviewText'
import { ListItemTitle } from './ListItemTitle'
import { log, LoggingDomain } from '@/Logging'
import { classNames } from '@/Utils/ConcatenateClassNames'
import { getIconAndTintForNoteType } from '@/Utils/Items/Icons/getIconAndTintForNoteType'
const NoteListItem: FunctionComponent<DisplayableListItemProps<SNNote>> = ({
application,
@@ -37,7 +38,7 @@ const NoteListItem: FunctionComponent<DisplayableListItemProps<SNNote>> = ({
const editorForNote = application.componentManager.editorForNote(item as SNNote)
const editorName = editorForNote?.name ?? PLAIN_EDITOR_NAME
const [icon, tint] = application.iconsController.getIconAndTintForNoteType(editorForNote?.package_info.note_type)
const [icon, tint] = getIconAndTintForNoteType(editorForNote?.package_info.note_type)
const hasFiles = application.items.itemsReferencingItem(item).filter(isFile).length > 0
const openNoteContextMenu = (posX: number, posY: number) => {

View File

@@ -9,6 +9,7 @@ import { KeyboardKey } from '@standardnotes/ui-services'
import { ViewControllerManager } from '@/Controllers/ViewControllerManager'
import { observer } from 'mobx-react-lite'
import FilePreview from './FilePreview'
import { getIconForFileType } from '@/Utils/Items/Icons/getIconForFileType'
type Props = {
application: WebApplication
@@ -67,12 +68,8 @@ const FilePreviewModal: FunctionComponent<Props> = observer(({ application, view
)
const IconComponent = useMemo(
() =>
getFileIconComponent(
application.iconsController.getIconForFileType(currentFile.mimeType),
'w-6 h-6 flex-shrink-0',
),
[application.iconsController, currentFile.mimeType],
() => getFileIconComponent(getIconForFileType(currentFile.mimeType), 'w-6 h-6 flex-shrink-0'),
[currentFile.mimeType],
)
return (

View File

@@ -18,6 +18,8 @@ import { LinkingController } from '@/Controllers/LinkingController'
import { KeyboardKey } from '@standardnotes/ui-services'
import { ElementIds } from '@/Constants/ElementIDs'
import Menu from '../Menu/Menu'
import { getLinkingSearchResults } from '@/Utils/Items/Search/getSearchResults'
import { useApplication } from '../ApplicationView/ApplicationProvider'
type Props = {
linkingController: LinkingController
@@ -27,18 +29,11 @@ type Props = {
}
const ItemLinkAutocompleteInput = ({ linkingController, focusPreviousItem, focusedId, setFocusedId }: Props) => {
const {
tags,
getTitleForLinkedTag,
getLinkedItemIcon,
getSearchResults,
linkItemToSelectedItem,
createAndAddNewTag,
isEntitledToNoteLinking,
} = linkingController
const application = useApplication()
const { tags, linkItemToSelectedItem, createAndAddNewTag, isEntitledToNoteLinking, activeItem } = linkingController
const [searchQuery, setSearchQuery] = useState('')
const { unlinkedResults, shouldShowCreateTag } = getSearchResults(searchQuery)
const { unlinkedItems, shouldShowCreateTag } = getLinkingSearchResults(searchQuery, application, activeItem)
const [dropdownVisible, setDropdownVisible] = useState(false)
const [dropdownMaxHeight, setDropdownMaxHeight] = useState<number | 'auto'>('auto')
@@ -105,7 +100,7 @@ const ItemLinkAutocompleteInput = ({ linkingController, focusPreviousItem, focus
}
}, [focusedId])
const areSearchResultsVisible = dropdownVisible && (unlinkedResults.length > 0 || shouldShowCreateTag)
const areSearchResultsVisible = dropdownVisible && (unlinkedItems.length > 0 || shouldShowCreateTag)
const handleMenuKeyDown: KeyboardEventHandler<HTMLMenuElement> = useCallback((event) => {
if (event.key === KeyboardKey.Escape) {
@@ -155,10 +150,8 @@ const ItemLinkAutocompleteInput = ({ linkingController, focusPreviousItem, focus
>
<LinkedItemSearchResults
createAndAddNewTag={createAndAddNewTag}
getLinkedItemIcon={getLinkedItemIcon}
getTitleForLinkedTag={getTitleForLinkedTag}
linkItemToSelectedItem={linkItemToSelectedItem}
results={unlinkedResults}
results={unlinkedItems}
searchQuery={searchQuery}
shouldShowCreateTag={shouldShowCreateTag}
onClickCallback={() => setSearchQuery('')}

View File

@@ -1,15 +1,18 @@
import { ItemLink, LinkableItem, LinkingController } from '@/Controllers/LinkingController'
import { LinkingController } from '@/Controllers/LinkingController'
import { classNames } from '@/Utils/ConcatenateClassNames'
import { KeyboardKey } from '@standardnotes/ui-services'
import { observer } from 'mobx-react-lite'
import { KeyboardEventHandler, MouseEventHandler, useEffect, useRef, useState } from 'react'
import { ContentType } from '@standardnotes/snjs'
import Icon from '../Icon/Icon'
import { ItemLink } from '@/Utils/Items/Search/ItemLink'
import { LinkableItem } from '@/Utils/Items/Search/LinkableItem'
import { getIconForItem } from '@/Utils/Items/Icons/getIconForItem'
import { useApplication } from '../ApplicationView/ApplicationProvider'
import { getTitleForLinkedTag } from '@/Utils/Items/Display/getTitleForLinkedTag'
type Props = {
link: ItemLink
getItemIcon: LinkingController['getLinkedItemIcon']
getTitleForLinkedTag: LinkingController['getTitleForLinkedTag']
activateItem: (item: LinkableItem) => Promise<void>
unlinkItem: LinkingController['unlinkItemFromSelectedItem']
focusPreviousItem: () => void
@@ -21,8 +24,6 @@ type Props = {
const LinkedItemBubble = ({
link,
getItemIcon,
getTitleForLinkedTag,
activateItem,
unlinkItem,
focusPreviousItem,
@@ -32,6 +33,7 @@ const LinkedItemBubble = ({
isBidirectional,
}: Props) => {
const ref = useRef<HTMLButtonElement>(null)
const application = useApplication()
const [showUnlinkButton, setShowUnlinkButton] = useState(false)
const unlinkButtonRef = useRef<HTMLAnchorElement | null>(null)
@@ -80,8 +82,8 @@ const LinkedItemBubble = ({
}
}
const [icon, iconClassName] = getItemIcon(link.item)
const tagTitle = getTitleForLinkedTag(link.item)
const [icon, iconClassName] = getIconForItem(link.item, application)
const tagTitle = getTitleForLinkedTag(link.item, application)
useEffect(() => {
if (link.id === focusedId) {

View File

@@ -1,12 +1,14 @@
import { observer } from 'mobx-react-lite'
import ItemLinkAutocompleteInput from './ItemLinkAutocompleteInput'
import { ItemLink, LinkableItem, LinkingController } from '@/Controllers/LinkingController'
import { LinkingController } from '@/Controllers/LinkingController'
import LinkedItemBubble from './LinkedItemBubble'
import { useCallback, useState } from 'react'
import { useResponsiveAppPane } from '../ResponsivePane/ResponsivePaneProvider'
import { ElementIds } from '@/Constants/ElementIDs'
import { classNames } from '@/Utils/ConcatenateClassNames'
import { ContentType } from '@standardnotes/snjs'
import { LinkableItem } from '@/Utils/Items/Search/LinkableItem'
import { ItemLink } from '@/Utils/Items/Search/ItemLink'
type Props = {
linkingController: LinkingController
@@ -19,8 +21,6 @@ const LinkedItemBubblesContainer = ({ linkingController }: Props) => {
notesLinkingToActiveItem,
filesLinkingToActiveItem,
unlinkItemFromSelectedItem: unlinkItem,
getTitleForLinkedTag,
getLinkedItemIcon: getItemIcon,
activateItem,
} = linkingController
@@ -86,8 +86,6 @@ const LinkedItemBubblesContainer = ({ linkingController }: Props) => {
<LinkedItemBubble
link={link}
key={link.id}
getItemIcon={getItemIcon}
getTitleForLinkedTag={getTitleForLinkedTag}
activateItem={activateItemAndTogglePane}
unlinkItem={unlinkItem}
focusPreviousItem={focusPreviousItem}

View File

@@ -1,22 +1,21 @@
import { LinkableItem, LinkingController } from '@/Controllers/LinkingController'
import { splitQueryInString } from '@/Utils'
import { classNames } from '@/Utils/ConcatenateClassNames'
import { getTitleForLinkedTag } from '@/Utils/Items/Display/getTitleForLinkedTag'
import { getIconForItem } from '@/Utils/Items/Icons/getIconForItem'
import { LinkableItem } from '@/Utils/Items/Search/LinkableItem'
import { observer } from 'mobx-react-lite'
import { useApplication } from '../ApplicationView/ApplicationProvider'
import Icon from '../Icon/Icon'
const LinkedItemMeta = ({
item,
getItemIcon,
getTitleForLinkedTag,
searchQuery,
}: {
type Props = {
item: LinkableItem
getItemIcon: LinkingController['getLinkedItemIcon']
getTitleForLinkedTag: LinkingController['getTitleForLinkedTag']
searchQuery?: string
}) => {
const [icon, className] = getItemIcon(item)
const tagTitle = getTitleForLinkedTag(item)
}
const LinkedItemMeta = ({ item, searchQuery }: Props) => {
const application = useApplication()
const [icon, className] = getIconForItem(item, application)
const tagTitle = getTitleForLinkedTag(item, application)
const title = item.title ?? ''
return (

View File

@@ -1,15 +1,14 @@
import { LinkableItem, LinkingController } from '@/Controllers/LinkingController'
import { LinkingController } from '@/Controllers/LinkingController'
import { usePremiumModal } from '@/Hooks/usePremiumModal'
import { observer } from 'mobx-react-lite'
import { SNNote } from '@standardnotes/snjs'
import Icon from '../Icon/Icon'
import { PremiumFeatureIconName } from '../Icon/PremiumFeatureIcon'
import LinkedItemMeta from './LinkedItemMeta'
import { LinkableItem } from '@/Utils/Items/Search/LinkableItem'
type Props = {
createAndAddNewTag: LinkingController['createAndAddNewTag']
getLinkedItemIcon: LinkingController['getLinkedItemIcon']
getTitleForLinkedTag: LinkingController['getTitleForLinkedTag']
linkItemToSelectedItem: LinkingController['linkItemToSelectedItem']
results: LinkableItem[]
searchQuery: string
@@ -20,8 +19,6 @@ type Props = {
const LinkedItemSearchResults = ({
createAndAddNewTag,
getLinkedItemIcon,
getTitleForLinkedTag,
linkItemToSelectedItem,
results,
searchQuery,
@@ -48,12 +45,7 @@ const LinkedItemSearchResults = ({
}
}}
>
<LinkedItemMeta
item={result}
getItemIcon={getLinkedItemIcon}
getTitleForLinkedTag={getTitleForLinkedTag}
searchQuery={searchQuery}
/>
<LinkedItemMeta item={result} searchQuery={searchQuery} />
{cannotLinkItem && <Icon type={PremiumFeatureIconName} className="ml-auto flex-shrink-0 text-info" />}
</button>
)

View File

@@ -1,13 +1,17 @@
import { FeaturesController } from '@/Controllers/FeaturesController'
import { FilesController } from '@/Controllers/FilesController'
import { LinkableItem, LinkingController } from '@/Controllers/LinkingController'
import { LinkingController } from '@/Controllers/LinkingController'
import { classNames } from '@/Utils/ConcatenateClassNames'
import { formatDateForContextMenu } from '@/Utils/DateUtils'
import { getIconForItem } from '@/Utils/Items/Icons/getIconForItem'
import { getLinkingSearchResults } from '@/Utils/Items/Search/getSearchResults'
import { LinkableItem } from '@/Utils/Items/Search/LinkableItem'
import { formatSizeToReadableString } from '@standardnotes/filepicker'
import { FileItem } from '@standardnotes/snjs'
import { KeyboardKey } from '@standardnotes/ui-services'
import { observer } from 'mobx-react-lite'
import { ChangeEventHandler, useEffect, useRef, useState } from 'react'
import { useApplication } from '../ApplicationView/ApplicationProvider'
import { PopoverFileItemActionType } from '../AttachedFilesPopover/PopoverFileItemAction'
import ClearInputButton from '../ClearInputButton/ClearInputButton'
import Icon from '../Icon/Icon'
@@ -22,29 +26,26 @@ import LinkedItemSearchResults from './LinkedItemSearchResults'
const LinkedItemsSectionItem = ({
activateItem,
getItemIcon,
getTitleForLinkedTag,
item,
searchQuery,
unlinkItem,
handleFileAction,
}: {
activateItem: LinkingController['activateItem']
getItemIcon: LinkingController['getLinkedItemIcon']
getTitleForLinkedTag: LinkingController['getTitleForLinkedTag']
item: LinkableItem
searchQuery?: string
unlinkItem: () => void
handleFileAction: FilesController['handleFileAction']
}) => {
const menuButtonRef = useRef<HTMLButtonElement>(null)
const application = useApplication()
const [isMenuOpen, setIsMenuOpen] = useState(false)
const toggleMenu = () => setIsMenuOpen((open) => !open)
const [isRenamingFile, setIsRenamingFile] = useState(false)
const [icon, className] = getItemIcon(item)
const [icon, className] = getIconForItem(item, application)
const title = item.title ?? ''
const renameFile = async (name: string) => {
@@ -93,12 +94,7 @@ const LinkedItemsSectionItem = ({
toggleMenu()
}}
>
<LinkedItemMeta
item={item}
getItemIcon={getItemIcon}
getTitleForLinkedTag={getTitleForLinkedTag}
searchQuery={searchQuery}
/>
<LinkedItemMeta item={item} searchQuery={searchQuery} />
</button>
)}
<button
@@ -174,23 +170,26 @@ const LinkedItemsPanel = ({
notesLinkedToItem,
notesLinkingToActiveItem,
allItemLinks: allLinkedItems,
getTitleForLinkedTag,
getLinkedItemIcon,
getSearchResults,
linkItemToSelectedItem,
unlinkItemFromSelectedItem,
activateItem,
createAndAddNewTag,
isEntitledToNoteLinking,
activeItem,
} = linkingController
const { hasFiles } = featuresController
const application = useApplication()
const fileInputRef = useRef<HTMLInputElement | null>(null)
const searchInputRef = useRef<HTMLInputElement | null>(null)
const [searchQuery, setSearchQuery] = useState('')
const isSearching = !!searchQuery.length
const { linkedResults, unlinkedResults, shouldShowCreateTag } = getSearchResults(searchQuery)
const { linkedResults, unlinkedItems, shouldShowCreateTag } = getLinkingSearchResults(
searchQuery,
application,
activeItem,
)
useEffect(() => {
if (isOpen && searchInputRef.current) {
@@ -227,7 +226,7 @@ const LinkedItemsPanel = ({
<form
className={classNames(
'sticky top-0 z-10 bg-default px-2.5 pt-2.5',
allLinkedItems.length || linkedResults.length || unlinkedResults.length || notesLinkingToActiveItem.length
allLinkedItems.length || linkedResults.length || unlinkedItems.length || notesLinkingToActiveItem.length
? 'border-b border-border pb-2.5'
: 'pb-1',
)}
@@ -254,15 +253,13 @@ const LinkedItemsPanel = ({
<div className="divide-y divide-border">
{isSearching ? (
<>
{(!!unlinkedResults.length || shouldShowCreateTag) && (
{(!!unlinkedItems.length || shouldShowCreateTag) && (
<div>
<div className="mt-3 mb-1 px-3 text-menu-item font-semibold uppercase text-passive-0">Unlinked</div>
<LinkedItemSearchResults
createAndAddNewTag={createAndAddNewTag}
getLinkedItemIcon={getLinkedItemIcon}
getTitleForLinkedTag={getTitleForLinkedTag}
linkItemToSelectedItem={linkItemToSelectedItem}
results={unlinkedResults}
results={unlinkedItems}
searchQuery={searchQuery}
shouldShowCreateTag={shouldShowCreateTag}
isEntitledToNoteLinking={isEntitledToNoteLinking}
@@ -281,8 +278,6 @@ const LinkedItemsPanel = ({
<LinkedItemsSectionItem
key={link.id}
item={link.item}
getItemIcon={getLinkedItemIcon}
getTitleForLinkedTag={getTitleForLinkedTag}
searchQuery={searchQuery}
unlinkItem={() => unlinkItemFromSelectedItem(link)}
activateItem={activateItem}
@@ -303,8 +298,6 @@ const LinkedItemsPanel = ({
<LinkedItemsSectionItem
key={link.id}
item={link.item}
getItemIcon={getLinkedItemIcon}
getTitleForLinkedTag={getTitleForLinkedTag}
searchQuery={searchQuery}
unlinkItem={() => unlinkItemFromSelectedItem(link)}
activateItem={activateItem}
@@ -336,8 +329,6 @@ const LinkedItemsPanel = ({
<LinkedItemsSectionItem
key={link.id}
item={link.item}
getItemIcon={getLinkedItemIcon}
getTitleForLinkedTag={getTitleForLinkedTag}
searchQuery={searchQuery}
unlinkItem={() => unlinkItemFromSelectedItem(link)}
activateItem={activateItem}
@@ -357,8 +348,6 @@ const LinkedItemsPanel = ({
<LinkedItemsSectionItem
key={link.id}
item={link.item}
getItemIcon={getLinkedItemIcon}
getTitleForLinkedTag={getTitleForLinkedTag}
searchQuery={searchQuery}
unlinkItem={() => unlinkItemFromSelectedItem(link)}
activateItem={activateItem}
@@ -376,8 +365,6 @@ const LinkedItemsPanel = ({
<LinkedItemsSectionItem
key={link.id}
item={link.item}
getItemIcon={getLinkedItemIcon}
getTitleForLinkedTag={getTitleForLinkedTag}
searchQuery={searchQuery}
unlinkItem={() => unlinkItemFromSelectedItem(link)}
activateItem={activateItem}
@@ -397,8 +384,6 @@ const LinkedItemsPanel = ({
<LinkedItemsSectionItem
key={link.id}
item={link.item}
getItemIcon={getLinkedItemIcon}
getTitleForLinkedTag={getTitleForLinkedTag}
searchQuery={searchQuery}
unlinkItem={() => unlinkItemFromSelectedItem(link)}
activateItem={activateItem}

View File

@@ -33,7 +33,7 @@ import {
import { confirmDialog, KeyboardKey, KeyboardModifier } from '@standardnotes/ui-services'
import { ChangeEventHandler, createRef, KeyboardEventHandler, RefObject } from 'react'
import { EditorEventSource } from '../../Types/EditorEventSource'
import { BlockEditor } from '../BlockEditor/BlockEditor'
import { BlockEditor } from '../BlockEditor/BlockEditorComponent'
import IndicatorCircle from '../IndicatorCircle/IndicatorCircle'
import LinkedItemBubblesContainer from '../LinkedItems/LinkedItemBubblesContainer'
import LinkedItemsButton from '../LinkedItems/LinkedItemsButton'

View File

@@ -5,13 +5,13 @@ import { NavigationController } from '@/Controllers/Navigation/NavigationControl
import { NotesController } from '@/Controllers/NotesController'
import { KeyboardKey } from '@standardnotes/ui-services'
import Popover from '../Popover/Popover'
import { LinkingController } from '@/Controllers/LinkingController'
import { IconType } from '@standardnotes/snjs'
import { getTitleForLinkedTag } from '@/Utils/Items/Display/getTitleForLinkedTag'
import { useApplication } from '../ApplicationView/ApplicationProvider'
type Props = {
navigationController: NavigationController
notesController: NotesController
linkingController: LinkingController
className: string
iconClassName: string
}
@@ -19,10 +19,10 @@ type Props = {
const AddTagOption: FunctionComponent<Props> = ({
navigationController,
notesController,
linkingController,
className,
iconClassName,
}) => {
const application = useApplication()
const menuContainerRef = useRef<HTMLDivElement>(null)
const buttonRef = useRef<HTMLButtonElement>(null)
@@ -79,7 +79,7 @@ const AddTagOption: FunctionComponent<Props> = ({
className={`overflow-hidden overflow-ellipsis whitespace-nowrap
${notesController.isTagInSelectedNotes(tag) ? 'font-bold' : ''}`}
>
{linkingController.getTitleForLinkedTag(tag)?.longTitle}
{getTitleForLinkedTag(tag, application)?.longTitle}
</span>
</button>
))}

View File

@@ -28,7 +28,10 @@ type DeletePermanentlyButtonProps = {
const DeletePermanentlyButton = ({ onClick }: DeletePermanentlyButtonProps) => (
<button
className="flex w-full cursor-pointer items-center border-0 bg-transparent px-3 py-1.5 text-left text-mobile-menu-item text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none md:text-menu-item"
className={classNames(
'flex w-full cursor-pointer items-center border-0 bg-transparent px-3 py-1.5 text-left text-mobile-menu-item',
'text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none md:text-menu-item',
)}
onClick={onClick}
>
<Icon type="close" className="mr-2 text-danger" />
@@ -183,7 +186,6 @@ const NotesOptions = ({
application,
navigationController,
notesController,
linkingController,
historyModalController,
closeMenu,
}: NotesOptionsProps) => {
@@ -353,7 +355,6 @@ const NotesOptions = ({
className={switchClassNames}
navigationController={navigationController}
notesController={notesController}
linkingController={linkingController}
/>
)}