@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.
Files changed (110) hide show
  1. package/.eslintrc.js +21 -0
  2. package/.vercelignore +4 -0
  3. package/CHANGELOG.md +56 -0
  4. package/LICENSE +21 -0
  5. package/README.md +50 -0
  6. package/index.html +125 -0
  7. package/package.json +61 -0
  8. package/prettier.config.js +1 -0
  9. package/public/favicon.ico +0 -0
  10. package/public/manifest.json +15 -0
  11. package/src/App.spec.tsx +42 -0
  12. package/src/App.tsx +232 -0
  13. package/src/AuthCallback.tsx +138 -0
  14. package/src/Layout.tsx +12 -0
  15. package/src/TenantsApp.tsx +115 -0
  16. package/src/auth0DataProvider.ts +1242 -0
  17. package/src/authProvider.ts +521 -0
  18. package/src/components/CertificateErrorDialog.tsx +116 -0
  19. package/src/components/DomainSelector.tsx +401 -0
  20. package/src/components/TenantAppBar.tsx +83 -0
  21. package/src/components/TenantLayout.tsx +25 -0
  22. package/src/components/TenantsAppBar.tsx +21 -0
  23. package/src/components/TenantsLayout.tsx +28 -0
  24. package/src/components/activity/ActivityDashboard.tsx +381 -0
  25. package/src/components/activity/index.ts +1 -0
  26. package/src/components/branding/BrandingList.tsx +0 -0
  27. package/src/components/branding/BrandingShow.tsx +0 -0
  28. package/src/components/branding/ThemesTab.tsx +286 -0
  29. package/src/components/branding/edit.tsx +149 -0
  30. package/src/components/branding/hooks/useThemesData.ts +123 -0
  31. package/src/components/branding/index.ts +2 -0
  32. package/src/components/branding/list.tsx +12 -0
  33. package/src/components/clients/create.tsx +12 -0
  34. package/src/components/clients/edit.tsx +1285 -0
  35. package/src/components/clients/index.ts +3 -0
  36. package/src/components/clients/list.tsx +37 -0
  37. package/src/components/common/DateAgo.tsx +6 -0
  38. package/src/components/common/JsonOutput.tsx +26 -0
  39. package/src/components/common/index.ts +1 -0
  40. package/src/components/connections/create.tsx +35 -0
  41. package/src/components/connections/edit.tsx +212 -0
  42. package/src/components/connections/index.ts +3 -0
  43. package/src/components/connections/list.tsx +15 -0
  44. package/src/components/custom-domains/create.tsx +26 -0
  45. package/src/components/custom-domains/edit.tsx +101 -0
  46. package/src/components/custom-domains/index.ts +3 -0
  47. package/src/components/custom-domains/list.tsx +16 -0
  48. package/src/components/flows/create.tsx +30 -0
  49. package/src/components/flows/edit.tsx +238 -0
  50. package/src/components/flows/index.ts +3 -0
  51. package/src/components/flows/list.tsx +15 -0
  52. package/src/components/forms/FlowEditor.tsx +1363 -0
  53. package/src/components/forms/NodeEditor.tsx +1119 -0
  54. package/src/components/forms/RichTextEditor.tsx +145 -0
  55. package/src/components/forms/create.tsx +30 -0
  56. package/src/components/forms/edit.tsx +256 -0
  57. package/src/components/forms/index.ts +3 -0
  58. package/src/components/forms/list.tsx +16 -0
  59. package/src/components/hooks/create.tsx +96 -0
  60. package/src/components/hooks/edit.tsx +114 -0
  61. package/src/components/hooks/index.ts +3 -0
  62. package/src/components/hooks/list.tsx +17 -0
  63. package/src/components/listActions/PostListActions.tsx +10 -0
  64. package/src/components/logs/LogIcon.tsx +32 -0
  65. package/src/components/logs/LogShow.tsx +82 -0
  66. package/src/components/logs/LogType.tsx +38 -0
  67. package/src/components/logs/index.ts +4 -0
  68. package/src/components/logs/list.tsx +41 -0
  69. package/src/components/organizations/create.tsx +13 -0
  70. package/src/components/organizations/edit.tsx +682 -0
  71. package/src/components/organizations/index.ts +3 -0
  72. package/src/components/organizations/list.tsx +21 -0
  73. package/src/components/resource-servers/create.tsx +87 -0
  74. package/src/components/resource-servers/edit.tsx +121 -0
  75. package/src/components/resource-servers/index.ts +3 -0
  76. package/src/components/resource-servers/list.tsx +47 -0
  77. package/src/components/roles/create.tsx +12 -0
  78. package/src/components/roles/edit.tsx +426 -0
  79. package/src/components/roles/index.ts +3 -0
  80. package/src/components/roles/list.tsx +24 -0
  81. package/src/components/sessions/edit.tsx +101 -0
  82. package/src/components/sessions/index.ts +3 -0
  83. package/src/components/sessions/list.tsx +20 -0
  84. package/src/components/sessions/show.tsx +113 -0
  85. package/src/components/settings/edit.tsx +236 -0
  86. package/src/components/settings/index.ts +2 -0
  87. package/src/components/settings/list.tsx +14 -0
  88. package/src/components/tenants/create.tsx +20 -0
  89. package/src/components/tenants/edit.tsx +54 -0
  90. package/src/components/tenants/index.ts +2 -0
  91. package/src/components/tenants/list.tsx +67 -0
  92. package/src/components/themes/edit.tsx +200 -0
  93. package/src/components/themes/index.ts +2 -0
  94. package/src/components/themes/list.tsx +12 -0
  95. package/src/components/users/create.tsx +144 -0
  96. package/src/components/users/edit.tsx +1711 -0
  97. package/src/components/users/index.ts +3 -0
  98. package/src/components/users/list.tsx +35 -0
  99. package/src/data.json +121 -0
  100. package/src/dataProvider.ts +97 -0
  101. package/src/index.tsx +106 -0
  102. package/src/lib/logs.ts +21 -0
  103. package/src/types/reactflow.d.ts +86 -0
  104. package/src/utils/domainUtils.ts +169 -0
  105. package/src/utils/tokenUtils.ts +75 -0
  106. package/src/vite-env.d.ts +1 -0
  107. package/tsconfig.json +37 -0
  108. package/tsconfig.node.json +10 -0
  109. package/vercel.json +17 -0
  110. 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,3 @@
1
+ export { FormCreate } from "./create.tsx";
2
+ export { FormsList } from "./list.tsx";
3
+ export { FormEdit } from "./edit.tsx";
@@ -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,3 @@
1
+ export * from "./create";
2
+ export * from "./list";
3
+ export * from "./edit";
@@ -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
+ }
@@ -0,0 +1,10 @@
1
+ import { CreateButton, ExportButton, TopToolbar } from "react-admin";
2
+
3
+ export function PostListActions() {
4
+ return (
5
+ <TopToolbar>
6
+ <CreateButton />
7
+ <ExportButton />
8
+ </TopToolbar>
9
+ );
10
+ }