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
@@ -1,5 +1,6 @@
export interface SuperConverterServiceInterface {
isValidSuperString(superString: string): boolean
convertSuperStringToOtherFormat: (superString: string, toFormat: 'txt' | 'md' | 'html' | 'json') => string
convertSuperStringToOtherFormat: (superString: string, toFormat: 'txt' | 'md' | 'html' | 'json') => Promise<string>
convertOtherFormatToSuperString: (otherFormatString: string, fromFormat: 'txt' | 'md' | 'html' | 'json') => string
getEmbeddedFileIDsFromSuperString(superString: string): string[]
}
2 changes: 2 additions & 0 deletions packages/models/src/Domain/Syncable/UserPrefs/PrefDefaults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ export const PrefDefaults = {
[PrefKey.ClipperDefaultTagUuid]: undefined,
[PrefKey.DefaultEditorIdentifier]: NativeFeatureIdentifier.TYPES.PlainEditor,
[PrefKey.SuperNoteExportFormat]: 'json',
[PrefKey.SuperNoteExportEmbedBehavior]: 'reference',
[PrefKey.SuperNoteExportUseMDFrontmatter]: true,
[PrefKey.SystemViewPreferences]: {},
[PrefKey.AuthenticatorNames]: '',
[PrefKey.ComponentPreferences]: {},
Expand Down
4 changes: 4 additions & 0 deletions packages/models/src/Domain/Syncable/UserPrefs/PrefKey.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ export enum PrefKey {
ClipperDefaultTagUuid = 'clipperDefaultTagUuid',
SystemViewPreferences = 'systemViewPreferences',
SuperNoteExportFormat = 'superNoteExportFormat',
SuperNoteExportEmbedBehavior = 'superNoteExportEmbedBehavior',
SuperNoteExportUseMDFrontmatter = 'superNoteExportUseMDFrontmatter',
AuthenticatorNames = 'authenticatorNames',
PaneGesturesEnabled = 'paneGesturesEnabled',
ComponentPreferences = 'componentPreferences',
Expand Down Expand Up @@ -83,6 +85,8 @@ export type PrefValue = {
[PrefKey.ClipperDefaultTagUuid]: string | undefined
[PrefKey.SystemViewPreferences]: Partial<Record<SystemViewId, TagPreferences>>
[PrefKey.SuperNoteExportFormat]: 'json' | 'md' | 'html'
[PrefKey.SuperNoteExportEmbedBehavior]: 'reference' | 'inline' | 'separate'
[PrefKey.SuperNoteExportUseMDFrontmatter]: boolean
[PrefKey.AuthenticatorNames]: string
[PrefKey.PaneGesturesEnabled]: boolean
[PrefKey.ComponentPreferences]: AllComponentPreferences
Expand Down
2 changes: 1 addition & 1 deletion packages/services/src/Domain/Backups/FilesBackupService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -462,7 +462,7 @@ export class FilesBackupService
const tagNames = tags.map((tag) => this.items.getTagLongTitle(tag))
const text =
note.noteType === NoteType.Super
? this.markdownConverter.convertSuperStringToOtherFormat(note.text, 'md')
? await this.markdownConverter.convertSuperStringToOtherFormat(note.text, 'md')
: note.text
await this.device.savePlaintextNoteBackup(location, note.uuid, note.title, tagNames, text)
}
Expand Down
5 changes: 5 additions & 0 deletions packages/ui-services/src/Archive/ArchiveManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ function zippableFileName(name: string, suffix = '', format = 'txt'): string {
return sanitizedName.slice(0, maxFileNameLength - nameEnd.length) + nameEnd
}

export function parseAndCreateZippableFileName(name: string) {
const { name: parsedName, ext } = parseFileName(name)
return zippableFileName(parsedName, '', ext)
}

type ZippableData = {
name: string
content: Blob
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ describe('EvernoteConverter', () => {
const superConverterService: SuperConverterServiceInterface = {
isValidSuperString: () => true,
convertOtherFormatToSuperString: (data: string) => data,
convertSuperStringToOtherFormat: (data: string) => data,
convertSuperStringToOtherFormat: async (data: string) => data,
getEmbeddedFileIDsFromSuperString: () => [],
}

const generateUuid = new GenerateUuid(crypto)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ describe('GoogleKeepConverter', () => {
const superConverterService: SuperConverterServiceInterface = {
isValidSuperString: () => true,
convertOtherFormatToSuperString: (data: string) => data,
convertSuperStringToOtherFormat: (data: string) => data,
convertSuperStringToOtherFormat: async (data: string) => data,
getEmbeddedFileIDsFromSuperString: () => [],
}
const generateUuid = new GenerateUuid(crypto)

Expand Down
1 change: 0 additions & 1 deletion packages/ui-services/src/Keyboard/KeyboardCommands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@ export const SUPER_SHOW_MARKDOWN_PREVIEW = createKeyboardCommand('SUPER_SHOW_MAR

export const SUPER_EXPORT_JSON = createKeyboardCommand('SUPER_EXPORT_JSON')
export const SUPER_EXPORT_MARKDOWN = createKeyboardCommand('SUPER_EXPORT_MARKDOWN')
export const SUPER_EXPORT_HTML = createKeyboardCommand('SUPER_EXPORT_HTML')
export const OPEN_PREFERENCES_COMMAND = createKeyboardCommand('OPEN_PREFERENCES_COMMAND')

export const CHANGE_EDITOR_WIDTH_COMMAND = createKeyboardCommand('CHANGE_EDITOR_WIDTH_COMMAND')
Original file line number Diff line number Diff line change
Expand Up @@ -27,25 +27,29 @@ export const DiffView = ({
const [textDiff, setTextDiff] = useState<fastdiff.Diff[]>([])

useEffect(() => {
const firstNote = selectedNotes[0]
const firstTitle = firstNote.title
const firstText =
firstNote.noteType === NoteType.Super && convertSuperToMarkdown
? new HeadlessSuperConverter().convertSuperStringToOtherFormat(firstNote.text, 'md')
: firstNote.text

const secondNote = selectedNotes[1]
const secondTitle = secondNote.title
const secondText =
secondNote.noteType === NoteType.Super && convertSuperToMarkdown
? new HeadlessSuperConverter().convertSuperStringToOtherFormat(secondNote.text, 'md')
: secondNote.text

const titleDiff = fastdiff(firstTitle, secondTitle, undefined, true)
const textDiff = fastdiff(firstText, secondText, undefined, true)

setTitleDiff(titleDiff)
setTextDiff(textDiff)
const setDiffs = async () => {
const firstNote = selectedNotes[0]
const firstTitle = firstNote.title
const firstText =
firstNote.noteType === NoteType.Super && convertSuperToMarkdown
? await new HeadlessSuperConverter().convertSuperStringToOtherFormat(firstNote.text, 'md')
: firstNote.text

const secondNote = selectedNotes[1]
const secondTitle = secondNote.title
const secondText =
secondNote.noteType === NoteType.Super && convertSuperToMarkdown
? await new HeadlessSuperConverter().convertSuperStringToOtherFormat(secondNote.text, 'md')
: secondNote.text

const titleDiff = fastdiff(firstTitle, secondTitle, undefined, true)
const textDiff = fastdiff(firstText, secondText, undefined, true)

setTitleDiff(titleDiff)
setTextDiff(textDiff)
}

setDiffs().catch(console.error)
}, [convertSuperToMarkdown, selectedNotes])

const [preElement, setPreElement] = useState<HTMLPreElement | null>(null)
Expand Down
162 changes: 58 additions & 104 deletions packages/web/src/javascripts/Components/NotesOptions/NotesOptions.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,13 @@
import Icon from '@/Components/Icon/Icon'
import { observer } from 'mobx-react-lite'
import { useState, useEffect, useMemo, useCallback, useRef } from 'react'
import { useState, useEffect, useMemo, useCallback } from 'react'
import { NoteType, Platform, SNNote } from '@standardnotes/snjs'
import {
CHANGE_EDITOR_WIDTH_COMMAND,
OPEN_NOTE_HISTORY_COMMAND,
PIN_NOTE_COMMAND,
SHOW_HIDDEN_OPTIONS_KEYBOARD_COMMAND,
STAR_NOTE_COMMAND,
SUPER_EXPORT_HTML,
SUPER_EXPORT_JSON,
SUPER_EXPORT_MARKDOWN,
SUPER_SHOW_MARKDOWN_PREVIEW,
} from '@standardnotes/ui-services'
import ChangeEditorOption from './ChangeEditorOption'
Expand All @@ -20,9 +17,7 @@ import { addToast, dismissToast, ToastType } from '@standardnotes/toast'
import { NotesOptionsProps } from './NotesOptionsProps'
import { useResponsiveAppPane } from '../Panes/ResponsivePaneProvider'
import { AppPaneId } from '../Panes/AppPaneMetadata'
import { getNoteBlob, getNoteFileName } from '@/Utils/NoteExportUtils'
import { shareSelectedNotes } from '@/NativeMobileWeb/ShareSelectedNotes'
import { downloadSelectedNotesOnAndroid } from '@/NativeMobileWeb/DownloadSelectedNotesOnAndroid'
import { createNoteExport } from '@/Utils/NoteExportUtils'
import ProtectedUnauthorizedLabel from '../ProtectedItemOverlay/ProtectedUnauthorizedLabel'
import { MenuItemIconSize } from '@/Constants/TailwindClassNames'
import { KeyboardShortcutIndicator } from '../KeyboardShortcutIndicator/KeyboardShortcutIndicator'
Expand All @@ -39,9 +34,9 @@ import SuperExportModal from './SuperExportModal'
import { useApplication } from '../ApplicationProvider'
import { MutuallyExclusiveMediaQueryBreakpoints } from '@/Hooks/useMediaQuery'
import AddToVaultMenuOption from '../Vaults/AddToVaultMenuOption'
import Menu from '../Menu/Menu'
import Popover from '../Popover/Popover'
import MenuSection from '../Menu/MenuSection'
import { downloadOrShareBlobBasedOnPlatform } from '@/Utils/DownloadOrShareBasedOnPlatform'
import { shareBlobOnMobile } from '@/NativeMobileWeb/ShareBlobOnMobile'

const iconSize = MenuItemIconSize
const iconClassDanger = `text-danger mr-2 ${iconSize}`
Expand Down Expand Up @@ -104,34 +99,40 @@ const NotesOptions = ({ notes, closeMenu }: NotesOptionsProps) => {
}, [])

const downloadSelectedItems = useCallback(async () => {
if (notes.length === 1) {
const note = notes[0]
const blob = getNoteBlob(application, note)
application.archiveService.downloadData(blob, getNoteFileName(application, note))
return
}

if (notes.length > 1) {
const loadingToastId = addToast({
type: ToastType.Loading,
message: `Exporting ${notes.length} notes...`,
try {
const result = await createNoteExport(application, notes)
if (!result) {
return
}
const { blob, fileName } = result
void downloadOrShareBlobBasedOnPlatform({
archiveService: application.archiveService,
platform: application.platform,
mobileDevice: application.mobileDevice,
blob: blob,
filename: fileName,
isNativeMobileWeb: application.isNativeMobileWeb(),
})
await application.archiveService.downloadDataAsZip(
notes.map((note) => {
return {
name: getNoteFileName(application, note),
content: getNoteBlob(application, note),
}
}),
)
dismissToast(loadingToastId)
} catch (error) {
console.error(error)
addToast({
type: ToastType.Success,
message: `Exported ${notes.length} notes`,
type: ToastType.Error,
message: 'Could not export notes',
})
}
}, [application, notes])

const exportSelectedItems = useCallback(() => {
const hasSuperNote = notes.some((note) => note.noteType === NoteType.Super)

if (hasSuperNote) {
setShowExportSuperModal(true)
return
}

downloadSelectedItems().catch(console.error)
}, [downloadSelectedItems, notes])

const closeMenuAndToggleNotesList = useCallback(() => {
const isMobileScreen = matchMedia(MutuallyExclusiveMediaQueryBreakpoints.sm).matches
if (isMobileScreen) {
Expand Down Expand Up @@ -199,9 +200,6 @@ const NotesOptions = ({ notes, closeMenu }: NotesOptionsProps) => {
[application],
)

const superExportButtonRef = useRef<HTMLButtonElement>(null)
const [isSuperExportMenuOpen, setIsSuperExportMenuOpen] = useState(false)

const unauthorized = notes.some((note) => !application.isAuthorizedToRenderItem(note))
if (unauthorized) {
return <ProtectedUnauthorizedLabel />
Expand All @@ -224,8 +222,6 @@ const NotesOptions = ({ notes, closeMenu }: NotesOptionsProps) => {
return null
}

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

return (
<>
{notes.length === 1 && (
Expand Down Expand Up @@ -342,77 +338,35 @@ const NotesOptions = ({ notes, closeMenu }: NotesOptionsProps) => {
{pinShortcut && <KeyboardShortcutIndicator className="ml-auto" shortcut={pinShortcut} />}
</MenuItem>
)}
{isOnlySuperNoteSelected ? (
<>
<MenuItem
ref={superExportButtonRef}
onClick={() => {
setIsSuperExportMenuOpen((open) => !open)
}}
>
<div className="flex items-center">
<Icon type="download" className={iconClass} />
Export
</div>
<Icon type="chevron-right" className="ml-auto text-neutral" />
</MenuItem>
<Popover
title="Export note"
side="left"
align="start"
open={isSuperExportMenuOpen}
anchorElement={superExportButtonRef.current}
togglePopover={() => {
setIsSuperExportMenuOpen(!isSuperExportMenuOpen)
}}
className="md:py-1"
>
<Menu a11yLabel={'Super note export menu'}>
<MenuSection>
<MenuItem onClick={() => commandService.triggerCommand(SUPER_EXPORT_JSON, notes[0].title)}>
<Icon type="code" className={iconClass} />
Export as JSON
</MenuItem>
<MenuItem onClick={() => commandService.triggerCommand(SUPER_EXPORT_MARKDOWN, notes[0].title)}>
<Icon type="markdown" className={iconClass} />
Export as Markdown
</MenuItem>
<MenuItem onClick={() => commandService.triggerCommand(SUPER_EXPORT_HTML, notes[0].title)}>
<Icon type="rich-text" className={iconClass} />
Export as HTML
</MenuItem>
</MenuSection>
</Menu>
</Popover>
</>
) : (
<>
<MenuItem
onClick={() => {
if (application.isNativeMobileWeb()) {
void shareSelectedNotes(application, notes)
} else {
const hasSuperNote = notes.some((note) => note.noteType === NoteType.Super)

if (hasSuperNote) {
setShowExportSuperModal(true)
<MenuItem
onClick={() => {
if (application.isNativeMobileWeb()) {
createNoteExport(application, notes)
.then((result) => {
if (!result) {
return
}

void downloadSelectedItems()
}
}}
>
<Icon type={application.platform === Platform.Android ? 'share' : 'download'} className={iconClass} />
{application.platform === Platform.Android ? 'Share' : 'Export'}
</MenuItem>
{application.platform === Platform.Android && (
<MenuItem onClick={() => downloadSelectedNotesOnAndroid(application, notes)}>
<Icon type="download" className={iconClass} />
Export
</MenuItem>
)}
</>
const { blob, fileName } = result

shareBlobOnMobile(application.mobileDevice, application.isNativeMobileWeb(), blob, fileName).catch(
console.error,
)
})
.catch(console.error)
} else {
exportSelectedItems()
}
}}
>
<Icon type={application.platform === Platform.Android ? 'share' : 'download'} className={iconClass} />
{application.platform === Platform.Android ? 'Share' : 'Export'}
</MenuItem>
{application.platform === Platform.Android && (
<MenuItem onClick={exportSelectedItems}>
<Icon type="download" className={iconClass} />
Export
</MenuItem>
)}
<MenuItem onClick={duplicateSelectedItems} disabled={areSomeNotesInReadonlySharedVault}>
<Icon type="copy" className={iconClass} />
Expand Down
Loading