Skip to content

Commit 7107342

Browse files
committed
fixes all nexxesary requiremetns for editing nodes
1 parent eb409d2 commit 7107342

File tree

2 files changed

+182
-10
lines changed

2 files changed

+182
-10
lines changed

src/features/modals/NodeModal/index.tsx

Lines changed: 121 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1-
import React from "react";
1+
import React, { useState, useMemo } from "react";
22
import type { ModalProps } from "@mantine/core";
3-
import { Modal, Stack, Text, ScrollArea, Flex, CloseButton } from "@mantine/core";
3+
import { Modal, Stack, Text, ScrollArea, Flex, CloseButton, Button, TextInput, Group } from "@mantine/core";
44
import { CodeHighlight } from "@mantine/code-highlight";
55
import type { NodeData } from "../../../types/graph";
66
import useGraph from "../../editor/views/GraphView/stores/useGraph";
7+
import useJson from "../../../store/useJson";
78

89
// return object from json removing array and object fields
910
const normalizeNodeData = (nodeRows: NodeData["text"]) => {
@@ -28,6 +29,85 @@ const jsonPathToString = (path?: NodeData["path"]) => {
2829

2930
export const NodeModal = ({ opened, onClose }: ModalProps) => {
3031
const nodeData = useGraph(state => state.selectedNode);
32+
const [editing, setEditing] = useState(false);
33+
const [name, setName] = useState("");
34+
const [value, setValue] = useState("");
35+
36+
// prepare initial fields when modal opens / nodeData changes
37+
React.useEffect(() => {
38+
setEditing(false);
39+
// prefer explicit 'name' and 'color' rows when available
40+
const nameRow = nodeData?.text?.find(r => r.key === "name");
41+
const colorRow = nodeData?.text?.find(r => r.key === "color");
42+
43+
if (nameRow) {
44+
setName(nameRow.value !== undefined ? String(nameRow.value) : "");
45+
} else {
46+
// fallback: use first primitive row value
47+
const first = nodeData?.text?.find(r => r.key !== null);
48+
setName(first?.value !== undefined ? String(first.value) : "");
49+
}
50+
51+
if (colorRow) {
52+
setValue(colorRow.value !== undefined ? String(colorRow.value) : "");
53+
} else {
54+
// fallback: if node itself is a single primitive, use that
55+
const single = nodeData?.text?.[0];
56+
setValue(single?.value !== undefined ? String(single.value) : "");
57+
}
58+
}, [nodeData]);
59+
60+
const pathForKey = (key: string | undefined) => {
61+
if (!nodeData) return undefined;
62+
// if node has the key as a row, target that; otherwise target parent path and key
63+
const row = nodeData.text?.find(r => r.key === key);
64+
if (row && key) return nodeData.path ? [...nodeData.path, key] : [key];
65+
if (nodeData.path) return [...nodeData.path, key ?? ""]; // create
66+
return key ? [key] : undefined;
67+
};
68+
69+
const handleSave = () => {
70+
if (!nodeData) return;
71+
try {
72+
// handle renaming a key named 'name' (rare) - we don't rename arbitrary keys here
73+
// set name value
74+
const namePath = pathForKey("name");
75+
if (namePath) {
76+
let parsedName: any = name;
77+
try {
78+
parsedName = JSON.parse(name);
79+
} catch (e) {
80+
parsedName = name;
81+
}
82+
useJson.getState().setValueAtPath(namePath, parsedName);
83+
}
84+
85+
// set color/value
86+
const colorPath = pathForKey("color");
87+
if (colorPath) {
88+
let parsedColor: any = value;
89+
try {
90+
parsedColor = JSON.parse(value);
91+
} catch (e) {
92+
parsedColor = value;
93+
}
94+
useJson.getState().setValueAtPath(colorPath, parsedColor);
95+
}
96+
97+
setEditing(false);
98+
onClose();
99+
} catch (err) {
100+
console.warn("Failed to save node edits", err);
101+
}
102+
};
103+
104+
const handleCancel = () => {
105+
// discard local changes
106+
const first = nodeData?.text?.[0];
107+
setName(first?.key ?? "");
108+
setValue(first?.value !== undefined ? String(first.value) : "");
109+
setEditing(false);
110+
};
31111

32112
return (
33113
<Modal size="auto" opened={opened} onClose={onClose} centered withCloseButton={false}>
@@ -37,16 +117,47 @@ export const NodeModal = ({ opened, onClose }: ModalProps) => {
37117
<Text fz="xs" fw={500}>
38118
Content
39119
</Text>
40-
<CloseButton onClick={onClose} />
120+
<Flex gap="xs" align="center">
121+
{!editing ? (
122+
<Button
123+
size="xs"
124+
variant="outline"
125+
onClick={() => setEditing(true)}
126+
data-testid="node-edit-button"
127+
>
128+
Edit
129+
</Button>
130+
) : (
131+
<Group spacing={6}>
132+
<Button size="xs" color="green" onClick={handleSave} data-testid="node-save-button">
133+
Save
134+
</Button>
135+
<Button size="xs" variant="outline" color="red" onClick={handleCancel} data-testid="node-cancel-button">
136+
Cancel
137+
</Button>
138+
</Group>
139+
)}
140+
<CloseButton onClick={onClose} />
141+
</Flex>
41142
</Flex>
42143
<ScrollArea.Autosize mah={250} maw={600}>
43-
<CodeHighlight
44-
code={normalizeNodeData(nodeData?.text ?? [])}
45-
miw={350}
46-
maw={600}
47-
language="json"
48-
withCopyButton
49-
/>
144+
{!editing ? (
145+
<CodeHighlight
146+
code={normalizeNodeData(nodeData?.text ?? [])}
147+
miw={350}
148+
maw={600}
149+
language="json"
150+
withCopyButton
151+
/>
152+
) : (
153+
<Stack spacing={8}>
154+
{/* Name input (only when key exists) */}
155+
{nodeData?.text?.[0]?.key ? (
156+
<TextInput label="Name" size="xs" value={name} onChange={e => setName(e.currentTarget.value)} />
157+
) : null}
158+
<TextInput label="Value / Color" size="xs" value={value} onChange={e => setValue(e.currentTarget.value)} />
159+
</Stack>
160+
)}
50161
</ScrollArea.Autosize>
51162
</Stack>
52163
<Text fz="xs" fw={500}>

src/store/useJson.ts

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@ interface JsonActions {
55
setJson: (json: string) => void;
66
getJson: () => string;
77
clear: () => void;
8+
// Set a value at a given JSON path and update the graph
9+
setValueAtPath: (path: any[] | undefined, value: unknown) => void;
10+
// Rename a key at a given JSON path (path points to the key to rename)
11+
setKeyAtPath: (path: any[] | undefined, newKey: string) => void;
812
}
913

1014
const initialStates = {
@@ -21,6 +25,63 @@ const useJson = create<JsonStates & JsonActions>()((set, get) => ({
2125
set({ json, loading: false });
2226
useGraph.getState().setGraph(json);
2327
},
28+
setValueAtPath: (path, value) => {
29+
try {
30+
const current = get().json || "{}";
31+
const obj = JSON.parse(current);
32+
33+
if (!path || path.length === 0) {
34+
// replace root
35+
const newJson = typeof value === "string" ? value : JSON.stringify(value, null, 2);
36+
get().setJson(newJson);
37+
return;
38+
}
39+
40+
// traverse
41+
let cur: any = obj;
42+
for (let i = 0; i < path.length - 1; i++) {
43+
const seg = path[i] as any;
44+
if (cur[seg] === undefined) cur[seg] = {};
45+
cur = cur[seg];
46+
}
47+
48+
const last = path[path.length - 1] as any;
49+
cur[last] = value;
50+
51+
const newJson = JSON.stringify(obj, null, 2);
52+
get().setJson(newJson);
53+
} catch (err) {
54+
// fallback: do nothing
55+
console.warn("Failed to set value at path", err);
56+
}
57+
},
58+
setKeyAtPath: (path, newKey) => {
59+
try {
60+
if (!path || path.length === 0) return;
61+
const current = get().json || "{}";
62+
const obj = JSON.parse(current);
63+
64+
// traverse to parent
65+
let cur: any = obj;
66+
for (let i = 0; i < path.length - 1; i++) {
67+
const seg = path[i] as any;
68+
if (cur[seg] === undefined) cur[seg] = {};
69+
cur = cur[seg];
70+
}
71+
72+
const last = path[path.length - 1] as any;
73+
// only rename if parent is object and has the key
74+
if (cur && Object.prototype.hasOwnProperty.call(cur, last)) {
75+
cur[newKey] = cur[last];
76+
delete cur[last];
77+
}
78+
79+
const newJson = JSON.stringify(obj, null, 2);
80+
get().setJson(newJson);
81+
} catch (err) {
82+
console.warn("Failed to set key at path", err);
83+
}
84+
},
2485
clear: () => {
2586
set({ json: "", loading: false });
2687
useGraph.getState().clearGraph();

0 commit comments

Comments
 (0)