From 718e043f559be5ab1ae4383d2c8c90a733a660c8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 13 Nov 2025 02:44:56 +0000 Subject: [PATCH 1/3] Initial plan From fabafdb8e6ea5e586f3aad2e0d2fb001e8519804 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 13 Nov 2025 02:57:13 +0000 Subject: [PATCH 2/3] Add edit button to object nodes (fruit boxes) Co-authored-by: phlogip <166570487+phlogip@users.noreply.github.com> --- .../views/GraphView/CustomNode/ObjectNode.tsx | 40 ++++++++++++------- .../views/GraphView/CustomNode/styles.tsx | 26 ++++++++++++ 2 files changed, 52 insertions(+), 14 deletions(-) diff --git a/src/features/editor/views/GraphView/CustomNode/ObjectNode.tsx b/src/features/editor/views/GraphView/CustomNode/ObjectNode.tsx index bd87e928f01..c54e3e9917c 100644 --- a/src/features/editor/views/GraphView/CustomNode/ObjectNode.tsx +++ b/src/features/editor/views/GraphView/CustomNode/ObjectNode.tsx @@ -1,4 +1,5 @@ import React from "react"; +import { FiEdit } from "react-icons/fi"; import type { CustomNodeProps } from "."; import { NODE_DIMENSIONS } from "../../../../../constants/graph"; import type { NodeData } from "../../../../../types/graph"; @@ -34,20 +35,31 @@ const Row = ({ row, x, y, index }: RowProps) => { ); }; -const Node = ({ node, x, y }: CustomNodeProps) => ( - - {node.text.map((row, index) => ( - - ))} - -); +const Node = ({ node, x, y }: CustomNodeProps) => { + const handleEditClick = (e: React.MouseEvent) => { + e.stopPropagation(); + // TODO: Implement edit functionality + console.log("Edit button clicked for node:", node.id); + }; + + return ( + + + + + {node.text.map((row, index) => ( + + ))} + + ); +}; function propsAreEqual(prev: CustomNodeProps, next: CustomNodeProps) { return ( diff --git a/src/features/editor/views/GraphView/CustomNode/styles.tsx b/src/features/editor/views/GraphView/CustomNode/styles.tsx index 175b4524b50..c02719670dc 100644 --- a/src/features/editor/views/GraphView/CustomNode/styles.tsx +++ b/src/features/editor/views/GraphView/CustomNode/styles.tsx @@ -99,3 +99,29 @@ export const StyledChildrenCount = styled.span` padding: 10px; margin-left: -15px; `; + +export const StyledEditButton = styled.button` + position: absolute; + top: 4px; + right: 4px; + background: ${({ theme }) => theme.BACKGROUND_MODIFIER_ACCENT}; + border: 1px solid ${({ theme }) => theme.NODE_COLORS.DIVIDER}; + border-radius: 4px; + padding: 4px; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + pointer-events: all; + transition: all 0.2s ease; + color: ${({ theme }) => theme.NODE_COLORS.TEXT}; + + &:hover { + background: ${({ theme }) => theme.INTERACTIVE_HOVER}; + border-color: ${({ theme }) => theme.TEXT_NORMAL}; + } + + &:active { + transform: scale(0.95); + } +`; From 0001e37220a30864d45a56e588ece4244d938569 Mon Sep 17 00:00:00 2001 From: Logan Phillips Date: Thu, 13 Nov 2025 17:03:05 -0500 Subject: [PATCH 3/3] Edit button Feature --- src/data/example.json | 2 +- .../views/GraphView/CustomNode/ObjectNode.tsx | 10 - .../views/GraphView/CustomNode/styles.tsx | 10 +- src/features/modals/NodeModal/index.tsx | 258 +++++++++++++++++- 4 files changed, 250 insertions(+), 30 deletions(-) diff --git a/src/data/example.json b/src/data/example.json index f9ae49e946f..efb8cf21769 100644 --- a/src/data/example.json +++ b/src/data/example.json @@ -40,4 +40,4 @@ } } ] -} +} \ No newline at end of file diff --git a/src/features/editor/views/GraphView/CustomNode/ObjectNode.tsx b/src/features/editor/views/GraphView/CustomNode/ObjectNode.tsx index c54e3e9917c..4ea4bc19f08 100644 --- a/src/features/editor/views/GraphView/CustomNode/ObjectNode.tsx +++ b/src/features/editor/views/GraphView/CustomNode/ObjectNode.tsx @@ -1,5 +1,4 @@ import React from "react"; -import { FiEdit } from "react-icons/fi"; import type { CustomNodeProps } from "."; import { NODE_DIMENSIONS } from "../../../../../constants/graph"; import type { NodeData } from "../../../../../types/graph"; @@ -36,12 +35,6 @@ const Row = ({ row, x, y, index }: RowProps) => { }; const Node = ({ node, x, y }: CustomNodeProps) => { - const handleEditClick = (e: React.MouseEvent) => { - e.stopPropagation(); - // TODO: Implement edit functionality - console.log("Edit button clicked for node:", node.id); - }; - return ( { y={0} $isObject > - - - {node.text.map((row, index) => ( ))} diff --git a/src/features/editor/views/GraphView/CustomNode/styles.tsx b/src/features/editor/views/GraphView/CustomNode/styles.tsx index c02719670dc..66dc4b26409 100644 --- a/src/features/editor/views/GraphView/CustomNode/styles.tsx +++ b/src/features/editor/views/GraphView/CustomNode/styles.tsx @@ -104,8 +104,8 @@ export const StyledEditButton = styled.button` position: absolute; top: 4px; right: 4px; - background: ${({ theme }) => theme.BACKGROUND_MODIFIER_ACCENT}; - border: 1px solid ${({ theme }) => theme.NODE_COLORS.DIVIDER}; + background: #3B82F6; + border: 1px solid #2563EB; border-radius: 4px; padding: 4px; cursor: pointer; @@ -114,11 +114,11 @@ export const StyledEditButton = styled.button` justify-content: center; pointer-events: all; transition: all 0.2s ease; - color: ${({ theme }) => theme.NODE_COLORS.TEXT}; + color: white; &:hover { - background: ${({ theme }) => theme.INTERACTIVE_HOVER}; - border-color: ${({ theme }) => theme.TEXT_NORMAL}; + background: #2563EB; + border-color: #1D4ED8; } &:active { diff --git a/src/features/modals/NodeModal/index.tsx b/src/features/modals/NodeModal/index.tsx index caba85febac..de686f11b56 100644 --- a/src/features/modals/NodeModal/index.tsx +++ b/src/features/modals/NodeModal/index.tsx @@ -1,9 +1,12 @@ 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 } 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"]) => { @@ -19,6 +22,25 @@ const normalizeNodeData = (nodeRows: NodeData["text"]) => { return JSON.stringify(obj, null, 2); }; +// Update JSON at a specific path +const updateJsonAtPath = (json: any, path: NodeData["path"], newValue: any): any => { + if (!path || path.length === 0) { + return newValue; + } + + const jsonCopy = JSON.parse(JSON.stringify(json)); + let current = jsonCopy; + + for (let i = 0; i < path.length - 1; i++) { + current = current[path[i]]; + } + + const lastKey = path[path.length - 1]; + current[lastKey] = newValue; + + return jsonCopy; +}; + // return json path in the format $["customer"] const jsonPathToString = (path?: NodeData["path"]) => { if (!path || path.length === 0) return "$"; @@ -28,18 +50,228 @@ const jsonPathToString = (path?: NodeData["path"]) => { export const NodeModal = ({ opened, onClose }: ModalProps) => { const nodeData = useGraph(state => state.selectedNode); + const getJson = useJson(state => state.getJson); + const fileContents = useFile.getState().getContents(); + const [isEditMode, setIsEditMode] = React.useState(false); + const [editedName, setEditedName] = React.useState(""); + const [editedColor, setEditedColor] = React.useState(""); + const [originalName, setOriginalName] = React.useState(""); + const [originalColor, setOriginalColor] = React.useState(""); + // Details node states + const [editedType, setEditedType] = React.useState(""); + const [editedSeason, setEditedSeason] = React.useState(""); + const [originalType, setOriginalType] = React.useState(""); + const [originalSeason, setOriginalSeason] = React.useState(""); + // Nutrients node states (dynamic key-value map) + const [editedNutrients, setEditedNutrients] = React.useState>({}); + const [originalNutrients, setOriginalNutrients] = React.useState>({}); + + const path = nodeData?.path; + const isFruitObjectNode = React.useMemo(() => { + if (!path || path.length < 2) return false; + return path[0] === "fruits" && typeof path[1] === "number" && path.length === 2; + }, [path]); + const isDetailsNode = React.useMemo(() => { + if (!path || path.length < 3) return false; + return path[0] === "fruits" && typeof path[1] === "number" && path[2] === "details"; + }, [path]); + const isNutrientsNode = React.useMemo(() => { + if (!path || path.length < 3) return false; + return path[0] === "fruits" && typeof path[1] === "number" && path[2] === "nutrients"; + }, [path]); + + React.useEffect(() => { + if (nodeData && isFruitObjectNode) { + const content = normalizeNodeData(nodeData.text ?? []); + try { + const parsed = JSON.parse(content); + const name = parsed.name || ""; + const color = parsed.color || ""; + setOriginalName(name); + setOriginalColor(color); + setEditedName(name); + setEditedColor(color); + } catch { + setOriginalName(""); + setOriginalColor(""); + setEditedName(""); + setEditedColor(""); + } + } else if (nodeData && isDetailsNode) { + const content = normalizeNodeData(nodeData.text ?? []); + try { + const parsed = JSON.parse(content); + const type = parsed.type || ""; + const season = parsed.season || ""; + setOriginalType(type); + setOriginalSeason(season); + setEditedType(type); + setEditedSeason(season); + } catch { + setOriginalType(""); + setOriginalSeason(""); + setEditedType(""); + setEditedSeason(""); + } + } else if (nodeData && isNutrientsNode) { + const content = normalizeNodeData(nodeData.text ?? []); + try { + const parsed = JSON.parse(content); + const initial = Object.fromEntries(Object.entries(parsed).map(([k, v]) => [k, String(v)])); + setOriginalNutrients(initial); + setEditedNutrients(initial); + } catch { + setOriginalNutrients({}); + setEditedNutrients({}); + } + } else { + // Clear edit fields for non-fruit nodes + setOriginalName(""); + setOriginalColor(""); + setEditedName(""); + setEditedColor(""); + setOriginalType(""); + setOriginalSeason(""); + setEditedType(""); + setEditedSeason(""); + setOriginalNutrients({}); + setEditedNutrients({}); + setIsEditMode(false); + } + }, [nodeData, isFruitObjectNode, isDetailsNode, isNutrientsNode]); + + React.useEffect(() => { + if (!opened) setIsEditMode(false); + }, [opened]); + + const handleEditClick = () => setIsEditMode(true); + + const handleSave = () => { + if (!nodeData) { + setIsEditMode(false); + return; + } + try { + const currentJson = JSON.parse(fileContents || getJson()); + const existingValue = (nodeData.path || []).reduce((obj, key) => obj[key], currentJson); + let updatedValue: any = existingValue; + if (isFruitObjectNode) { + updatedValue = { ...existingValue, name: editedName, color: editedColor }; + } else if (isDetailsNode) { + updatedValue = { ...existingValue, type: editedType, season: editedSeason }; + } else if (isNutrientsNode) { + updatedValue = { ...existingValue, ...editedNutrients }; + } else { + // Non-editable node + setIsEditMode(false); + return; + } + const updatedJson = updateJsonAtPath(currentJson, nodeData.path, updatedValue); + const updatedStr = JSON.stringify(updatedJson, null, 2); + // Update editor contents (which will propagate back to useJson via debounce) + useFile.getState().setContents({ contents: updatedStr, hasChanges: true }); + // Ensure immediate graph update even if debounce is delayed + useJson.getState().setJson(updatedStr); + if (isFruitObjectNode) { + setOriginalName(editedName); + setOriginalColor(editedColor); + } else if (isDetailsNode) { + setOriginalType(editedType); + setOriginalSeason(editedSeason); + } else if (isNutrientsNode) { + setOriginalNutrients({ ...editedNutrients }); + } + setIsEditMode(false); + toast.success("Changes saved successfully!"); + } catch (error) { + console.error("Error saving changes:", error); + toast.error("Error saving changes. Please try again."); + } + }; + + const handleCancel = () => { + if (isFruitObjectNode) { + setEditedName(originalName); + setEditedColor(originalColor); + } else if (isDetailsNode) { + setEditedType(originalType); + setEditedSeason(originalSeason); + } else if (isNutrientsNode) { + setEditedNutrients({ ...originalNutrients }); + } + setIsEditMode(false); + }; return ( - + { setIsEditMode(false); onClose(); }} centered withCloseButton={false}> - - - - Content - - + + Content + + {(isFruitObjectNode || isDetailsNode || isNutrientsNode) && ( + isEditMode ? ( + <> + + + + ) : ( + + ) + )} + { setIsEditMode(false); onClose(); }} /> - + + + {(isFruitObjectNode || isDetailsNode || isNutrientsNode) && isEditMode ? ( + + {isFruitObjectNode && ( + <> + setEditedName(e.currentTarget.value)} + placeholder="Enter name" + styles={{ input: { minWidth: "350px" } }} + /> + setEditedColor(e.currentTarget.value)} + placeholder="Enter color (e.g., #FF0000)" + styles={{ input: { minWidth: "350px" } }} + /> + + )} + {isDetailsNode && ( + <> + setEditedType(e.currentTarget.value)} + placeholder="Enter type" + styles={{ input: { minWidth: "350px" } }} + /> + setEditedSeason(e.currentTarget.value)} + placeholder="Enter season" + styles={{ input: { minWidth: "350px" } }} + /> + + )} + {isNutrientsNode && Object.keys(editedNutrients).map(key => ( + setEditedNutrients(prev => ({ ...prev, [key]: e.currentTarget.value }))} + placeholder={`Enter ${key}`} + styles={{ input: { minWidth: "350px" } }} + /> + ))} + + ) : ( { language="json" withCopyButton /> - - - - JSON Path - + )} + + JSON Path