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
79 changes: 79 additions & 0 deletions packages/app/src/App.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ import {
mockedNotifyAppExportSettingsChanged,
mockedNotifyAppLanguageChanged,
mockedNotifyAppThemeChanged,
mockedOpenNativeContainingFolder,
mockedOpenNativeMarkdownFileInNewWindow,
mockedOpenNativeLocalImages,
mockedOpenNativeLocalFiles,
Expand Down Expand Up @@ -9427,6 +9428,84 @@ describe("Markra workspace", () => {
await waitFor(() => expect(mockedInstallNativeApplicationMenu.mock.calls.at(-1)?.[3]).toEqual([]));
});

const exportCases = [
["exportHtml", mockedSaveNativeHtmlFile, "html"],
["exportPdf", mockedSaveNativePdfFile, "pdf"],
["exportMarkdown", mockedSaveNativeMarkdownBundleFile, "md"],
["exportDocx", mockedSaveNativePandocFile, "docx"],
["exportEpub", mockedSaveNativePandocFile, "epub"],
["exportLatex", mockedSaveNativePandocFile, "tex"]
] as const;

async function prepareExportFeedback() {
const runtime = createDefaultAppRuntime();
configureAppRuntime({ ...runtime, features: { ...runtime.features, markdownBundle: true } });
mockOpenMarkdownFile({ content: "# Example export", name: "example.md", path: mockNativePath });
renderApp();
await waitFor(() => expect(mockedInstallNativeApplicationMenu).toHaveBeenCalledTimes(1));
const handlers = mockedInstallNativeApplicationMenu.mock.calls[0]?.[0] as NativeMenuHandlers;
await act(async () => { await handlers.openDocument?.(); });
return handlers;
}

it.each(exportCases)("shows export feedback and reveals the saved file for %s", async (command, save, extension) => {
const file = { name: `renamed.${extension}`, path: `/mock-exports/renamed.${extension}` };
save.mockResolvedValue(file);
const handlers = await prepareExportFeedback();
await act(async () => { await handlers[command]?.(); });
const button = await screen.findByRole("button", { name: "Show in folder" });
expect(button.closest(".app-toast")).toHaveClass("app-toast-notice");
expect(button.closest(".app-toast")).toHaveTextContent(file.name);
expect(mockedOpenNativeContainingFolder).not.toHaveBeenCalled();
fireEvent.click(button);
await waitFor(() => expect(mockedOpenNativeContainingFolder).toHaveBeenCalledWith(file.path));
});

it.each(exportCases)("keeps cancelled export feedback silent for %s", async (command, save, _extension) => {
save.mockResolvedValue(null);
const handlers = await prepareExportFeedback();
await act(async () => { await handlers[command]?.(); });
await waitFor(() => expect(save).toHaveBeenCalled());
expect(document.querySelector(".app-toast")).not.toBeInTheDocument();
});

it.each(exportCases)("shows failed export feedback for %s", async (command, save, _extension) => {
save.mockRejectedValue(new Error("Synthetic export failure"));
const handlers = await prepareExportFeedback();
await act(async () => { await handlers[command]?.(); });
await waitFor(() => expect(document.querySelector(".app-toast")).toHaveTextContent(/Could not export/));
expect(screen.queryByRole("button", { name: "Show in folder" })).not.toBeInTheDocument();
});

it("waits for the export to finish before showing success feedback", async () => {
let finishExport!: (file: { name: string; path: string }) => unknown;
mockedSaveNativeHtmlFile.mockReturnValue(new Promise((resolve) => { finishExport = resolve; }));
const handlers = await prepareExportFeedback();
await act(async () => { await handlers.exportHtml?.(); });
await waitFor(() => expect(mockedSaveNativeHtmlFile).toHaveBeenCalledTimes(1));
expect(document.querySelector(".app-toast")).not.toBeInTheDocument();
await act(async () => { finishExport({ name: "finished.html", path: "/mock-exports/finished.html" }); });
expect(await screen.findByRole("button", { name: "Show in folder" })).toBeInTheDocument();
expect(mockedSaveNativeHtmlFile).toHaveBeenCalledTimes(1);
});

it("reports a folder-opening failure without reporting the export as failed", async () => {
mockedSaveNativeHtmlFile.mockResolvedValue({ name: "example.html", path: "/mock-exports/example.html" });
mockedOpenNativeContainingFolder.mockRejectedValue(new Error("Synthetic folder failure"));
const handlers = await prepareExportFeedback();
await act(async () => { await handlers.exportHtml?.(); });
fireEvent.click(await screen.findByRole("button", { name: "Show in folder" }));
expect(await screen.findByText("Could not show the exported file in its folder.")).toBeInTheDocument();
});

it.each(["web-print://example.pdf", "web-download://example.html"])("does not claim a local export completed for %s", async (path) => {
mockedSaveNativeHtmlFile.mockResolvedValue({ name: "example.html", path });
const handlers = await prepareExportFeedback();
await act(async () => { await handlers.exportHtml?.(); });
await waitFor(() => expect(mockedSaveNativeHtmlFile).toHaveBeenCalled());
expect(document.querySelector(".app-toast")).not.toBeInTheDocument();
});

it("exports the current markdown document as standalone HTML from the native menu", async () => {
mockedGetStoredExportSettings.mockResolvedValue({
...defaultExportSettings,
Expand Down
66 changes: 47 additions & 19 deletions packages/app/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3881,6 +3881,39 @@ function WorkspaceApp() {
title: context.name
});
}, [exportFeatureEnabled, readCurrentMarkdownForDocument]);
const notifyExportSaved = useCallback((file: { name: string; path: string } | null) => {
if (!file) return;
// Browser download/print requests do not confirm that a local file was saved.
if (file.path.startsWith("web-download://") || file.path.startsWith("web-print://")) return;

showAppToast({
action: {
label: translate("app.exportShowInFolder"),
onClick: () => {
openNativeContainingFolder(file.path).catch(() => {
showAppToast({
message: translate("app.exportRevealFailed"),
status: "error",
surface: "notice"
});
});
}
},
description: file.name,
duration: 10000,
id: `document-export:${file.path}`,
message: translate("app.exportSucceeded"),
status: "success",
surface: "notice"
});
}, [translate]);
const notifyExportFailed = useCallback(() => {
showAppToast({
message: translate("app.exportFailed"),
status: "error",
surface: "notice"
});
}, [translate]);
const handleRenderedExport = useCallback((exported: RenderedMarkdownExport) => {
if (!exportFeatureEnabled || exportSnapshot?.id !== exported.id) return;

Expand All @@ -3900,23 +3933,14 @@ function WorkspaceApp() {
});
const suggestedName = exportDocumentFileName(exported.title, exported.kind);

if (exported.kind === "html") {
saveNativeHtmlFile({
contents,
suggestedName
}).catch(() => {}).finally(() => {
const saveFile = exported.kind === "html" ? saveNativeHtmlFile : saveNativePdfFile;
saveFile({ contents, suggestedName })
.then(notifyExportSaved)
.catch(notifyExportFailed)
.finally(() => {
clearExportSnapshot(exported.id);
});
return;
}

saveNativePdfFile({
contents,
suggestedName
}).catch(() => {}).finally(() => {
clearExportSnapshot(exported.id);
});
}, [appLanguage.language, clearExportSnapshot, exportFeatureEnabled, exportSettings.settings, exportSnapshot?.id]);
}, [appLanguage.language, clearExportSnapshot, exportFeatureEnabled, exportSettings.settings, exportSnapshot?.id, notifyExportFailed, notifyExportSaved]);
const exportHtmlDocument = useCallback(() => beginDocumentExport("html"), [beginDocumentExport]);
const exportPdfDocument = useCallback(() => beginDocumentExport("pdf"), [beginDocumentExport]);
const exportMarkdownDocument = useCallback(() => {
Expand Down Expand Up @@ -3946,16 +3970,18 @@ function WorkspaceApp() {
references,
rootPath: fileTree.sourcePath ?? context.path,
suggestedName: exportDocumentFileName(context.name, "markdown")
}).catch(() => {
}).then(notifyExportSaved).catch(() => {
showAppToast({
message: translate("app.markdownExportFailed"),
status: "error"
status: "error",
surface: "notice"
});
});
}, [
editorPreferences.preferences.clipboardImageFolder,
fileTree.sourcePath,
markdownBundleFeatureEnabled,
notifyExportSaved,
readCurrentMarkdownForDocument,
translate
]);
Expand All @@ -3972,11 +3998,12 @@ function WorkspaceApp() {
pandocArgs: exportSettings.settings.pandocArgs,
pandocPath: exportSettings.settings.pandocPath,
suggestedName: exportDocumentFileName(context.name, format)
}).catch((error: unknown) => {
}).then(notifyExportSaved).catch((error: unknown) => {
if (!isPandocSetupError(error)) {
showAppToast({
message: translate("app.pandocExportFailed"),
status: "error"
status: "error",
surface: "notice"
});
return;
}
Expand All @@ -3996,6 +4023,7 @@ function WorkspaceApp() {
exportSettings.settings.pandocArgs,
exportSettings.settings.pandocPath,
pandocFeatureEnabled,
notifyExportSaved,
readCurrentMarkdownForDocument,
translate
]);
Expand Down
4 changes: 4 additions & 0 deletions packages/shared/src/i18n/locales/de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -422,6 +422,10 @@ const messages: LocaleMessages = {
"app.installPandoc": "Pandoc installieren",
"app.setPandocPath": "Pandoc-Pfad festlegen",
"app.cancelPandocSetup": "Abbrechen",
"app.exportSucceeded": "Export abgeschlossen",
"app.exportShowInFolder": "Im Ordner anzeigen",
"app.exportFailed": "Das Dokument konnte nicht exportiert werden.",
"app.exportRevealFailed": "Die exportierte Datei konnte nicht im Ordner angezeigt werden.",
"app.pandocExportFailed": "Export mit Pandoc fehlgeschlagen.",
"app.markdownExportFailed": "Markdown mit Anhängen konnte nicht exportiert werden.",
"app.markdownExportRequiresSavedDocument": "Speichern Sie das Dokument, bevor Sie Markdown mit Anhängen exportieren.",
Expand Down
4 changes: 4 additions & 0 deletions packages/shared/src/i18n/locales/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -777,6 +777,10 @@ const messages: BaseLocaleMessages = {
"app.installPandoc": "Install Pandoc",
"app.setPandocPath": "Set Pandoc path",
"app.cancelPandocSetup": "Cancel",
"app.exportSucceeded": "Export complete",
"app.exportShowInFolder": "Show in folder",
"app.exportFailed": "Could not export the document.",
"app.exportRevealFailed": "Could not show the exported file in its folder.",
"app.pandocExportFailed": "Could not export with Pandoc.",
"app.markdownExportFailed": "Could not export Markdown with attachments.",
"app.markdownExportRequiresSavedDocument": "Save the document before exporting Markdown with attachments.",
Expand Down
4 changes: 4 additions & 0 deletions packages/shared/src/i18n/locales/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -422,6 +422,10 @@ const messages: LocaleMessages = {
"app.installPandoc": "Instalar Pandoc",
"app.setPandocPath": "Definir ruta de Pandoc",
"app.cancelPandocSetup": "Cancelar",
"app.exportSucceeded": "Exportación completada",
"app.exportShowInFolder": "Mostrar en la carpeta",
"app.exportFailed": "No se pudo exportar el documento.",
"app.exportRevealFailed": "No se pudo mostrar el archivo exportado en su carpeta.",
"app.pandocExportFailed": "No se pudo exportar con Pandoc.",
"app.markdownExportFailed": "No se pudo exportar Markdown con archivos adjuntos.",
"app.markdownExportRequiresSavedDocument": "Guarda el documento antes de exportar Markdown con archivos adjuntos.",
Expand Down
4 changes: 4 additions & 0 deletions packages/shared/src/i18n/locales/fr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -422,6 +422,10 @@ const messages: LocaleMessages = {
"app.installPandoc": "Installer Pandoc",
"app.setPandocPath": "Définir le chemin Pandoc",
"app.cancelPandocSetup": "Annuler",
"app.exportSucceeded": "Export terminé",
"app.exportShowInFolder": "Afficher dans le dossier",
"app.exportFailed": "Impossible d’exporter le document.",
"app.exportRevealFailed": "Impossible d’afficher le fichier exporté dans son dossier.",
"app.pandocExportFailed": "Impossible d’exporter avec Pandoc.",
"app.markdownExportFailed": "Impossible d’exporter Markdown avec les pièces jointes.",
"app.markdownExportRequiresSavedDocument": "Enregistrez le document avant d’exporter Markdown avec les pièces jointes.",
Expand Down
4 changes: 4 additions & 0 deletions packages/shared/src/i18n/locales/it.ts
Original file line number Diff line number Diff line change
Expand Up @@ -422,6 +422,10 @@ const messages: LocaleMessages = {
"app.installPandoc": "Installa Pandoc",
"app.setPandocPath": "Imposta percorso Pandoc",
"app.cancelPandocSetup": "Annulla",
"app.exportSucceeded": "Esportazione completata",
"app.exportShowInFolder": "Mostra nella cartella",
"app.exportFailed": "Impossibile esportare il documento.",
"app.exportRevealFailed": "Impossibile mostrare il file esportato nella sua cartella.",
"app.pandocExportFailed": "Impossibile esportare con Pandoc.",
"app.markdownExportFailed": "Impossibile esportare Markdown con allegati.",
"app.markdownExportRequiresSavedDocument": "Salva il documento prima di esportare Markdown con allegati.",
Expand Down
4 changes: 4 additions & 0 deletions packages/shared/src/i18n/locales/ja.ts
Original file line number Diff line number Diff line change
Expand Up @@ -431,6 +431,10 @@ const messages: LocaleMessages = {
"app.installPandoc": "Pandoc をインストール",
"app.setPandocPath": "Pandoc パスを設定",
"app.cancelPandocSetup": "キャンセル",
"app.exportSucceeded": "書き出し完了",
"app.exportShowInFolder": "フォルダーに表示",
"app.exportFailed": "ドキュメントを書き出せませんでした。",
"app.exportRevealFailed": "書き出したファイルをフォルダーに表示できませんでした。",
"app.pandocExportFailed": "Pandoc で書き出せませんでした。",
"app.markdownExportFailed": "添付ファイル付き Markdown を書き出せませんでした。",
"app.markdownExportRequiresSavedDocument": "添付ファイル付き Markdown を書き出す前に文書を保存してください。",
Expand Down
4 changes: 4 additions & 0 deletions packages/shared/src/i18n/locales/ko.ts
Original file line number Diff line number Diff line change
Expand Up @@ -422,6 +422,10 @@ const messages: LocaleMessages = {
"app.installPandoc": "Pandoc 설치",
"app.setPandocPath": "Pandoc 경로 설정",
"app.cancelPandocSetup": "취소",
"app.exportSucceeded": "내보내기 완료",
"app.exportShowInFolder": "폴더에서 보기",
"app.exportFailed": "문서를 내보낼 수 없습니다.",
"app.exportRevealFailed": "내보낸 파일을 폴더에서 표시할 수 없습니다.",
"app.pandocExportFailed": "Pandoc으로 내보낼 수 없습니다.",
"app.markdownExportFailed": "첨부 파일과 함께 Markdown을 내보낼 수 없습니다.",
"app.markdownExportRequiresSavedDocument": "첨부 파일과 함께 Markdown을 내보내기 전에 문서를 저장하세요.",
Expand Down
4 changes: 4 additions & 0 deletions packages/shared/src/i18n/locales/pt-BR.ts
Original file line number Diff line number Diff line change
Expand Up @@ -422,6 +422,10 @@ const messages: LocaleMessages = {
"app.installPandoc": "Instalar Pandoc",
"app.setPandocPath": "Definir caminho do Pandoc",
"app.cancelPandocSetup": "Cancelar",
"app.exportSucceeded": "Exportação concluída",
"app.exportShowInFolder": "Mostrar na pasta",
"app.exportFailed": "Não foi possível exportar o documento.",
"app.exportRevealFailed": "Não foi possível mostrar o arquivo exportado na pasta.",
"app.pandocExportFailed": "Não foi possível exportar com Pandoc.",
"app.markdownExportFailed": "Não foi possível exportar Markdown com anexos.",
"app.markdownExportRequiresSavedDocument": "Salve o documento antes de exportar Markdown com anexos.",
Expand Down
4 changes: 4 additions & 0 deletions packages/shared/src/i18n/locales/ru.ts
Original file line number Diff line number Diff line change
Expand Up @@ -422,6 +422,10 @@ const messages: LocaleMessages = {
"app.installPandoc": "Установить Pandoc",
"app.setPandocPath": "Задать путь к Pandoc",
"app.cancelPandocSetup": "Отмена",
"app.exportSucceeded": "Экспорт завершён",
"app.exportShowInFolder": "Показать в папке",
"app.exportFailed": "Не удалось экспортировать документ.",
"app.exportRevealFailed": "Не удалось показать экспортированный файл в папке.",
"app.pandocExportFailed": "Не удалось экспортировать через Pandoc.",
"app.markdownExportFailed": "Не удалось экспортировать Markdown с вложениями.",
"app.markdownExportRequiresSavedDocument": "Сохраните документ перед экспортом Markdown с вложениями.",
Expand Down
4 changes: 4 additions & 0 deletions packages/shared/src/i18n/locales/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -788,6 +788,10 @@ export type I18nKey =
| "app.installPandoc"
| "app.setPandocPath"
| "app.cancelPandocSetup"
| "app.exportSucceeded"
| "app.exportShowInFolder"
| "app.exportFailed"
| "app.exportRevealFailed"
| "app.pandocExportFailed"
| "app.markdownExportFailed"
| "app.markdownExportRequiresSavedDocument"
Expand Down
4 changes: 4 additions & 0 deletions packages/shared/src/i18n/locales/zh-CN.ts
Original file line number Diff line number Diff line change
Expand Up @@ -777,6 +777,10 @@ const messages: LocaleMessages = {
"app.installPandoc": "安装 Pandoc",
"app.setPandocPath": "设置 Pandoc 路径",
"app.cancelPandocSetup": "取消",
"app.exportSucceeded": "导出完成",
"app.exportShowInFolder": "在文件夹中显示",
"app.exportFailed": "无法导出文档。",
"app.exportRevealFailed": "无法在文件夹中显示已导出的文件。",
"app.pandocExportFailed": "无法使用 Pandoc 导出。",
"app.markdownExportFailed": "无法导出带附件的 Markdown。",
"app.markdownExportRequiresSavedDocument": "请先保存文档,再导出带附件的 Markdown。",
Expand Down
4 changes: 4 additions & 0 deletions packages/shared/src/i18n/locales/zh-TW.ts
Original file line number Diff line number Diff line change
Expand Up @@ -431,6 +431,10 @@ const messages: LocaleMessages = {
"app.installPandoc": "安裝 Pandoc",
"app.setPandocPath": "設定 Pandoc 路徑",
"app.cancelPandocSetup": "取消",
"app.exportSucceeded": "匯出完成",
"app.exportShowInFolder": "在資料夾中顯示",
"app.exportFailed": "無法匯出文件。",
"app.exportRevealFailed": "無法在資料夾中顯示已匯出的檔案。",
"app.pandocExportFailed": "無法使用 Pandoc 匯出。",
"app.markdownExportFailed": "無法匯出含附件的 Markdown。",
"app.markdownExportRequiresSavedDocument": "請先儲存文件,再匯出含附件的 Markdown。",
Expand Down
Loading