Skip to content

Commit cbc4a5b

Browse files
committed
feat(workflow): add node properties panel and related hooks
- Implement NodePropertiesPanel component for editing node data - Add useNodePropertiesPanel hook for panel state management - Add useEdgeLabelSync hook for synchronizing edge labels - Export new components and hooks from main index file
1 parent 159f5cd commit cbc4a5b

File tree

4 files changed

+566
-1
lines changed

4 files changed

+566
-1
lines changed
Lines changed: 391 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,391 @@
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

Comments
 (0)