diff --git a/packages/ui-services/src/Archive/ArchiveManager.ts b/packages/ui-services/src/Archive/ArchiveManager.ts index e24cc89dd..62f15f55d 100644 --- a/packages/ui-services/src/Archive/ArchiveManager.ts +++ b/packages/ui-services/src/Archive/ArchiveManager.ts @@ -19,9 +19,9 @@ function zippableFileName(name: string, suffix = '', format = 'txt'): string { return sanitizedName.slice(0, maxFileNameLength - nameEnd.length) + nameEnd } -export function parseAndCreateZippableFileName(name: string) { +export function parseAndCreateZippableFileName(name: string, suffix = '') { const { name: parsedName, ext } = parseFileName(name) - return zippableFileName(parsedName, '', ext) + return zippableFileName(parsedName, suffix, ext) } type ZippableData = { diff --git a/packages/web/src/javascripts/Components/NotesOptions/NotesOptions.tsx b/packages/web/src/javascripts/Components/NotesOptions/NotesOptions.tsx index 8d96ca118..4a4405451 100644 --- a/packages/web/src/javascripts/Components/NotesOptions/NotesOptions.tsx +++ b/packages/web/src/javascripts/Components/NotesOptions/NotesOptions.tsx @@ -1,7 +1,7 @@ import Icon from '@/Components/Icon/Icon' import { observer } from 'mobx-react-lite' import { useState, useEffect, useMemo, useCallback } from 'react' -import { NoteType, Platform, SNNote } from '@standardnotes/snjs' +import { NoteType, Platform, SNNote, pluralize } from '@standardnotes/snjs' import { CHANGE_EDITOR_WIDTH_COMMAND, OPEN_NOTE_HISTORY_COMMAND, @@ -99,6 +99,13 @@ const NotesOptions = ({ notes, closeMenu }: NotesOptionsProps) => { }, []) const downloadSelectedItems = useCallback(async () => { + if (notes.length === 0) { + return + } + const toast = addToast({ + type: ToastType.Progress, + message: `Exporting ${notes.length} ${pluralize(notes.length, 'note', 'notes')}...`, + }) try { const result = await createNoteExport(application, notes) if (!result) { @@ -113,12 +120,14 @@ const NotesOptions = ({ notes, closeMenu }: NotesOptionsProps) => { filename: fileName, isNativeMobileWeb: application.isNativeMobileWeb(), }) + dismissToast(toast) } catch (error) { console.error(error) addToast({ type: ToastType.Error, message: 'Could not export notes', }) + dismissToast(toast) } }, [application, notes]) diff --git a/packages/web/src/javascripts/Utils/NoteExportUtils.ts b/packages/web/src/javascripts/Utils/NoteExportUtils.ts index e09be6fee..17b65bbff 100644 --- a/packages/web/src/javascripts/Utils/NoteExportUtils.ts +++ b/packages/web/src/javascripts/Utils/NoteExportUtils.ts @@ -1,8 +1,16 @@ import { WebApplication } from '@/Application/WebApplication' import { HeadlessSuperConverter } from '@/Components/SuperEditor/Tools/HeadlessSuperConverter' -import { NoteType, PrefKey, SNNote, PrefDefaults, FileItem, PrefValue, pluralize } from '@standardnotes/snjs' -import { WebApplicationInterface, parseAndCreateZippableFileName, sanitizeFileName } from '@standardnotes/ui-services' +import { NoteType, PrefKey, SNNote, PrefDefaults, FileItem, PrefValue } from '@standardnotes/snjs' +import { WebApplicationInterface, parseAndCreateZippableFileName } from '@standardnotes/ui-services' import { ZipDirectoryEntry } from '@zip.js/zip.js' +// @ts-expect-error Using inline loaders to load CSS as string +import superEditorCSS from '!css-loader!sass-loader!../Components/SuperEditor/Lexical/Theme/editor.scss' +// @ts-expect-error Using inline loaders to load CSS as string +import snColorsCSS from '!css-loader!sass-loader!@standardnotes/styles/src/Styles/_colors.scss' +// @ts-expect-error Using inline loaders to load CSS as string +import exportOverridesCSS from '!css-loader!sass-loader!../Components/SuperEditor/Lexical/Theme/export-overrides.scss' +import { getBase64FromBlob } from './Utils' +import { parseFileName } from '@standardnotes/filepicker' export const getNoteFormat = (application: WebApplicationInterface, note: SNNote) => { if (note.noteType === NoteType.Super) { @@ -25,15 +33,6 @@ export const getNoteFileName = (application: WebApplicationInterface, note: SNNo const headlessSuperConverter = new HeadlessSuperConverter() -// @ts-expect-error Using inline loaders to load CSS as string -import superEditorCSS from '!css-loader!sass-loader!../Components/SuperEditor/Lexical/Theme/editor.scss' -// @ts-expect-error Using inline loaders to load CSS as string -import snColorsCSS from '!css-loader!sass-loader!@standardnotes/styles/src/Styles/_colors.scss' -// @ts-expect-error Using inline loaders to load CSS as string -import exportOverridesCSS from '!css-loader!sass-loader!../Components/SuperEditor/Lexical/Theme/export-overrides.scss' -import { getBase64FromBlob } from './Utils' -import { ToastType, addToast, dismissToast } from '@standardnotes/toast' - const superHTML = (note: SNNote, content: string) => ` @@ -175,11 +174,6 @@ export const createNoteExport = async ( return } - const toast = addToast({ - type: ToastType.Progress, - message: `Exporting ${notes.length} ${pluralize(notes.length, 'note', 'notes')}...`, - }) - const superExportFormatPref = application.getPreference( PrefKey.SuperNoteExportFormat, PrefDefaults[PrefKey.SuperNoteExportFormat], @@ -192,7 +186,6 @@ export const createNoteExport = async ( if (notes.length === 1 && !noteRequiresFolder(notes[0], superExportFormatPref, superEmbedBehaviorPref)) { const blob = await getNoteBlob(application, notes[0], superEmbedBehaviorPref) const fileName = getNoteFileName(application, notes[0]) - dismissToast(toast) return { blob, fileName, @@ -211,31 +204,37 @@ export const createNoteExport = async ( await addEmbeddedFilesToFolder(application, notes[0], root) const zippedBlob = await zipFS.exportBlob() - dismissToast(toast) return { blob: zippedBlob, fileName: fileName + '.zip', } } + const filenameCounts: Record = {} + for (const note of notes) { const blob = await getNoteBlob(application, note, superEmbedBehaviorPref) - const fileName = parseAndCreateZippableFileName(getNoteFileName(application, note)) + const _name = getNoteFileName(application, note) + + filenameCounts[_name] = filenameCounts[_name] == undefined ? 0 : filenameCounts[_name] + 1 + + const currentFileNameIndex = filenameCounts[_name] + + const fileName = parseAndCreateZippableFileName(_name, currentFileNameIndex > 0 ? ` - ${currentFileNameIndex}` : '') if (!noteRequiresFolder(note, superExportFormatPref, superEmbedBehaviorPref)) { root.addBlob(fileName, blob) continue } - const folder = root.addDirectory(sanitizeFileName(note.title)) + const { name } = parseFileName(fileName) + const folder = root.addDirectory(name) folder.addBlob(fileName, blob) await addEmbeddedFilesToFolder(application, note, folder) } const zippedBlob = await zipFS.exportBlob() - dismissToast(toast) - return { blob: zippedBlob, fileName: `Standard Notes Export - ${application.archiveService.formattedDateForExports()}.zip`,