11import React 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 , Textarea , 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 useFile from "../../../store/useFile" ;
8+ import useJson from "../../../store/useJson" ;
9+ import { contentToJson } from "../../../lib/utils/jsonAdapter" ;
710
811// return object from json removing array and object fields
912const normalizeNodeData = ( nodeRows : NodeData [ "text" ] ) => {
@@ -28,6 +31,20 @@ const jsonPathToString = (path?: NodeData["path"]) => {
2831
2932export const NodeModal = ( { opened, onClose } : ModalProps ) => {
3033 const nodeData = useGraph ( state => state . selectedNode ) ;
34+ const contents = useFile ( state => state . contents ) ;
35+ const setContents = useFile ( state => state . setContents ) ;
36+ const setJson = useJson ( state => state . setJson ) ;
37+
38+ const [ editing , setEditing ] = React . useState ( false ) ;
39+ const [ localValue , setLocalValue ] = React . useState ( "" ) ;
40+
41+ React . useEffect ( ( ) => {
42+ // when opening modal reset editing state and local value
43+ if ( opened ) {
44+ setEditing ( false ) ;
45+ setLocalValue ( normalizeNodeData ( nodeData ?. text ?? [ ] ) ) ;
46+ }
47+ } , [ opened , nodeData ] ) ;
3148
3249 return (
3350 < Modal size = "auto" opened = { opened } onClose = { onClose } centered withCloseButton = { false } >
@@ -37,16 +54,116 @@ export const NodeModal = ({ opened, onClose }: ModalProps) => {
3754 < Text fz = "xs" fw = { 500 } >
3855 Content
3956 </ Text >
40- < CloseButton onClick = { onClose } />
57+ < Group spacing = "xs" >
58+ { ! editing && (
59+ < Button size = "xs" variant = "default" onClick = { ( ) => setEditing ( true ) } >
60+ Edit
61+ </ Button >
62+ ) }
63+ { editing && (
64+ < >
65+ < Button
66+ size = "xs"
67+ color = "green"
68+ onClick = { async ( ) => {
69+ try {
70+ // parse the left editor contents into an object
71+ const root = await contentToJson ( contents ) ;
72+ // parse the edited content
73+ const edited = await contentToJson ( localValue ) ;
74+
75+ // helper to get value by path
76+ const getByPath = ( obj : any , path ?: any [ ] ) => {
77+ if ( ! path || path . length === 0 ) return obj ;
78+ return path . reduce ( ( acc : any , cur : any ) => ( acc ? acc [ cur ] : undefined ) , obj ) ;
79+ } ;
80+
81+ // helper to set value at path
82+ const setByPath = ( obj : any , path : any [ ] | undefined , value : any ) => {
83+ if ( ! path || path . length === 0 ) return value ;
84+ const last = path [ path . length - 1 ] ;
85+ const parentPath = path . slice ( 0 , - 1 ) ;
86+ const parent = parentPath . length === 0 ? obj : getByPath ( obj , parentPath ) ;
87+ if ( ! parent ) return ;
88+ parent [ last ] = value ;
89+ } ;
90+
91+ const path = nodeData ?. path ;
92+
93+ const currentTarget = getByPath ( root , path as any [ ] | undefined ) ;
94+
95+ if ( currentTarget && typeof currentTarget === "object" && ! Array . isArray ( currentTarget ) && edited && typeof edited === "object" ) {
96+ // merge primitive fields into object node
97+ Object . keys ( edited ) . forEach ( k => {
98+ currentTarget [ k ] = edited [ k ] ;
99+ } ) ;
100+ } else if ( typeof path !== "undefined" && path && path . length > 0 ) {
101+ setByPath ( root , path as any [ ] , edited ) ;
102+ } else {
103+ // root replacement
104+ // if edited is object merge, otherwise replace root
105+ if ( edited && typeof edited === "object" && ! Array . isArray ( edited ) ) {
106+ Object . keys ( edited ) . forEach ( k => {
107+ ( root as any ) [ k ] = edited [ k ] ;
108+ } ) ;
109+ } else {
110+ // replace root completely
111+ // setContents expects a string
112+ await setContents ( { contents : JSON . stringify ( edited , null , 2 ) , hasChanges : true } ) ;
113+ setJson ( JSON . stringify ( edited , null , 2 ) ) ;
114+ setEditing ( false ) ;
115+ return ;
116+ }
117+ }
118+
119+ const newContents = JSON . stringify ( root , null , 2 ) ;
120+ await setContents ( { contents : newContents , hasChanges : true } ) ;
121+ setJson ( newContents ) ;
122+ setEditing ( false ) ;
123+ } catch ( error ) {
124+ // if parsing fails, keep editing mode
125+ // ideally show an error toast
126+ // console.error(error);
127+ }
128+ } }
129+ >
130+ Save
131+ </ Button >
132+ < Button
133+ size = "xs"
134+ color = "gray"
135+ variant = "outline"
136+ onClick = { ( ) => {
137+ // discard changes
138+ setLocalValue ( normalizeNodeData ( nodeData ?. text ?? [ ] ) ) ;
139+ setEditing ( false ) ;
140+ } }
141+ >
142+ Cancel
143+ </ Button >
144+ </ >
145+ ) }
146+ < CloseButton onClick = { onClose } />
147+ </ Group >
41148 </ Flex >
42149 < 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- />
150+ { ! editing ? (
151+ < CodeHighlight
152+ code = { normalizeNodeData ( nodeData ?. text ?? [ ] ) }
153+ miw = { 350 }
154+ maw = { 600 }
155+ language = "json"
156+ withCopyButton
157+ />
158+ ) : (
159+ < Textarea
160+ styles = { { input : { fontFamily : "monospace" } } }
161+ minRows = { 6 }
162+ maw = { 600 }
163+ value = { localValue }
164+ onChange = { e => setLocalValue ( e . currentTarget . value ) }
165+ />
166+ ) }
50167 </ ScrollArea . Autosize >
51168 </ Stack >
52169 < Text fz = "xs" fw = { 500 } >
0 commit comments