@authhero/react-admin 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintrc.js +21 -0
- package/.vercelignore +4 -0
- package/CHANGELOG.md +56 -0
- package/LICENSE +21 -0
- package/README.md +50 -0
- package/index.html +125 -0
- package/package.json +61 -0
- package/prettier.config.js +1 -0
- package/public/favicon.ico +0 -0
- package/public/manifest.json +15 -0
- package/src/App.spec.tsx +42 -0
- package/src/App.tsx +232 -0
- package/src/AuthCallback.tsx +138 -0
- package/src/Layout.tsx +12 -0
- package/src/TenantsApp.tsx +115 -0
- package/src/auth0DataProvider.ts +1242 -0
- package/src/authProvider.ts +521 -0
- package/src/components/CertificateErrorDialog.tsx +116 -0
- package/src/components/DomainSelector.tsx +401 -0
- package/src/components/TenantAppBar.tsx +83 -0
- package/src/components/TenantLayout.tsx +25 -0
- package/src/components/TenantsAppBar.tsx +21 -0
- package/src/components/TenantsLayout.tsx +28 -0
- package/src/components/activity/ActivityDashboard.tsx +381 -0
- package/src/components/activity/index.ts +1 -0
- package/src/components/branding/BrandingList.tsx +0 -0
- package/src/components/branding/BrandingShow.tsx +0 -0
- package/src/components/branding/ThemesTab.tsx +286 -0
- package/src/components/branding/edit.tsx +149 -0
- package/src/components/branding/hooks/useThemesData.ts +123 -0
- package/src/components/branding/index.ts +2 -0
- package/src/components/branding/list.tsx +12 -0
- package/src/components/clients/create.tsx +12 -0
- package/src/components/clients/edit.tsx +1285 -0
- package/src/components/clients/index.ts +3 -0
- package/src/components/clients/list.tsx +37 -0
- package/src/components/common/DateAgo.tsx +6 -0
- package/src/components/common/JsonOutput.tsx +26 -0
- package/src/components/common/index.ts +1 -0
- package/src/components/connections/create.tsx +35 -0
- package/src/components/connections/edit.tsx +212 -0
- package/src/components/connections/index.ts +3 -0
- package/src/components/connections/list.tsx +15 -0
- package/src/components/custom-domains/create.tsx +26 -0
- package/src/components/custom-domains/edit.tsx +101 -0
- package/src/components/custom-domains/index.ts +3 -0
- package/src/components/custom-domains/list.tsx +16 -0
- package/src/components/flows/create.tsx +30 -0
- package/src/components/flows/edit.tsx +238 -0
- package/src/components/flows/index.ts +3 -0
- package/src/components/flows/list.tsx +15 -0
- package/src/components/forms/FlowEditor.tsx +1363 -0
- package/src/components/forms/NodeEditor.tsx +1119 -0
- package/src/components/forms/RichTextEditor.tsx +145 -0
- package/src/components/forms/create.tsx +30 -0
- package/src/components/forms/edit.tsx +256 -0
- package/src/components/forms/index.ts +3 -0
- package/src/components/forms/list.tsx +16 -0
- package/src/components/hooks/create.tsx +96 -0
- package/src/components/hooks/edit.tsx +114 -0
- package/src/components/hooks/index.ts +3 -0
- package/src/components/hooks/list.tsx +17 -0
- package/src/components/listActions/PostListActions.tsx +10 -0
- package/src/components/logs/LogIcon.tsx +32 -0
- package/src/components/logs/LogShow.tsx +82 -0
- package/src/components/logs/LogType.tsx +38 -0
- package/src/components/logs/index.ts +4 -0
- package/src/components/logs/list.tsx +41 -0
- package/src/components/organizations/create.tsx +13 -0
- package/src/components/organizations/edit.tsx +682 -0
- package/src/components/organizations/index.ts +3 -0
- package/src/components/organizations/list.tsx +21 -0
- package/src/components/resource-servers/create.tsx +87 -0
- package/src/components/resource-servers/edit.tsx +121 -0
- package/src/components/resource-servers/index.ts +3 -0
- package/src/components/resource-servers/list.tsx +47 -0
- package/src/components/roles/create.tsx +12 -0
- package/src/components/roles/edit.tsx +426 -0
- package/src/components/roles/index.ts +3 -0
- package/src/components/roles/list.tsx +24 -0
- package/src/components/sessions/edit.tsx +101 -0
- package/src/components/sessions/index.ts +3 -0
- package/src/components/sessions/list.tsx +20 -0
- package/src/components/sessions/show.tsx +113 -0
- package/src/components/settings/edit.tsx +236 -0
- package/src/components/settings/index.ts +2 -0
- package/src/components/settings/list.tsx +14 -0
- package/src/components/tenants/create.tsx +20 -0
- package/src/components/tenants/edit.tsx +54 -0
- package/src/components/tenants/index.ts +2 -0
- package/src/components/tenants/list.tsx +67 -0
- package/src/components/themes/edit.tsx +200 -0
- package/src/components/themes/index.ts +2 -0
- package/src/components/themes/list.tsx +12 -0
- package/src/components/users/create.tsx +144 -0
- package/src/components/users/edit.tsx +1711 -0
- package/src/components/users/index.ts +3 -0
- package/src/components/users/list.tsx +35 -0
- package/src/data.json +121 -0
- package/src/dataProvider.ts +97 -0
- package/src/index.tsx +106 -0
- package/src/lib/logs.ts +21 -0
- package/src/types/reactflow.d.ts +86 -0
- package/src/utils/domainUtils.ts +169 -0
- package/src/utils/tokenUtils.ts +75 -0
- package/src/vite-env.d.ts +1 -0
- package/tsconfig.json +37 -0
- package/tsconfig.node.json +10 -0
- package/vercel.json +17 -0
- package/vite.config.ts +30 -0
|
@@ -0,0 +1,1119 @@
|
|
|
1
|
+
import React, { useState, useEffect, useCallback } from "react";
|
|
2
|
+
import {
|
|
3
|
+
Box,
|
|
4
|
+
Typography,
|
|
5
|
+
Drawer,
|
|
6
|
+
TextField,
|
|
7
|
+
Button,
|
|
8
|
+
IconButton,
|
|
9
|
+
FormControl,
|
|
10
|
+
InputLabel,
|
|
11
|
+
Select,
|
|
12
|
+
MenuItem,
|
|
13
|
+
Divider,
|
|
14
|
+
FormHelperText,
|
|
15
|
+
Switch,
|
|
16
|
+
FormControlLabel,
|
|
17
|
+
Tab,
|
|
18
|
+
Tabs,
|
|
19
|
+
Paper,
|
|
20
|
+
Dialog,
|
|
21
|
+
DialogTitle,
|
|
22
|
+
DialogContent,
|
|
23
|
+
DialogActions,
|
|
24
|
+
Menu,
|
|
25
|
+
ListItemIcon,
|
|
26
|
+
ListItemText,
|
|
27
|
+
Autocomplete,
|
|
28
|
+
} from "@mui/material";
|
|
29
|
+
import CloseIcon from "@mui/icons-material/Close";
|
|
30
|
+
import EditIcon from "@mui/icons-material/Edit";
|
|
31
|
+
import CodeIcon from "@mui/icons-material/Code";
|
|
32
|
+
import AddCircleOutlineIcon from "@mui/icons-material/AddCircleOutline";
|
|
33
|
+
import TextFieldsIcon from "@mui/icons-material/TextFields";
|
|
34
|
+
import GavelIcon from "@mui/icons-material/Gavel";
|
|
35
|
+
import SmartButtonIcon from "@mui/icons-material/SmartButton";
|
|
36
|
+
import ShortTextIcon from "@mui/icons-material/ShortText";
|
|
37
|
+
import EmailIcon from "@mui/icons-material/Email";
|
|
38
|
+
import NumbersIcon from "@mui/icons-material/Numbers";
|
|
39
|
+
import PhoneIcon from "@mui/icons-material/Phone";
|
|
40
|
+
import { Node } from "@xyflow/react";
|
|
41
|
+
|
|
42
|
+
import DeleteIcon from "@mui/icons-material/Delete";
|
|
43
|
+
import ArrowUpwardIcon from "@mui/icons-material/ArrowUpward";
|
|
44
|
+
import ArrowDownwardIcon from "@mui/icons-material/ArrowDownward";
|
|
45
|
+
|
|
46
|
+
import RichTextEditor from "./RichTextEditor";
|
|
47
|
+
|
|
48
|
+
// Available fields for router condition rules
|
|
49
|
+
// These are the user context fields that can be used in router conditions
|
|
50
|
+
const ROUTER_FIELD_OPTIONS = [
|
|
51
|
+
{
|
|
52
|
+
value: "{{context.user.email}}",
|
|
53
|
+
label: "User Email",
|
|
54
|
+
description: "The user's email address",
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
value: "{{context.user.email_verified}}",
|
|
58
|
+
label: "Email Verified",
|
|
59
|
+
description: "Whether the user's email is verified (true/false)",
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
value: "{{context.user.name}}",
|
|
63
|
+
label: "Name",
|
|
64
|
+
description: "The user's full name",
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
value: "{{context.user.given_name}}",
|
|
68
|
+
label: "Given Name",
|
|
69
|
+
description: "The user's first/given name",
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
value: "{{context.user.family_name}}",
|
|
73
|
+
label: "Family Name",
|
|
74
|
+
description: "The user's last/family name",
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
value: "{{context.user.nickname}}",
|
|
78
|
+
label: "Nickname",
|
|
79
|
+
description: "The user's nickname",
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
value: "{{context.user.picture}}",
|
|
83
|
+
label: "Picture URL",
|
|
84
|
+
description: "URL to the user's profile picture",
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
value: "{{context.user.locale}}",
|
|
88
|
+
label: "Locale",
|
|
89
|
+
description: "The user's locale/language preference",
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
value: "{{context.user.username}}",
|
|
93
|
+
label: "Username",
|
|
94
|
+
description: "The user's username",
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
value: "{{context.user.phone_number}}",
|
|
98
|
+
label: "Phone Number",
|
|
99
|
+
description: "The user's phone number",
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
value: "{{context.user.connection}}",
|
|
103
|
+
label: "Connection",
|
|
104
|
+
description: "The authentication connection used",
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
value: "{{context.user.provider}}",
|
|
108
|
+
label: "Provider",
|
|
109
|
+
description: "The authentication provider (e.g., auth0, google)",
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
value: "{{context.user.is_social}}",
|
|
113
|
+
label: "Is Social",
|
|
114
|
+
description: "Whether the user logged in via social provider (true/false)",
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
value: "{{context.user.user_id}}",
|
|
118
|
+
label: "User ID",
|
|
119
|
+
description: "The unique user identifier",
|
|
120
|
+
},
|
|
121
|
+
];
|
|
122
|
+
|
|
123
|
+
import type {
|
|
124
|
+
ComponentConfig,
|
|
125
|
+
FlowNodeData,
|
|
126
|
+
FlowChoice,
|
|
127
|
+
RouterRule,
|
|
128
|
+
StartNode,
|
|
129
|
+
EndingNode,
|
|
130
|
+
} from "./FlowEditor";
|
|
131
|
+
|
|
132
|
+
interface NodeEditorProps {
|
|
133
|
+
open: boolean;
|
|
134
|
+
selectedNode: Node | null;
|
|
135
|
+
nodes: FlowNodeData[];
|
|
136
|
+
start?: StartNode;
|
|
137
|
+
ending?: EndingNode;
|
|
138
|
+
flows?: FlowChoice[];
|
|
139
|
+
onClose: () => void;
|
|
140
|
+
onNodeUpdate: (
|
|
141
|
+
nodeId: string,
|
|
142
|
+
updates: Partial<FlowNodeData> | Partial<StartNode> | Partial<EndingNode>,
|
|
143
|
+
) => void;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
interface TabPanelProps {
|
|
147
|
+
children?: React.ReactNode;
|
|
148
|
+
index: number;
|
|
149
|
+
value: number;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const TabPanel = (props: TabPanelProps) => {
|
|
153
|
+
const { children, value, index, ...other } = props;
|
|
154
|
+
|
|
155
|
+
return (
|
|
156
|
+
<div
|
|
157
|
+
role="tabpanel"
|
|
158
|
+
hidden={value !== index}
|
|
159
|
+
id={`node-editor-tabpanel-${index}`}
|
|
160
|
+
aria-labelledby={`node-editor-tab-${index}`}
|
|
161
|
+
{...other}
|
|
162
|
+
style={{ padding: "16px 0" }}
|
|
163
|
+
>
|
|
164
|
+
{value === index && children}
|
|
165
|
+
</div>
|
|
166
|
+
);
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
export const NodeEditor: React.FC<NodeEditorProps> = ({
|
|
170
|
+
open,
|
|
171
|
+
selectedNode,
|
|
172
|
+
nodes,
|
|
173
|
+
start,
|
|
174
|
+
ending,
|
|
175
|
+
flows,
|
|
176
|
+
onClose,
|
|
177
|
+
onNodeUpdate,
|
|
178
|
+
}) => {
|
|
179
|
+
const [tabValue, setTabValue] = useState(0);
|
|
180
|
+
const [formData, setFormData] = useState<any>({});
|
|
181
|
+
const [editingComponent, setEditingComponent] =
|
|
182
|
+
useState<ComponentConfig | null>(null);
|
|
183
|
+
const [componentDialogOpen, setComponentDialogOpen] = useState(false);
|
|
184
|
+
const [addComponentAnchor, setAddComponentAnchor] =
|
|
185
|
+
useState<null | HTMLElement>(null);
|
|
186
|
+
|
|
187
|
+
// Initialize form data when selected node changes
|
|
188
|
+
useEffect(() => {
|
|
189
|
+
if (!selectedNode) {
|
|
190
|
+
setFormData({});
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (selectedNode.id === "start" && start) {
|
|
195
|
+
setFormData({
|
|
196
|
+
next_node: start.next_node || "",
|
|
197
|
+
});
|
|
198
|
+
} else if (selectedNode.id === "end" && ending) {
|
|
199
|
+
setFormData({
|
|
200
|
+
resume_flow: ending.resume_flow || false,
|
|
201
|
+
});
|
|
202
|
+
} else {
|
|
203
|
+
// Find the node data in the nodes array
|
|
204
|
+
const nodeData = nodes.find((node) => node.id === selectedNode.id);
|
|
205
|
+
if (nodeData) {
|
|
206
|
+
setFormData({
|
|
207
|
+
id: nodeData.id,
|
|
208
|
+
alias: nodeData.alias || "",
|
|
209
|
+
type: nodeData.type,
|
|
210
|
+
next_node: nodeData.config?.next_node || "",
|
|
211
|
+
components: nodeData.config?.components || [],
|
|
212
|
+
flow_id: nodeData.config?.flow_id || "",
|
|
213
|
+
rules: nodeData.config?.rules || [],
|
|
214
|
+
fallback: nodeData.config?.fallback || "",
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}, [selectedNode, nodes, start, ending]);
|
|
219
|
+
|
|
220
|
+
const handleInputChange = useCallback(
|
|
221
|
+
(
|
|
222
|
+
e:
|
|
223
|
+
| React.ChangeEvent<HTMLInputElement>
|
|
224
|
+
| {
|
|
225
|
+
target: { name?: string; value: unknown };
|
|
226
|
+
},
|
|
227
|
+
) => {
|
|
228
|
+
const { name, value } = e.target;
|
|
229
|
+
if (name) {
|
|
230
|
+
setFormData((prev: any) => ({
|
|
231
|
+
...prev,
|
|
232
|
+
[name]: value,
|
|
233
|
+
}));
|
|
234
|
+
}
|
|
235
|
+
},
|
|
236
|
+
[],
|
|
237
|
+
);
|
|
238
|
+
|
|
239
|
+
const handleSwitchChange = useCallback(
|
|
240
|
+
(e: React.ChangeEvent<HTMLInputElement>) => {
|
|
241
|
+
const { name, checked } = e.target;
|
|
242
|
+
if (name) {
|
|
243
|
+
setFormData((prev: any) => ({
|
|
244
|
+
...prev,
|
|
245
|
+
[name]: checked,
|
|
246
|
+
}));
|
|
247
|
+
}
|
|
248
|
+
},
|
|
249
|
+
[],
|
|
250
|
+
);
|
|
251
|
+
|
|
252
|
+
// Component editing handlers
|
|
253
|
+
const handleEditComponent = useCallback((component: ComponentConfig) => {
|
|
254
|
+
setEditingComponent({ ...component });
|
|
255
|
+
setComponentDialogOpen(true);
|
|
256
|
+
}, []);
|
|
257
|
+
|
|
258
|
+
const handleCloseComponentDialog = useCallback(() => {
|
|
259
|
+
setComponentDialogOpen(false);
|
|
260
|
+
setEditingComponent(null);
|
|
261
|
+
}, []);
|
|
262
|
+
|
|
263
|
+
const handleSaveComponent = useCallback(() => {
|
|
264
|
+
if (!editingComponent) return;
|
|
265
|
+
|
|
266
|
+
setFormData((prev: any) => ({
|
|
267
|
+
...prev,
|
|
268
|
+
components: prev.components.map((c: ComponentConfig) =>
|
|
269
|
+
c.id === editingComponent.id ? editingComponent : c,
|
|
270
|
+
),
|
|
271
|
+
}));
|
|
272
|
+
handleCloseComponentDialog();
|
|
273
|
+
}, [editingComponent, handleCloseComponentDialog]);
|
|
274
|
+
|
|
275
|
+
const handleComponentFieldChange = useCallback(
|
|
276
|
+
(field: string, value: string) => {
|
|
277
|
+
setEditingComponent((prev) => {
|
|
278
|
+
if (!prev) return null;
|
|
279
|
+
return {
|
|
280
|
+
...prev,
|
|
281
|
+
config: {
|
|
282
|
+
...prev.config,
|
|
283
|
+
[field]: value,
|
|
284
|
+
},
|
|
285
|
+
};
|
|
286
|
+
});
|
|
287
|
+
},
|
|
288
|
+
[],
|
|
289
|
+
);
|
|
290
|
+
|
|
291
|
+
const handleAddComponent = useCallback((type: ComponentConfig["type"]) => {
|
|
292
|
+
const getDefaultConfig = () => {
|
|
293
|
+
switch (type) {
|
|
294
|
+
case "RICH_TEXT":
|
|
295
|
+
return { content: "" };
|
|
296
|
+
case "LEGAL":
|
|
297
|
+
return { text: "" };
|
|
298
|
+
case "NEXT_BUTTON":
|
|
299
|
+
return { text: "Continue" };
|
|
300
|
+
case "TEXT":
|
|
301
|
+
return { label: "Text Field", placeholder: "" };
|
|
302
|
+
case "EMAIL":
|
|
303
|
+
return { label: "Email", placeholder: "Enter your email" };
|
|
304
|
+
case "NUMBER":
|
|
305
|
+
return { label: "Number", placeholder: "" };
|
|
306
|
+
case "PHONE":
|
|
307
|
+
return { label: "Phone", placeholder: "Enter your phone number" };
|
|
308
|
+
default:
|
|
309
|
+
return {};
|
|
310
|
+
}
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
const newComponent: ComponentConfig = {
|
|
314
|
+
id: `component_${Date.now()}`,
|
|
315
|
+
type,
|
|
316
|
+
config: getDefaultConfig(),
|
|
317
|
+
};
|
|
318
|
+
setFormData((prev: any) => ({
|
|
319
|
+
...prev,
|
|
320
|
+
components: [...(prev.components || []), newComponent],
|
|
321
|
+
}));
|
|
322
|
+
// Open the editor for the new component
|
|
323
|
+
setEditingComponent(newComponent);
|
|
324
|
+
setComponentDialogOpen(true);
|
|
325
|
+
}, []);
|
|
326
|
+
|
|
327
|
+
const handleMoveComponent = useCallback(
|
|
328
|
+
(index: number, direction: "up" | "down") => {
|
|
329
|
+
setFormData((prev: any) => {
|
|
330
|
+
const components = [...(prev.components || [])];
|
|
331
|
+
const newIndex = direction === "up" ? index - 1 : index + 1;
|
|
332
|
+
if (newIndex < 0 || newIndex >= components.length) return prev;
|
|
333
|
+
|
|
334
|
+
// Swap components
|
|
335
|
+
[components[index], components[newIndex]] = [
|
|
336
|
+
components[newIndex],
|
|
337
|
+
components[index],
|
|
338
|
+
];
|
|
339
|
+
return { ...prev, components };
|
|
340
|
+
});
|
|
341
|
+
},
|
|
342
|
+
[],
|
|
343
|
+
);
|
|
344
|
+
|
|
345
|
+
const handleDeleteComponent = useCallback((componentId: string) => {
|
|
346
|
+
setFormData((prev: any) => ({
|
|
347
|
+
...prev,
|
|
348
|
+
components: (prev.components || []).filter(
|
|
349
|
+
(c: ComponentConfig) => c.id !== componentId,
|
|
350
|
+
),
|
|
351
|
+
}));
|
|
352
|
+
}, []);
|
|
353
|
+
|
|
354
|
+
const handleSave = useCallback(() => {
|
|
355
|
+
if (!selectedNode) return;
|
|
356
|
+
|
|
357
|
+
if (selectedNode.id === "start") {
|
|
358
|
+
onNodeUpdate("start", { next_node: formData.next_node || undefined });
|
|
359
|
+
} else if (selectedNode.id === "end") {
|
|
360
|
+
onNodeUpdate("end", { resume_flow: formData.resume_flow || false });
|
|
361
|
+
} else {
|
|
362
|
+
const updates: Partial<FlowNodeData> = {
|
|
363
|
+
alias: formData.alias,
|
|
364
|
+
config: {
|
|
365
|
+
next_node: formData.next_node || undefined,
|
|
366
|
+
components: formData.components || [],
|
|
367
|
+
},
|
|
368
|
+
};
|
|
369
|
+
|
|
370
|
+
if (selectedNode.type === "flow") {
|
|
371
|
+
updates.config!.flow_id = formData.flow_id;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
if (selectedNode.type === "router") {
|
|
375
|
+
updates.config!.rules = formData.rules;
|
|
376
|
+
updates.config!.fallback = formData.fallback || undefined;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
onNodeUpdate(selectedNode.id, updates);
|
|
380
|
+
}
|
|
381
|
+
onClose();
|
|
382
|
+
}, [selectedNode, formData, onNodeUpdate, onClose]);
|
|
383
|
+
|
|
384
|
+
// Render the proper editor based on node type
|
|
385
|
+
const renderEditor = () => {
|
|
386
|
+
if (!selectedNode) return null;
|
|
387
|
+
|
|
388
|
+
switch (selectedNode.type) {
|
|
389
|
+
case "start":
|
|
390
|
+
return renderStartNodeEditor();
|
|
391
|
+
case "end":
|
|
392
|
+
return renderEndNodeEditor();
|
|
393
|
+
case "step":
|
|
394
|
+
return renderStepNodeEditor();
|
|
395
|
+
case "flow":
|
|
396
|
+
return renderFlowNodeEditor();
|
|
397
|
+
case "router":
|
|
398
|
+
return renderRouterNodeEditor();
|
|
399
|
+
default:
|
|
400
|
+
return (
|
|
401
|
+
<Typography color="text.secondary">
|
|
402
|
+
Unknown node type: {selectedNode.type}
|
|
403
|
+
</Typography>
|
|
404
|
+
);
|
|
405
|
+
}
|
|
406
|
+
};
|
|
407
|
+
|
|
408
|
+
const renderStartNodeEditor = () => (
|
|
409
|
+
<Box>
|
|
410
|
+
<Typography variant="h6" sx={{ mb: 2 }}>
|
|
411
|
+
Start Node
|
|
412
|
+
</Typography>
|
|
413
|
+
<FormControl fullWidth sx={{ mb: 2 }}>
|
|
414
|
+
<InputLabel id="next-node-label">Next Node</InputLabel>
|
|
415
|
+
<Select
|
|
416
|
+
labelId="next-node-label"
|
|
417
|
+
id="next-node"
|
|
418
|
+
name="next_node"
|
|
419
|
+
value={formData.next_node || ""}
|
|
420
|
+
label="Next Node"
|
|
421
|
+
onChange={handleInputChange}
|
|
422
|
+
>
|
|
423
|
+
<MenuItem value="">
|
|
424
|
+
<em>None</em>
|
|
425
|
+
</MenuItem>
|
|
426
|
+
{nodes.map((node) => (
|
|
427
|
+
<MenuItem key={node.id} value={node.id}>
|
|
428
|
+
{node.alias || node.id}
|
|
429
|
+
</MenuItem>
|
|
430
|
+
))}
|
|
431
|
+
</Select>
|
|
432
|
+
<FormHelperText>Select the first node in the flow</FormHelperText>
|
|
433
|
+
</FormControl>
|
|
434
|
+
</Box>
|
|
435
|
+
);
|
|
436
|
+
|
|
437
|
+
const renderEndNodeEditor = () => (
|
|
438
|
+
<Box>
|
|
439
|
+
<Typography variant="h6" sx={{ mb: 2 }}>
|
|
440
|
+
End Node
|
|
441
|
+
</Typography>
|
|
442
|
+
<FormControlLabel
|
|
443
|
+
control={
|
|
444
|
+
<Switch
|
|
445
|
+
name="resume_flow"
|
|
446
|
+
checked={!!formData.resume_flow}
|
|
447
|
+
onChange={handleSwitchChange}
|
|
448
|
+
/>
|
|
449
|
+
}
|
|
450
|
+
label="Resume authentication flow"
|
|
451
|
+
/>
|
|
452
|
+
<FormHelperText>
|
|
453
|
+
When enabled, the flow will resume the authentication process after
|
|
454
|
+
completion
|
|
455
|
+
</FormHelperText>
|
|
456
|
+
</Box>
|
|
457
|
+
);
|
|
458
|
+
|
|
459
|
+
const renderStepNodeEditor = () => (
|
|
460
|
+
<Box>
|
|
461
|
+
<TextField
|
|
462
|
+
fullWidth
|
|
463
|
+
label="Node Alias"
|
|
464
|
+
name="alias"
|
|
465
|
+
value={formData.alias || ""}
|
|
466
|
+
onChange={handleInputChange}
|
|
467
|
+
margin="normal"
|
|
468
|
+
helperText="A friendly name for this step"
|
|
469
|
+
/>
|
|
470
|
+
|
|
471
|
+
<Divider sx={{ my: 3 }} />
|
|
472
|
+
|
|
473
|
+
<Typography variant="h6" gutterBottom>
|
|
474
|
+
Components
|
|
475
|
+
</Typography>
|
|
476
|
+
|
|
477
|
+
{formData.components && formData.components.length > 0 ? (
|
|
478
|
+
<Box sx={{ mt: 2 }}>
|
|
479
|
+
{formData.components.map(
|
|
480
|
+
(component: ComponentConfig, index: number) => (
|
|
481
|
+
<Paper key={component.id} sx={{ p: 2, mb: 1 }}>
|
|
482
|
+
<Box
|
|
483
|
+
sx={{
|
|
484
|
+
display: "flex",
|
|
485
|
+
justifyContent: "space-between",
|
|
486
|
+
alignItems: "center",
|
|
487
|
+
mb: 1,
|
|
488
|
+
}}
|
|
489
|
+
>
|
|
490
|
+
<Typography variant="subtitle2">{component.type}</Typography>
|
|
491
|
+
<Box sx={{ display: "flex", gap: 0.5 }}>
|
|
492
|
+
<IconButton
|
|
493
|
+
size="small"
|
|
494
|
+
aria-label="move up"
|
|
495
|
+
onClick={() => handleMoveComponent(index, "up")}
|
|
496
|
+
disabled={index === 0}
|
|
497
|
+
>
|
|
498
|
+
<ArrowUpwardIcon fontSize="small" />
|
|
499
|
+
</IconButton>
|
|
500
|
+
<IconButton
|
|
501
|
+
size="small"
|
|
502
|
+
aria-label="move down"
|
|
503
|
+
onClick={() => handleMoveComponent(index, "down")}
|
|
504
|
+
disabled={index === formData.components.length - 1}
|
|
505
|
+
>
|
|
506
|
+
<ArrowDownwardIcon fontSize="small" />
|
|
507
|
+
</IconButton>
|
|
508
|
+
<IconButton
|
|
509
|
+
size="small"
|
|
510
|
+
aria-label="edit component"
|
|
511
|
+
onClick={() => handleEditComponent(component)}
|
|
512
|
+
>
|
|
513
|
+
<EditIcon fontSize="small" />
|
|
514
|
+
</IconButton>
|
|
515
|
+
<IconButton
|
|
516
|
+
size="small"
|
|
517
|
+
aria-label="delete component"
|
|
518
|
+
onClick={() => handleDeleteComponent(component.id)}
|
|
519
|
+
color="error"
|
|
520
|
+
>
|
|
521
|
+
<DeleteIcon fontSize="small" />
|
|
522
|
+
</IconButton>
|
|
523
|
+
</Box>
|
|
524
|
+
</Box>
|
|
525
|
+
<Typography variant="caption" color="text.secondary">
|
|
526
|
+
{component.type === "RICH_TEXT" && component.config?.content
|
|
527
|
+
? `Content: ${component.config.content.substring(0, 30)}...`
|
|
528
|
+
: component.type === "LEGAL" && component.config?.text
|
|
529
|
+
? `Text: ${component.config.text}`
|
|
530
|
+
: component.type === "NEXT_BUTTON" &&
|
|
531
|
+
component.config?.text
|
|
532
|
+
? `Button text: ${component.config.text}`
|
|
533
|
+
: (component.type === "TEXT" ||
|
|
534
|
+
component.type === "EMAIL" ||
|
|
535
|
+
component.type === "NUMBER" ||
|
|
536
|
+
component.type === "PHONE") &&
|
|
537
|
+
component.config?.label
|
|
538
|
+
? `Label: ${component.config.label}`
|
|
539
|
+
: component.id}
|
|
540
|
+
</Typography>
|
|
541
|
+
</Paper>
|
|
542
|
+
),
|
|
543
|
+
)}
|
|
544
|
+
</Box>
|
|
545
|
+
) : (
|
|
546
|
+
<Typography variant="body2" color="text.secondary">
|
|
547
|
+
No components added to this step.
|
|
548
|
+
</Typography>
|
|
549
|
+
)}
|
|
550
|
+
|
|
551
|
+
<Button
|
|
552
|
+
variant="outlined"
|
|
553
|
+
startIcon={<AddCircleOutlineIcon />}
|
|
554
|
+
fullWidth
|
|
555
|
+
sx={{ mt: 2 }}
|
|
556
|
+
onClick={(e) => setAddComponentAnchor(e.currentTarget)}
|
|
557
|
+
>
|
|
558
|
+
Add Component
|
|
559
|
+
</Button>
|
|
560
|
+
<Menu
|
|
561
|
+
anchorEl={addComponentAnchor}
|
|
562
|
+
open={Boolean(addComponentAnchor)}
|
|
563
|
+
onClose={() => setAddComponentAnchor(null)}
|
|
564
|
+
>
|
|
565
|
+
<Typography
|
|
566
|
+
variant="caption"
|
|
567
|
+
sx={{ px: 2, py: 0.5, color: "text.secondary", display: "block" }}
|
|
568
|
+
>
|
|
569
|
+
Content
|
|
570
|
+
</Typography>
|
|
571
|
+
<MenuItem
|
|
572
|
+
onClick={() => {
|
|
573
|
+
handleAddComponent("RICH_TEXT");
|
|
574
|
+
setAddComponentAnchor(null);
|
|
575
|
+
}}
|
|
576
|
+
>
|
|
577
|
+
<ListItemIcon>
|
|
578
|
+
<TextFieldsIcon fontSize="small" />
|
|
579
|
+
</ListItemIcon>
|
|
580
|
+
<ListItemText>Rich Text</ListItemText>
|
|
581
|
+
</MenuItem>
|
|
582
|
+
<Divider />
|
|
583
|
+
<Typography
|
|
584
|
+
variant="caption"
|
|
585
|
+
sx={{ px: 2, py: 0.5, color: "text.secondary", display: "block" }}
|
|
586
|
+
>
|
|
587
|
+
Fields
|
|
588
|
+
</Typography>
|
|
589
|
+
<MenuItem
|
|
590
|
+
onClick={() => {
|
|
591
|
+
handleAddComponent("TEXT");
|
|
592
|
+
setAddComponentAnchor(null);
|
|
593
|
+
}}
|
|
594
|
+
>
|
|
595
|
+
<ListItemIcon>
|
|
596
|
+
<ShortTextIcon fontSize="small" />
|
|
597
|
+
</ListItemIcon>
|
|
598
|
+
<ListItemText>Text Field</ListItemText>
|
|
599
|
+
</MenuItem>
|
|
600
|
+
<MenuItem
|
|
601
|
+
onClick={() => {
|
|
602
|
+
handleAddComponent("EMAIL");
|
|
603
|
+
setAddComponentAnchor(null);
|
|
604
|
+
}}
|
|
605
|
+
>
|
|
606
|
+
<ListItemIcon>
|
|
607
|
+
<EmailIcon fontSize="small" />
|
|
608
|
+
</ListItemIcon>
|
|
609
|
+
<ListItemText>Email Field</ListItemText>
|
|
610
|
+
</MenuItem>
|
|
611
|
+
<MenuItem
|
|
612
|
+
onClick={() => {
|
|
613
|
+
handleAddComponent("NUMBER");
|
|
614
|
+
setAddComponentAnchor(null);
|
|
615
|
+
}}
|
|
616
|
+
>
|
|
617
|
+
<ListItemIcon>
|
|
618
|
+
<NumbersIcon fontSize="small" />
|
|
619
|
+
</ListItemIcon>
|
|
620
|
+
<ListItemText>Number Field</ListItemText>
|
|
621
|
+
</MenuItem>
|
|
622
|
+
<MenuItem
|
|
623
|
+
onClick={() => {
|
|
624
|
+
handleAddComponent("PHONE");
|
|
625
|
+
setAddComponentAnchor(null);
|
|
626
|
+
}}
|
|
627
|
+
>
|
|
628
|
+
<ListItemIcon>
|
|
629
|
+
<PhoneIcon fontSize="small" />
|
|
630
|
+
</ListItemIcon>
|
|
631
|
+
<ListItemText>Phone Field</ListItemText>
|
|
632
|
+
</MenuItem>
|
|
633
|
+
<MenuItem
|
|
634
|
+
onClick={() => {
|
|
635
|
+
handleAddComponent("LEGAL");
|
|
636
|
+
setAddComponentAnchor(null);
|
|
637
|
+
}}
|
|
638
|
+
>
|
|
639
|
+
<ListItemIcon>
|
|
640
|
+
<GavelIcon fontSize="small" />
|
|
641
|
+
</ListItemIcon>
|
|
642
|
+
<ListItemText>Legal Checkbox</ListItemText>
|
|
643
|
+
</MenuItem>
|
|
644
|
+
<Divider />
|
|
645
|
+
<Typography
|
|
646
|
+
variant="caption"
|
|
647
|
+
sx={{ px: 2, py: 0.5, color: "text.secondary", display: "block" }}
|
|
648
|
+
>
|
|
649
|
+
Actions
|
|
650
|
+
</Typography>
|
|
651
|
+
<MenuItem
|
|
652
|
+
onClick={() => {
|
|
653
|
+
handleAddComponent("NEXT_BUTTON");
|
|
654
|
+
setAddComponentAnchor(null);
|
|
655
|
+
}}
|
|
656
|
+
>
|
|
657
|
+
<ListItemIcon>
|
|
658
|
+
<SmartButtonIcon fontSize="small" />
|
|
659
|
+
</ListItemIcon>
|
|
660
|
+
<ListItemText>Next Button</ListItemText>
|
|
661
|
+
</MenuItem>
|
|
662
|
+
</Menu>
|
|
663
|
+
</Box>
|
|
664
|
+
);
|
|
665
|
+
|
|
666
|
+
const renderFlowNodeEditor = () => (
|
|
667
|
+
<Box>
|
|
668
|
+
<FormControl fullWidth margin="normal">
|
|
669
|
+
<InputLabel id="flow-id-label">Flow</InputLabel>
|
|
670
|
+
<Select
|
|
671
|
+
labelId="flow-id-label"
|
|
672
|
+
name="flow_id"
|
|
673
|
+
value={formData.flow_id || ""}
|
|
674
|
+
onChange={handleInputChange}
|
|
675
|
+
label="Flow"
|
|
676
|
+
>
|
|
677
|
+
<MenuItem value="">
|
|
678
|
+
<em>None</em>
|
|
679
|
+
</MenuItem>
|
|
680
|
+
{flows?.map((flow) => (
|
|
681
|
+
<MenuItem key={flow.id} value={flow.id}>
|
|
682
|
+
{flow.name} ({flow.id})
|
|
683
|
+
</MenuItem>
|
|
684
|
+
))}
|
|
685
|
+
</Select>
|
|
686
|
+
<FormHelperText>Select the flow to execute</FormHelperText>
|
|
687
|
+
</FormControl>
|
|
688
|
+
</Box>
|
|
689
|
+
);
|
|
690
|
+
|
|
691
|
+
const handleRuleChange = useCallback(
|
|
692
|
+
(ruleId: string, field: keyof RouterRule, value: any) => {
|
|
693
|
+
setFormData((prev: any) => ({
|
|
694
|
+
...prev,
|
|
695
|
+
rules: prev.rules.map((rule: RouterRule) =>
|
|
696
|
+
rule.id === ruleId ? { ...rule, [field]: value } : rule,
|
|
697
|
+
),
|
|
698
|
+
}));
|
|
699
|
+
},
|
|
700
|
+
[],
|
|
701
|
+
);
|
|
702
|
+
|
|
703
|
+
const handleAddRule = useCallback(() => {
|
|
704
|
+
const newRule: RouterRule = {
|
|
705
|
+
id: `rule_${Date.now()}`,
|
|
706
|
+
alias: "",
|
|
707
|
+
condition: {},
|
|
708
|
+
next_node: "",
|
|
709
|
+
};
|
|
710
|
+
setFormData((prev: any) => ({
|
|
711
|
+
...prev,
|
|
712
|
+
rules: [...(prev.rules || []), newRule],
|
|
713
|
+
}));
|
|
714
|
+
}, []);
|
|
715
|
+
|
|
716
|
+
const handleDeleteRule = useCallback((ruleId: string) => {
|
|
717
|
+
setFormData((prev: any) => ({
|
|
718
|
+
...prev,
|
|
719
|
+
rules: prev.rules.filter((rule: RouterRule) => rule.id !== ruleId),
|
|
720
|
+
}));
|
|
721
|
+
}, []);
|
|
722
|
+
|
|
723
|
+
const renderRouterNodeEditor = () => (
|
|
724
|
+
<Box>
|
|
725
|
+
<TextField
|
|
726
|
+
fullWidth
|
|
727
|
+
label="Router Alias"
|
|
728
|
+
name="alias"
|
|
729
|
+
value={formData.alias || ""}
|
|
730
|
+
onChange={handleInputChange}
|
|
731
|
+
margin="normal"
|
|
732
|
+
helperText="A friendly name for this router"
|
|
733
|
+
/>
|
|
734
|
+
|
|
735
|
+
<Divider sx={{ my: 3 }} />
|
|
736
|
+
|
|
737
|
+
<Typography variant="h6" gutterBottom>
|
|
738
|
+
Rules
|
|
739
|
+
</Typography>
|
|
740
|
+
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
|
|
741
|
+
Rules are evaluated in order. The first matching rule determines the
|
|
742
|
+
next node.
|
|
743
|
+
</Typography>
|
|
744
|
+
|
|
745
|
+
{formData.rules && formData.rules.length > 0 ? (
|
|
746
|
+
<Box sx={{ mt: 2 }}>
|
|
747
|
+
{formData.rules.map((rule: RouterRule, index: number) => (
|
|
748
|
+
<Paper key={rule.id} sx={{ p: 2, mb: 2 }}>
|
|
749
|
+
<Box
|
|
750
|
+
sx={{
|
|
751
|
+
display: "flex",
|
|
752
|
+
justifyContent: "space-between",
|
|
753
|
+
alignItems: "center",
|
|
754
|
+
mb: 1,
|
|
755
|
+
}}
|
|
756
|
+
>
|
|
757
|
+
<Typography variant="subtitle2">Rule {index + 1}</Typography>
|
|
758
|
+
<IconButton
|
|
759
|
+
size="small"
|
|
760
|
+
onClick={() => handleDeleteRule(rule.id)}
|
|
761
|
+
aria-label="delete rule"
|
|
762
|
+
>
|
|
763
|
+
<DeleteIcon fontSize="small" />
|
|
764
|
+
</IconButton>
|
|
765
|
+
</Box>
|
|
766
|
+
|
|
767
|
+
<TextField
|
|
768
|
+
fullWidth
|
|
769
|
+
label="Rule Alias"
|
|
770
|
+
value={rule.alias || ""}
|
|
771
|
+
onChange={(e) =>
|
|
772
|
+
handleRuleChange(rule.id, "alias", e.target.value)
|
|
773
|
+
}
|
|
774
|
+
margin="dense"
|
|
775
|
+
size="small"
|
|
776
|
+
/>
|
|
777
|
+
|
|
778
|
+
<Typography
|
|
779
|
+
variant="caption"
|
|
780
|
+
color="text.secondary"
|
|
781
|
+
sx={{ display: "block", mt: 1, mb: 0.5 }}
|
|
782
|
+
>
|
|
783
|
+
Condition
|
|
784
|
+
</Typography>
|
|
785
|
+
<Box
|
|
786
|
+
sx={{
|
|
787
|
+
display: "flex",
|
|
788
|
+
gap: 1,
|
|
789
|
+
flexWrap: "wrap",
|
|
790
|
+
alignItems: "flex-start",
|
|
791
|
+
}}
|
|
792
|
+
>
|
|
793
|
+
<Autocomplete
|
|
794
|
+
freeSolo
|
|
795
|
+
size="small"
|
|
796
|
+
options={ROUTER_FIELD_OPTIONS}
|
|
797
|
+
getOptionLabel={(option) =>
|
|
798
|
+
typeof option === "string" ? option : option.value
|
|
799
|
+
}
|
|
800
|
+
value={rule.condition?.field || ""}
|
|
801
|
+
onChange={(_, newValue) => {
|
|
802
|
+
const fieldValue =
|
|
803
|
+
typeof newValue === "string"
|
|
804
|
+
? newValue
|
|
805
|
+
: newValue?.value || "";
|
|
806
|
+
handleRuleChange(rule.id, "condition", {
|
|
807
|
+
...rule.condition,
|
|
808
|
+
field: fieldValue,
|
|
809
|
+
});
|
|
810
|
+
}}
|
|
811
|
+
onInputChange={(_, inputValue) => {
|
|
812
|
+
handleRuleChange(rule.id, "condition", {
|
|
813
|
+
...rule.condition,
|
|
814
|
+
field: inputValue,
|
|
815
|
+
});
|
|
816
|
+
}}
|
|
817
|
+
renderOption={(props, option) => (
|
|
818
|
+
<Box component="li" {...props}>
|
|
819
|
+
<Box>
|
|
820
|
+
<Typography variant="body2">{option.label}</Typography>
|
|
821
|
+
<Typography variant="caption" color="text.secondary">
|
|
822
|
+
{option.value}
|
|
823
|
+
</Typography>
|
|
824
|
+
</Box>
|
|
825
|
+
</Box>
|
|
826
|
+
)}
|
|
827
|
+
renderInput={(params) => (
|
|
828
|
+
<TextField
|
|
829
|
+
{...params}
|
|
830
|
+
label="Field"
|
|
831
|
+
placeholder="Select or type a field"
|
|
832
|
+
helperText="e.g., {{context.user.email}}"
|
|
833
|
+
/>
|
|
834
|
+
)}
|
|
835
|
+
sx={{ flex: 1, minWidth: "200px" }}
|
|
836
|
+
/>
|
|
837
|
+
<FormControl size="small" sx={{ minWidth: "120px" }}>
|
|
838
|
+
<InputLabel id={`rule-operator-${rule.id}-label`}>
|
|
839
|
+
Operator
|
|
840
|
+
</InputLabel>
|
|
841
|
+
<Select
|
|
842
|
+
labelId={`rule-operator-${rule.id}-label`}
|
|
843
|
+
value={rule.condition?.operator || ""}
|
|
844
|
+
label="Operator"
|
|
845
|
+
onChange={(e) =>
|
|
846
|
+
handleRuleChange(rule.id, "condition", {
|
|
847
|
+
...rule.condition,
|
|
848
|
+
operator: e.target.value,
|
|
849
|
+
})
|
|
850
|
+
}
|
|
851
|
+
>
|
|
852
|
+
<MenuItem value="equals">equals</MenuItem>
|
|
853
|
+
<MenuItem value="not_equals">not equals</MenuItem>
|
|
854
|
+
<MenuItem value="contains">contains</MenuItem>
|
|
855
|
+
<MenuItem value="not_contains">not contains</MenuItem>
|
|
856
|
+
<MenuItem value="starts_with">starts with</MenuItem>
|
|
857
|
+
<MenuItem value="ends_with">ends with</MenuItem>
|
|
858
|
+
<MenuItem value="exists">exists</MenuItem>
|
|
859
|
+
<MenuItem value="not_exists">not exists</MenuItem>
|
|
860
|
+
</Select>
|
|
861
|
+
</FormControl>
|
|
862
|
+
<TextField
|
|
863
|
+
label="Value"
|
|
864
|
+
value={rule.condition?.value || ""}
|
|
865
|
+
onChange={(e) =>
|
|
866
|
+
handleRuleChange(rule.id, "condition", {
|
|
867
|
+
...rule.condition,
|
|
868
|
+
value: e.target.value,
|
|
869
|
+
})
|
|
870
|
+
}
|
|
871
|
+
size="small"
|
|
872
|
+
sx={{ flex: 1, minWidth: "100px" }}
|
|
873
|
+
placeholder="e.g., admin"
|
|
874
|
+
disabled={
|
|
875
|
+
rule.condition?.operator === "exists" ||
|
|
876
|
+
rule.condition?.operator === "not_exists"
|
|
877
|
+
}
|
|
878
|
+
/>
|
|
879
|
+
</Box>
|
|
880
|
+
</Paper>
|
|
881
|
+
))}
|
|
882
|
+
</Box>
|
|
883
|
+
) : (
|
|
884
|
+
<Typography variant="body2" color="text.secondary">
|
|
885
|
+
No rules defined. Add a rule to route to different nodes based on
|
|
886
|
+
conditions.
|
|
887
|
+
</Typography>
|
|
888
|
+
)}
|
|
889
|
+
|
|
890
|
+
<Button
|
|
891
|
+
variant="outlined"
|
|
892
|
+
startIcon={<AddCircleOutlineIcon />}
|
|
893
|
+
fullWidth
|
|
894
|
+
sx={{ mt: 2 }}
|
|
895
|
+
onClick={handleAddRule}
|
|
896
|
+
>
|
|
897
|
+
Add Rule
|
|
898
|
+
</Button>
|
|
899
|
+
</Box>
|
|
900
|
+
);
|
|
901
|
+
|
|
902
|
+
return (
|
|
903
|
+
<>
|
|
904
|
+
<Drawer
|
|
905
|
+
anchor="right"
|
|
906
|
+
open={open}
|
|
907
|
+
onClose={onClose}
|
|
908
|
+
sx={{
|
|
909
|
+
flexShrink: 0,
|
|
910
|
+
width: 350,
|
|
911
|
+
"& .MuiDrawer-paper": {
|
|
912
|
+
width: 350,
|
|
913
|
+
boxSizing: "border-box",
|
|
914
|
+
padding: 2,
|
|
915
|
+
},
|
|
916
|
+
}}
|
|
917
|
+
>
|
|
918
|
+
<Box
|
|
919
|
+
sx={{
|
|
920
|
+
display: "flex",
|
|
921
|
+
justifyContent: "space-between",
|
|
922
|
+
alignItems: "center",
|
|
923
|
+
mb: 2,
|
|
924
|
+
}}
|
|
925
|
+
>
|
|
926
|
+
<Box>
|
|
927
|
+
<Typography variant="h5" sx={{ display: "block" }}>
|
|
928
|
+
{selectedNode
|
|
929
|
+
? selectedNode.id === "start"
|
|
930
|
+
? "Start Node"
|
|
931
|
+
: selectedNode.id === "end"
|
|
932
|
+
? "End Node"
|
|
933
|
+
: String(selectedNode?.data?.label || "Node")
|
|
934
|
+
: "Node"}
|
|
935
|
+
</Typography>
|
|
936
|
+
<Typography variant="caption" color="text.secondary">
|
|
937
|
+
ID: {selectedNode?.id || ""}
|
|
938
|
+
</Typography>
|
|
939
|
+
</Box>
|
|
940
|
+
<IconButton onClick={onClose} size="small">
|
|
941
|
+
<CloseIcon />
|
|
942
|
+
</IconButton>
|
|
943
|
+
</Box>
|
|
944
|
+
|
|
945
|
+
<Divider sx={{ mb: 2 }} />
|
|
946
|
+
|
|
947
|
+
<Box sx={{ borderBottom: 1, borderColor: "divider" }}>
|
|
948
|
+
<Tabs
|
|
949
|
+
value={tabValue}
|
|
950
|
+
onChange={(_, newValue) => setTabValue(newValue)}
|
|
951
|
+
>
|
|
952
|
+
<Tab label="Properties" />
|
|
953
|
+
<Tab label="JSON" />
|
|
954
|
+
</Tabs>
|
|
955
|
+
</Box>
|
|
956
|
+
|
|
957
|
+
<Box sx={{ mt: 2, mb: 2 }}>
|
|
958
|
+
<TabPanel value={tabValue} index={0}>
|
|
959
|
+
{renderEditor()}
|
|
960
|
+
</TabPanel>
|
|
961
|
+
<TabPanel value={tabValue} index={1}>
|
|
962
|
+
<Box sx={{ position: "relative" }}>
|
|
963
|
+
<IconButton
|
|
964
|
+
size="small"
|
|
965
|
+
sx={{ position: "absolute", top: 0, right: 0 }}
|
|
966
|
+
title="Copy JSON"
|
|
967
|
+
>
|
|
968
|
+
<CodeIcon fontSize="small" />
|
|
969
|
+
</IconButton>
|
|
970
|
+
<Typography
|
|
971
|
+
component="pre"
|
|
972
|
+
sx={{
|
|
973
|
+
p: 1,
|
|
974
|
+
backgroundColor: (theme) =>
|
|
975
|
+
theme.palette.mode === "dark"
|
|
976
|
+
? theme.palette.grey[900]
|
|
977
|
+
: "#f5f5f5",
|
|
978
|
+
color: "text.primary",
|
|
979
|
+
borderRadius: 1,
|
|
980
|
+
overflow: "auto",
|
|
981
|
+
fontSize: "0.8rem",
|
|
982
|
+
maxHeight: "400px",
|
|
983
|
+
}}
|
|
984
|
+
>
|
|
985
|
+
{JSON.stringify(
|
|
986
|
+
selectedNode?.id === "start"
|
|
987
|
+
? { next_node: formData.next_node || null, id: "start" }
|
|
988
|
+
: selectedNode?.id === "end"
|
|
989
|
+
? {
|
|
990
|
+
resume_flow: formData.resume_flow || false,
|
|
991
|
+
id: "end",
|
|
992
|
+
}
|
|
993
|
+
: formData,
|
|
994
|
+
null,
|
|
995
|
+
2,
|
|
996
|
+
)}
|
|
997
|
+
</Typography>
|
|
998
|
+
</Box>
|
|
999
|
+
</TabPanel>
|
|
1000
|
+
</Box>
|
|
1001
|
+
|
|
1002
|
+
<Box
|
|
1003
|
+
sx={{
|
|
1004
|
+
position: "sticky",
|
|
1005
|
+
bottom: 0,
|
|
1006
|
+
backgroundColor: "background.paper",
|
|
1007
|
+
pt: 2,
|
|
1008
|
+
pb: 2,
|
|
1009
|
+
px: 2,
|
|
1010
|
+
mx: -2,
|
|
1011
|
+
borderTop: 1,
|
|
1012
|
+
borderColor: "divider",
|
|
1013
|
+
display: "flex",
|
|
1014
|
+
justifyContent: "flex-end",
|
|
1015
|
+
gap: 1,
|
|
1016
|
+
}}
|
|
1017
|
+
>
|
|
1018
|
+
<Button variant="outlined" onClick={onClose}>
|
|
1019
|
+
Cancel
|
|
1020
|
+
</Button>
|
|
1021
|
+
<Button variant="contained" color="primary" onClick={handleSave}>
|
|
1022
|
+
Save
|
|
1023
|
+
</Button>
|
|
1024
|
+
</Box>
|
|
1025
|
+
</Drawer>
|
|
1026
|
+
|
|
1027
|
+
{/* Component Edit Dialog */}
|
|
1028
|
+
<Dialog
|
|
1029
|
+
open={componentDialogOpen}
|
|
1030
|
+
onClose={handleCloseComponentDialog}
|
|
1031
|
+
maxWidth="sm"
|
|
1032
|
+
fullWidth
|
|
1033
|
+
>
|
|
1034
|
+
<DialogTitle>Edit {editingComponent?.type} Component</DialogTitle>
|
|
1035
|
+
<DialogContent>
|
|
1036
|
+
{editingComponent?.type === "RICH_TEXT" && (
|
|
1037
|
+
<Box sx={{ mt: 2 }}>
|
|
1038
|
+
<Typography variant="subtitle2" sx={{ mb: 1 }}>
|
|
1039
|
+
Content
|
|
1040
|
+
</Typography>
|
|
1041
|
+
<RichTextEditor
|
|
1042
|
+
value={editingComponent?.config?.content || ""}
|
|
1043
|
+
onChange={(value) =>
|
|
1044
|
+
handleComponentFieldChange("content", value)
|
|
1045
|
+
}
|
|
1046
|
+
/>
|
|
1047
|
+
<FormHelperText>Rich text content with formatting</FormHelperText>
|
|
1048
|
+
</Box>
|
|
1049
|
+
)}
|
|
1050
|
+
{editingComponent?.type === "LEGAL" && (
|
|
1051
|
+
<TextField
|
|
1052
|
+
fullWidth
|
|
1053
|
+
label="Legal Text"
|
|
1054
|
+
value={editingComponent?.config?.text || ""}
|
|
1055
|
+
onChange={(e) =>
|
|
1056
|
+
handleComponentFieldChange("text", e.target.value)
|
|
1057
|
+
}
|
|
1058
|
+
margin="normal"
|
|
1059
|
+
multiline
|
|
1060
|
+
rows={3}
|
|
1061
|
+
helperText="Text for the legal checkbox"
|
|
1062
|
+
/>
|
|
1063
|
+
)}
|
|
1064
|
+
{editingComponent?.type === "NEXT_BUTTON" && (
|
|
1065
|
+
<TextField
|
|
1066
|
+
fullWidth
|
|
1067
|
+
label="Button Text"
|
|
1068
|
+
value={editingComponent?.config?.text || ""}
|
|
1069
|
+
onChange={(e) =>
|
|
1070
|
+
handleComponentFieldChange("text", e.target.value)
|
|
1071
|
+
}
|
|
1072
|
+
margin="normal"
|
|
1073
|
+
helperText="Text to display on the button"
|
|
1074
|
+
/>
|
|
1075
|
+
)}
|
|
1076
|
+
{(editingComponent?.type === "TEXT" ||
|
|
1077
|
+
editingComponent?.type === "EMAIL" ||
|
|
1078
|
+
editingComponent?.type === "NUMBER" ||
|
|
1079
|
+
editingComponent?.type === "PHONE") && (
|
|
1080
|
+
<Box>
|
|
1081
|
+
<TextField
|
|
1082
|
+
fullWidth
|
|
1083
|
+
label="Label"
|
|
1084
|
+
value={editingComponent?.config?.label || ""}
|
|
1085
|
+
onChange={(e) =>
|
|
1086
|
+
handleComponentFieldChange("label", e.target.value)
|
|
1087
|
+
}
|
|
1088
|
+
margin="normal"
|
|
1089
|
+
helperText="Label displayed above the field"
|
|
1090
|
+
/>
|
|
1091
|
+
<TextField
|
|
1092
|
+
fullWidth
|
|
1093
|
+
label="Placeholder"
|
|
1094
|
+
value={editingComponent?.config?.placeholder || ""}
|
|
1095
|
+
onChange={(e) =>
|
|
1096
|
+
handleComponentFieldChange("placeholder", e.target.value)
|
|
1097
|
+
}
|
|
1098
|
+
margin="normal"
|
|
1099
|
+
helperText="Placeholder text shown when field is empty"
|
|
1100
|
+
/>
|
|
1101
|
+
</Box>
|
|
1102
|
+
)}
|
|
1103
|
+
</DialogContent>
|
|
1104
|
+
<DialogActions>
|
|
1105
|
+
<Button onClick={handleCloseComponentDialog}>Cancel</Button>
|
|
1106
|
+
<Button
|
|
1107
|
+
onClick={handleSaveComponent}
|
|
1108
|
+
variant="contained"
|
|
1109
|
+
color="primary"
|
|
1110
|
+
>
|
|
1111
|
+
Save
|
|
1112
|
+
</Button>
|
|
1113
|
+
</DialogActions>
|
|
1114
|
+
</Dialog>
|
|
1115
|
+
</>
|
|
1116
|
+
);
|
|
1117
|
+
};
|
|
1118
|
+
|
|
1119
|
+
export default NodeEditor;
|