From 22da2ca6649d6a586b1d5d2c131e18ed946e5847 Mon Sep 17 00:00:00 2001 From: Pim Hutting Date: Wed, 12 Nov 2025 11:17:15 +0100 Subject: [PATCH] feat: added functionality of saving and loadiing for supported browsers, using the File System Access API. otherwise, fallback to download the file and then you can load from download ref: N25B-189 --- src/pages/VisProgPage/VisProg.tsx | 4 + .../components/DragDropSidebar.tsx | 15 -- .../components/SaveLoadPanel.tsx | 136 ++++++++++++++++++ 3 files changed, 140 insertions(+), 15 deletions(-) create mode 100644 src/pages/VisProgPage/visualProgrammingUI/components/SaveLoadPanel.tsx diff --git a/src/pages/VisProgPage/VisProg.tsx b/src/pages/VisProgPage/VisProg.tsx index 8208a70..8be3696 100644 --- a/src/pages/VisProgPage/VisProg.tsx +++ b/src/pages/VisProgPage/VisProg.tsx @@ -20,6 +20,7 @@ import graphReducer from "./visualProgrammingUI/GraphReducer.ts"; import useFlowStore from './visualProgrammingUI/VisProgStores.tsx'; import type {FlowState} from './visualProgrammingUI/VisProgTypes.tsx'; import styles from './VisProg.module.css' +import SaveLoadPanel from './visualProgrammingUI/components/SaveLoadPanel.tsx'; // --| config starting params for flow |-- @@ -100,6 +101,9 @@ const VisProgUI = () => { {/* contains the drag and drop panel for nodes */} + + + diff --git a/src/pages/VisProgPage/visualProgrammingUI/components/DragDropSidebar.tsx b/src/pages/VisProgPage/visualProgrammingUI/components/DragDropSidebar.tsx index 6169b7b..91ba510 100644 --- a/src/pages/VisProgPage/visualProgrammingUI/components/DragDropSidebar.tsx +++ b/src/pages/VisProgPage/visualProgrammingUI/components/DragDropSidebar.tsx @@ -150,22 +150,7 @@ export function DndToolbar() { ); return ( - -
-
-
- You can save and load your graph here. -
-
- - -
-
diff --git a/src/pages/VisProgPage/visualProgrammingUI/components/SaveLoadPanel.tsx b/src/pages/VisProgPage/visualProgrammingUI/components/SaveLoadPanel.tsx new file mode 100644 index 0000000..ff0c041 --- /dev/null +++ b/src/pages/VisProgPage/visualProgrammingUI/components/SaveLoadPanel.tsx @@ -0,0 +1,136 @@ +import React from "react"; +import useFlowStore from "../VisProgStores"; +import styles from "../../VisProg.module.css"; +import { useReactFlow, type Edge } from "@xyflow/react"; +import type { AppNode } from "../VisProgTypes"; + +type SavedProject = { + version: 1; + name: string; + savedAt: string; // ISO timestamp + nodes: AppNode[]; + edges: Edge[]; +}; + + + +function makeProjectBlob(name: string, nodes: AppNode[], edges: Edge[]): Blob { + const payload = { + version: 1, + name, + savedAt: new Date().toISOString(), + nodes, + edges, + }; + return new Blob([JSON.stringify(payload, null, 2)], { type: "application/json" }); +} + +async function saveWithPicker(defaultName: string, blob: Blob) { + // @ts-expect-error: not in lib.dom.d.ts everywhere + if (window.showSaveFilePicker) { + // @ts-expect-error + const handle = await window.showSaveFilePicker({ + suggestedName: `${defaultName}.visprog.json`, + types: [{ description: "Visual Program Project", accept: { "application/json": [".visprog.json", ".json"] } }], + }); + const writable = await handle.createWritable(); + await writable.write(blob); + await writable.close(); + return; + } + // Fallback if File system API is not supported + const url = URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = `${defaultName}.visprog.json`; + a.click(); + URL.revokeObjectURL(url); +} + +async function loadWithPicker(): Promise { + try { + // @ts-expect-error + if (window.showOpenFilePicker) { + // @ts-expect-error + const [handle] = await window.showOpenFilePicker({ + multiple: false, + types: [{ description: "Visual Program Project", accept: { "application/json": [".visprog.json", ".json", ".txt"] } }], + }); + const file = await handle.getFile(); + return JSON.parse(await file.text()) as SavedProject; + } + // Fallback: input + return await new Promise((resolve) => { + const input = document.createElement("input"); + input.type = "file"; + input.accept = ".visprog.json,.json,.txt,application/json,text/plain"; + input.onchange = async () => { + const file = input.files?.[0]; + if (!file) return resolve(null); + try { + resolve(JSON.parse(await file.text()) as SavedProject); + } catch { + resolve(null); + } + }; + input.click(); + }); + } catch { + return null; + } +} + +export default function SaveLoadPanel() { + const nodes = useFlowStore((s) => s.nodes) as AppNode[]; + const edges = useFlowStore((s) => s.edges) as Edge[]; + const setNodes = useFlowStore((s) => s.setNodes); + const setEdges = useFlowStore((s) => s.setEdges); + + const onSave = async () => { + try { + const nameGuess = + (nodes.find((n) => n.type === "start")?.data?.label as string) || "visual-program"; + const blob = makeProjectBlob(nameGuess, nodes, edges); + await saveWithPicker(nameGuess, blob); + } catch (e) { + console.error(e); + alert("Saving failed. See console."); + } + }; + + const onLoad = async () => { + try { + const proj = await loadWithPicker(); + if (!proj) return; + + if (proj.version !== 1 || !Array.isArray(proj.nodes) || !Array.isArray(proj.edges)) { + alert("Invalid project file format."); + return; + } + + //We clear all the current edges and nodes + setEdges([]); + setNodes([]); + + //set all loaded nodes and edges into the VisProg + const loadedNodes = proj.nodes as AppNode[]; + const loadedEdges = proj.edges as Edge[]; + setNodes(loadedNodes); + setEdges(loadedEdges); + + } catch (e) { + console.error(e); + alert("Loading failed. See console."); + } + }; + + return ( +
+
You can save and load your graph here.
+
+ + +
+
+ ); +}