@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,145 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { useEditor, EditorContent } from "@tiptap/react";
|
|
3
|
+
import StarterKit from "@tiptap/starter-kit";
|
|
4
|
+
import Link from "@tiptap/extension-link";
|
|
5
|
+
import Underline from "@tiptap/extension-underline";
|
|
6
|
+
import { Box, IconButton, Divider } from "@mui/material";
|
|
7
|
+
import FormatBoldIcon from "@mui/icons-material/FormatBold";
|
|
8
|
+
import FormatItalicIcon from "@mui/icons-material/FormatItalic";
|
|
9
|
+
import FormatUnderlinedIcon from "@mui/icons-material/FormatUnderlined";
|
|
10
|
+
import LinkIcon from "@mui/icons-material/Link";
|
|
11
|
+
import FormatListBulletedIcon from "@mui/icons-material/FormatListBulleted";
|
|
12
|
+
import FormatListNumberedIcon from "@mui/icons-material/FormatListNumbered";
|
|
13
|
+
|
|
14
|
+
interface RichTextEditorProps {
|
|
15
|
+
value: string;
|
|
16
|
+
onChange: (value: string) => void;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const RichTextEditor: React.FC<RichTextEditorProps> = ({ value, onChange }) => {
|
|
20
|
+
const editor = useEditor({
|
|
21
|
+
extensions: [
|
|
22
|
+
StarterKit,
|
|
23
|
+
Underline,
|
|
24
|
+
Link.configure({
|
|
25
|
+
openOnClick: false,
|
|
26
|
+
}),
|
|
27
|
+
],
|
|
28
|
+
content: value,
|
|
29
|
+
onUpdate: ({ editor }) => {
|
|
30
|
+
onChange(editor.getHTML());
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
if (!editor) {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const handleLinkClick = () => {
|
|
39
|
+
const previousUrl = editor.getAttributes("link").href;
|
|
40
|
+
const url = window.prompt("URL", previousUrl);
|
|
41
|
+
|
|
42
|
+
if (url === null) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (url === "") {
|
|
47
|
+
editor.chain().focus().extendMarkRange("link").unsetLink().run();
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
editor.chain().focus().extendMarkRange("link").setLink({ href: url }).run();
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<Box
|
|
56
|
+
sx={{
|
|
57
|
+
border: "1px solid",
|
|
58
|
+
borderColor: "divider",
|
|
59
|
+
borderRadius: 1,
|
|
60
|
+
overflow: "hidden",
|
|
61
|
+
}}
|
|
62
|
+
>
|
|
63
|
+
<Box
|
|
64
|
+
sx={{
|
|
65
|
+
display: "flex",
|
|
66
|
+
flexWrap: "wrap",
|
|
67
|
+
gap: 0.5,
|
|
68
|
+
p: 0.5,
|
|
69
|
+
borderBottom: "1px solid",
|
|
70
|
+
borderColor: "divider",
|
|
71
|
+
bgcolor: "action.hover",
|
|
72
|
+
}}
|
|
73
|
+
>
|
|
74
|
+
<IconButton
|
|
75
|
+
size="small"
|
|
76
|
+
onClick={() => editor.chain().focus().toggleBold().run()}
|
|
77
|
+
color={editor.isActive("bold") ? "primary" : "default"}
|
|
78
|
+
>
|
|
79
|
+
<FormatBoldIcon fontSize="small" />
|
|
80
|
+
</IconButton>
|
|
81
|
+
<IconButton
|
|
82
|
+
size="small"
|
|
83
|
+
onClick={() => editor.chain().focus().toggleItalic().run()}
|
|
84
|
+
color={editor.isActive("italic") ? "primary" : "default"}
|
|
85
|
+
>
|
|
86
|
+
<FormatItalicIcon fontSize="small" />
|
|
87
|
+
</IconButton>
|
|
88
|
+
<IconButton
|
|
89
|
+
size="small"
|
|
90
|
+
onClick={() => editor.chain().focus().toggleUnderline().run()}
|
|
91
|
+
color={editor.isActive("underline") ? "primary" : "default"}
|
|
92
|
+
>
|
|
93
|
+
<FormatUnderlinedIcon fontSize="small" />
|
|
94
|
+
</IconButton>
|
|
95
|
+
<Divider orientation="vertical" flexItem sx={{ mx: 0.5 }} />
|
|
96
|
+
<IconButton
|
|
97
|
+
size="small"
|
|
98
|
+
onClick={handleLinkClick}
|
|
99
|
+
color={editor.isActive("link") ? "primary" : "default"}
|
|
100
|
+
>
|
|
101
|
+
<LinkIcon fontSize="small" />
|
|
102
|
+
</IconButton>
|
|
103
|
+
<Divider orientation="vertical" flexItem sx={{ mx: 0.5 }} />
|
|
104
|
+
<IconButton
|
|
105
|
+
size="small"
|
|
106
|
+
onClick={() => editor.chain().focus().toggleBulletList().run()}
|
|
107
|
+
color={editor.isActive("bulletList") ? "primary" : "default"}
|
|
108
|
+
>
|
|
109
|
+
<FormatListBulletedIcon fontSize="small" />
|
|
110
|
+
</IconButton>
|
|
111
|
+
<IconButton
|
|
112
|
+
size="small"
|
|
113
|
+
onClick={() => editor.chain().focus().toggleOrderedList().run()}
|
|
114
|
+
color={editor.isActive("orderedList") ? "primary" : "default"}
|
|
115
|
+
>
|
|
116
|
+
<FormatListNumberedIcon fontSize="small" />
|
|
117
|
+
</IconButton>
|
|
118
|
+
</Box>
|
|
119
|
+
<Box
|
|
120
|
+
sx={{
|
|
121
|
+
p: 1,
|
|
122
|
+
minHeight: 150,
|
|
123
|
+
"& .ProseMirror": {
|
|
124
|
+
outline: "none",
|
|
125
|
+
minHeight: 150,
|
|
126
|
+
"& p": {
|
|
127
|
+
margin: 0,
|
|
128
|
+
},
|
|
129
|
+
"& ul, & ol": {
|
|
130
|
+
paddingLeft: 2,
|
|
131
|
+
},
|
|
132
|
+
"& a": {
|
|
133
|
+
color: "primary.main",
|
|
134
|
+
textDecoration: "underline",
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
}}
|
|
138
|
+
>
|
|
139
|
+
<EditorContent editor={editor} />
|
|
140
|
+
</Box>
|
|
141
|
+
</Box>
|
|
142
|
+
);
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
export default RichTextEditor;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { Create, TextInput, required, SimpleForm } from "react-admin";
|
|
2
|
+
import { Box, Typography } from "@mui/material";
|
|
3
|
+
|
|
4
|
+
export const FormCreate = () => {
|
|
5
|
+
return (
|
|
6
|
+
<Create>
|
|
7
|
+
<SimpleForm>
|
|
8
|
+
<TextInput source="name" validate={[required()]} fullWidth />
|
|
9
|
+
|
|
10
|
+
<Box
|
|
11
|
+
sx={{
|
|
12
|
+
mt: 3,
|
|
13
|
+
p: 2,
|
|
14
|
+
bgcolor: "#f5f5f5",
|
|
15
|
+
borderRadius: 1,
|
|
16
|
+
color: "text.secondary",
|
|
17
|
+
}}
|
|
18
|
+
>
|
|
19
|
+
<Typography variant="body1">
|
|
20
|
+
The flow diagram will be available after creating the form.
|
|
21
|
+
</Typography>
|
|
22
|
+
<Typography variant="body2" sx={{ mt: 1 }}>
|
|
23
|
+
You can add nodes and connections to build your form flow in the
|
|
24
|
+
edit view.
|
|
25
|
+
</Typography>
|
|
26
|
+
</Box>
|
|
27
|
+
</SimpleForm>
|
|
28
|
+
</Create>
|
|
29
|
+
);
|
|
30
|
+
};
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DateField,
|
|
3
|
+
Edit,
|
|
4
|
+
FieldTitle,
|
|
5
|
+
Labeled,
|
|
6
|
+
TextInput,
|
|
7
|
+
required,
|
|
8
|
+
useRecordContext,
|
|
9
|
+
TabbedForm,
|
|
10
|
+
FormTab,
|
|
11
|
+
useUpdate,
|
|
12
|
+
useNotify,
|
|
13
|
+
useRefresh,
|
|
14
|
+
useGetList,
|
|
15
|
+
} from "react-admin";
|
|
16
|
+
import FlowEditor, { FlowNodeData, StartNode, EndingNode } from "./FlowEditor";
|
|
17
|
+
import { ReactFlowProvider } from "@xyflow/react";
|
|
18
|
+
import { Box, Typography, useTheme } from "@mui/material";
|
|
19
|
+
import * as React from "react";
|
|
20
|
+
import { useSaveContext } from "react-admin";
|
|
21
|
+
|
|
22
|
+
// A component to render the flow diagram
|
|
23
|
+
const FlowDiagram = () => {
|
|
24
|
+
const record = useRecordContext();
|
|
25
|
+
const [update] = useUpdate();
|
|
26
|
+
const notify = useNotify();
|
|
27
|
+
const refresh = useRefresh();
|
|
28
|
+
|
|
29
|
+
// Fetch flows for dropdown selection
|
|
30
|
+
const { data: flows } = useGetList("flows", {
|
|
31
|
+
pagination: { page: 1, perPage: 100 },
|
|
32
|
+
sort: { field: "name", order: "ASC" },
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
// Allow rendering if there is a start or ending node, even if nodes is missing or empty
|
|
36
|
+
if (!record || (!record.nodes && !record.start && !record.ending)) {
|
|
37
|
+
return <div>No flow data available</div>;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Handle node updates from the FlowEditor
|
|
41
|
+
const handleNodeUpdate = (
|
|
42
|
+
nodeId: string,
|
|
43
|
+
updates: Partial<FlowNodeData> | Partial<StartNode> | Partial<EndingNode>,
|
|
44
|
+
) => {
|
|
45
|
+
let updatedRecord = { ...record };
|
|
46
|
+
|
|
47
|
+
if (nodeId === "start") {
|
|
48
|
+
// Update the start node
|
|
49
|
+
updatedRecord.start = { ...record.start, ...updates };
|
|
50
|
+
} else if (nodeId === "end") {
|
|
51
|
+
// Update the ending node
|
|
52
|
+
updatedRecord.ending = { ...record.ending, ...updates };
|
|
53
|
+
} else {
|
|
54
|
+
// Check if this is a new node (has 'type' property in updates indicating full node data)
|
|
55
|
+
const isNewNode =
|
|
56
|
+
"type" in updates && (updates as FlowNodeData).type !== undefined;
|
|
57
|
+
|
|
58
|
+
if (isNewNode) {
|
|
59
|
+
// Adding a new node
|
|
60
|
+
const newNode = { id: nodeId, ...updates } as FlowNodeData;
|
|
61
|
+
updatedRecord.nodes = [...(record.nodes || []), newNode];
|
|
62
|
+
} else {
|
|
63
|
+
// Update an existing node
|
|
64
|
+
const nodeIndex = (record.nodes || []).findIndex(
|
|
65
|
+
(n: FlowNodeData) => n.id === nodeId,
|
|
66
|
+
);
|
|
67
|
+
if (nodeIndex >= 0) {
|
|
68
|
+
const existingNode = record.nodes[nodeIndex];
|
|
69
|
+
updatedRecord.nodes = [...record.nodes];
|
|
70
|
+
updatedRecord.nodes[nodeIndex] = {
|
|
71
|
+
...existingNode,
|
|
72
|
+
...updates,
|
|
73
|
+
config: {
|
|
74
|
+
...existingNode.config,
|
|
75
|
+
...(updates as Partial<FlowNodeData>).config,
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Save the updated record
|
|
83
|
+
update(
|
|
84
|
+
"forms",
|
|
85
|
+
{ id: record.id, data: updatedRecord, previousData: record },
|
|
86
|
+
{
|
|
87
|
+
onSuccess: () => {
|
|
88
|
+
notify("Flow updated successfully", { type: "success" });
|
|
89
|
+
refresh();
|
|
90
|
+
},
|
|
91
|
+
onError: (error: any) => {
|
|
92
|
+
notify(`Error updating flow: ${error.message}`, { type: "error" });
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
);
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
return (
|
|
99
|
+
<Box
|
|
100
|
+
sx={{
|
|
101
|
+
height: "700px",
|
|
102
|
+
width: "100%",
|
|
103
|
+
border: "1px solid #e0e0e0",
|
|
104
|
+
borderRadius: "4px",
|
|
105
|
+
bgcolor: "#fcfcfc",
|
|
106
|
+
}}
|
|
107
|
+
>
|
|
108
|
+
<ReactFlowProvider>
|
|
109
|
+
<FlowEditor
|
|
110
|
+
nodes={record.nodes || []}
|
|
111
|
+
start={record.start}
|
|
112
|
+
ending={record.ending}
|
|
113
|
+
flows={flows?.map((f) => ({ id: f.id, name: f.name })) || []}
|
|
114
|
+
onNodeUpdate={handleNodeUpdate}
|
|
115
|
+
/>
|
|
116
|
+
</ReactFlowProvider>
|
|
117
|
+
</Box>
|
|
118
|
+
);
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
// A component to display raw JSON
|
|
122
|
+
const RawJsonEditor = () => {
|
|
123
|
+
const record = useRecordContext();
|
|
124
|
+
const saveContext = useSaveContext();
|
|
125
|
+
const theme = useTheme();
|
|
126
|
+
const [jsonValue, setJsonValue] = React.useState(() =>
|
|
127
|
+
record ? JSON.stringify(record, null, 2) : "",
|
|
128
|
+
);
|
|
129
|
+
const [error, setError] = React.useState<string | null>(null);
|
|
130
|
+
const [success, setSuccess] = React.useState(false);
|
|
131
|
+
|
|
132
|
+
// Sync jsonValue with record when it changes (e.g., after flow diagram updates)
|
|
133
|
+
React.useEffect(() => {
|
|
134
|
+
if (record) {
|
|
135
|
+
setJsonValue(JSON.stringify(record, null, 2));
|
|
136
|
+
setError(null);
|
|
137
|
+
setSuccess(false);
|
|
138
|
+
}
|
|
139
|
+
}, [record]);
|
|
140
|
+
|
|
141
|
+
if (!record) {
|
|
142
|
+
return <div>No form data available</div>;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Handle JSON edit
|
|
146
|
+
const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
|
147
|
+
const value = e.target.value;
|
|
148
|
+
setJsonValue(value);
|
|
149
|
+
setSuccess(false);
|
|
150
|
+
try {
|
|
151
|
+
JSON.parse(value);
|
|
152
|
+
setError(null);
|
|
153
|
+
} catch (err: any) {
|
|
154
|
+
setError(err.message || "Invalid JSON");
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
// Save JSON to record
|
|
159
|
+
const handleSave = () => {
|
|
160
|
+
try {
|
|
161
|
+
const parsed = JSON.parse(jsonValue);
|
|
162
|
+
setError(null);
|
|
163
|
+
if (saveContext && typeof saveContext.save === "function") {
|
|
164
|
+
saveContext.save(parsed);
|
|
165
|
+
setSuccess(true);
|
|
166
|
+
}
|
|
167
|
+
} catch (err: any) {
|
|
168
|
+
setError(err.message || "Invalid JSON");
|
|
169
|
+
setSuccess(false);
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
// Theme-aware colors
|
|
174
|
+
const isDark = theme.palette.mode === "dark";
|
|
175
|
+
const textareaBg = isDark ? theme.palette.background.paper : "#f5f5f5";
|
|
176
|
+
const textareaBorder = isDark ? theme.palette.divider : "#e0e0e0";
|
|
177
|
+
const textareaColor = isDark ? theme.palette.text.primary : "inherit";
|
|
178
|
+
|
|
179
|
+
return (
|
|
180
|
+
<Box sx={{ mt: 2 }}>
|
|
181
|
+
<Typography variant="subtitle2" sx={{ mb: 1 }}>
|
|
182
|
+
Raw JSON representation of the form data:
|
|
183
|
+
</Typography>
|
|
184
|
+
<Box
|
|
185
|
+
component="textarea"
|
|
186
|
+
value={jsonValue}
|
|
187
|
+
onChange={handleChange}
|
|
188
|
+
sx={{
|
|
189
|
+
backgroundColor: textareaBg,
|
|
190
|
+
border: `1px solid ${textareaBorder}`,
|
|
191
|
+
borderRadius: "4px",
|
|
192
|
+
color: textareaColor,
|
|
193
|
+
padding: 2,
|
|
194
|
+
maxHeight: "600px",
|
|
195
|
+
minHeight: "300px",
|
|
196
|
+
width: "100%",
|
|
197
|
+
overflow: "auto",
|
|
198
|
+
fontSize: "0.9rem",
|
|
199
|
+
fontFamily: "monospace",
|
|
200
|
+
resize: "vertical",
|
|
201
|
+
}}
|
|
202
|
+
/>
|
|
203
|
+
<Box sx={{ mt: 1, display: "flex", gap: 2, alignItems: "center" }}>
|
|
204
|
+
<button
|
|
205
|
+
onClick={handleSave}
|
|
206
|
+
disabled={!!error || !saveContext?.save}
|
|
207
|
+
style={{
|
|
208
|
+
padding: "6px 16px",
|
|
209
|
+
borderRadius: 4,
|
|
210
|
+
border: `1px solid ${theme.palette.primary.main}`,
|
|
211
|
+
background: theme.palette.primary.main,
|
|
212
|
+
color: theme.palette.primary.contrastText,
|
|
213
|
+
cursor: error ? "not-allowed" : "pointer",
|
|
214
|
+
}}
|
|
215
|
+
>
|
|
216
|
+
Save JSON
|
|
217
|
+
</button>
|
|
218
|
+
{success && <Typography color="success.main">Saved!</Typography>}
|
|
219
|
+
</Box>
|
|
220
|
+
{error && (
|
|
221
|
+
<Typography color="error" sx={{ mt: 1 }}>
|
|
222
|
+
Invalid JSON: {error}
|
|
223
|
+
</Typography>
|
|
224
|
+
)}
|
|
225
|
+
</Box>
|
|
226
|
+
);
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
export const FormEdit = () => {
|
|
230
|
+
return (
|
|
231
|
+
<Edit>
|
|
232
|
+
<TabbedForm>
|
|
233
|
+
<FormTab label="Basic Information">
|
|
234
|
+
<TextInput source="id" disabled fullWidth />
|
|
235
|
+
<TextInput source="name" validate={[required()]} fullWidth />
|
|
236
|
+
<Labeled label={<FieldTitle source="created_at" />}>
|
|
237
|
+
<DateField source="created_at" showTime={true} />
|
|
238
|
+
</Labeled>
|
|
239
|
+
<Labeled label={<FieldTitle source="updated_at" />}>
|
|
240
|
+
<DateField source="updated_at" showTime={true} />
|
|
241
|
+
</Labeled>
|
|
242
|
+
</FormTab>
|
|
243
|
+
|
|
244
|
+
<FormTab label="Flow Diagram">
|
|
245
|
+
<FlowDiagram />
|
|
246
|
+
</FormTab>
|
|
247
|
+
|
|
248
|
+
<FormTab label="Raw">
|
|
249
|
+
<Box sx={{ width: "100%" }}>
|
|
250
|
+
<RawJsonEditor />
|
|
251
|
+
</Box>
|
|
252
|
+
</FormTab>
|
|
253
|
+
</TabbedForm>
|
|
254
|
+
</Edit>
|
|
255
|
+
);
|
|
256
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { List, Datagrid, TextField, DateField } from "react-admin";
|
|
2
|
+
import { PostListActions } from "../listActions/PostListActions";
|
|
3
|
+
|
|
4
|
+
export const FormsList = () => {
|
|
5
|
+
return (
|
|
6
|
+
<List actions={<PostListActions />}>
|
|
7
|
+
<Datagrid rowClick="edit" bulkActionButtons={false}>
|
|
8
|
+
<TextField source="id" />
|
|
9
|
+
<TextField source="name" />
|
|
10
|
+
<TextField source="type" />
|
|
11
|
+
<DateField source="created_at" showTime={true} />
|
|
12
|
+
<DateField source="updated_at" showTime={true} />
|
|
13
|
+
</Datagrid>
|
|
14
|
+
</List>
|
|
15
|
+
);
|
|
16
|
+
};
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BooleanInput,
|
|
3
|
+
Create,
|
|
4
|
+
NumberInput,
|
|
5
|
+
SelectInput,
|
|
6
|
+
SimpleForm,
|
|
7
|
+
TextInput,
|
|
8
|
+
required,
|
|
9
|
+
useGetList,
|
|
10
|
+
FormDataConsumer,
|
|
11
|
+
} from "react-admin";
|
|
12
|
+
|
|
13
|
+
export function HooksCreate() {
|
|
14
|
+
// Fetch forms for the current tenant
|
|
15
|
+
const { data: forms, isLoading: formsLoading } = useGetList("forms", {
|
|
16
|
+
pagination: { page: 1, perPage: 100 },
|
|
17
|
+
sort: { field: "name", order: "ASC" },
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
// Choices for the type selector
|
|
21
|
+
const typeChoices = [
|
|
22
|
+
{ id: "webhook", name: "Webhook" },
|
|
23
|
+
{ id: "form", name: "Form" },
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<Create>
|
|
28
|
+
<SimpleForm>
|
|
29
|
+
<SelectInput
|
|
30
|
+
source="type"
|
|
31
|
+
label="Type"
|
|
32
|
+
choices={typeChoices}
|
|
33
|
+
validate={[required()]}
|
|
34
|
+
/>
|
|
35
|
+
<FormDataConsumer>
|
|
36
|
+
{({ formData }) => {
|
|
37
|
+
if (formData.type === "webhook") {
|
|
38
|
+
return (
|
|
39
|
+
<TextInput
|
|
40
|
+
source="url"
|
|
41
|
+
validate={[required()]}
|
|
42
|
+
label="Webhook URL"
|
|
43
|
+
fullWidth
|
|
44
|
+
/>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
if (formData.type === "form") {
|
|
48
|
+
return (
|
|
49
|
+
<SelectInput
|
|
50
|
+
source="form_id"
|
|
51
|
+
label="Form"
|
|
52
|
+
choices={
|
|
53
|
+
formsLoading
|
|
54
|
+
? []
|
|
55
|
+
: (forms || []).map((form) => ({
|
|
56
|
+
id: form.id,
|
|
57
|
+
name: form.name,
|
|
58
|
+
}))
|
|
59
|
+
}
|
|
60
|
+
validate={[required()]}
|
|
61
|
+
fullWidth
|
|
62
|
+
/>
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
return null;
|
|
66
|
+
}}
|
|
67
|
+
</FormDataConsumer>
|
|
68
|
+
<SelectInput
|
|
69
|
+
source="trigger_id"
|
|
70
|
+
choices={[
|
|
71
|
+
{
|
|
72
|
+
id: "validate-registration-username",
|
|
73
|
+
name: "Validate Registration Username",
|
|
74
|
+
},
|
|
75
|
+
{ id: "pre-user-registration", name: "Pre User Registration" },
|
|
76
|
+
{ id: "post-user-registration", name: "Post User Registration" },
|
|
77
|
+
{ id: "post-user-login", name: "Post User Login" },
|
|
78
|
+
{ id: "pre-user-update", name: "Pre User Update" },
|
|
79
|
+
{ id: "pre-user-deletion", name: "Pre User Deletion" },
|
|
80
|
+
{ id: "post-user-deletion", name: "Post User Deletion" },
|
|
81
|
+
]}
|
|
82
|
+
required
|
|
83
|
+
/>
|
|
84
|
+
<BooleanInput source="enabled" />
|
|
85
|
+
<BooleanInput
|
|
86
|
+
source="synchronous"
|
|
87
|
+
helperText="The event waits for the webhook to complete and can be canceled"
|
|
88
|
+
/>
|
|
89
|
+
<NumberInput
|
|
90
|
+
source="priority"
|
|
91
|
+
helperText="A hook with higher priority will be executed first"
|
|
92
|
+
/>
|
|
93
|
+
</SimpleForm>
|
|
94
|
+
</Create>
|
|
95
|
+
);
|
|
96
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BooleanInput,
|
|
3
|
+
DateField,
|
|
4
|
+
Edit,
|
|
5
|
+
FieldTitle,
|
|
6
|
+
Labeled,
|
|
7
|
+
NumberInput,
|
|
8
|
+
regex,
|
|
9
|
+
required,
|
|
10
|
+
SelectInput,
|
|
11
|
+
SimpleForm,
|
|
12
|
+
TextInput,
|
|
13
|
+
useGetList,
|
|
14
|
+
FormDataConsumer,
|
|
15
|
+
useRecordContext,
|
|
16
|
+
} from "react-admin";
|
|
17
|
+
import { Typography } from "@mui/material";
|
|
18
|
+
|
|
19
|
+
export function HookEdit() {
|
|
20
|
+
// Fetch forms for the current tenant
|
|
21
|
+
const { data: forms, isLoading: formsLoading } = useGetList("forms", {
|
|
22
|
+
pagination: { page: 1, perPage: 100 },
|
|
23
|
+
sort: { field: "name", order: "ASC" },
|
|
24
|
+
});
|
|
25
|
+
const record = useRecordContext();
|
|
26
|
+
|
|
27
|
+
// Determine type from record or formData
|
|
28
|
+
const getType = (formData: any) => {
|
|
29
|
+
if (formData?.url) return "webhook";
|
|
30
|
+
if (formData?.form_id) return "form";
|
|
31
|
+
return undefined;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<Edit>
|
|
36
|
+
<SimpleForm>
|
|
37
|
+
<FormDataConsumer>
|
|
38
|
+
{({ formData }) => {
|
|
39
|
+
const type = getType(formData ?? record);
|
|
40
|
+
return (
|
|
41
|
+
<>
|
|
42
|
+
<Typography variant="subtitle1" sx={{ mb: 2 }}>
|
|
43
|
+
{type === "webhook"
|
|
44
|
+
? "Webhook"
|
|
45
|
+
: type === "form"
|
|
46
|
+
? "Form hook"
|
|
47
|
+
: ""}
|
|
48
|
+
</Typography>
|
|
49
|
+
{type === "webhook" && (
|
|
50
|
+
<TextInput
|
|
51
|
+
source="url"
|
|
52
|
+
validate={[
|
|
53
|
+
required(),
|
|
54
|
+
regex(/^https?:\/\/.*/, "Must be a valid HTTP/HTTPS URL"),
|
|
55
|
+
]}
|
|
56
|
+
label="Webhook URL"
|
|
57
|
+
fullWidth
|
|
58
|
+
helperText="The webhook endpoint URL that will be called"
|
|
59
|
+
/>
|
|
60
|
+
)}
|
|
61
|
+
{type === "form" && (
|
|
62
|
+
<SelectInput
|
|
63
|
+
source="form_id"
|
|
64
|
+
label="Form"
|
|
65
|
+
choices={
|
|
66
|
+
formsLoading
|
|
67
|
+
? []
|
|
68
|
+
: (forms || []).map((form) => ({
|
|
69
|
+
id: form.id,
|
|
70
|
+
name: form.name,
|
|
71
|
+
}))
|
|
72
|
+
}
|
|
73
|
+
validate={[required()]}
|
|
74
|
+
fullWidth
|
|
75
|
+
/>
|
|
76
|
+
)}
|
|
77
|
+
</>
|
|
78
|
+
);
|
|
79
|
+
}}
|
|
80
|
+
</FormDataConsumer>
|
|
81
|
+
<SelectInput
|
|
82
|
+
source="trigger_id"
|
|
83
|
+
choices={[
|
|
84
|
+
{ id: "pre-user-registration", name: "Pre User Registration" },
|
|
85
|
+
{ id: "post-user-registration", name: "Post User Registration" },
|
|
86
|
+
{ id: "post-user-login", name: "Post User Login" },
|
|
87
|
+
{
|
|
88
|
+
id: "validate-registration-username",
|
|
89
|
+
name: "Validate Registration Username",
|
|
90
|
+
},
|
|
91
|
+
{ id: "pre-user-deletion", name: "Pre User Deletion" },
|
|
92
|
+
{ id: "post-user-deletion", name: "Post User Deletion" },
|
|
93
|
+
]}
|
|
94
|
+
required
|
|
95
|
+
/>
|
|
96
|
+
<BooleanInput source="enabled" />
|
|
97
|
+
<BooleanInput
|
|
98
|
+
source="synchronous"
|
|
99
|
+
helperText="The event waits for the webhook to complete and can be canceled"
|
|
100
|
+
/>
|
|
101
|
+
<NumberInput
|
|
102
|
+
source="priority"
|
|
103
|
+
helperText="A hook with higher priority will be executed first"
|
|
104
|
+
/>
|
|
105
|
+
<Labeled label={<FieldTitle source="created_at" />}>
|
|
106
|
+
<DateField source="created_at" showTime={true} />
|
|
107
|
+
</Labeled>
|
|
108
|
+
<Labeled label={<FieldTitle source="updated_at" />}>
|
|
109
|
+
<DateField source="updated_at" showTime={true} />
|
|
110
|
+
</Labeled>
|
|
111
|
+
</SimpleForm>
|
|
112
|
+
</Edit>
|
|
113
|
+
);
|
|
114
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { List, Datagrid, TextField, BooleanField } from "react-admin";
|
|
2
|
+
import { PostListActions } from "../listActions/PostListActions";
|
|
3
|
+
|
|
4
|
+
export function HookList() {
|
|
5
|
+
return (
|
|
6
|
+
<List actions={<PostListActions />}>
|
|
7
|
+
<Datagrid rowClick="edit" bulkActionButtons={false}>
|
|
8
|
+
<TextField source="id" />
|
|
9
|
+
<TextField source="trigger_id" />
|
|
10
|
+
<TextField source="url" />
|
|
11
|
+
<TextField source="form_id" label="Form" />
|
|
12
|
+
<BooleanField source="enabled" />
|
|
13
|
+
<BooleanField source="synchronous" />
|
|
14
|
+
</Datagrid>
|
|
15
|
+
</List>
|
|
16
|
+
);
|
|
17
|
+
}
|