feat: Changing note type from Super to any other type will now show a conversion dialog
This commit is contained in:
@@ -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)
|
# [3.152.0](https://github.com/standardnotes/app/compare/@standardnotes/web@3.151.10...@standardnotes/web@3.152.0) (2023-04-24)
|
||||||
|
|
||||||
### Features
|
**Note:** Version bump only for package @standardnotes/web
|
||||||
|
|
||||||
* **clipper:** Added default tag setting to clipper ([2ce2b84](https://github.com/standardnotes/app/commit/2ce2b8410f5053f77d02b93dbb80aad5f33c728c))
|
|
||||||
|
|
||||||
## [3.151.10](https://github.com/standardnotes/app/compare/@standardnotes/web@3.151.9...@standardnotes/web@3.151.10) (2023-04-23)
|
## [3.151.10](https://github.com/standardnotes/app/compare/@standardnotes/web@3.151.9...@standardnotes/web@3.151.10) (2023-04-23)
|
||||||
|
|
||||||
|
|||||||
@@ -40,10 +40,7 @@
|
|||||||
"body": "### Features\n\n* **clipper:** Added default tag setting to clipper ([2ce2b84](https://github.com/standardnotes/app/commit/2ce2b8410f5053f77d02b93dbb80aad5f33c728c))",
|
"body": "### Features\n\n* **clipper:** Added default tag setting to clipper ([2ce2b84](https://github.com/standardnotes/app/commit/2ce2b8410f5053f77d02b93dbb80aad5f33c728c))",
|
||||||
"parsed": {
|
"parsed": {
|
||||||
"_": [
|
"_": [
|
||||||
"clipper: Added default tag setting to clipper (2ce2b84)"
|
"Note: Version bump only for package @standardnotes/web"
|
||||||
],
|
|
||||||
"Features": [
|
|
||||||
"clipper: Added default tag setting to clipper (2ce2b84)"
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import { SuperNoteImporter } from '../SuperEditor/SuperNoteImporter'
|
|||||||
import MenuRadioButtonItem from '../Menu/MenuRadioButtonItem'
|
import MenuRadioButtonItem from '../Menu/MenuRadioButtonItem'
|
||||||
import { Pill } from '../Preferences/PreferencesComponents/Content'
|
import { Pill } from '../Preferences/PreferencesComponents/Content'
|
||||||
import ModalOverlay from '../Modal/ModalOverlay'
|
import ModalOverlay from '../Modal/ModalOverlay'
|
||||||
|
import SuperNoteConverter from '../SuperEditor/SuperNoteConverter'
|
||||||
|
|
||||||
type ChangeEditorMenuProps = {
|
type ChangeEditorMenuProps = {
|
||||||
application: WebApplication
|
application: WebApplication
|
||||||
@@ -43,8 +44,12 @@ const ChangeEditorMenu: FunctionComponent<ChangeEditorMenuProps> = ({
|
|||||||
)
|
)
|
||||||
const groups = useMemo(() => createEditorMenuGroups(application, editors), [application, editors])
|
const groups = useMemo(() => createEditorMenuGroups(application, editors), [application, editors])
|
||||||
const [currentComponent, setCurrentComponent] = useState<SNComponent>()
|
const [currentComponent, setCurrentComponent] = useState<SNComponent>()
|
||||||
const [showSuperImporter, setShowSuperImporter] = useState(false)
|
const [pendingConversionItem, setPendingConversionItem] = useState<EditorMenuItem | null>(null)
|
||||||
const [pendingSuperItem, setPendingSuperItem] = useState<EditorMenuItem | null>(null)
|
|
||||||
|
const showSuperNoteImporter =
|
||||||
|
!!pendingConversionItem && note?.noteType !== NoteType.Super && pendingConversionItem.noteType === NoteType.Super
|
||||||
|
const showSuperNoteConverter =
|
||||||
|
!!pendingConversionItem && note?.noteType === NoteType.Super && pendingConversionItem.noteType !== NoteType.Super
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (note) {
|
if (note) {
|
||||||
@@ -114,15 +119,28 @@ const ChangeEditorMenu: FunctionComponent<ChangeEditorMenuProps> = ({
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (note?.locked) {
|
if (!note) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (note.locked) {
|
||||||
application.alertService.alert(STRING_EDIT_LOCKED_ATTEMPT).catch(console.error)
|
application.alertService.alert(STRING_EDIT_LOCKED_ATTEMPT).catch(console.error)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (itemToBeSelected.noteType === NoteType.Super) {
|
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?.()
|
handleDisableClickoutsideRequest?.()
|
||||||
setShowSuperImporter(true)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -139,7 +157,7 @@ const ChangeEditorMenu: FunctionComponent<ChangeEditorMenuProps> = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (shouldMakeSelection && note) {
|
if (shouldMakeSelection) {
|
||||||
if (itemToBeSelected.component) {
|
if (itemToBeSelected.component) {
|
||||||
selectComponent(itemToBeSelected.component, note).catch(console.error)
|
selectComponent(itemToBeSelected.component, note).catch(console.error)
|
||||||
} else {
|
} else {
|
||||||
@@ -166,16 +184,23 @@ const ChangeEditorMenu: FunctionComponent<ChangeEditorMenuProps> = ({
|
|||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
const handleSuperNoteConversionCompletion = useCallback(() => {
|
const handleConversionCompletion = useCallback(() => {
|
||||||
if (!pendingSuperItem || !note) {
|
if (!pendingConversionItem || !note) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
selectNonComponent(pendingSuperItem, note).catch(console.error)
|
if (pendingConversionItem.component) {
|
||||||
closeMenu()
|
selectComponent(pendingConversionItem.component, note).catch(console.error)
|
||||||
}, [note, pendingSuperItem, selectNonComponent, closeMenu])
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -223,16 +248,26 @@ const ChangeEditorMenu: FunctionComponent<ChangeEditorMenuProps> = ({
|
|||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
</Menu>
|
</Menu>
|
||||||
<ModalOverlay isOpen={showSuperImporter}>
|
<ModalOverlay isOpen={showSuperNoteImporter}>
|
||||||
{note && (
|
{note && (
|
||||||
<SuperNoteImporter
|
<SuperNoteImporter
|
||||||
note={note}
|
note={note}
|
||||||
application={application}
|
application={application}
|
||||||
onConvertComplete={handleSuperNoteConversionCompletion}
|
onComplete={handleConversionCompletion}
|
||||||
closeDialog={closeSuperNoteImporter}
|
closeDialog={closeSuperNoteImporter}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</ModalOverlay>
|
</ModalOverlay>
|
||||||
|
<ModalOverlay isOpen={showSuperNoteConverter}>
|
||||||
|
{note && pendingConversionItem && (
|
||||||
|
<SuperNoteConverter
|
||||||
|
note={note}
|
||||||
|
convertTo={pendingConversionItem}
|
||||||
|
closeDialog={closeSuperNoteConverter}
|
||||||
|
onComplete={handleConversionCompletion}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</ModalOverlay>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -185,7 +185,7 @@ const ChangeMultipleMenu = ({ application, notes, setDisableClickOutside }: Prop
|
|||||||
<SuperNoteImporter
|
<SuperNoteImporter
|
||||||
note={confirmationQueue[0]}
|
note={confirmationQueue[0]}
|
||||||
application={application}
|
application={application}
|
||||||
onConvertComplete={handleSuperNoteConversionCompletion}
|
onComplete={handleSuperNoteConversionCompletion}
|
||||||
closeDialog={closeCurrentSuperNoteImporter}
|
closeDialog={closeCurrentSuperNoteImporter}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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<NoteContent, SNNote>(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 (
|
||||||
|
<Modal
|
||||||
|
title={`Convert to ${name}`}
|
||||||
|
close={closeDialog}
|
||||||
|
actions={modalActions}
|
||||||
|
className={{
|
||||||
|
content: 'md:h-full md:max-h-[90%]',
|
||||||
|
description: 'flex flex-col !overflow-hidden',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{format === 'txt' || format === 'md' ? (
|
||||||
|
<div className="flex items-start border-b border-border p-4 text-sm">
|
||||||
|
<Icon type="warning" className="mr-2 flex-shrink-0" />
|
||||||
|
Conversion from Super's format to Markdown/Plaintext can be lossy. Please review the converted note before
|
||||||
|
saving.
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
{componentViewer ? (
|
||||||
|
<div className="component-view min-h-0">
|
||||||
|
<ComponentView key={componentViewer.identifier} componentViewer={componentViewer} application={application} />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="h-full min-h-0 overflow-hidden">
|
||||||
|
<textarea
|
||||||
|
readOnly={true}
|
||||||
|
className="font-editor h-full w-full resize-none border-0 bg-default p-4 text-editor text-text"
|
||||||
|
value={convertedContent}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Modal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SuperNoteConverter
|
||||||
@@ -21,6 +21,7 @@ export const exportSuperNote = (note: SNNote, format: 'txt' | 'md' | 'html' | 'j
|
|||||||
|
|
||||||
headlessEditor.update(() => {
|
headlessEditor.update(() => {
|
||||||
switch (format) {
|
switch (format) {
|
||||||
|
case 'txt':
|
||||||
case 'md':
|
case 'md':
|
||||||
content = $convertToMarkdownString(MarkdownTransformers)
|
content = $convertToMarkdownString(MarkdownTransformers)
|
||||||
break
|
break
|
||||||
@@ -28,9 +29,6 @@ export const exportSuperNote = (note: SNNote, format: 'txt' | 'md' | 'html' | 'j
|
|||||||
content = $generateHtmlFromNodes(headlessEditor)
|
content = $generateHtmlFromNodes(headlessEditor)
|
||||||
break
|
break
|
||||||
case 'json':
|
case 'json':
|
||||||
content = JSON.stringify(headlessEditor.toJSON())
|
|
||||||
break
|
|
||||||
case 'txt':
|
|
||||||
default:
|
default:
|
||||||
content = note.text
|
content = note.text
|
||||||
break
|
break
|
||||||
|
|||||||
@@ -15,10 +15,10 @@ type Props = {
|
|||||||
application: WebApplication
|
application: WebApplication
|
||||||
note: SNNote
|
note: SNNote
|
||||||
closeDialog: () => void
|
closeDialog: () => void
|
||||||
onConvertComplete: () => void
|
onComplete: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SuperNoteImporter: FunctionComponent<Props> = ({ note, application, closeDialog, onConvertComplete }) => {
|
export const SuperNoteImporter: FunctionComponent<Props> = ({ note, application, closeDialog, onComplete }) => {
|
||||||
const isSeamlessConvert = note.text.length === 0
|
const isSeamlessConvert = note.text.length === 0
|
||||||
const [lastValue, setLastValue] = useState({ text: '', previewPlain: '' })
|
const [lastValue, setLastValue] = useState({ text: '', previewPlain: '' })
|
||||||
|
|
||||||
@@ -50,8 +50,8 @@ export const SuperNoteImporter: FunctionComponent<Props> = ({ note, application,
|
|||||||
|
|
||||||
await performConvert(lastValue.text, lastValue.previewPlain)
|
await performConvert(lastValue.text, lastValue.previewPlain)
|
||||||
|
|
||||||
onConvertComplete()
|
onComplete()
|
||||||
}, [closeDialog, performConvert, onConvertComplete, lastValue])
|
}, [closeDialog, performConvert, onComplete, lastValue])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isSeamlessConvert) {
|
if (isSeamlessConvert) {
|
||||||
@@ -76,8 +76,8 @@ export const SuperNoteImporter: FunctionComponent<Props> = ({ note, application,
|
|||||||
|
|
||||||
await performConvert(note.text, note.preview_plain)
|
await performConvert(note.text, note.preview_plain)
|
||||||
|
|
||||||
onConvertComplete()
|
onComplete()
|
||||||
}, [closeDialog, application, note, onConvertComplete, performConvert])
|
}, [closeDialog, application, note, onComplete, performConvert])
|
||||||
|
|
||||||
const modalActions: ModalAction[] = useMemo(
|
const modalActions: ModalAction[] = useMemo(
|
||||||
() => [
|
() => [
|
||||||
|
|||||||
Reference in New Issue
Block a user