feat(ckeditor/watchdog): functional copy to clipboard button

This commit is contained in:
Elian Doran 2025-12-07 21:21:55 +02:00
parent 75a1fcc933
commit 397fb785d6
No known key found for this signature in database
7 changed files with 58 additions and 16 deletions

View File

@ -205,7 +205,8 @@
"info": { "info": {
"modalTitle": "Info message", "modalTitle": "Info message",
"closeButton": "Close", "closeButton": "Close",
"okButton": "OK" "okButton": "OK",
"copy_to_clipboard": "Copy to clipboard"
}, },
"jump_to_note": { "jump_to_note": {
"search_placeholder": "Search for note by its name or type > for commands...", "search_placeholder": "Search for note by its name or type > for commands...",

View File

@ -8,11 +8,19 @@ import { useTriliumEvent } from "../react/hooks";
import { isValidElement } from "preact"; import { isValidElement } from "preact";
import { ConfirmWithMessageOptions } from "./confirm"; import { ConfirmWithMessageOptions } from "./confirm";
import "./info.css"; import "./info.css";
import server from "../../services/server";
import { ToMarkdownResponse } from "@triliumnext/commons";
import { copyTextWithToast } from "../../services/clipboard_ext";
export interface InfoExtraProps extends Partial<Pick<ModalProps, "size" | "title">> {
/** Adds a button in the footer that allows easily copying the content of the infobox to clipboard. */
copyToClipboardButton?: boolean;
}
export type InfoExtraProps = Partial<Pick<ModalProps, "size" | "title">>;
export type InfoProps = ConfirmWithMessageOptions & InfoExtraProps; export type InfoProps = ConfirmWithMessageOptions & InfoExtraProps;
export default function InfoDialog() { export default function InfoDialog() {
const modalRef = useRef<HTMLDivElement>(null);
const [ opts, setOpts ] = useState<EventData<"showInfoDialog">>(); const [ opts, setOpts ] = useState<EventData<"showInfoDialog">>();
const [ shown, setShown ] = useState(false); const [ shown, setShown ] = useState(false);
const okButtonRef = useRef<HTMLButtonElement>(null); const okButtonRef = useRef<HTMLButtonElement>(null);
@ -31,11 +39,28 @@ export default function InfoDialog() {
setShown(false); setShown(false);
}} }}
onShown={() => okButtonRef.current?.focus?.()} onShown={() => okButtonRef.current?.focus?.()}
footer={<Button modalRef={modalRef}
buttonRef={okButtonRef} footer={<>
text={t("info.okButton")} {opts?.copyToClipboardButton && (
onClick={() => setShown(false)} <Button
/>} text={t("info.copy_to_clipboard")}
icon="bx bx-copy"
onClick={async () => {
const htmlContent = modalRef.current?.querySelector<HTMLDivElement>(".modal-body")?.innerHTML;
if (!htmlContent) return;
const { markdownContent } = await server.post<ToMarkdownResponse>("other/to-markdown", { htmlContent });
copyTextWithToast(markdownContent);
}}
/>
)}
<Button
buttonRef={okButtonRef}
text={t("info.okButton")}
onClick={() => setShown(false)}
/>
</>}
show={shown} show={shown}
stackable stackable
scrollable scrollable

View File

@ -7,15 +7,12 @@ import Modal from "../react/Modal";
import Button from "../react/Button"; import Button from "../react/Button";
import { useTriliumEvent } from "../react/hooks"; import { useTriliumEvent } from "../react/hooks";
import { CKEditorApi } from "../type_widgets/text/CKEditorWithWatchdog"; import { CKEditorApi } from "../type_widgets/text/CKEditorWithWatchdog";
import { RenderMarkdownResponse } from "@triliumnext/commons";
export interface MarkdownImportOpts { export interface MarkdownImportOpts {
editorApi: CKEditorApi; editorApi: CKEditorApi;
} }
interface RenderMarkdownResponse {
htmlContent: string;
}
export default function MarkdownImportDialog() { export default function MarkdownImportDialog() {
const markdownImportTextArea = useRef<HTMLTextAreaElement>(null); const markdownImportTextArea = useRef<HTMLTextAreaElement>(null);
const editorApiRef = useRef<CKEditorApi>(null); const editorApiRef = useRef<CKEditorApi>(null);

View File

@ -310,10 +310,11 @@ function useWatchdogCrashHandling() {
dialog.info(<> dialog.info(<>
<p>{t("editable_text.editor_crashed_details_intro")}</p> <p>{t("editable_text.editor_crashed_details_intro")}</p>
<h3>{t("editable_text.editor_crashed_details_title")}</h3> <h3>{t("editable_text.editor_crashed_details_title")}</h3>
<pre>{formattedCrash}</pre> <pre><code class="language-application-json">{formattedCrash}</code></pre>
</>, { </>, {
title: t("editable_text.editor_crashed_title"), title: t("editable_text.editor_crashed_title"),
size: "lg" size: "lg",
copyToClipboardButton: true
}); });
} }
} }

View File

@ -2,6 +2,8 @@ import type { Request } from "express";
import becca from "../../becca/becca.js"; import becca from "../../becca/becca.js";
import markdownService from "../../services/import/markdown.js"; import markdownService from "../../services/import/markdown.js";
import markdown from "../../services/export/markdown.js";
import { RenderMarkdownResponse, ToMarkdownResponse } from "@triliumnext/commons";
function getIconUsage() { function getIconUsage() {
const iconClassToCountMap: Record<string, number> = {}; const iconClassToCountMap: Record<string, number> = {};
@ -29,13 +31,20 @@ function getIconUsage() {
function renderMarkdown(req: Request) { function renderMarkdown(req: Request) {
const { markdownContent } = req.body; const { markdownContent } = req.body;
return { return {
htmlContent: markdownService.renderToHtml(markdownContent, "") htmlContent: markdownService.renderToHtml(markdownContent, "")
}; } satisfies RenderMarkdownResponse;
}
function toMarkdown(req: Request) {
const { htmlContent } = req.body;
return {
markdownContent: markdown.toMarkdown(htmlContent)
} satisfies ToMarkdownResponse;
} }
export default { export default {
getIconUsage, getIconUsage,
renderMarkdown renderMarkdown,
toMarkdown
}; };

View File

@ -348,6 +348,7 @@ function register(app: express.Application) {
route(GET, "/api/fonts", [auth.checkApiAuthOrElectron], fontsRoute.getFontCss); route(GET, "/api/fonts", [auth.checkApiAuthOrElectron], fontsRoute.getFontCss);
apiRoute(GET, "/api/other/icon-usage", otherRoute.getIconUsage); apiRoute(GET, "/api/other/icon-usage", otherRoute.getIconUsage);
apiRoute(PST, "/api/other/render-markdown", otherRoute.renderMarkdown); apiRoute(PST, "/api/other/render-markdown", otherRoute.renderMarkdown);
apiRoute(PST, "/api/other/to-markdown", otherRoute.toMarkdown);
apiRoute(GET, "/api/recent-changes/:ancestorNoteId", recentChangesApiRoute.getRecentChanges); apiRoute(GET, "/api/recent-changes/:ancestorNoteId", recentChangesApiRoute.getRecentChanges);
apiRoute(GET, "/api/edited-notes/:date", revisionsApiRoute.getEditedNotesOnDate); apiRoute(GET, "/api/edited-notes/:date", revisionsApiRoute.getEditedNotesOnDate);

View File

@ -277,3 +277,11 @@ export interface NoteMapPostResponse {
export interface UpdateAttributeResponse { export interface UpdateAttributeResponse {
attributeId: string; attributeId: string;
} }
export interface RenderMarkdownResponse {
htmlContent: string;
}
export interface ToMarkdownResponse {
markdownContent: string;
}