Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ export class NoteViewController implements ItemViewControllerInterface {

this.needsInit = false

const addTagHierarchy = this.preferences.getValue(PrefKey.NoteAddToParentFolders, true)
const shouldAddTagHierarchy = this.preferences.getValue(PrefKey.NoteAddToParentFolders, true)

if (!this.item) {
log(LoggingDomain.NoteView, 'Initializing as template note')
Expand Down Expand Up @@ -141,7 +141,7 @@ export class NoteViewController implements ItemViewControllerInterface {

if (this.defaultTagUuid) {
const tag = this.items.findItem(this.defaultTagUuid) as SNTag
await this.mutator.addTagToNote(note, tag, addTagHierarchy)
await this.mutator.addTagToNote(note, tag, shouldAddTagHierarchy)
}

this.notifyObservers(this.item, PayloadEmitSource.InitialObserverRegistrationPush)
Expand Down
4 changes: 4 additions & 0 deletions packages/web/src/javascripts/Components/NoteView/NoteView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -800,6 +800,10 @@ class NoteView extends AbstractComponent<NoteViewProps, State> {
}

triggerSyncOnAction = () => {
if (!this.controller) {
// component might've already unmounted
return
}
this.controller.syncNow()
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
import { $getSelection, $isRangeSelection, COMMAND_PRIORITY_EDITOR, createCommand } from 'lexical'
import { useEffect } from 'react'
import { useApplication } from '../../ApplicationProvider'
import { NativeFeatureIdentifier, SNNote } from '@standardnotes/snjs'
import { $generateJSONFromSelectedNodes } from '@lexical/clipboard'
import { HeadlessSuperConverter } from '../Tools/HeadlessSuperConverter'
import { INSERT_BUBBLE_COMMAND } from './Commands'

export const CREATE_NOTE_FROM_SELECTION_COMMAND = createCommand<void>('CREATE_NOTE_FROM_SELECTION_COMMAND')

export function NoteFromSelectionPlugin({ currentNote }: { currentNote: SNNote }) {
const application = useApplication()
const [editor] = useLexicalComposerContext()

useEffect(() => {
async function insertAndLinkNewNoteFromJSON(json: string) {
editor.setEditable(false)
try {
const insertedNote = await application.notesController.createNoteWithContent(
NativeFeatureIdentifier.TYPES.SuperEditor,
application.itemListController.titleForNewNote(),
json,
)
await application.linkingController.linkItems(currentNote, insertedNote)
editor.dispatchCommand(INSERT_BUBBLE_COMMAND, insertedNote.uuid)
} catch (error) {
console.error(error)
} finally {
editor.setEditable(true)
}
}

return editor.registerCommand(
CREATE_NOTE_FROM_SELECTION_COMMAND,
function createNoteFromSelection() {
const selection = $getSelection()
if (!$isRangeSelection(selection)) {
return true
}
const { nodes } = $generateJSONFromSelectedNodes(editor, selection)
const converter = new HeadlessSuperConverter()
const json = converter.getStringifiedJSONFromSerializedNodes(nodes)
insertAndLinkNewNoteFromJSON(json).catch(console.error)
return true
},
COMMAND_PRIORITY_EDITOR,
)
}, [application.itemListController, application.linkingController, application.notesController, currentNote, editor])

return null
}
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ import { ElementIds } from '@/Constants/ElementIDs'
import { $isDecoratorBlockNode } from '@lexical/react/LexicalDecoratorBlockNode'
import LinkViewer from './LinkViewer'
import { OPEN_FILE_UPLOAD_MODAL_COMMAND } from '../EncryptedFilePlugin/FilePlugin'
import { CREATE_NOTE_FROM_SELECTION_COMMAND } from '../NoteFromSelectionPlugin'

const TOGGLE_LINK_AND_EDIT_COMMAND = createCommand<string | null>('TOGGLE_LINK_AND_EDIT_COMMAND')

Expand Down Expand Up @@ -110,8 +111,8 @@ const blockTypeToIconName = {
quote: 'quote',
}

interface ToolbarButtonProps extends ComponentPropsWithoutRef<'button'> {
name: string
interface ToolbarButtonProps extends Omit<ComponentPropsWithoutRef<'button'>, 'name'> {
name: NonNullable<ReactNode>
active?: boolean
iconName?: string
children?: ReactNode
Expand Down Expand Up @@ -222,6 +223,8 @@ const ToolbarPlugin = () => {
const [isCode, setIsCode] = useState(false)
const [isHighlight, setIsHighlight] = useState(false)

const [hasNonCollapsedSelection, setHasNonCollapsedSelection] = useState(false)

const [linkNode, setLinkNode] = useState<LinkNode | null>(null)
const [linkTextNode, setLinkTextNode] = useState<TextNode | null>(null)
const [isEditingLink, setIsEditingLink] = useState(false)
Expand Down Expand Up @@ -312,6 +315,8 @@ const ToolbarPlugin = () => {
return
}

setHasNonCollapsedSelection(!selection.isCollapsed())

const anchorNode = selection.anchor.getNode()
const focusNode = selection.focus.getNode()
const isAnchorSameAsFocus = anchorNode === focusNode
Expand Down Expand Up @@ -796,6 +801,23 @@ const ToolbarPlugin = () => {
<Icon type="chevron-down" size="custom" className="ml-2 h-4 w-4 md:h-3.5 md:w-3.5" />
</ToolbarButton>
)}
{hasNonCollapsedSelection && (
<ToolbarButton
name={
<>
<div className="mb-1 font-semibold">Create new note from selection</div>
<div className="max-w-[35ch] text-xs">
Creates a new note containing the current selection and replaces the selection with a link to the
new note.
</div>
</>
}
iconName="notes"
onSelect={() => {
editor.dispatchCommand(CREATE_NOTE_FROM_SELECTION_COMMAND, undefined)
}}
/>
)}
</Toolbar>
{isMobile && (
<button
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import { useLocalPreference } from '@/Hooks/usePreference'
import BlockPickerMenuPlugin from './Plugins/BlockPickerPlugin/BlockPickerPlugin'
import { EditorEventSource } from '@/Types/EditorEventSource'
import { ElementIds } from '@/Constants/ElementIDs'
import { NoteFromSelectionPlugin } from './Plugins/NoteFromSelectionPlugin'

export const SuperNotePreviewCharLimit = 160

Expand Down Expand Up @@ -171,6 +172,7 @@ export const SuperEditor: FunctionComponent<Props> = ({
(itemUuid: string) => {
const item = application.items.findItem(itemUuid)
if (item) {
// TODO: We should only unlink item if all link bubbles to that item have been removed from the note
linkingController.unlinkItemFromSelectedItem(item).catch(console.error)
}
},
Expand Down Expand Up @@ -272,6 +274,7 @@ export const SuperEditor: FunctionComponent<Props> = ({
{readonly === undefined && <ReadonlyPlugin note={note.current} />}
<AutoFocusPlugin isEnabled={controller.isTemplateNote} />
<BlockPickerMenuPlugin />
<NoteFromSelectionPlugin currentNote={note.current} />
</BlocksEditor>
</BlocksEditorComposer>
</FilesControllerProvider>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
import { createHeadlessEditor } from '@lexical/headless'
import { FileItem, PrefKey, PrefValue, SuperConverterServiceInterface } from '@standardnotes/snjs'
import { $createParagraphNode, $getRoot, $insertNodes, $isParagraphNode, LexicalEditor, LexicalNode } from 'lexical'
import {
$createParagraphNode,
$getRoot,
$insertNodes,
$isParagraphNode,
LexicalEditor,
LexicalNode,
SerializedLexicalNode,
} from 'lexical'
import BlocksEditorTheme from '../Lexical/Theme/Theme'
import { BlockEditorNodes, SuperExportNodes } from '../Lexical/Nodes/AllNodes'
import { MarkdownTransformers } from '../MarkdownTransformers'
Expand All @@ -12,6 +20,7 @@ import { $convertToMarkdownString } from '../Lexical/Utils/MarkdownExport'
import { parseFileName } from '@standardnotes/utils'
import { $dfs } from '@lexical/utils'
import { $isFileNode } from '../Plugins/EncryptedFilePlugin/Nodes/FileUtils'
import { $generateNodesFromSerializedNodes, $insertGeneratedNodes } from '@lexical/clipboard'

export class HeadlessSuperConverter implements SuperConverterServiceInterface {
private importEditor: LexicalEditor
Expand Down Expand Up @@ -295,4 +304,28 @@ export class HeadlessSuperConverter implements SuperConverterServiceInterface {

return ids
}

/**
* Serialized nodes (usually generated by `$generateJSONFromSelectedNodes`) cannot be imported into
* Lexical if they were directly stringified. This function handles the process of generating actual
* Lexical nodes from the serialized ones, inserting them into an empty editor and then exporting the
* editor state of that as a JSON, which can then be used to create a new note.
*/
getStringifiedJSONFromSerializedNodes(serializedNodes: SerializedLexicalNode[]) {
this.exportEditor.update(
() => {
const root = $getRoot()
root.clear()
const selection = root.selectEnd()
const generatedNodes = $generateNodesFromSerializedNodes(serializedNodes)
$insertGeneratedNodes(this.exportEditor, generatedNodes, selection)
},
{
discrete: true,
},
)
return this.exportEditor.read(() => {
return JSON.stringify(this.exportEditor.getEditorState().toJSON())
})
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ import {
AlertService,
ProtectionsClientInterface,
LocalPrefKey,
NoteContent,
noteTypeForEditorIdentifier,
ContentReference,
} from '@standardnotes/snjs'
import { makeObservable, observable, action, computed, runInAction } from 'mobx'
import { AbstractViewController } from '../Abstract/AbstractViewController'
Expand Down Expand Up @@ -408,4 +411,27 @@ export class NotesController
private getSelectedNotesList(): SNNote[] {
return Object.values(this.selectedNotes)
}

async createNoteWithContent(
editorIdentifier: string,
title: string,
text: string,
references: ContentReference[] = [],
): Promise<SNNote> {
const noteType = noteTypeForEditorIdentifier(editorIdentifier)
const selectedTag = this.navigationController.selected
const templateNote = this.items.createTemplateItem<NoteContent, SNNote>(ContentType.TYPES.Note, {
title,
text,
references,
noteType,
editorIdentifier,
})
const note = await this.mutator.insertItem<SNNote>(templateNote)
if (selectedTag instanceof SNTag) {
const shouldAddTagHierarchy = this.preferences.getValue(PrefKey.NoteAddToParentFolders, true)
await this.mutator.addTagToNote(templateNote, selectedTag, shouldAddTagHierarchy)
}
return note
}
}
Loading