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
Binary file not shown.
2 changes: 2 additions & 0 deletions packages/models/src/Domain/Syncable/UserPrefs/PrefKey.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ export enum PrefKey {
DefaultEditorIdentifier = 'defaultEditorIdentifier',
MomentsDefaultTagUuid = 'momentsDefaultTagUuid',
SystemViewPreferences = 'systemViewPreferences',
SuperNoteExportFormat = 'superNoteExportFormat',
}

export enum NewNoteTitleFormat {
Expand Down Expand Up @@ -109,4 +110,5 @@ export type PrefValue = {
[PrefKey.DefaultEditorIdentifier]: EditorIdentifier
[PrefKey.MomentsDefaultTagUuid]: string | undefined
[PrefKey.SystemViewPreferences]: Partial<Record<SystemViewId, TagPreferences>>
[PrefKey.SuperNoteExportFormat]: 'json' | 'md' | 'html'
}
1 change: 1 addition & 0 deletions packages/models/src/Domain/Utilities/Icon/IconType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export type IconType =
| 'arrow-right'
| 'arrow-up'
| 'arrows-horizontal'
| 'arrows-vertical'
| 'arrows-sort-down'
| 'arrows-sort-up'
| 'asterisk'
Expand Down
3 changes: 3 additions & 0 deletions packages/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -114,5 +114,8 @@
"lint-staged": {
"app/**/*.{js,ts,jsx,tsx}": "eslint --cache --fix",
"app/**/*.{js,ts,jsx,tsx,css,md}": "prettier --write"
},
"dependencies": {
"@lexical/headless": "^0.7.6"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export const IconNameToSvgMapping = {
'arrow-up': icons.ArrowUpIcon,
'arrows-sort-down': icons.ArrowsSortDownIcon,
'arrows-sort-up': icons.ArrowsSortUpIcon,
'arrows-vertical': icons.ArrowsVerticalIcon,
'attachment-file': icons.AttachmentFileIcon,
'check-bold': icons.CheckBoldIcon,
'check-circle': icons.CheckCircleIcon,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import ReadonlyPlugin from './Plugins/ReadonlyPlugin/ReadonlyPlugin'
import { SuperSearchContextProvider } from './Plugins/SearchPlugin/Context'
import { SearchPlugin } from './Plugins/SearchPlugin/SearchPlugin'
import ModalOverlay from '@/Components/Modal/ModalOverlay'
import { SuperEditorNodes } from './SuperEditorNodes'

const NotePreviewCharLimit = 160

Expand Down Expand Up @@ -170,7 +171,7 @@ export const SuperEditor: FunctionComponent<Props> = ({
<BlocksEditorComposer
readonly={note.current.locked}
initialValue={note.current.text}
nodes={[FileNode, BubbleNode]}
nodes={SuperEditorNodes}
>
<BlocksEditor
onChange={handleChange}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { FileNode } from './Plugins/EncryptedFilePlugin/Nodes/FileNode'
import { BubbleNode } from './Plugins/ItemBubblePlugin/Nodes/BubbleNode'

export const SuperEditorNodes = [FileNode, BubbleNode]
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { createHeadlessEditor } from '@lexical/headless'
import { $convertToMarkdownString } from '@lexical/markdown'
import { MarkdownTransformers } from '@standardnotes/blocks-editor'
import { $generateHtmlFromNodes } from '@lexical/html'
import { BlockEditorNodes } from '@standardnotes/blocks-editor/src/Lexical/Nodes/AllNodes'
import BlocksEditorTheme from '@standardnotes/blocks-editor/src/Lexical/Theme/Theme'
import { SNNote } from '@standardnotes/models'
import { SuperEditorNodes } from './SuperEditorNodes'

export const exportSuperNote = (note: SNNote, format: 'txt' | 'md' | 'html' | 'json') => {
const headlessEditor = createHeadlessEditor({
namespace: 'BlocksEditor',
theme: BlocksEditorTheme,
editable: false,
onError: (error: Error) => console.error(error),
nodes: [...SuperEditorNodes, ...BlockEditorNodes],
})

headlessEditor.setEditorState(headlessEditor.parseEditorState(note.text))

let content: string | undefined

headlessEditor.update(() => {
switch (format) {
case 'md':
content = $convertToMarkdownString(MarkdownTransformers)
break
case 'html':
content = $generateHtmlFromNodes(headlessEditor)
break
case 'json':
content = JSON.stringify(headlessEditor.toJSON())
break
case 'txt':
default:
content = note.text
break
}
})

if (!content) {
throw new Error('Could not export note')
}

return content
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ import { iconClass } from './ClassNames'
import SuperNoteOptions from './SuperNoteOptions'
import MenuSwitchButtonItem from '../Menu/MenuSwitchButtonItem'
import MenuItem from '../Menu/MenuItem'
import ModalOverlay from '../Modal/ModalOverlay'
import SuperExportModal from './SuperExportModal'

const iconSize = MenuItemIconSize
const iconClassDanger = `text-danger mr-2 ${iconSize}`
Expand Down Expand Up @@ -94,6 +96,11 @@ const NotesOptions = ({
}
}, [application])

const [showExportSuperModal, setShowExportSuperModal] = useState(false)
const closeSuperExportModal = useCallback(() => {
setShowExportSuperModal(false)
}, [])

const downloadSelectedItems = useCallback(async () => {
if (notes.length === 1) {
const note = notes[0]
Expand Down Expand Up @@ -165,6 +172,8 @@ const NotesOptions = ({
return null
}

const isOnlySuperNoteSelected = notes.length === 1 && notes[0].noteType === NoteType.Super

return (
<>
{notes.length === 1 && (
Expand Down Expand Up @@ -251,13 +260,22 @@ const NotesOptions = ({
{pinShortcut && <KeyboardShortcutIndicator className="ml-auto" shortcut={pinShortcut} />}
</MenuItem>
)}
{notes[0].noteType !== NoteType.Super && (
{!isOnlySuperNoteSelected && (
<>
<MenuItem
onClick={() => {
application.isNativeMobileWeb()
? void shareSelectedNotes(application, notes)
: void downloadSelectedItems()
if (application.isNativeMobileWeb()) {
void shareSelectedNotes(application, notes)
} else {
const hasSuperNote = notes.some((note) => note.noteType === NoteType.Super)

if (hasSuperNote) {
setShowExportSuperModal(true)
return
}

void downloadSelectedItems()
}
}}
>
<Icon type={application.platform === Platform.Android ? 'share' : 'download'} className={iconClass} />
Expand Down Expand Up @@ -374,6 +392,10 @@ const NotesOptions = ({
<NoteSizeWarning note={notes[0]} />
</>
) : null}

<ModalOverlay isOpen={showExportSuperModal} onDismiss={closeSuperExportModal}>
<SuperExportModal exportNotes={downloadSelectedItems} close={closeSuperExportModal} />
</ModalOverlay>
</>
)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { ApplicationEvent, PrefKey, PrefValue } from '@standardnotes/snjs'
import { useEffect, useState } from 'react'
import { useApplication } from '../ApplicationProvider'
import Dropdown from '../Dropdown/Dropdown'
import Modal from '../Modal/Modal'

type Props = {
exportNotes: () => void
close: () => void
}

const SuperExportModal = ({ exportNotes, close }: Props) => {
const application = useApplication()
const [superNoteExportFormat, setSuperNoteExportFormat] = useState<PrefValue[PrefKey.SuperNoteExportFormat]>(
() => application.getPreference(PrefKey.SuperNoteExportFormat) || 'json',
)
useEffect(() => {
return application.addSingleEventObserver(ApplicationEvent.PreferencesChanged, async () => {
setSuperNoteExportFormat(application.getPreference(PrefKey.SuperNoteExportFormat) || 'json')
})
}, [application, superNoteExportFormat])

return (
<Modal
title="Export notes"
className={{
description: 'p-4',
}}
close={close}
actions={[
{
label: 'Cancel',
type: 'cancel',
onClick: close,
mobileSlot: 'left',
},
{
label: 'Export',
type: 'primary',
onClick: exportNotes,
mobileSlot: 'right',
},
]}
>
<div className="mb-4">
<div className="mb-1 text-base">
We detected your selection includes Super notes. How do you want to export them?
</div>
<Dropdown
id="export-format-dropdown"
label="Super notes export format"
items={[
{ label: 'Keep as Super', value: 'json' },
{ label: 'Markdown', value: 'md' },
{ label: 'HTML', value: 'html' },
]}
value={superNoteExportFormat}
onChange={(value) => {
void application.setPreference(
PrefKey.SuperNoteExportFormat,
value as PrefValue[PrefKey.SuperNoteExportFormat],
)
}}
portal={false}
/>
</div>
<div className="text-passive-0">
Note that if you convert Super notes to Markdown then import them back into Standard Notes in the future, you
will lose some formatting that the Markdown format is incapable of expressing, such as collapsible blocks and
embeds.
</div>
</Modal>
)
}

export default SuperExportModal
18 changes: 14 additions & 4 deletions packages/web/src/javascripts/Utils/NoteExportUtils.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,19 @@
import { WebApplication } from '@/Application/Application'
import { SNNote } from '@standardnotes/snjs'
import { exportSuperNote } from '@/Components/NoteView/SuperEditor/SuperNoteExporter'
import { NoteType, PrefKey, SNNote } from '@standardnotes/snjs'

export const getNoteFormat = (application: WebApplication, note: SNNote) => {
const editor = application.componentManager.editorForNote(note)
const format = editor?.package_info?.file_type || 'txt'
return format

const isSuperNote = note.noteType === NoteType.Super

if (isSuperNote) {
const superNoteExportFormatPref = application.getPreference(PrefKey.SuperNoteExportFormat) || 'json'

return superNoteExportFormatPref
}

return editor?.package_info?.file_type || 'txt'
}

export const getNoteFileName = (application: WebApplication, note: SNNote): string => {
Expand All @@ -29,7 +38,8 @@ export const getNoteBlob = (application: WebApplication, note: SNNote) => {
type = 'text/plain'
break
}
const blob = new Blob([note.text], {
const content = note.noteType === NoteType.Super ? exportSuperNote(note, format) : note.text
const blob = new Blob([content], {
type,
})
return blob
Expand Down
10 changes: 10 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -3008,6 +3008,15 @@ __metadata:
languageName: node
linkType: hard

"@lexical/headless@npm:^0.7.6":
version: 0.7.6
resolution: "@lexical/headless@npm:0.7.6"
peerDependencies:
lexical: 0.7.6
checksum: 9dd9cacba2a45a2e9b0fce5e8ccda1642f7c7c1f04ecf96b2393c1534004f55c04dcce819d88fd61c47204f78b3864fa67ffb4611c94806548b307c622498352
languageName: node
linkType: hard

"@lexical/history@npm:0.7.6":
version: 0.7.6
resolution: "@lexical/history@npm:0.7.6"
Expand Down Expand Up @@ -5247,6 +5256,7 @@ __metadata:
"@babel/plugin-transform-react-jsx": ^7.19.0
"@babel/preset-env": "*"
"@babel/preset-typescript": ^7.18.6
"@lexical/headless": ^0.7.6
"@lexical/react": 0.7.6
"@pmmmwh/react-refresh-webpack-plugin": ^0.5.10
"@reach/alert": ^0.18.0
Expand Down