From 5eb32744c3e04284ef9b4ae9c4507cea4f117d7a Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Tue, 10 Feb 2026 17:26:58 +0200 Subject: [PATCH] feat(desktop): display print errors --- apps/client/src/print.tsx | 4 + .../src/translations/en/translation.json | 2 + apps/client/src/widgets/NoteDetail.tsx | 28 +++- apps/server/src/services/window.ts | 157 +++++++++++------- 4 files changed, 131 insertions(+), 60 deletions(-) diff --git a/apps/client/src/print.tsx b/apps/client/src/print.tsx index 96461db2dc..dc7817d9b5 100644 --- a/apps/client/src/print.tsx +++ b/apps/client/src/print.tsx @@ -18,6 +18,10 @@ export type PrintReport = { } | { type: "collection"; ignoredNoteIds: string[]; +} | { + type: "error"; + message: string; + stack?: string; }; async function main() { diff --git a/apps/client/src/translations/en/translation.json b/apps/client/src/translations/en/translation.json index 01d69ec156..c8242986b5 100644 --- a/apps/client/src/translations/en/translation.json +++ b/apps/client/src/translations/en/translation.json @@ -1797,6 +1797,8 @@ "printing": "Printing in progress...", "printing_pdf": "Exporting to PDF in progress...", "print_report_title": "Print report", + "print_report_error_title": "Failed to print", + "print_report_stack_trace": "Stack trace", "print_report_collection_content_one": "{{count}} note in the collection could not be printed because they are not supported or they are protected.", "print_report_collection_content_other": "{{count}} notes in the collection could not be printed because they are not supported or they are protected.", "print_report_collection_details_button": "See details", diff --git a/apps/client/src/widgets/NoteDetail.tsx b/apps/client/src/widgets/NoteDetail.tsx index 78450811da..c49802a7a4 100644 --- a/apps/client/src/widgets/NoteDetail.tsx +++ b/apps/client/src/widgets/NoteDetail.tsx @@ -370,7 +370,33 @@ function showToast(type: "printing" | "exporting_pdf", progress: number = 0) { } function handlePrintReport(printReport?: PrintReport) { - if (printReport?.type === "collection" && printReport.ignoredNoteIds.length > 0) { + if (!printReport) return; + + if (printReport.type === "error") { + toast.showPersistent({ + id: "print-error", + icon: "bx bx-error-circle", + title: t("note_detail.print_report_error_title"), + message: printReport.message, + buttons: printReport.stack ? [ + { + text: t("note_detail.print_report_collection_details_button"), + onClick(api) { + api.dismissToast(); + dialog.info(<> +

{printReport.message}

+
+ {t("note_detail.print_report_stack_trace")} +
{printReport.stack}
+
+ , { + title: t("note_detail.print_report_error_title") + }); + } + } + ] : undefined + }); + } else if (printReport.type === "collection" && printReport.ignoredNoteIds.length > 0) { toast.showPersistent({ id: "print-report", icon: "bx bx-collection", diff --git a/apps/server/src/services/window.ts b/apps/server/src/services/window.ts index 96c7ce6ff3..13cfee2818 100644 --- a/apps/server/src/services/window.ts +++ b/apps/server/src/services/window.ts @@ -81,66 +81,82 @@ interface ExportAsPdfOpts { } electron.ipcMain.on("print-note", async (e, { notePath }: PrintOpts) => { - const { browserWindow, printReport } = await getBrowserWindowForPrinting(e, notePath, "printing"); - browserWindow.webContents.print({}, (success, failureReason) => { - if (!success && failureReason !== "Print job canceled") { - electron.dialog.showErrorBox(t("pdf.unable-to-print"), failureReason); - } - e.sender.send("print-done", printReport); - browserWindow.destroy(); - }); + try { + const { browserWindow, printReport } = await getBrowserWindowForPrinting(e, notePath, "printing"); + browserWindow.webContents.print({}, (success, failureReason) => { + if (!success && failureReason !== "Print job canceled") { + electron.dialog.showErrorBox(t("pdf.unable-to-print"), failureReason); + } + e.sender.send("print-done", printReport); + browserWindow.destroy(); + }); + } catch (err) { + e.sender.send("print-done", { + type: "error", + message: err instanceof Error ? err.message : String(err), + stack: err instanceof Error ? err.stack : undefined + }); + } }); electron.ipcMain.on("export-as-pdf", async (e, { title, notePath, landscape, pageSize }: ExportAsPdfOpts) => { - const { browserWindow, printReport } = await getBrowserWindowForPrinting(e, notePath, "exporting_pdf"); - - async function print() { - const filePath = electron.dialog.showSaveDialogSync(browserWindow, { - defaultPath: formatDownloadTitle(title, "file", "application/pdf"), - filters: [ - { - name: t("pdf.export_filter"), - extensions: ["pdf"] - } - ] - }); - if (!filePath) return; - - let buffer: Buffer; - try { - buffer = await browserWindow.webContents.printToPDF({ - landscape, - pageSize, - generateDocumentOutline: true, - generateTaggedPDF: true, - printBackground: true, - displayHeaderFooter: true, - headerTemplate: `
`, - footerTemplate: ` -
-
- ` - }); - } catch (_e) { - electron.dialog.showErrorBox(t("pdf.unable-to-export-title"), t("pdf.unable-to-export-message")); - return; - } - - try { - await fs.writeFile(filePath, buffer); - } catch (_e) { - electron.dialog.showErrorBox(t("pdf.unable-to-export-title"), t("pdf.unable-to-save-message")); - return; - } - - electron.shell.openPath(filePath); - } - try { - await print(); - } finally { - e.sender.send("print-done", printReport); - browserWindow.destroy(); + const { browserWindow, printReport } = await getBrowserWindowForPrinting(e, notePath, "exporting_pdf"); + + async function print() { + const filePath = electron.dialog.showSaveDialogSync(browserWindow, { + defaultPath: formatDownloadTitle(title, "file", "application/pdf"), + filters: [ + { + name: t("pdf.export_filter"), + extensions: ["pdf"] + } + ] + }); + if (!filePath) return; + + let buffer: Buffer; + try { + buffer = await browserWindow.webContents.printToPDF({ + landscape, + pageSize, + generateDocumentOutline: true, + generateTaggedPDF: true, + printBackground: true, + displayHeaderFooter: true, + headerTemplate: `
`, + footerTemplate: ` +
+
+ ` + }); + } catch (_e) { + electron.dialog.showErrorBox(t("pdf.unable-to-export-title"), t("pdf.unable-to-export-message")); + return; + } + + try { + await fs.writeFile(filePath, buffer); + } catch (_e) { + electron.dialog.showErrorBox(t("pdf.unable-to-export-title"), t("pdf.unable-to-save-message")); + return; + } + + electron.shell.openPath(filePath); + } + + try { + await print(); + } finally { + e.sender.send("print-done", printReport); + browserWindow.destroy(); + } + } catch (err) { + e.sender.send("print-done", { + type: "error", + message: err instanceof Error ? err.message : String(err), + stack: err instanceof Error ? err.stack : undefined + }); } }); @@ -176,16 +192,29 @@ async function getBrowserWindowForPrinting(e: IpcMainEvent, notePath: string, ac throw err; } - // Set up error logging in the renderer process before content loads + // Set up error tracking and logging in the renderer process await browserWindow.webContents.executeJavaScript(` (function() { + window._printWindowErrors = []; window.addEventListener("error", (e) => { - console.error("Uncaught error:", e.message, "at", e.filename + ":" + e.lineno + ":" + e.colno); + const errorMsg = "Uncaught error: " + e.message + " at " + e.filename + ":" + e.lineno + ":" + e.colno; + console.error(errorMsg); if (e.error?.stack) console.error(e.error.stack); + window._printWindowErrors.push({ + type: 'error', + message: errorMsg, + stack: e.error?.stack + }); }); window.addEventListener("unhandledrejection", (e) => { - console.error("Unhandled rejection:", String(e.reason)); + const errorMsg = "Unhandled rejection: " + String(e.reason); + console.error(errorMsg); if (e.reason?.stack) console.error(e.reason.stack); + window._printWindowErrors.push({ + type: 'rejection', + message: errorMsg, + stack: e.reason?.stack + }); }); })(); `).catch(err => log.error(`Failed to set up error handlers in print window: ${err}`)); @@ -196,7 +225,17 @@ async function getBrowserWindowForPrinting(e: IpcMainEvent, notePath: string, ac new Promise((resolve, reject) => { if (window._noteReady) return resolve(window._noteReady); + // Check for errors periodically + const errorChecker = setInterval(() => { + if (window._printWindowErrors && window._printWindowErrors.length > 0) { + clearInterval(errorChecker); + const errors = window._printWindowErrors.map(e => e.message).join('; '); + reject(new Error("Print window errors: " + errors)); + } + }, 100); + window.addEventListener("note-ready", (data) => { + clearInterval(errorChecker); resolve(data.detail); }); });