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