Skip to content

Commit 25e3352

Browse files
authored
Export diagram from model to mermaid code (#174)
1 parent a80f319 commit 25e3352

15 files changed

Lines changed: 425 additions & 8 deletions

File tree

.changeset/mermaid-export.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@serverlessworkflow/diagram-editor": minor
3+
---
4+
5+
Add mermaid export functionality

packages/serverless-workflow-diagram-editor/src/core/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,4 @@ export * from "./graph";
1919
export * from "./taskDetails";
2020
export * from "./taskSubType";
2121
export * from "./elkjs";
22+
export * from "./mermaidExport";
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/*
2+
* Copyright 2021-Present The Serverless Workflow Specification Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import { convertToMermaidCode } from "@serverlessworkflow/sdk";
18+
import type { Specification } from "@serverlessworkflow/sdk";
19+
20+
/**
21+
* Converts a workflow model to Mermaid diagram code
22+
* @param workflow - The workflow object (parsed from JSON/YAML)
23+
* @returns Mermaid diagram code as a string
24+
*/
25+
export function exportToMermaid(workflow: Specification.Workflow): string {
26+
return convertToMermaidCode(workflow);
27+
}

packages/serverless-workflow-diagram-editor/src/i18n/locales/en.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,9 @@ export const en = {
3838
"sidebar.noDetails": "No additional details for this node",
3939
"node.entry": "Entry",
4040
"node.exit": "Exit",
41+
"sidebar.exportMermaid.copy": "Copy Mermaid Code",
42+
"sidebar.exportMermaid.download": "Download as Mermaid File",
43+
"sidebar.exportMermaid.copied": "Copied!",
4144
} as const;
4245

4346
export type TranslationKeys = keyof typeof en;
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/*
2+
* Copyright 2021-Present The Serverless Workflow Specification Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
export function copyToClipboard(text: string): Promise<void> {
18+
if (typeof navigator === "undefined" || !navigator.clipboard) {
19+
return Promise.reject(new Error("Clipboard API is not available in this environment"));
20+
}
21+
return navigator.clipboard.writeText(text);
22+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/*
2+
* Copyright 2021-Present The Serverless Workflow Specification Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
export function downloadFile(content: string, filename: string, mimeType = "text/plain"): void {
18+
if (typeof document === "undefined") {
19+
throw new Error("Document API is not available in this environment");
20+
}
21+
22+
const blob = new Blob([content], { type: mimeType });
23+
const url = URL.createObjectURL(blob);
24+
const link = document.createElement("a");
25+
link.href = url;
26+
link.download = filename;
27+
document.body.appendChild(link);
28+
link.click();
29+
document.body.removeChild(link);
30+
setTimeout(() => {
31+
URL.revokeObjectURL(url);
32+
}, 100);
33+
}
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
/*
2+
* Copyright 2021-Present The Serverless Workflow Specification Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import * as React from "react";
18+
import { useI18n } from "@serverlessworkflow/i18n";
19+
import { ClipboardPen, Download, ClipboardCheck } from "lucide-react";
20+
import { Button } from "@/components/ui/button";
21+
import { exportToMermaid } from "@/core";
22+
import { copyToClipboard } from "@/lib/clipboard";
23+
import { downloadFile } from "@/lib/download";
24+
import type { Specification } from "@serverlessworkflow/sdk";
25+
26+
export function MermaidActions({ model }: { model: Specification.Workflow }): React.JSX.Element {
27+
const { t } = useI18n();
28+
const [isCopied, setIsCopied] = React.useState(false);
29+
const copyTimeoutRef = React.useRef<ReturnType<typeof setTimeout> | null>(null);
30+
31+
React.useEffect(() => {
32+
return () => {
33+
if (copyTimeoutRef.current) {
34+
clearTimeout(copyTimeoutRef.current);
35+
}
36+
};
37+
}, []);
38+
39+
const handleCopyMermaid = async () => {
40+
try {
41+
const mermaidCode = exportToMermaid(model);
42+
await copyToClipboard(mermaidCode);
43+
setIsCopied(true);
44+
45+
if (copyTimeoutRef.current) {
46+
clearTimeout(copyTimeoutRef.current);
47+
}
48+
49+
copyTimeoutRef.current = setTimeout(() => {
50+
setIsCopied(false);
51+
copyTimeoutRef.current = null;
52+
}, 2000);
53+
} catch (error) {
54+
console.error("Failed to copy mermaid code:", error);
55+
// TODO: Create component to show errors to users
56+
}
57+
};
58+
59+
const handleDownloadMermaid = () => {
60+
try {
61+
const mermaidCode = exportToMermaid(model);
62+
const sanitizedName = (model.document?.name || "workflow")
63+
.replace(/[/\\:*?"<>|]/g, "_")
64+
.replace(/\s+/g, "_")
65+
.trim()
66+
.substring(0, 200);
67+
const filename = `${sanitizedName}.mmd`;
68+
downloadFile(mermaidCode, filename);
69+
} catch (error) {
70+
console.error("Failed to download mermaid file:", error);
71+
// TODO: Create component to show errors to users
72+
}
73+
};
74+
75+
return (
76+
<>
77+
<Button onClick={handleCopyMermaid} variant="outline" size="sm">
78+
{isCopied ? <ClipboardCheck /> : <ClipboardPen />}
79+
{isCopied ? t("sidebar.exportMermaid.copied") : t("sidebar.exportMermaid.copy")}
80+
</Button>
81+
<Button onClick={handleDownloadMermaid} variant="outline" size="sm">
82+
<Download />
83+
{t("sidebar.exportMermaid.download")}
84+
</Button>
85+
</>
86+
);
87+
}

packages/serverless-workflow-diagram-editor/src/side-panel/NodeDetailsView.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,10 @@ export function NodeDetailsView({ node }: NodeDetailsViewProps) {
6969
<>
7070
<div className="dec-sidebar-section-spacer" />
7171
<SectionHeader label={t("sidebar.sectionSource")} />
72-
<YamlField yaml={yaml.dump(task, { indent: 2, lineWidth: -1 })} summary={t("sidebar.viewSource")} />
72+
<YamlField
73+
yaml={yaml.dump(task, { indent: 2, lineWidth: -1 })}
74+
summary={t("sidebar.viewSource")}
75+
/>
7376
</>
7477
)}
7578
</div>

packages/serverless-workflow-diagram-editor/src/side-panel/SidePanel.tsx

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,17 @@ import * as React from "react";
1818
import type * as RF from "@xyflow/react";
1919
import { useI18n } from "@serverlessworkflow/i18n";
2020
import { Workflow, Info, Box } from "lucide-react";
21-
import { Sidebar, SidebarContent, SidebarHeader, useSidebar } from "@/components/ui/sidebar";
21+
import {
22+
Sidebar,
23+
SidebarContent,
24+
SidebarHeader,
25+
useSidebar,
26+
SidebarFooter,
27+
} from "@/components/ui/sidebar";
2228
import { useDiagramEditorContext } from "@/store/DiagramEditorContext";
2329
import { WorkflowInfoView } from "@/side-panel/WorkflowInfoView";
2430
import { NodeDetailsView } from "@/side-panel/NodeDetailsView";
31+
import { MermaidActions } from "@/side-panel/MermaidActions";
2532
import { getNodeVisualConfig } from "@/react-flow/nodes/taskNodeConfig";
2633
import type { BaseNodeData } from "@/react-flow/nodes/Nodes";
2734
import "./SidePanel.css";
@@ -90,6 +97,9 @@ export function SidePanel() {
9097
</>
9198
)}
9299
</SidebarContent>
100+
<SidebarFooter>
101+
{model !== null && selectedNodeId === null && <MermaidActions model={model} />}
102+
</SidebarFooter>
93103
</Sidebar>
94104
);
95105
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/*
2+
* Copyright 2021-Present The Serverless Workflow Specification Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import { describe, it, expect } from "vitest";
18+
import { exportToMermaid } from "../../src/core/mermaidExport";
19+
import { parseWorkflow } from "../../src/core/workflowSdk";
20+
import { BASIC_VALID_WORKFLOW_YAML } from "../fixtures/workflows";
21+
22+
describe("exportToMermaid", () => {
23+
it("converts a valid workflow to Mermaid code", () => {
24+
const { model } = parseWorkflow(BASIC_VALID_WORKFLOW_YAML);
25+
expect(model).not.toBeNull();
26+
27+
const mermaidCode = exportToMermaid(model!);
28+
expect(mermaidCode).toBeTruthy();
29+
expect(typeof mermaidCode).toBe("string");
30+
// Mermaid diagrams start with flowchart
31+
expect(mermaidCode).toMatch(/flowchart/i);
32+
});
33+
});

0 commit comments

Comments
 (0)