2025-03-26 00:30:00 +00:00
|
|
|
"use client";
|
|
|
|
|
|
|
|
|
|
import React, { createContext, useContext, useRef, useState } from "react";
|
|
|
|
|
import type { DrawIoEmbedRef } from "react-drawio";
|
2025-03-27 06:45:38 +00:00
|
|
|
import { extractDiagramXML } from "../lib/utils";
|
2025-12-04 22:56:59 +09:00
|
|
|
import type { ExportFormat } from "@/components/save-dialog";
|
2025-03-26 00:30:00 +00:00
|
|
|
|
|
|
|
|
interface DiagramContextType {
|
|
|
|
|
chartXML: string;
|
|
|
|
|
latestSvg: string;
|
|
|
|
|
diagramHistory: { svg: string; xml: string }[];
|
|
|
|
|
loadDiagram: (chart: string) => void;
|
|
|
|
|
handleExport: () => void;
|
2025-12-03 21:58:48 +09:00
|
|
|
handleExportWithoutHistory: () => void;
|
2025-03-26 06:47:44 +00:00
|
|
|
resolverRef: React.Ref<((value: string) => void) | null>;
|
|
|
|
|
drawioRef: React.Ref<DrawIoEmbedRef | null>;
|
2025-03-26 00:30:00 +00:00
|
|
|
handleDiagramExport: (data: any) => void;
|
2025-03-27 08:09:22 +00:00
|
|
|
clearDiagram: () => void;
|
2025-12-05 01:30:02 +09:00
|
|
|
saveDiagramToFile: (filename: string, format: ExportFormat) => void;
|
2025-03-26 00:30:00 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const DiagramContext = createContext<DiagramContextType | undefined>(undefined);
|
|
|
|
|
|
|
|
|
|
export function DiagramProvider({ children }: { children: React.ReactNode }) {
|
|
|
|
|
const [chartXML, setChartXML] = useState<string>("");
|
|
|
|
|
const [latestSvg, setLatestSvg] = useState<string>("");
|
|
|
|
|
const [diagramHistory, setDiagramHistory] = useState<
|
|
|
|
|
{ svg: string; xml: string }[]
|
|
|
|
|
>([]);
|
2025-03-26 06:47:44 +00:00
|
|
|
const drawioRef = useRef<DrawIoEmbedRef | null>(null);
|
2025-03-26 00:30:00 +00:00
|
|
|
const resolverRef = useRef<((value: string) => void) | null>(null);
|
2025-12-03 13:53:16 +09:00
|
|
|
// Track if we're expecting an export for history (user-initiated)
|
|
|
|
|
const expectHistoryExportRef = useRef<boolean>(false);
|
2025-12-04 22:56:59 +09:00
|
|
|
// Track if we're expecting an export for file save (stores raw export data)
|
|
|
|
|
const saveResolverRef = useRef<{
|
|
|
|
|
resolver: ((data: string) => void) | null;
|
|
|
|
|
format: ExportFormat | null;
|
|
|
|
|
}>({ resolver: null, format: null });
|
2025-03-26 00:30:00 +00:00
|
|
|
|
|
|
|
|
const handleExport = () => {
|
|
|
|
|
if (drawioRef.current) {
|
2025-12-03 13:53:16 +09:00
|
|
|
// Mark that this export should be saved to history
|
|
|
|
|
expectHistoryExportRef.current = true;
|
2025-03-26 00:30:00 +00:00
|
|
|
drawioRef.current.exportDiagram({
|
|
|
|
|
format: "xmlsvg",
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2025-12-03 21:58:48 +09:00
|
|
|
const handleExportWithoutHistory = () => {
|
|
|
|
|
if (drawioRef.current) {
|
|
|
|
|
// Export without saving to history (for edit_diagram fetching current state)
|
|
|
|
|
drawioRef.current.exportDiagram({
|
|
|
|
|
format: "xmlsvg",
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2025-03-26 00:30:00 +00:00
|
|
|
const loadDiagram = (chart: string) => {
|
|
|
|
|
if (drawioRef.current) {
|
|
|
|
|
drawioRef.current.load({
|
|
|
|
|
xml: chart,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleDiagramExport = (data: any) => {
|
2025-12-04 22:56:59 +09:00
|
|
|
// Handle save to file if requested (process raw data before extraction)
|
|
|
|
|
if (saveResolverRef.current.resolver) {
|
|
|
|
|
const format = saveResolverRef.current.format;
|
|
|
|
|
saveResolverRef.current.resolver(data.data);
|
|
|
|
|
saveResolverRef.current = { resolver: null, format: null };
|
|
|
|
|
// For non-xmlsvg formats, skip XML extraction as it will fail
|
|
|
|
|
// Only drawio (which uses xmlsvg internally) has the content attribute
|
|
|
|
|
if (format === "png" || format === "svg") {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-03-26 00:30:00 +00:00
|
|
|
const extractedXML = extractDiagramXML(data.data);
|
|
|
|
|
setChartXML(extractedXML);
|
|
|
|
|
setLatestSvg(data.data);
|
2025-12-03 13:53:16 +09:00
|
|
|
|
|
|
|
|
// Only add to history if this was a user-initiated export
|
|
|
|
|
if (expectHistoryExportRef.current) {
|
|
|
|
|
setDiagramHistory((prev) => [
|
|
|
|
|
...prev,
|
|
|
|
|
{
|
|
|
|
|
svg: data.data,
|
|
|
|
|
xml: extractedXML,
|
|
|
|
|
},
|
|
|
|
|
]);
|
|
|
|
|
expectHistoryExportRef.current = false;
|
|
|
|
|
}
|
|
|
|
|
|
2025-03-26 00:30:00 +00:00
|
|
|
if (resolverRef.current) {
|
|
|
|
|
resolverRef.current(extractedXML);
|
|
|
|
|
resolverRef.current = null;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2025-03-27 08:09:22 +00:00
|
|
|
const clearDiagram = () => {
|
|
|
|
|
const emptyDiagram = `<mxfile><diagram name="Page-1" id="page-1"><mxGraphModel><root><mxCell id="0"/><mxCell id="1" parent="0"/></root></mxGraphModel></diagram></mxfile>`;
|
|
|
|
|
loadDiagram(emptyDiagram);
|
|
|
|
|
setChartXML(emptyDiagram);
|
|
|
|
|
setLatestSvg("");
|
|
|
|
|
setDiagramHistory([]);
|
|
|
|
|
};
|
|
|
|
|
|
2025-12-05 01:30:02 +09:00
|
|
|
const saveDiagramToFile = (filename: string, format: ExportFormat) => {
|
2025-12-03 21:02:26 +09:00
|
|
|
if (!drawioRef.current) {
|
|
|
|
|
console.warn("Draw.io editor not ready");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-04 22:56:59 +09:00
|
|
|
// Map format to draw.io export format
|
|
|
|
|
const drawioFormat = format === "drawio" ? "xmlsvg" : format;
|
|
|
|
|
|
|
|
|
|
// Set up the resolver before triggering export
|
|
|
|
|
saveResolverRef.current = {
|
|
|
|
|
resolver: (exportData: string) => {
|
|
|
|
|
let fileContent: string | Blob;
|
|
|
|
|
let mimeType: string;
|
|
|
|
|
let extension: string;
|
|
|
|
|
|
|
|
|
|
if (format === "drawio") {
|
|
|
|
|
// Extract XML from SVG for .drawio format
|
|
|
|
|
const xml = extractDiagramXML(exportData);
|
|
|
|
|
let xmlContent = xml;
|
|
|
|
|
if (!xml.includes("<mxfile")) {
|
|
|
|
|
xmlContent = `<mxfile><diagram name="Page-1" id="page-1">${xml}</diagram></mxfile>`;
|
|
|
|
|
}
|
|
|
|
|
fileContent = xmlContent;
|
|
|
|
|
mimeType = "application/xml";
|
|
|
|
|
extension = ".drawio";
|
|
|
|
|
} else if (format === "png") {
|
|
|
|
|
// PNG data comes as base64 data URL
|
|
|
|
|
fileContent = exportData;
|
|
|
|
|
mimeType = "image/png";
|
|
|
|
|
extension = ".png";
|
|
|
|
|
} else {
|
|
|
|
|
// SVG format
|
|
|
|
|
fileContent = exportData;
|
|
|
|
|
mimeType = "image/svg+xml";
|
|
|
|
|
extension = ".svg";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Handle download
|
|
|
|
|
let url: string;
|
|
|
|
|
if (typeof fileContent === "string" && fileContent.startsWith("data:")) {
|
|
|
|
|
// Already a data URL (PNG)
|
|
|
|
|
url = fileContent;
|
|
|
|
|
} else {
|
|
|
|
|
const blob = new Blob([fileContent], { type: mimeType });
|
|
|
|
|
url = URL.createObjectURL(blob);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const a = document.createElement("a");
|
|
|
|
|
a.href = url;
|
|
|
|
|
a.download = `${filename}${extension}`;
|
|
|
|
|
document.body.appendChild(a);
|
|
|
|
|
a.click();
|
|
|
|
|
document.body.removeChild(a);
|
|
|
|
|
|
|
|
|
|
// Delay URL revocation to ensure download completes
|
|
|
|
|
if (!url.startsWith("data:")) {
|
|
|
|
|
setTimeout(() => URL.revokeObjectURL(url), 100);
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
format,
|
2025-12-03 21:02:26 +09:00
|
|
|
};
|
2025-12-04 22:56:59 +09:00
|
|
|
|
|
|
|
|
// Export diagram - callback will be handled in handleDiagramExport
|
|
|
|
|
drawioRef.current.exportDiagram({ format: drawioFormat });
|
|
|
|
|
};
|
|
|
|
|
|
2025-03-26 00:30:00 +00:00
|
|
|
return (
|
|
|
|
|
<DiagramContext.Provider
|
|
|
|
|
value={{
|
|
|
|
|
chartXML,
|
|
|
|
|
latestSvg,
|
|
|
|
|
diagramHistory,
|
|
|
|
|
loadDiagram,
|
|
|
|
|
handleExport,
|
2025-12-03 21:58:48 +09:00
|
|
|
handleExportWithoutHistory,
|
2025-03-26 00:30:00 +00:00
|
|
|
resolverRef,
|
|
|
|
|
drawioRef,
|
|
|
|
|
handleDiagramExport,
|
2025-03-27 08:09:22 +00:00
|
|
|
clearDiagram,
|
2025-12-03 21:02:26 +09:00
|
|
|
saveDiagramToFile,
|
2025-03-26 00:30:00 +00:00
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
{children}
|
|
|
|
|
</DiagramContext.Provider>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function useDiagram() {
|
|
|
|
|
const context = useContext(DiagramContext);
|
|
|
|
|
if (context === undefined) {
|
|
|
|
|
throw new Error("useDiagram must be used within a DiagramProvider");
|
|
|
|
|
}
|
|
|
|
|
return context;
|
|
|
|
|
}
|