diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 10f909157b4..d9983fd7266 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1060,8 +1060,8 @@ packages: camelize@1.0.1: resolution: {integrity: sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==} - caniuse-lite@1.0.30001718: - resolution: {integrity: sha512-AflseV1ahcSunK53NfEs9gFWgOEmzr0f+kaMFA4xiLZlr9Hzt7HxcSpIFcnNCUkz6R6dWKa54rUz3HUmI3nVcw==} + caniuse-lite@1.0.30001754: + resolution: {integrity: sha512-x6OeBXueoAceOmotzx3PO4Zpt4rzpeIFsSr6AAePTZxSkXiYDUmpypEl7e2+8NCd9bD7bXjqyef8CJYPC1jfxg==} chalk@3.0.0: resolution: {integrity: sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==} @@ -4009,7 +4009,7 @@ snapshots: camelize@1.0.1: {} - caniuse-lite@1.0.30001718: {} + caniuse-lite@1.0.30001754: {} chalk@3.0.0: dependencies: @@ -4319,7 +4319,7 @@ snapshots: eslint: 8.56.0 eslint-import-resolver-node: 0.3.9 eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.32.1(eslint@8.56.0)(typescript@5.8.2))(eslint@8.56.0))(eslint@8.56.0) - eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.32.1(eslint@8.56.0)(typescript@5.8.2))(eslint-import-resolver-typescript@3.10.1)(eslint@8.56.0) + eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.32.1(eslint@8.56.0)(typescript@5.8.2))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.32.1(eslint@8.56.0)(typescript@5.8.2))(eslint@8.56.0))(eslint@8.56.0))(eslint@8.56.0) eslint-plugin-jsx-a11y: 6.10.2(eslint@8.56.0) eslint-plugin-react: 7.37.5(eslint@8.56.0) eslint-plugin-react-hooks: 5.0.0-canary-7118f5dd7-20230705(eslint@8.56.0) @@ -4353,7 +4353,7 @@ snapshots: tinyglobby: 0.2.13 unrs-resolver: 1.7.2 optionalDependencies: - eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.32.1(eslint@8.56.0)(typescript@5.8.2))(eslint-import-resolver-typescript@3.10.1)(eslint@8.56.0) + eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.32.1(eslint@8.56.0)(typescript@5.8.2))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.32.1(eslint@8.56.0)(typescript@5.8.2))(eslint@8.56.0))(eslint@8.56.0))(eslint@8.56.0) transitivePeerDependencies: - supports-color @@ -4368,7 +4368,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.32.1(eslint@8.56.0)(typescript@5.8.2))(eslint-import-resolver-typescript@3.10.1)(eslint@8.56.0): + eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.32.1(eslint@8.56.0)(typescript@5.8.2))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.32.1(eslint@8.56.0)(typescript@5.8.2))(eslint@8.56.0))(eslint@8.56.0))(eslint@8.56.0): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.8 @@ -5232,7 +5232,7 @@ snapshots: '@next/env': 14.2.28 '@swc/helpers': 0.5.5 busboy: 1.6.0 - caniuse-lite: 1.0.30001718 + caniuse-lite: 1.0.30001754 graceful-fs: 4.2.11 postcss: 8.4.31 react: 18.3.1 diff --git a/src/features/modals/NodeModal/index.tsx b/src/features/modals/NodeModal/index.tsx index caba85febac..70383641945 100644 --- a/src/features/modals/NodeModal/index.tsx +++ b/src/features/modals/NodeModal/index.tsx @@ -1,9 +1,11 @@ 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, CloseButton, Button, TextInput, Group } from "@mantine/core"; import { CodeHighlight } from "@mantine/code-highlight"; -import type { NodeData } from "../../../types/graph"; +import type { NodeData, NodeRow } 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"]) => { @@ -26,8 +28,145 @@ const jsonPathToString = (path?: NodeData["path"]) => { return `$[${segments.join("][")}]`; }; +// Update JSON at specified path with new value +const updateJsonAtPath = (json: string, path: NodeData["path"], value: any): string => { + try { + const obj = JSON.parse(json); + if (!path || path.length === 0) return JSON.stringify(value); + + let current = obj; + for (let i = 0; i < path.length - 1; i++) { + current = current[path[i]]; + } + current[path[path.length - 1]] = value; + return JSON.stringify(obj, null, 2); + } catch { + return json; + } +}; + export const NodeModal = ({ opened, onClose }: ModalProps) => { const nodeData = useGraph(state => state.selectedNode); + const setJson = useJson(state => state.setJson); + const currentJson = useJson(state => state.json); + const setSelectedNode = useGraph(state => state.setSelectedNode); + + const [isEditing, setIsEditing] = React.useState(false); + const [editedRows, setEditedRows] = React.useState([]); + + // Initialize edited rows when modal opens or nodeData changes + React.useEffect(() => { + if (nodeData && opened) { + setEditedRows(nodeData.text.filter(row => row.type !== "array" && row.type !== "object")); + setIsEditing(false); + } + }, [nodeData, opened]); + + const handleEditClick = () => { + setIsEditing(true); + }; + + const handleSave = () => { + if (!nodeData) return; + // Build updatedNodeText (for immediate UI feedback) + const updatedNodeText = nodeData.text.map(row => { + const editedRow = editedRows.find(er => er.key === row.key && er.type === row.type && er.key !== null); + // For primitive value nodes (key === null) match by index + if (!editedRow && row.key === null) { + const prim = editedRows.find((r, idx) => r.key === null && nodeData.text[idx] && nodeData.text[idx].key === null); + return prim || row; + } + return editedRow || row; + }); + + // Helper to coerce string input back to original types + const coerceValue = (input: any, originalType: string | undefined) => { + if (input === null) return null; + if (originalType === "number") { + const n = Number(input); + return Number.isNaN(n) ? input : n; + } + if (originalType === "boolean") { + if (typeof input === "boolean") return input; + if (String(input).toLowerCase() === "true") return true; + if (String(input).toLowerCase() === "false") return false; + return input; + } + if (originalType === "null") return null; + return input; + }; + + // Parse current JSON and apply edits to the node's path + try { + const fullJson = JSON.parse(currentJson); + + if (nodeData.path && nodeData.path.length >= 0) { + // Locate parent and target key/index + let parent: any = fullJson; + for (let i = 0; i < (nodeData.path.length - 1); i++) { + parent = parent[nodeData.path[i]]; + if (parent === undefined) break; + } + + const lastKey = nodeData.path.length > 0 ? nodeData.path[nodeData.path.length - 1] : undefined; + + // If node represents a primitive value (single row with no key), replace the value directly + if (nodeData.text.length === 1 && nodeData.text[0].key === null) { + const editedPrim = editedRows[0]; + if (lastKey !== undefined) { + parent[lastKey as any] = coerceValue(editedPrim?.value ?? nodeData.text[0].value, nodeData.text[0].type as string); + } + } else { + // Ensure target object exists + const target = lastKey !== undefined ? parent[lastKey as any] : parent; + if (target && typeof target === "object") { + editedRows.forEach(edited => { + if (edited.key) { + // find original row to get its type + const original = nodeData.text.find(r => r.key === edited.key && r.type === edited.type); + const coerced = coerceValue(edited.value, original?.type as string | undefined); + target[edited.key] = coerced; + } + }); + } + } + + // Persist updated JSON and also update the text editor on the left + const updatedJsonString = JSON.stringify(fullJson, null, 2); + setJson(updatedJsonString); + useFile.getState().setContents({ contents: updatedJsonString, hasChanges: true }); + } + } catch (err) { + // fallback: if something goes wrong, don't crash — keep node update in-memory + console.error("Failed to update JSON in NodeModal.save:", err); + } + + // Update the selected node with edited text for immediate feedback + const updatedNodeData: NodeData = { + ...nodeData, + text: updatedNodeText, + }; + setSelectedNode(updatedNodeData); + + setIsEditing(false); + }; + + const handleCancel = () => { + setIsEditing(false); + if (nodeData) { + setEditedRows(nodeData.text.filter(row => row.type !== "array" && row.type !== "object")); + } + }; + + const handleFieldChange = (index: number, newValue: string) => { + const updated = [...editedRows]; + updated[index] = { ...updated[index], value: newValue }; + setEditedRows(updated); + }; + + if (!nodeData) return null; + + const editableFields = editedRows; return ( @@ -37,18 +176,52 @@ export const NodeModal = ({ opened, onClose }: ModalProps) => { Content - + + {!isEditing && } + {isEditing && ( + + + + + )} + + - - - + + {!isEditing ? ( + + + + ) : ( + + {editableFields.map((field, index) => ( +
+ + {field.key || "Value"} + + handleFieldChange(index, e.currentTarget.value)} + /> +
+ ))} +
+ )} + JSON Path