diff --git a/src/index.css b/src/index.css index 986e666..6e28fe5 100644 --- a/src/index.css +++ b/src/index.css @@ -26,7 +26,6 @@ html, body, #root { } a { - font-weight: 500; color: canvastext; } diff --git a/src/pages/VisProgPage/VisProg.module.css b/src/pages/VisProgPage/VisProg.module.css index df38224..5f2aa78 100644 --- a/src/pages/VisProgPage/VisProg.module.css +++ b/src/pages/VisProgPage/VisProg.module.css @@ -26,14 +26,6 @@ align-items: center; } -.save-load-panel { - outline: 2.5pt solid black; - border-radius: 0 0 5pt 5pt; - border-color: dimgrey; - background-color: canvas; - align-items: center; -} - .dnd-node-container { background-color: canvas; justify-content: center; @@ -134,23 +126,3 @@ outline: red solid 2pt; filter: drop-shadow(0 0 0.25rem red); } - -.save-button-like { - padding: 3px 10px; - background-color: canvas; - border-radius: 5pt; - outline: dodgerblue solid 2pt; - filter: drop-shadow(0 0 0.25rem dodgerblue); -} - -a.save-button-like { - display: inline-block; - text-decoration: none; - color: inherit; - cursor: pointer; - transition: filter 200ms, background-color 200ms; -} - -a.save-button-like:hover { - filter: drop-shadow(0 0 0.5rem dodgerblue); -} \ No newline at end of file diff --git a/src/pages/VisProgPage/visualProgrammingUI/components/SaveLoadPanel.module.css b/src/pages/VisProgPage/visualProgrammingUI/components/SaveLoadPanel.module.css new file mode 100644 index 0000000..9dbafa2 --- /dev/null +++ b/src/pages/VisProgPage/visualProgrammingUI/components/SaveLoadPanel.module.css @@ -0,0 +1,30 @@ +.save-load-panel { + border-radius: 0 0 5pt 5pt; + background-color: canvas; +} + +label.file-input-button { + cursor: pointer; + outline: forestgreen solid 2pt; + filter: drop-shadow(0 0 0.25rem forestgreen); + transition: filter 200ms; + + input[type="file"] { + display: none; + } + + &:hover { + filter: drop-shadow(0 0 0.5rem forestgreen); + } +} + +.save-button { + text-decoration: none; + outline: dodgerblue solid 2pt; + filter: drop-shadow(0 0 0.25rem dodgerblue); + transition: filter 200ms; + + &:hover { + filter: drop-shadow(0 0 0.5rem dodgerblue); + } +} diff --git a/src/pages/VisProgPage/visualProgrammingUI/components/SaveLoadPanel.tsx b/src/pages/VisProgPage/visualProgrammingUI/components/SaveLoadPanel.tsx index 7e34147..baac724 100644 --- a/src/pages/VisProgPage/visualProgrammingUI/components/SaveLoadPanel.tsx +++ b/src/pages/VisProgPage/visualProgrammingUI/components/SaveLoadPanel.tsx @@ -1,7 +1,7 @@ -import { useRef, useState, useEffect } from "react"; +import {type ChangeEvent, useRef, useState} from "react"; import useFlowStore from "../VisProgStores"; -import styles from "../../VisProg.module.css"; -import { cleanup } from "@testing-library/react"; +import visProgStyles from "../../VisProg.module.css"; +import styles from "./SaveLoadPanel.module.css"; import { makeProjectBlob, type SavedProject } from "../../../../utils/SaveLoad"; export default function SaveLoadPanel() { @@ -14,99 +14,55 @@ export default function SaveLoadPanel() { // ref to the file input const inputRef = useRef(null); - // ref to hold the resolver for the currently pending load promise - const resolverRef = useRef<((p: SavedProject | null) => void) | null>(null); - useEffect(() => { - return () => { - if (resolverRef.current) { - resolverRef.current(null); - resolverRef.current = null; - } - }; - }, []); - - const onSave = async () => { - const nameGuess = "visual-program"; + const onSave = async (nameGuess = "visual-program") => { const blob = makeProjectBlob(nameGuess, nodes, edges); const url = URL.createObjectURL(blob); setSaveUrl(url); }; - const onLoad = async () => { + // input change handler updates the graph with a parsed JSON file + const handleFileChange = async (e: ChangeEvent) => { + const file = e.target.files?.[0]; + if (!file) return; try { - const proj = await new Promise((resolve) => { - resolverRef.current = resolve; - inputRef.current?.click(); - }); - // clear stored resolver - resolverRef.current = null; - - if (!proj) return; - - cleanup(); - setNodes(proj.nodes); - setEdges(proj.edges); + const text = await file.text(); + const parsed = JSON.parse(text) as SavedProject; + if (!parsed.nodes || !parsed.edges) throw new Error("Invalid file format"); + setNodes(parsed.nodes); + setEdges(parsed.edges); } catch (e) { console.error(e); alert("Loading failed. See console."); - } - }; - - // input change handler resolves the onLoad promise with parsed project or null - const handleFileChange = async (e: React.ChangeEvent) => { - try { - const file = e.target.files?.[0]; - if (!file) { - resolverRef.current?.(null); - resolverRef.current = null; - return; - } - try { - const text = await file.text(); - const parsed = JSON.parse(text) as SavedProject; - resolverRef.current?.(parsed ?? null); - } catch { - resolverRef.current?.(null); - } finally { - // allow re-selecting same file next time - if (inputRef.current) inputRef.current.value = ""; - resolverRef.current = null; - } - } catch { - resolverRef.current?.(null); - resolverRef.current = null; + } finally { + // allow re-selecting same file next time + if (inputRef.current) inputRef.current.value = ""; } }; const defaultName = "visual-program"; return ( -
+
You can save and load your graph here.
- ); diff --git a/test/pages/visProgPage/visualProgrammingUI/components/SaveLoadPanel.test.tsx b/test/pages/visProgPage/visualProgrammingUI/components/SaveLoadPanel.test.tsx index 97bbf11..9d85323 100644 --- a/test/pages/visProgPage/visualProgrammingUI/components/SaveLoadPanel.test.tsx +++ b/test/pages/visProgPage/visualProgrammingUI/components/SaveLoadPanel.test.tsx @@ -106,22 +106,22 @@ describe("SaveLoadPanel - combined tests", () => { test("onLoad with invalid JSON does not update store", async () => { const file = new File(["not json"], "bad.json", { type: "application/json" }); + file.text = jest.fn(() => Promise.resolve(`{"bad json`)); + + window.alert = jest.fn(); render(); const input = document.querySelector('input[type="file"]') as HTMLInputElement; expect(input).toBeTruthy(); - // Click Load to install the resolver - const loadButton = screen.getByRole("button", { name: /load graph/i }); - - // Do click and change inside same act to ensure resolver is set - await act(async () => { - fireEvent.click(loadButton); + // Give some input + act(() => { fireEvent.change(input, { target: { files: [file] } }); - await Promise.resolve(); }); await waitFor(() => { + expect(window.alert).toHaveBeenCalledTimes(1); + const nodesAfter = useFlowStore.getState().nodes; expect(nodesAfter).toHaveLength(0); expect(input.value).toBe(""); @@ -134,7 +134,7 @@ describe("SaveLoadPanel - combined tests", () => { expect(input).toBeTruthy(); // Click Load to set resolver - const loadButton = screen.getByRole("button", { name: /load graph/i }); + const loadButton = screen.getByLabelText(/load graph/i); await act(async () => { fireEvent.click(loadButton);