From 1409889399a6bf41fc22643569ec50fc1d300f51 Mon Sep 17 00:00:00 2001 From: GCWing Date: Fri, 13 Mar 2026 16:41:00 +0800 Subject: [PATCH] =?UTF-8?q?=EF=BB=BFfeat(mermaid-editor):=20implement=20PN?= =?UTF-8?q?G=20export=20with=20direct=20file=20save=20and=20e2e=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Refactor export to save PNG directly to downloads folder via Tauri fs API - Simplify export button from SVG/PNG dropdown to a single PNG export button - Expose getSvgElement/getSvgDimensions via MermaidPreview ref for export use - Rewrite SVG-to-PNG conversion using Image/Canvas instead of blob URL approach - Integrate notification system to show loading/success/error during export - Prepare self-contained export SVG: strip interactive styles, add background rect - Add data-testid attributes to NotificationButton, NotificationCenter, MermaidEditor - Add l1-mermaid-export e2e test spec and corresponding npm script - Add .bitfun/ to e2e .gitignore - Add export i18n keys for en-US and zh-CN Made-with: Cursor --- .../TitleBar/NotificationButton.tsx | 1 + .../src/locales/en-US/mermaid-editor.json | 12 ++ .../src/locales/zh-CN/mermaid-editor.json | 12 ++ .../components/NotificationCenter.tsx | 4 +- .../components/MermaidEditor.tsx | 169 +++++++++++++++-- .../components/MermaidEditorHeader.css | 55 ------ .../components/MermaidEditorHeader.tsx | 26 ++- .../components/MermaidPreview.tsx | 4 + .../mermaid-editor/services/MermaidService.ts | 99 +++++++--- tests/e2e/.gitignore | 3 + tests/e2e/package.json | 1 + tests/e2e/specs/l1-mermaid-export.spec.ts | 171 ++++++++++++++++++ 12 files changed, 444 insertions(+), 113 deletions(-) create mode 100644 tests/e2e/specs/l1-mermaid-export.spec.ts diff --git a/src/web-ui/src/app/components/TitleBar/NotificationButton.tsx b/src/web-ui/src/app/components/TitleBar/NotificationButton.tsx index b0179e8b..c9dc60b7 100644 --- a/src/web-ui/src/app/components/TitleBar/NotificationButton.tsx +++ b/src/web-ui/src/app/components/TitleBar/NotificationButton.tsx @@ -55,6 +55,7 @@ const NotificationButton: React.FC = ({ className = '' ].filter(Boolean).join(' ')} onClick={() => notificationService.toggleCenter()} type="button" + data-testid="notification-button" > {activeNotification ? ( <> diff --git a/src/web-ui/src/locales/en-US/mermaid-editor.json b/src/web-ui/src/locales/en-US/mermaid-editor.json index d23dc7ef..87b65529 100644 --- a/src/web-ui/src/locales/en-US/mermaid-editor.json +++ b/src/web-ui/src/locales/en-US/mermaid-editor.json @@ -97,6 +97,18 @@ "loading": { "processing": "Processing..." }, + "export": { + "loading": { + "svgTitle": "Exporting SVG", + "svgMessage": "Generating SVG file...", + "pngTitle": "Exporting PNG", + "pngMessage": "Rendering diagram and generating PNG..." + }, + "success": { + "svg": "SVG exported successfully", + "png": "PNG exported successfully" + } + }, "panel": { "diagramName": "Mermaid Diagram" }, diff --git a/src/web-ui/src/locales/zh-CN/mermaid-editor.json b/src/web-ui/src/locales/zh-CN/mermaid-editor.json index 6c5cfb45..23bd608c 100644 --- a/src/web-ui/src/locales/zh-CN/mermaid-editor.json +++ b/src/web-ui/src/locales/zh-CN/mermaid-editor.json @@ -97,6 +97,18 @@ "loading": { "processing": "处理中..." }, + "export": { + "loading": { + "svgTitle": "正在导出 SVG", + "svgMessage": "正在生成 SVG 文件...", + "pngTitle": "正在导出 PNG", + "pngMessage": "正在渲染图表并生成 PNG..." + }, + "success": { + "svg": "SVG 导出成功", + "png": "PNG 导出成功" + } + }, "panel": { "diagramName": "Mermaid图表" }, diff --git a/src/web-ui/src/shared/notification-system/components/NotificationCenter.tsx b/src/web-ui/src/shared/notification-system/components/NotificationCenter.tsx index cbbbd76f..48a7af46 100644 --- a/src/web-ui/src/shared/notification-system/components/NotificationCenter.tsx +++ b/src/web-ui/src/shared/notification-system/components/NotificationCenter.tsx @@ -329,7 +329,7 @@ export const NotificationCenter: React.FC = () => { showCloseButton={false} size="large" > -
+

{t('components:notificationCenter.title')}

@@ -401,7 +401,7 @@ export const NotificationCenter: React.FC = () => {
{activeTaskNotifications.length > 0 && ( -
+
{t('components:notificationCenter.activeTasks.title', { count: activeTaskNotifications.length })}
diff --git a/src/web-ui/src/tools/mermaid-editor/components/MermaidEditor.tsx b/src/web-ui/src/tools/mermaid-editor/components/MermaidEditor.tsx index 10b171a3..aec1ab3d 100644 --- a/src/web-ui/src/tools/mermaid-editor/components/MermaidEditor.tsx +++ b/src/web-ui/src/tools/mermaid-editor/components/MermaidEditor.tsx @@ -12,6 +12,10 @@ import { CubeLoading } from '@/component-library/components/CubeLoading'; import { Sparkles } from 'lucide-react'; import { aiApi } from '../../../infrastructure/api'; import { useI18n } from '@/infrastructure/i18n'; +import { notificationService } from '@/shared/notification-system'; + +import { downloadDir, join } from '@tauri-apps/api/path'; +import { writeFile } from '@tauri-apps/plugin-fs'; import './MermaidEditor.css'; /** Escape regex special characters. */ @@ -29,6 +33,130 @@ const detectDiagramType = (code: string): MermaidDiagramContext['diagramType'] = return 'other'; }; +const SVG_NS = 'http://www.w3.org/2000/svg'; +const XHTML_NS = 'http://www.w3.org/1999/xhtml'; + +const sanitizeFileName = (name: string) => name.replace(/[<>:"/\\|?*\u0000-\u001F]/g, '_').trim() || 'diagram'; + +const createTimestampSuffix = () => { + const now = new Date(); + const pad = (value: number) => String(value).padStart(2, '0'); + return `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}_${pad(now.getHours())}-${pad(now.getMinutes())}-${pad(now.getSeconds())}`; +}; + +const saveExportBlob = async (blob: Blob, baseName: string, extension: 'svg' | 'png') => { + const downloadsPath = await downloadDir(); + const fileName = `${sanitizeFileName(baseName)}_${createTimestampSuffix()}.${extension}`; + const filePath = await join(downloadsPath, fileName); + const bytes = new Uint8Array(await blob.arrayBuffer()); + await writeFile(filePath, bytes); + return filePath; +}; + +/** + * Prepare a self-contained SVG element for export. + * + * cloneNode(true) already preserves: + * - Mermaid's internal