1+ import React from 'react' ;
2+ import { X , Save , RotateCcw , Settings } from 'lucide-react' ;
3+
4+ /**
5+ * A simple node properties panel component for editing node data
6+ * @param {Object } props - Component props
7+ * @param {boolean } props.isOpen - Whether the panel is open
8+ * @param {Object } props.node - The selected node object
9+ * @param {Object } props.formData - Current form data
10+ * @param {boolean } props.isDirty - Whether there are unsaved changes
11+ * @param {Function } props.onClose - Callback to close the panel
12+ * @param {Function } props.onFieldChange - Callback when a field changes
13+ * @param {Function } props.onSave - Callback to save changes
14+ * @param {Function } props.onReset - Callback to reset changes
15+ * @param {string } props.className - Additional CSS classes
16+ * @param {Object } props.style - Inline styles
17+ */
18+ export function NodePropertiesPanel ( {
19+ isOpen = false ,
20+ node = null ,
21+ formData = { } ,
22+ isDirty = false ,
23+ onClose,
24+ onFieldChange,
25+ onSave,
26+ onReset,
27+ className = '' ,
28+ style = { } ,
29+ autoSave = false ,
30+ } ) {
31+ if ( ! isOpen || ! node ) {
32+ return null ;
33+ }
34+
35+ const handleFieldChange = ( field , value ) => {
36+ if ( onFieldChange ) {
37+ onFieldChange ( field , value ) ;
38+
39+ // When name changes, also update label to keep them in sync
40+ if ( field === 'name' ) {
41+ onFieldChange ( 'label' , value ) ;
42+ }
43+ }
44+ } ;
45+
46+ const handleSave = ( ) => {
47+ if ( onSave && isDirty ) {
48+ onSave ( ) ;
49+ }
50+ } ;
51+
52+ const handleReset = ( ) => {
53+ if ( onReset && isDirty ) {
54+ onReset ( ) ;
55+ }
56+ } ;
57+
58+ const getNodeTypeLabel = ( type ) => {
59+ const labels = {
60+ start : 'Start Node' ,
61+ end : 'End Node' ,
62+ operation : 'Operation Node' ,
63+ switch : 'Switch Node' ,
64+ event : 'Event Node' ,
65+ sleep : 'Sleep Node' ,
66+ } ;
67+ return labels [ type ] || 'Unknown Node' ;
68+ } ;
69+
70+ return (
71+ < div
72+ className = { `node-properties-panel ${ className } ` }
73+ style = { {
74+ position : 'fixed' ,
75+ top : '20px' ,
76+ right : '20px' ,
77+ width : '320px' ,
78+ maxHeight : '80vh' ,
79+ backgroundColor : 'white' ,
80+ border : '1px solid #e5e7eb' ,
81+ borderRadius : '8px' ,
82+ boxShadow : '0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05)' ,
83+ zIndex : 1000 ,
84+ overflow : 'hidden' ,
85+ display : 'flex' ,
86+ flexDirection : 'column' ,
87+ ...style ,
88+ } }
89+ >
90+ { /* Header */ }
91+ < div
92+ style = { {
93+ padding : '16px' ,
94+ borderBottom : '1px solid #e5e7eb' ,
95+ backgroundColor : '#f9fafb' ,
96+ display : 'flex' ,
97+ alignItems : 'center' ,
98+ justifyContent : 'space-between' ,
99+ } }
100+ >
101+ < div style = { { display : 'flex' , alignItems : 'center' , gap : '8px' } } >
102+ < Settings size = { 16 } color = "#6b7280" />
103+ < h3 style = { { margin : 0 , fontSize : '14px' , fontWeight : '600' , color : '#374151' } } >
104+ Node Properties
105+ </ h3 >
106+ </ div >
107+ < button
108+ onClick = { onClose }
109+ style = { {
110+ background : 'none' ,
111+ border : 'none' ,
112+ cursor : 'pointer' ,
113+ padding : '4px' ,
114+ borderRadius : '4px' ,
115+ display : 'flex' ,
116+ alignItems : 'center' ,
117+ justifyContent : 'center' ,
118+ } }
119+ onMouseEnter = { ( e ) => {
120+ e . target . style . backgroundColor = '#f3f4f6' ;
121+ } }
122+ onMouseLeave = { ( e ) => {
123+ e . target . style . backgroundColor = 'transparent' ;
124+ } }
125+ >
126+ < X size = { 16 } color = "#6b7280" />
127+ </ button >
128+ </ div >
129+
130+ { /* Content */ }
131+ < div
132+ style = { {
133+ padding : '16px' ,
134+ flex : 1 ,
135+ overflow : 'auto' ,
136+ } }
137+ >
138+ { /* Node Type */ }
139+ < div style = { { marginBottom : '16px' } } >
140+ < label style = { {
141+ display : 'block' ,
142+ fontSize : '12px' ,
143+ fontWeight : '500' ,
144+ color : '#374151' ,
145+ marginBottom : '4px'
146+ } } >
147+ Node Type
148+ </ label >
149+ < div style = { {
150+ padding : '8px 12px' ,
151+ backgroundColor : '#f3f4f6' ,
152+ border : '1px solid #d1d5db' ,
153+ borderRadius : '6px' ,
154+ fontSize : '14px' ,
155+ color : '#6b7280' ,
156+ } } >
157+ { getNodeTypeLabel ( node . type ) }
158+ </ div >
159+ </ div >
160+
161+ { /* Node ID */ }
162+ < div style = { { marginBottom : '16px' } } >
163+ < label style = { {
164+ display : 'block' ,
165+ fontSize : '12px' ,
166+ fontWeight : '500' ,
167+ color : '#374151' ,
168+ marginBottom : '4px'
169+ } } >
170+ Node ID
171+ </ label >
172+ < div style = { {
173+ padding : '8px 12px' ,
174+ backgroundColor : '#f3f4f6' ,
175+ border : '1px solid #d1d5db' ,
176+ borderRadius : '6px' ,
177+ fontSize : '14px' ,
178+ color : '#6b7280' ,
179+ fontFamily : 'monospace' ,
180+ } } >
181+ { node . id }
182+ </ div >
183+ </ div >
184+
185+ { /* Node Name */ }
186+ < div style = { { marginBottom : '16px' } } >
187+ < label style = { {
188+ display : 'block' ,
189+ fontSize : '12px' ,
190+ fontWeight : '500' ,
191+ color : '#374151' ,
192+ marginBottom : '4px'
193+ } } >
194+ Name
195+ </ label >
196+ < input
197+ type = "text"
198+ value = { formData . name || '' }
199+ onChange = { ( e ) => handleFieldChange ( 'name' , e . target . value ) }
200+ placeholder = "Enter node name"
201+ style = { {
202+ width : '100%' ,
203+ padding : '8px 12px' ,
204+ border : '1px solid #d1d5db' ,
205+ borderRadius : '6px' ,
206+ fontSize : '14px' ,
207+ outline : 'none' ,
208+ transition : 'border-color 0.2s' ,
209+ } }
210+ onFocus = { ( e ) => {
211+ e . target . style . borderColor = '#3b82f6' ;
212+ e . target . style . boxShadow = '0 0 0 3px rgba(59, 130, 246, 0.1)' ;
213+ } }
214+ onBlur = { ( e ) => {
215+ e . target . style . borderColor = '#d1d5db' ;
216+ e . target . style . boxShadow = 'none' ;
217+ } }
218+ />
219+ </ div >
220+
221+ { /* Node-specific fields based on type */ }
222+ { node . type === 'operation' && (
223+ < div style = { { marginBottom : '16px' } } >
224+ < label style = { {
225+ display : 'block' ,
226+ fontSize : '12px' ,
227+ fontWeight : '500' ,
228+ color : '#374151' ,
229+ marginBottom : '4px'
230+ } } >
231+ Function Reference
232+ </ label >
233+ < input
234+ type = "text"
235+ value = { formData . functionRef ?. refName || '' }
236+ onChange = { ( e ) => handleFieldChange ( 'functionRef.refName' , e . target . value ) }
237+ placeholder = "Enter function name"
238+ style = { {
239+ width : '100%' ,
240+ padding : '8px 12px' ,
241+ border : '1px solid #d1d5db' ,
242+ borderRadius : '6px' ,
243+ fontSize : '14px' ,
244+ outline : 'none' ,
245+ transition : 'border-color 0.2s' ,
246+ } }
247+ onFocus = { ( e ) => {
248+ e . target . style . borderColor = '#3b82f6' ;
249+ e . target . style . boxShadow = '0 0 0 3px rgba(59, 130, 246, 0.1)' ;
250+ } }
251+ onBlur = { ( e ) => {
252+ e . target . style . borderColor = '#d1d5db' ;
253+ e . target . style . boxShadow = 'none' ;
254+ } }
255+ />
256+ </ div >
257+ ) }
258+
259+ { node . type === 'sleep' && (
260+ < div style = { { marginBottom : '16px' } } >
261+ < label style = { {
262+ display : 'block' ,
263+ fontSize : '12px' ,
264+ fontWeight : '500' ,
265+ color : '#374151' ,
266+ marginBottom : '4px'
267+ } } >
268+ Duration
269+ </ label >
270+ < input
271+ type = "text"
272+ value = { formData . duration || '' }
273+ onChange = { ( e ) => handleFieldChange ( 'duration' , e . target . value ) }
274+ placeholder = "e.g., PT30S, PT5M, PT1H"
275+ style = { {
276+ width : '100%' ,
277+ padding : '8px 12px' ,
278+ border : '1px solid #d1d5db' ,
279+ borderRadius : '6px' ,
280+ fontSize : '14px' ,
281+ outline : 'none' ,
282+ transition : 'border-color 0.2s' ,
283+ } }
284+ onFocus = { ( e ) => {
285+ e . target . style . borderColor = '#3b82f6' ;
286+ e . target . style . boxShadow = '0 0 0 3px rgba(59, 130, 246, 0.1)' ;
287+ } }
288+ onBlur = { ( e ) => {
289+ e . target . style . borderColor = '#d1d5db' ;
290+ e . target . style . boxShadow = 'none' ;
291+ } }
292+ />
293+ </ div >
294+ ) }
295+ </ div >
296+
297+ { /* Footer with action buttons (only show if not auto-save) */ }
298+ { ! autoSave && (
299+ < div
300+ style = { {
301+ padding : '16px' ,
302+ borderTop : '1px solid #e5e7eb' ,
303+ backgroundColor : '#f9fafb' ,
304+ display : 'flex' ,
305+ gap : '8px' ,
306+ justifyContent : 'flex-end' ,
307+ } }
308+ >
309+ < button
310+ onClick = { handleReset }
311+ disabled = { ! isDirty }
312+ style = { {
313+ padding : '8px 12px' ,
314+ border : '1px solid #d1d5db' ,
315+ backgroundColor : 'white' ,
316+ color : isDirty ? '#374151' : '#9ca3af' ,
317+ borderRadius : '6px' ,
318+ fontSize : '12px' ,
319+ fontWeight : '500' ,
320+ cursor : isDirty ? 'pointer' : 'not-allowed' ,
321+ display : 'flex' ,
322+ alignItems : 'center' ,
323+ gap : '4px' ,
324+ transition : 'all 0.2s' ,
325+ } }
326+ onMouseEnter = { ( e ) => {
327+ if ( isDirty ) {
328+ e . target . style . backgroundColor = '#f3f4f6' ;
329+ }
330+ } }
331+ onMouseLeave = { ( e ) => {
332+ e . target . style . backgroundColor = 'white' ;
333+ } }
334+ >
335+ < RotateCcw size = { 12 } />
336+ Reset
337+ </ button >
338+ < button
339+ onClick = { handleSave }
340+ disabled = { ! isDirty }
341+ style = { {
342+ padding : '8px 12px' ,
343+ border : '1px solid #3b82f6' ,
344+ backgroundColor : isDirty ? '#3b82f6' : '#e5e7eb' ,
345+ color : isDirty ? 'white' : '#9ca3af' ,
346+ borderRadius : '6px' ,
347+ fontSize : '12px' ,
348+ fontWeight : '500' ,
349+ cursor : isDirty ? 'pointer' : 'not-allowed' ,
350+ display : 'flex' ,
351+ alignItems : 'center' ,
352+ gap : '4px' ,
353+ transition : 'all 0.2s' ,
354+ } }
355+ onMouseEnter = { ( e ) => {
356+ if ( isDirty ) {
357+ e . target . style . backgroundColor = '#2563eb' ;
358+ }
359+ } }
360+ onMouseLeave = { ( e ) => {
361+ if ( isDirty ) {
362+ e . target . style . backgroundColor = '#3b82f6' ;
363+ }
364+ } }
365+ >
366+ < Save size = { 12 } />
367+ Save
368+ </ button >
369+ </ div >
370+ ) }
371+
372+ { /* Dirty indicator */ }
373+ { isDirty && (
374+ < div
375+ style = { {
376+ position : 'absolute' ,
377+ top : '8px' ,
378+ right : '40px' ,
379+ width : '8px' ,
380+ height : '8px' ,
381+ backgroundColor : '#f59e0b' ,
382+ borderRadius : '50%' ,
383+ border : '2px solid white' ,
384+ } }
385+ />
386+ ) }
387+ </ div >
388+ ) ;
389+ }
390+
391+ export default NodePropertiesPanel ;
0 commit comments