Skip to content
Open
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
3 changes: 3 additions & 0 deletions apps/client/src/translations/en/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,9 @@
"opml_version_1": "OPML v1.0 - plain text only",
"opml_version_2": "OPML v2.0 - allows also HTML",
"export_type_single": "Only this note without its descendants",
"export_to_clipboard": "Export to clipboard",
"export_to_clipboard_on_tooltip": "Export the note content to clipboard.",
"export_to_clipboard_off_tooltip": "Download the note as a file.",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

To support the error handling suggested in export.tsx, a new translation key for a failed clipboard export should be added here. For example:

"export_failed_to_clipboard": "Failed to export note content to clipboard."

Suggested change
"export_to_clipboard_off_tooltip": "Download the note as a file.",
"export_to_clipboard_off_tooltip": "Download the note as a file.",
"export_failed_to_clipboard": "Failed to export note content to clipboard.",

"export": "Export",
"choose_export_type": "Choose export type first please",
"export_status": "Export status",
Expand Down
6 changes: 5 additions & 1 deletion apps/client/src/widgets/dialogs/export.css
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,8 @@

.export-dialog form .form-check-label {
padding: 2px;
}
}

.export-dialog form .export-single-formats .switch-widget {
margin-top: 10px;
}
49 changes: 44 additions & 5 deletions apps/client/src/widgets/dialogs/export.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { useState } from "preact/hooks";
import { t } from "../../services/i18n";
import tree from "../../services/tree";
import { copyTextWithToast } from "../../services/clipboard_ext.js";
import Button from "../react/Button";
import FormRadioGroup from "../react/FormRadioGroup";
import FormToggle from "../react/FormToggle";
import Modal from "../react/Modal";
import "./export.css";
import ws from "../../services/ws";
Expand All @@ -21,10 +23,12 @@ interface ExportDialogProps {
export default function ExportDialog() {
const [ opts, setOpts ] = useState<ExportDialogProps>();
const [ exportType, setExportType ] = useState<string>(opts?.defaultType ?? "subtree");
const [ exportToClipboard, setExportToClipboard ] = useState(false);
const [ subtreeFormat, setSubtreeFormat ] = useState("html");
const [ singleFormat, setSingleFormat ] = useState("html");
const [ opmlVersion, setOpmlVersion ] = useState("2.0");
const [ shown, setShown ] = useState(false);
const [ exporting, setExporting ] = useState(false);

useTriliumEvent("showExportDialog", async ({ notePath, defaultType }) => {
const { noteId, parentNoteId } = tree.getNoteIdAndParentIdFromUrl(notePath);
Expand All @@ -47,18 +51,20 @@ export default function ExportDialog() {
className="export-dialog"
title={`${t("export.export_note_title")} ${opts?.noteTitle ?? ""}`}
size="lg"
onSubmit={() => {
onSubmit={async () => {
if (!opts || !opts.branchId) {
return;
}

const format = (exportType === "subtree" ? subtreeFormat : singleFormat);
const version = (format === "opml" ? opmlVersion : "1.0");
exportBranch(opts.branchId, exportType, format, version);
setExporting(true);
await exportBranch(opts.branchId, exportType, format, version, exportToClipboard);
setExporting(false);
setShown(false);
}}
onHidden={() => setShown(false)}
footer={<Button className="export-button" text={t("export.export")} primary />}
footer={<Button className="export-button" text={t("export.export")} primary disabled={exporting} />}
show={shown}
>

Expand Down Expand Up @@ -118,17 +124,50 @@ export default function ExportDialog() {
{ value: "markdown", label: t("export.format_markdown") }
]}
/>

<FormToggle
switchOnName={t("export.export_to_clipboard")} switchOnTooltip={t("export.export_to_clipboard_on_tooltip")}
switchOffName={t("export.export_to_clipboard")} switchOffTooltip={t("export.export_to_clipboard_off_tooltip")}
currentValue={exportToClipboard} onChange={setExportToClipboard}
/>
</div>
}

</Modal>
);
}

function exportBranch(branchId: string, type: string, format: string, version: string) {
async function exportBranch(branchId: string, type: string, format: string, version: string, exportToClipboard: boolean) {
const taskId = utils.randomString(10);
const url = open.getUrlForDownload(`api/branches/${branchId}/export/${type}/${format}/${version}/${taskId}`);
open.download(url);
if (type === "single" && exportToClipboard) {
await exportSingleToClipboard(url);
} else {
open.download(url);
}
}

async function exportSingleToClipboard(url: string) {
try {
const res = await fetch(url);
if (!res.ok) {
throw new Error(`${res.status} ${res.statusText}`);
}
const blob = await res.blob();

// Try reading as text (HTML/Markdown are text); if that fails, fall back to ArrayBuffer->UTF-8
let text: string;
try {
text = await blob.text();
} catch {
const ab = await blob.arrayBuffer();
text = new TextDecoder("utf-8").decode(new Uint8Array(ab));
}

await copyTextWithToast(text);
} catch (error) {
console.error("Failed to copy exported note to clipboard:", error);
}
Comment on lines +168 to +170
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The catch block in exportSingleToClipboard currently only logs the error to the console. It would be beneficial to also provide user-facing feedback, such as a toast message, if the export to clipboard operation fails (e.g., due to network issues during the fetch request). This ensures the user is aware that the action did not complete successfully.

Consider adding a new translation key, for example, export.export_failed_to_clipboard, to apps/client/src/translations/en/translation.json for this error message.

        await copyTextWithToast(text);
    } catch (error) {
        console.error("Failed to copy exported note to clipboard:", error);
        toastService.showError(t("export.export_failed_to_clipboard"));
    }

}

ws.subscribeToMessages(async (message) => {
Expand Down