diff --git a/src/features/modals/NodeModal/index.tsx b/src/features/modals/NodeModal/index.tsx index caba85febac..1c176a51696 100644 --- a/src/features/modals/NodeModal/index.tsx +++ b/src/features/modals/NodeModal/index.tsx @@ -1,16 +1,19 @@ import React from "react"; import type { ModalProps } from "@mantine/core"; -import { Modal, Stack, Text, ScrollArea, Flex, CloseButton } from "@mantine/core"; +import { Modal, Stack, Text, ScrollArea, Flex, Button, Textarea, Group } from "@mantine/core"; import { CodeHighlight } from "@mantine/code-highlight"; +import { toast } from "react-hot-toast"; import type { NodeData } from "../../../types/graph"; import useGraph from "../../editor/views/GraphView/stores/useGraph"; +import useJson from "../../../store/useJson"; +import useFile from "../../../store/useFile"; // return object from json removing array and object fields const normalizeNodeData = (nodeRows: NodeData["text"]) => { if (!nodeRows || nodeRows.length === 0) return "{}"; if (nodeRows.length === 1 && !nodeRows[0].key) return `${nodeRows[0].value}`; - const obj = {}; + const obj: Record = {}; nodeRows?.forEach(row => { if (row.type !== "array" && row.type !== "object") { if (row.key) obj[row.key] = row.value; @@ -26,42 +29,163 @@ const jsonPathToString = (path?: NodeData["path"]) => { return `$[${segments.join("][")}]`; }; +// set a value at path (mutates target) +function setValueAtPath(target: any, path: (string | number)[], value: any) { + if (!path || path.length === 0) { + return value; + } + let cur = target; + for (let i = 0; i < path.length - 1; i++) { + const seg = path[i]; + if (typeof seg === "number") { + if (!Array.isArray(cur)) cur = []; // defensive + if (typeof cur[seg] === "undefined") cur[seg] = {}; + cur = cur[seg]; + } else { + if (typeof cur[seg] === "undefined") cur[seg] = {}; + cur = cur[seg]; + } + } + const last = path[path.length - 1]; + cur[last as any] = value; + return target; +} + export const NodeModal = ({ opened, onClose }: ModalProps) => { const nodeData = useGraph(state => state.selectedNode); + const getJson = useJson(state => state.getJson); + const setContents = useFile(state => state.setContents); + + const [isEditing, setIsEditing] = React.useState(false); + const [value, setValue] = React.useState(""); + + React.useEffect(() => { + if (opened) { + setIsEditing(false); + setValue(normalizeNodeData(nodeData?.text ?? [])); + } + }, [opened, nodeData]); + + const startEdit = () => { + setValue(normalizeNodeData(nodeData?.text ?? [])); + setIsEditing(true); + }; + + const handleCancel = () => { + setIsEditing(false); + setValue(normalizeNodeData(nodeData?.text ?? [])); + }; + + const handleSave = () => { + try { + // try parse edited value + let parsed: any; + try { + parsed = JSON.parse(value); + } catch (err) { + toast.error("Invalid JSON. Fix the content before saving."); + return; + } + + const docJson = JSON.parse(getJson()); + const path = nodeData?.path ?? []; + + let updated: any; + if (!path || path.length === 0) { + updated = parsed; + } else { + const clone = JSON.parse(JSON.stringify(docJson)); + setValueAtPath(clone, path, parsed); + updated = clone; + } + + setContents({ contents: JSON.stringify(updated, null, 2), hasChanges: true }); + toast.success("Node updated"); + setIsEditing(false); + onClose?.(); + } catch (error: any) { + toast.error(error?.message ?? "Failed to save node content."); + } + }; return ( - - - - - - Content - - - - + { + setIsEditing(false); + onClose?.(); + }} + centered + > + + {/* New header row: "Content" on left, edit controls aligned to the right */} + + + Content + + + + {isEditing ? ( + <> + + + + ) : ( + + )} + + + + + +
+ {isEditing ? ( +