1- import React from "react" ;
1+ import React , { useState , useMemo } from "react" ;
22import 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" ;
44import { CodeHighlight } from "@mantine/code-highlight" ;
55import type { NodeData } from "../../../types/graph" ;
66import useGraph from "../../editor/views/GraphView/stores/useGraph" ;
7+ import useJson from "../../../store/useJson" ;
78
89// return object from json removing array and object fields
910const normalizeNodeData = ( nodeRows : NodeData [ "text" ] ) => {
@@ -28,6 +29,85 @@ const jsonPathToString = (path?: NodeData["path"]) => {
2829
2930export 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 } >
0 commit comments