diff --git a/web/src/components/MemoActionMenu/MemoActionMenu.tsx b/web/src/components/MemoActionMenu/MemoActionMenu.tsx index afe2c0ce26e43..0270ccbe3f456 100644 --- a/web/src/components/MemoActionMenu/MemoActionMenu.tsx +++ b/web/src/components/MemoActionMenu/MemoActionMenu.tsx @@ -5,6 +5,7 @@ import { BookmarkPlusIcon, CopyIcon, Edit3Icon, + FileDownIcon, FileTextIcon, LinkIcon, MoreVerticalIcon, @@ -49,6 +50,7 @@ const MemoActionMenu = (props: MemoActionMenuProps) => { handleToggleMemoStatusClick, handleCopyLink, handleCopyContent, + handleExportAsPDF, handleDeleteMemoClick, confirmDeleteMemo, handleRemoveCompletedTaskListItemsClick, @@ -100,6 +102,10 @@ const MemoActionMenu = (props: MemoActionMenuProps) => { {t("memo.copy-content")} + + + {t("memo.export-as-pdf")} + )} diff --git a/web/src/components/MemoActionMenu/hooks.ts b/web/src/components/MemoActionMenu/hooks.ts index 18db5b28d7d15..987c8afb3ae40 100644 --- a/web/src/components/MemoActionMenu/hooks.ts +++ b/web/src/components/MemoActionMenu/hooks.ts @@ -12,6 +12,7 @@ import { State } from "@/types/proto/api/v1/common_pb"; import type { Memo } from "@/types/proto/api/v1/memo_service_pb"; import { useTranslate } from "@/utils/i18n"; import { removeCompletedTasks } from "@/utils/markdown-manipulation"; +import { printMemoAsPDF } from "@/utils/print"; interface UseMemoActionHandlersOptions { memo: Memo; @@ -95,6 +96,17 @@ export const useMemoActionHandlers = ({ memo, onEdit, setDeleteDialogOpen, setRe toast.success(t("message.succeed-copy-content")); }, [memo.content, t]); + const handleExportAsPDF = useCallback(() => { + try { + printMemoAsPDF(memo); + } catch (error: unknown) { + handleError(error, toast.error, { + context: "Export memo as PDF", + fallbackMessage: "Failed to export PDF. Please allow popups for this site.", + }); + } + }, [memo]); + const handleDeleteMemoClick = useCallback(() => { setDeleteDialogOpen(true); }, [setDeleteDialogOpen]); @@ -131,6 +143,7 @@ export const useMemoActionHandlers = ({ memo, onEdit, setDeleteDialogOpen, setRe handleToggleMemoStatusClick, handleCopyLink, handleCopyContent, + handleExportAsPDF, handleDeleteMemoClick, confirmDeleteMemo, handleRemoveCompletedTaskListItemsClick, diff --git a/web/src/components/MemoActionMenu/types.ts b/web/src/components/MemoActionMenu/types.ts index 9133f95ea5282..19b8eaf3060ec 100644 --- a/web/src/components/MemoActionMenu/types.ts +++ b/web/src/components/MemoActionMenu/types.ts @@ -13,6 +13,7 @@ export interface UseMemoActionHandlersReturn { handleToggleMemoStatusClick: () => Promise; handleCopyLink: () => void; handleCopyContent: () => void; + handleExportAsPDF: () => void; handleDeleteMemoClick: () => void; confirmDeleteMemo: () => Promise; handleRemoveCompletedTaskListItemsClick: () => void; diff --git a/web/src/components/MemoView/MemoView.tsx b/web/src/components/MemoView/MemoView.tsx index 9d617b7f86894..089cd2f2782cc 100644 --- a/web/src/components/MemoView/MemoView.tsx +++ b/web/src/components/MemoView/MemoView.tsx @@ -68,7 +68,7 @@ const MemoView: React.FC = (props: MemoViewProps) => { return ( -
+
{ + // Create a new window for printing + const printWindow = window.open("", "_blank"); + if (!printWindow) { + throw new Error("Failed to open print window. Please allow popups for this site."); + } + + // Get the current theme's styles + const themeStyles = Array.from(document.styleSheets) + .map((styleSheet) => { + try { + return Array.from(styleSheet.cssRules) + .map((rule) => rule.cssText) + .join("\n"); + } catch (e) { + // Cross-origin stylesheets may throw an error + return ""; + } + }) + .join("\n"); + + // Get memo content + const memoContent = document.querySelector(`[data-memo-name="${memo.name}"] .markdown-content`)?.innerHTML || memo.content; + + // Format timestamps + const createTime = memo.createTime ? timestampDate(memo.createTime).toLocaleString() : "Unknown"; + const updateTime = memo.updateTime ? timestampDate(memo.updateTime).toLocaleString() : null; + const shouldShowUpdateTime = updateTime && memo.updateTime !== memo.createTime; + + // Create the print HTML + const printHTML = ` + + + + + + ${memo.name} - Memos + + + +
+ +
+ ${memoContent} +
+
+ + + + `; + + // Write the HTML to the new window + printWindow.document.write(printHTML); + printWindow.document.close(); +}; + +/** + * Prints the current page/memo using the browser's print dialog. + * This is a simpler alternative that doesn't create a new window. + */ +export const printCurrentPage = () => { + window.print(); +};