diff --git a/packages/web/CHANGELOG.md b/packages/web/CHANGELOG.md index 8d8a1f5e2..2d7f6fdfb 100644 --- a/packages/web/CHANGELOG.md +++ b/packages/web/CHANGELOG.md @@ -17,9 +17,7 @@ See [Conventional Commits](https://conventionalcommits.org) for commit guideline # [3.152.0](https://github.com/standardnotes/app/compare/@standardnotes/web@3.151.10...@standardnotes/web@3.152.0) (2023-04-24) -### Features - -* **clipper:** Added default tag setting to clipper ([2ce2b84](https://github.com/standardnotes/app/commit/2ce2b8410f5053f77d02b93dbb80aad5f33c728c)) +**Note:** Version bump only for package @standardnotes/web ## [3.151.10](https://github.com/standardnotes/app/compare/@standardnotes/web@3.151.9...@standardnotes/web@3.151.10) (2023-04-23) diff --git a/packages/web/CHANGELOG.md.json b/packages/web/CHANGELOG.md.json index 16daa935d..bc2ffc3c6 100644 --- a/packages/web/CHANGELOG.md.json +++ b/packages/web/CHANGELOG.md.json @@ -40,10 +40,7 @@ "body": "### Features\n\n* **clipper:** Added default tag setting to clipper ([2ce2b84](https://github.com/standardnotes/app/commit/2ce2b8410f5053f77d02b93dbb80aad5f33c728c))", "parsed": { "_": [ - "clipper: Added default tag setting to clipper (2ce2b84)" - ], - "Features": [ - "clipper: Added default tag setting to clipper (2ce2b84)" + "Note: Version bump only for package @standardnotes/web" ] } }, diff --git a/packages/web/src/javascripts/Components/ChangeEditor/ChangeEditorMenu.tsx b/packages/web/src/javascripts/Components/ChangeEditor/ChangeEditorMenu.tsx index dd2b53472..a5c75d99b 100644 --- a/packages/web/src/javascripts/Components/ChangeEditor/ChangeEditorMenu.tsx +++ b/packages/web/src/javascripts/Components/ChangeEditor/ChangeEditorMenu.tsx @@ -14,6 +14,7 @@ import { SuperNoteImporter } from '../SuperEditor/SuperNoteImporter' import MenuRadioButtonItem from '../Menu/MenuRadioButtonItem' import { Pill } from '../Preferences/PreferencesComponents/Content' import ModalOverlay from '../Modal/ModalOverlay' +import SuperNoteConverter from '../SuperEditor/SuperNoteConverter' type ChangeEditorMenuProps = { application: WebApplication @@ -43,8 +44,12 @@ const ChangeEditorMenu: FunctionComponent = ({ ) const groups = useMemo(() => createEditorMenuGroups(application, editors), [application, editors]) const [currentComponent, setCurrentComponent] = useState() - const [showSuperImporter, setShowSuperImporter] = useState(false) - const [pendingSuperItem, setPendingSuperItem] = useState(null) + const [pendingConversionItem, setPendingConversionItem] = useState(null) + + const showSuperNoteImporter = + !!pendingConversionItem && note?.noteType !== NoteType.Super && pendingConversionItem.noteType === NoteType.Super + const showSuperNoteConverter = + !!pendingConversionItem && note?.noteType === NoteType.Super && pendingConversionItem.noteType !== NoteType.Super useEffect(() => { if (note) { @@ -114,15 +119,28 @@ const ChangeEditorMenu: FunctionComponent = ({ return } - if (note?.locked) { + if (!note) { + return + } + + if (note.locked) { application.alertService.alert(STRING_EDIT_LOCKED_ATTEMPT).catch(console.error) return } if (itemToBeSelected.noteType === NoteType.Super) { - setPendingSuperItem(itemToBeSelected) + if (note.noteType === NoteType.Super) { + return + } + + setPendingConversionItem(itemToBeSelected) + handleDisableClickoutsideRequest?.() + return + } + + if (note.noteType === NoteType.Super) { + setPendingConversionItem(itemToBeSelected) handleDisableClickoutsideRequest?.() - setShowSuperImporter(true) return } @@ -139,7 +157,7 @@ const ChangeEditorMenu: FunctionComponent = ({ } } - if (shouldMakeSelection && note) { + if (shouldMakeSelection) { if (itemToBeSelected.component) { selectComponent(itemToBeSelected.component, note).catch(console.error) } else { @@ -166,16 +184,23 @@ const ChangeEditorMenu: FunctionComponent = ({ ], ) - const handleSuperNoteConversionCompletion = useCallback(() => { - if (!pendingSuperItem || !note) { + const handleConversionCompletion = useCallback(() => { + if (!pendingConversionItem || !note) { return } - selectNonComponent(pendingSuperItem, note).catch(console.error) - closeMenu() - }, [note, pendingSuperItem, selectNonComponent, closeMenu]) + if (pendingConversionItem.component) { + selectComponent(pendingConversionItem.component, note).catch(console.error) + closeMenu() + return + } - const closeSuperNoteImporter = () => setShowSuperImporter(false) + selectNonComponent(pendingConversionItem, note).catch(console.error) + closeMenu() + }, [pendingConversionItem, note, selectNonComponent, closeMenu, selectComponent]) + + const closeSuperNoteImporter = () => setPendingConversionItem(null) + const closeSuperNoteConverter = () => setPendingConversionItem(null) return ( <> @@ -223,16 +248,26 @@ const ChangeEditorMenu: FunctionComponent = ({ ) })} - + {note && ( )} + + {note && pendingConversionItem && ( + + )} + ) } diff --git a/packages/web/src/javascripts/Components/ChangeEditor/ChangeMultipleMenu.tsx b/packages/web/src/javascripts/Components/ChangeEditor/ChangeMultipleMenu.tsx index 8dcb8bb75..cc6333b87 100644 --- a/packages/web/src/javascripts/Components/ChangeEditor/ChangeMultipleMenu.tsx +++ b/packages/web/src/javascripts/Components/ChangeEditor/ChangeMultipleMenu.tsx @@ -185,7 +185,7 @@ const ChangeMultipleMenu = ({ application, notes, setDisableClickOutside }: Prop )} diff --git a/packages/web/src/javascripts/Components/SuperEditor/SuperNoteConverter.tsx b/packages/web/src/javascripts/Components/SuperEditor/SuperNoteConverter.tsx new file mode 100644 index 000000000..f713e4d9a --- /dev/null +++ b/packages/web/src/javascripts/Components/SuperEditor/SuperNoteConverter.tsx @@ -0,0 +1,177 @@ +import { ContentType, NoteContent, NoteType, SNNote, spaceSeparatedStrings } from '@standardnotes/snjs' +import { useCallback, useEffect, useMemo } from 'react' +import { useApplication } from '../ApplicationProvider' +import ComponentView from '../ComponentView/ComponentView' +import Icon from '../Icon/Icon' +import Modal, { ModalAction } from '../Modal/Modal' +import { EditorMenuItem } from '../NotesOptions/EditorMenuItem' +import { NoteViewController } from '../NoteView/Controller/NoteViewController' +import { exportSuperNote } from './SuperNoteExporter' + +const SuperNoteConverter = ({ + note, + convertTo, + closeDialog, + onComplete, +}: { + note: SNNote + convertTo: EditorMenuItem + closeDialog: () => void + onComplete: () => void +}) => { + const application = useApplication() + const { name, noteType, component } = convertTo + + const format = useMemo(() => { + if (component && component.package_info.file_type) { + return component.package_info.file_type + } + + if (noteType === NoteType.Markdown) { + return 'md' + } + + if (noteType === NoteType.RichText) { + return 'html' + } + + if (noteType === NoteType.Plain) { + return 'txt' + } + + return 'json' + }, [component, noteType]) + + const convertedContent = useMemo(() => { + return exportSuperNote(note, format) + }, [format, note]) + + const componentViewer = useMemo(() => { + if (!component) { + return undefined + } + + const templateNoteForRevision = application.mutator.createTemplateItem(ContentType.Note, { + title: note.title, + text: convertedContent, + references: note.references, + }) + + const componentViewer = application.componentManager.createComponentViewer(component) + componentViewer.setReadonly(true) + componentViewer.lockReadonly = true + componentViewer.overrideContextItem = templateNoteForRevision + return componentViewer + }, [application.componentManager, application.mutator, component, convertedContent, note.references, note.title]) + + useEffect(() => { + return () => { + if (componentViewer) { + application.componentManager.destroyComponentViewer(componentViewer) + } + } + }, [application.componentManager, componentViewer]) + + const performConvert = useCallback( + async (text: string) => { + const controller = new NoteViewController(application, note) + await controller.initialize() + await controller.saveAndAwaitLocalPropagation({ + text, + isUserModified: true, + bypassDebouncer: true, + }) + }, + [application, note], + ) + + const confirmConvert = useCallback(async () => { + await performConvert(convertedContent) + onComplete() + closeDialog() + }, [closeDialog, convertedContent, onComplete, performConvert]) + + const isSeamlessConvert = note.text.length === 0 + useEffect(() => { + if (isSeamlessConvert) { + void confirmConvert() + } + }, [isSeamlessConvert, confirmConvert]) + + const convertAsIs = useCallback(async () => { + const confirmed = await application.alertService.confirm( + spaceSeparatedStrings( + "This option is useful if you want to edit the note's content which is in Super's JSON format directly.", + 'This format is not human-readable. If you want to convert the note to a human-readable format, please use the "Convert" option instead.', + ), + 'Are you sure?', + ) + if (!confirmed) { + return + } + + closeDialog() + + await performConvert(note.text) + + onComplete() + }, [closeDialog, application, note, onComplete, performConvert]) + + const modalActions = useMemo( + (): ModalAction[] => [ + { + label: 'Cancel', + onClick: closeDialog, + type: 'cancel', + mobileSlot: 'left', + }, + { + label: 'Convert', + onClick: confirmConvert, + mobileSlot: 'right', + type: 'primary', + }, + { + label: 'Convert As-Is', + onClick: convertAsIs, + type: 'secondary', + }, + ], + [closeDialog, confirmConvert, convertAsIs], + ) + + return ( + + {format === 'txt' || format === 'md' ? ( +
+ + Conversion from Super's format to Markdown/Plaintext can be lossy. Please review the converted note before + saving. +
+ ) : null} + {componentViewer ? ( +
+ +
+ ) : ( +
+