Skip to content

Commit abdffce

Browse files
committed
refactor: DRY up code and extract shared constants
Frontend: - Create constants/editor.ts with shared edge styling, node types, and DragData - Extract reusable ParamForm component from configPanel - DRY up node creation logic in editor store with createFlowNode helper - Use shared constants across editor, node-palette, and utils - Fix missing React keys in SelectItem components Backend: - Create nodes/utils.py with parse_json_field and render_template_field - Refactor node handlers to use shared utilities - Add ensure_context helper for consistent context handling
1 parent 423b0cf commit abdffce

12 files changed

Lines changed: 488 additions & 281 deletions

File tree

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import { Label } from '@/components/ui/label';
2+
import { Input } from '@/components/ui/input';
3+
import { DocTypeAutoComplete } from '@/components/common/doctype-autocomplete';
4+
import {
5+
Select,
6+
SelectContent,
7+
SelectItem,
8+
SelectTrigger,
9+
SelectValue,
10+
} from '@/components/ui/select';
11+
12+
export interface ParamFieldProps {
13+
param: HazelNodeParam;
14+
value: string;
15+
onChange: (fieldname: string, value: string) => void;
16+
disabled?: boolean;
17+
}
18+
19+
export function ParamField({
20+
param,
21+
value,
22+
onChange,
23+
disabled = false,
24+
}: ParamFieldProps) {
25+
const handleChange = (newValue: string) => {
26+
onChange(param.fieldname, newValue);
27+
};
28+
29+
return (
30+
<div className="mb-3">
31+
<Label htmlFor={param.fieldname}>{param.label}</Label>
32+
33+
{param.fieldtype === 'Link' && (
34+
<DocTypeAutoComplete
35+
onChange={handleChange}
36+
doctype={param.options || 'DocType'}
37+
disabled={disabled}
38+
/>
39+
)}
40+
41+
{param.fieldtype === 'Select' && (
42+
<Select
43+
value={value || ''}
44+
onValueChange={handleChange}
45+
disabled={disabled}
46+
>
47+
<SelectTrigger className="w-full">
48+
<SelectValue placeholder={`Select ${param.label}`} />
49+
</SelectTrigger>
50+
<SelectContent>
51+
{param.options?.split('\n').map((option) => (
52+
<SelectItem key={option} value={option}>
53+
{option}
54+
</SelectItem>
55+
))}
56+
</SelectContent>
57+
</Select>
58+
)}
59+
60+
{(param.fieldtype === 'Data' || param.fieldtype === 'Int') && (
61+
<Input
62+
value={value || ''}
63+
onChange={(e) => handleChange(e.target.value)}
64+
type={param.fieldtype === 'Int' ? 'number' : 'text'}
65+
name={param.fieldname}
66+
disabled={disabled}
67+
/>
68+
)}
69+
</div>
70+
);
71+
}
72+
73+
export interface ParamFormProps {
74+
params: HazelNodeParam[];
75+
values: Record<string, string>;
76+
onChange: (fieldname: string, value: string) => void;
77+
disabled?: boolean;
78+
}
79+
80+
export function ParamForm({
81+
params,
82+
values,
83+
onChange,
84+
disabled = false,
85+
}: ParamFormProps) {
86+
return (
87+
<>
88+
{params.map((param) => (
89+
<ParamField
90+
key={param.fieldname}
91+
param={param}
92+
value={values[param.fieldname] || ''}
93+
onChange={onChange}
94+
disabled={disabled}
95+
/>
96+
))}
97+
</>
98+
);
99+
}

frontend/src/components/workflows/configPanel.tsx

Lines changed: 83 additions & 136 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,6 @@ import {
33
useFrappeUpdateDoc,
44
useFrappeDeleteDoc,
55
} from 'frappe-react-sdk';
6-
import { Label } from '@/components/ui/label';
7-
import { Input } from '@/components/ui/input';
86
import { Button } from '@/components/ui/button';
97
import { ScrollArea } from '@/components/ui/scroll-area';
108
import SetTriggerDialog from '@/components/workflows/set-trigger-dialog';
@@ -13,21 +11,10 @@ import { useNavigate } from '@tanstack/react-router';
1311
import { useConfirm } from '@/hooks/confirm';
1412
import { toast } from 'sonner';
1513
import { useEditorStore } from '@/stores/editor';
16-
import { DocTypeAutoComplete } from '../common/doctype-autocomplete';
1714
import { nodesToHazelNodes, edgesToHazelConnections } from '@/utils/editor';
15+
import { ParamForm } from '@/components/common/param-field';
1816

19-
import {
20-
Select,
21-
SelectContent,
22-
SelectItem,
23-
SelectTrigger,
24-
SelectValue,
25-
} from "@/components/ui/select"
26-
27-
28-
interface TriggerConfig {
29-
[index: string]: string;
30-
}
17+
type FormState = Record<string, string>;
3118

3219
export function WorkflowConfigPanel({
3320
hazelWorkflow,
@@ -56,30 +43,55 @@ export function WorkflowConfigPanel({
5643
}
5744
);
5845

59-
6046
const { updateDoc } = useFrappeUpdateDoc<HazelWorkflow>();
6147
const { deleteDoc } = useFrappeDeleteDoc();
6248

6349
const navigate = useNavigate();
6450
const confirm = useConfirm();
6551

66-
const [triggerFormState, setTriggerFormState] = useState<TriggerConfig>({});
52+
const [triggerFormState, setTriggerFormState] = useState<FormState>({});
53+
const [actionFormState, setActionFormState] = useState<FormState>({});
6754
const [updateTriggerDialogOpen, setUpdateTriggerDialogOpen] = useState(false);
6855

56+
// Initialize trigger form state
6957
useEffect(() => {
70-
const initTriggerFormState: TriggerConfig = {};
71-
const initConfig = hazelWorkflow.trigger_config
58+
const initState: FormState = {};
59+
const savedConfig = hazelWorkflow.trigger_config
7260
? JSON.parse(hazelWorkflow.trigger_config)
7361
: {};
7462

7563
if (triggerDoc) {
7664
for (const param of triggerDoc.params || []) {
77-
initTriggerFormState[param.fieldname] = initConfig[param.fieldname];
65+
initState[param.fieldname] = savedConfig[param.fieldname] || '';
7866
}
7967
}
80-
setTriggerFormState(initTriggerFormState);
68+
setTriggerFormState(initState);
8169
}, [triggerDoc, hazelWorkflow]);
8270

71+
// Initialize action form state when selected node changes
72+
useEffect(() => {
73+
const initState: FormState = {};
74+
const savedParams = editorStore.selectedNode?.data?.parameters;
75+
76+
if (actionDoc && savedParams) {
77+
for (const param of actionDoc.params || []) {
78+
const savedParam = Array.isArray(savedParams)
79+
? savedParams.find((p: { fieldname: string }) => p.fieldname === param.fieldname)
80+
: null;
81+
initState[param.fieldname] = savedParam?.value || '';
82+
}
83+
}
84+
setActionFormState(initState);
85+
}, [actionDoc, editorStore.selectedNode]);
86+
87+
const handleTriggerFieldChange = (fieldname: string, value: string) => {
88+
setTriggerFormState((prev) => ({ ...prev, [fieldname]: value }));
89+
};
90+
91+
const handleActionFieldChange = (fieldname: string, value: string) => {
92+
setActionFormState((prev) => ({ ...prev, [fieldname]: value }));
93+
};
94+
8395
async function handleDeleteWorkflow() {
8496
const deleteConfirmed = await confirm({
8597
title: 'Delete Workflow',
@@ -117,135 +129,70 @@ export function WorkflowConfigPanel({
117129
}
118130
}
119131

120-
121132
return (
122-
<ScrollArea className="h-full p-3">
123-
<strong>{hazelWorkflow.title}</strong>
124-
<ul>
125-
{hazelWorkflow.trigger_type && (
126-
<li>
127-
Trigger: {hazelWorkflow.trigger_type}
133+
<ScrollArea className="h-full p-3">
134+
<h2 className="text-lg font-semibold mb-3">{hazelWorkflow.title}</h2>
135+
136+
{/* Trigger Section */}
137+
{hazelWorkflow.trigger_type && (
138+
<section className="mb-4">
139+
<div className="flex items-center justify-between mb-2">
140+
<span className="text-sm text-gray-600">
141+
Trigger: <strong>{hazelWorkflow.trigger_type}</strong>
142+
</span>
128143
<Button
129144
onClick={() => setUpdateTriggerDialogOpen(true)}
130145
outline={true}
146+
className="text-xs"
131147
>
132148
Change
133149
</Button>
134-
</li>
135-
)}
136-
</ul>
137-
{(triggerDoc?.params || []).map((param) => {
138-
return (
139-
<div key={param.name}>
140-
<Label htmlFor={param.fieldname}>{param.label}</Label>
141-
142-
{param.fieldtype === "Link" && <DocTypeAutoComplete
143-
onChange={(v) =>
144-
setTriggerFormState({
145-
...triggerFormState,
146-
[param.fieldname]: v,
147-
})}
148-
doctype='DocType' />}
149-
150-
{param.fieldtype === "Select" &&
151-
<Select
152-
value={triggerFormState[param.fieldname]}
153-
onValueChange={(v) =>
154-
setTriggerFormState({
155-
...triggerFormState,
156-
[param.fieldname]: v,
157-
})
158-
}
159-
>
160-
<SelectTrigger className="w-[180px]">
161-
<SelectValue placeholder="Event" />
162-
</SelectTrigger>
163-
<SelectContent>
164-
{param.options?.split("\n").map((option) => {
165-
return <SelectItem value={option}>{ option }</SelectItem>
166-
})}
167-
168-
169-
</SelectContent>
170-
</Select>
171-
}
172-
{param.fieldtype === "Data" &&
173-
<Input
174-
value={triggerFormState[param.fieldname]}
175-
onChange={(v) =>
176-
setTriggerFormState({
177-
...triggerFormState,
178-
[param.fieldname]: v.target.value,
179-
})
180-
}
181-
type="text"
182-
name={param.fieldname}
183-
/>
184-
}
185150
</div>
186-
);
187-
})}
151+
152+
{triggerDoc?.params && triggerDoc.params.length > 0 && (
153+
<ParamForm
154+
params={triggerDoc.params}
155+
values={triggerFormState}
156+
onChange={handleTriggerFieldChange}
157+
/>
158+
)}
159+
</section>
160+
)}
188161

189162
<SetTriggerDialog
190163
open={updateTriggerDialogOpen}
191164
onClose={setUpdateTriggerDialogOpen}
192165
/>
193-
<Button color="white" onClick={handleSaveWorkflow}>
194-
Save
195-
</Button>
196-
<br />
197-
<Button color="rose" onClick={handleDeleteWorkflow}>
198-
Delete Workflow
199-
</Button>
200-
<h2 className=" mt-4 text-xl font-bold text-gray-900">Action Settings</h2>
201-
{editorStore.selectedNode?.data.type}
202-
203-
{actionDoc?.params?.map(param => {
204-
return (
205-
<div key={param.name}>
206-
<Label htmlFor={param.fieldname}>{param.label}</Label>
207-
208-
{param.fieldtype === "Link" && <DocTypeAutoComplete
209-
onChange={(v) =>
210-
setTriggerFormState({
211-
...triggerFormState,
212-
[param.fieldname]: v,
213-
})}
214-
doctype='DocType' />}
215-
216-
{param.fieldtype === "Select" &&
217-
<Select
218-
value={triggerFormState[param.fieldname]}
219-
onValueChange={(v) =>
220-
setTriggerFormState({
221-
...triggerFormState,
222-
[param.fieldname]: v,
223-
})
224-
}
225-
>
226-
<SelectTrigger className="w-[180px]">
227-
<SelectValue placeholder="Event" />
228-
</SelectTrigger>
229-
<SelectContent>
230-
{param.options?.split("\n").map((option) => {
231-
return <SelectItem value={option}>{ option }</SelectItem>
232-
})}
233-
234-
235-
</SelectContent>
236-
</Select>
237-
}
238-
{param.fieldtype === "Data" &&
239-
<Input
240-
value={triggerFormState[param.fieldname]}
241-
onChange={() => { /* TODO: set this in backend */ }}
242-
type="text"
243-
name={param.fieldname}
166+
167+
{/* Action Buttons */}
168+
<div className="flex gap-2 mb-4">
169+
<Button color="white" onClick={handleSaveWorkflow}>
170+
Save
171+
</Button>
172+
<Button color="rose" onClick={handleDeleteWorkflow}>
173+
Delete
174+
</Button>
175+
</div>
176+
177+
{/* Action Settings Section */}
178+
{editorStore.selectedNode && (
179+
<section className="border-t pt-4">
180+
<h3 className="text-md font-semibold text-gray-900 mb-2">
181+
Action Settings
182+
</h3>
183+
<p className="text-sm text-gray-600 mb-3">
184+
{editorStore.selectedNode.data.type}
185+
</p>
186+
187+
{actionDoc?.params && actionDoc.params.length > 0 && (
188+
<ParamForm
189+
params={actionDoc.params}
190+
values={actionFormState}
191+
onChange={handleActionFieldChange}
244192
/>
245-
}
246-
</div>
247-
);
248-
})}
193+
)}
194+
</section>
195+
)}
249196
</ScrollArea>
250197
);
251198
}

0 commit comments

Comments
 (0)