@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,682 @@
1
+ import {
2
+ Edit,
3
+ TextInput,
4
+ required,
5
+ TabbedForm,
6
+ ReferenceManyField,
7
+ Datagrid,
8
+ Pagination,
9
+ TextField,
10
+ FunctionField,
11
+ useDataProvider,
12
+ useNotify,
13
+ useRefresh,
14
+ useRecordContext,
15
+ useRedirect,
16
+ } from "react-admin";
17
+ import { useState } from "react";
18
+ import {
19
+ Box,
20
+ Button,
21
+ Dialog,
22
+ DialogActions,
23
+ DialogContent,
24
+ DialogContentText,
25
+ DialogTitle,
26
+ Typography,
27
+ CircularProgress,
28
+ IconButton,
29
+ TextField as MuiTextField,
30
+ List,
31
+ ListItem,
32
+ ListItemButton,
33
+ ListItemText,
34
+ Checkbox,
35
+ Chip,
36
+ Stack,
37
+ } from "@mui/material";
38
+ import AddIcon from "@mui/icons-material/Add";
39
+ import DeleteIcon from "@mui/icons-material/Delete";
40
+ import { useParams } from "react-router-dom";
41
+
42
+ const AddOrganizationMemberButton = () => {
43
+ const [open, setOpen] = useState(false);
44
+ const [users, setUsers] = useState<any[]>([]);
45
+ const [selectedUsers, setSelectedUsers] = useState<any[]>([]);
46
+ const [searchText, setSearchText] = useState("");
47
+ const [searchLoading, setSearchLoading] = useState(false);
48
+ const dataProvider = useDataProvider();
49
+ const notify = useNotify();
50
+ const refresh = useRefresh();
51
+
52
+ // Get organization id from the route params (/:tenantId/organizations/:id)
53
+ const { id: organizationId } = useParams();
54
+
55
+ const toggleUserSelection = (user: any) => {
56
+ const isSelected = selectedUsers.some(
57
+ (selected) => selected.user_id === user.user_id,
58
+ );
59
+ if (isSelected) {
60
+ setSelectedUsers(
61
+ selectedUsers.filter((selected) => selected.user_id !== user.user_id),
62
+ );
63
+ } else {
64
+ setSelectedUsers([...selectedUsers, user]);
65
+ }
66
+ };
67
+
68
+ const removeSelectedUser = (userToRemove: any) => {
69
+ setSelectedUsers(
70
+ selectedUsers.filter((user) => user.user_id !== userToRemove.user_id),
71
+ );
72
+ };
73
+
74
+ const selectAllUsers = () => {
75
+ setSelectedUsers([...users]);
76
+ };
77
+
78
+ const clearAllUsers = () => {
79
+ setSelectedUsers([]);
80
+ };
81
+
82
+ const searchUsers = async (query: string) => {
83
+ if (!query.trim()) {
84
+ setUsers([]);
85
+ return;
86
+ }
87
+
88
+ setSearchLoading(true);
89
+ try {
90
+ // Search users by email, name, or user_id
91
+ const { data } = await dataProvider.getList("users", {
92
+ pagination: { page: 1, perPage: 50 },
93
+ sort: { field: "email", order: "ASC" },
94
+ filter: { q: query },
95
+ });
96
+
97
+ // Get existing organization members to filter them out
98
+ if (organizationId) {
99
+ try {
100
+ // Use getManyReference to get organization members
101
+ const existingMembers = await dataProvider.getManyReference(
102
+ "organization-members",
103
+ {
104
+ target: "organization_id",
105
+ id: organizationId,
106
+ pagination: { page: 1, perPage: 1000 },
107
+ sort: { field: "user_id", order: "ASC" },
108
+ filter: {},
109
+ },
110
+ );
111
+
112
+ const existingUserIds = new Set(
113
+ existingMembers.data.map((member: any) => member.user_id),
114
+ );
115
+
116
+ // Filter out users who are already members
117
+ const availableUsers = data.filter(
118
+ (user: any) => !existingUserIds.has(user.user_id),
119
+ );
120
+ setUsers(availableUsers);
121
+ } catch (memberError) {
122
+ // If we can't get members, just show all users
123
+ console.warn("Could not fetch existing members:", memberError);
124
+ setUsers(data);
125
+ }
126
+ } else {
127
+ setUsers(data);
128
+ }
129
+ } catch (error) {
130
+ notify("Error searching users", { type: "error" });
131
+ setUsers([]);
132
+ } finally {
133
+ setSearchLoading(false);
134
+ }
135
+ };
136
+
137
+ // Debounced search function
138
+ const [searchTimeout, setSearchTimeout] = useState<NodeJS.Timeout | null>(
139
+ null,
140
+ );
141
+
142
+ const handleSearchChange = (value: string) => {
143
+ setSearchText(value);
144
+
145
+ // Clear previous timeout
146
+ if (searchTimeout) {
147
+ clearTimeout(searchTimeout);
148
+ }
149
+
150
+ // Set new timeout for debounced search
151
+ const timeout = setTimeout(() => {
152
+ searchUsers(value);
153
+ }, 300); // 300ms delay
154
+
155
+ setSearchTimeout(timeout);
156
+ };
157
+
158
+ const handleOpen = () => {
159
+ setOpen(true);
160
+ setUsers([]);
161
+ setSearchText("");
162
+ setSelectedUsers([]);
163
+ };
164
+
165
+ const handleClose = () => {
166
+ setOpen(false);
167
+ setSelectedUsers([]);
168
+ setUsers([]);
169
+ setSearchText("");
170
+ if (searchTimeout) {
171
+ clearTimeout(searchTimeout);
172
+ }
173
+ };
174
+
175
+ const handleAddMembers = async () => {
176
+ if (!organizationId || selectedUsers.length === 0) return;
177
+
178
+ try {
179
+ // Use the bulk add members endpoint with all selected users
180
+ await dataProvider.create("organization-members", {
181
+ data: {
182
+ organization_id: organizationId,
183
+ user_ids: selectedUsers.map((user) => user.user_id), // Send all user IDs at once
184
+ },
185
+ });
186
+
187
+ notify(`Added ${selectedUsers.length} member(s) to organization`, {
188
+ type: "success",
189
+ });
190
+ refresh();
191
+ handleClose();
192
+ } catch (error) {
193
+ notify("Error adding members to organization", { type: "error" });
194
+ }
195
+ };
196
+
197
+ return (
198
+ <>
199
+ <Button
200
+ variant="contained"
201
+ startIcon={<AddIcon />}
202
+ onClick={handleOpen}
203
+ sx={{ mb: 2 }}
204
+ >
205
+ Add Members
206
+ </Button>
207
+
208
+ <Dialog open={open} onClose={handleClose} maxWidth="md" fullWidth>
209
+ <DialogTitle>Add Members to Organization</DialogTitle>
210
+ <DialogContent>
211
+ <DialogContentText sx={{ mb: 2 }}>
212
+ Search and select users to add as members of this organization.
213
+ </DialogContentText>
214
+
215
+ {/* Search Field */}
216
+ <MuiTextField
217
+ fullWidth
218
+ label="Search Users"
219
+ placeholder="Search by email, name, or user ID..."
220
+ value={searchText}
221
+ onChange={(e) => handleSearchChange(e.target.value)}
222
+ sx={{ mb: 2 }}
223
+ InputProps={{
224
+ endAdornment: searchLoading && <CircularProgress size={20} />,
225
+ }}
226
+ />
227
+
228
+ {/* Selected Users Display */}
229
+ {selectedUsers.length > 0 && (
230
+ <Box sx={{ mb: 2 }}>
231
+ <Typography variant="subtitle2" gutterBottom>
232
+ Selected Users ({selectedUsers.length})
233
+ </Typography>
234
+ <Stack direction="row" spacing={1} flexWrap="wrap" useFlexGap>
235
+ {selectedUsers.map((user) => (
236
+ <Chip
237
+ key={user.user_id}
238
+ label={`${user.name || user.email || user.user_id}`}
239
+ onDelete={() => removeSelectedUser(user)}
240
+ size="small"
241
+ variant="outlined"
242
+ />
243
+ ))}
244
+ </Stack>
245
+ </Box>
246
+ )}
247
+
248
+ {/* User Search Results */}
249
+ {users.length > 0 ? (
250
+ <>
251
+ <Box
252
+ sx={{
253
+ display: "flex",
254
+ justifyContent: "space-between",
255
+ alignItems: "center",
256
+ mb: 1,
257
+ }}
258
+ >
259
+ <Typography variant="body2" color="text.secondary">
260
+ Found {users.length} available user
261
+ {users.length !== 1 ? "s" : ""}
262
+ </Typography>
263
+ <Box sx={{ display: "flex", gap: 1 }}>
264
+ <Button
265
+ size="small"
266
+ onClick={selectAllUsers}
267
+ disabled={selectedUsers.length === users.length}
268
+ >
269
+ Select All
270
+ </Button>
271
+ <Button
272
+ size="small"
273
+ onClick={clearAllUsers}
274
+ disabled={selectedUsers.length === 0}
275
+ >
276
+ Clear All
277
+ </Button>
278
+ </Box>
279
+ </Box>
280
+ <Box
281
+ sx={{
282
+ maxHeight: 300,
283
+ overflow: "auto",
284
+ border: 1,
285
+ borderColor: "divider",
286
+ borderRadius: 1,
287
+ }}
288
+ >
289
+ <List dense>
290
+ {users.map((user) => {
291
+ const isSelected = selectedUsers.some(
292
+ (selected) => selected.user_id === user.user_id,
293
+ );
294
+ return (
295
+ <ListItem key={user.user_id} disablePadding>
296
+ <ListItemButton
297
+ onClick={() => toggleUserSelection(user)}
298
+ sx={{
299
+ "&:hover": { backgroundColor: "action.hover" },
300
+ backgroundColor: isSelected
301
+ ? "action.selected"
302
+ : "transparent",
303
+ }}
304
+ >
305
+ <Checkbox
306
+ checked={isSelected}
307
+ tabIndex={-1}
308
+ disableRipple
309
+ sx={{ mr: 1 }}
310
+ />
311
+ <ListItemText
312
+ primary={
313
+ <Typography variant="body2" fontWeight="medium">
314
+ {user.name || user.email || user.user_id}
315
+ </Typography>
316
+ }
317
+ secondary={
318
+ <Typography
319
+ variant="caption"
320
+ color="text.secondary"
321
+ >
322
+ {user.email} • {user.user_id}
323
+ </Typography>
324
+ }
325
+ />
326
+ </ListItemButton>
327
+ </ListItem>
328
+ );
329
+ })}
330
+ </List>
331
+ </Box>
332
+ </>
333
+ ) : searchText.trim() ? (
334
+ <Box textAlign="center" py={3}>
335
+ {searchLoading ? (
336
+ <CircularProgress />
337
+ ) : (
338
+ <Typography color="text.secondary">
339
+ No users found matching "{searchText}"
340
+ </Typography>
341
+ )}
342
+ </Box>
343
+ ) : (
344
+ <Box textAlign="center" py={3}>
345
+ <Typography color="text.secondary">
346
+ Start typing to search for users
347
+ </Typography>
348
+ </Box>
349
+ )}
350
+ </DialogContent>
351
+ <DialogActions>
352
+ <Button onClick={handleClose}>Cancel</Button>
353
+ <Button
354
+ onClick={handleAddMembers}
355
+ variant="contained"
356
+ disabled={selectedUsers.length === 0}
357
+ >
358
+ Add Members
359
+ </Button>
360
+ </DialogActions>
361
+ </Dialog>
362
+ </>
363
+ );
364
+ };
365
+
366
+ const RemoveMemberButton = ({ record }: { record: any }) => {
367
+ const dataProvider = useDataProvider();
368
+ const notify = useNotify();
369
+ const refresh = useRefresh();
370
+ const { id: organizationId } = useParams();
371
+
372
+ const handleRemoveMember = async () => {
373
+ if (!organizationId || !record?.user_id) return;
374
+
375
+ try {
376
+ // Use the bulk remove members endpoint
377
+ await dataProvider.delete("organization-members", {
378
+ id: organizationId,
379
+ previousData: {
380
+ id: organizationId,
381
+ members: [record.user_id],
382
+ },
383
+ });
384
+
385
+ notify("Member removed from organization", { type: "success" });
386
+ refresh();
387
+ } catch (error) {
388
+ notify("Error removing member from organization", { type: "error" });
389
+ }
390
+ };
391
+
392
+ return (
393
+ <IconButton
394
+ onClick={handleRemoveMember}
395
+ color="error"
396
+ size="small"
397
+ title="Remove from organization"
398
+ >
399
+ <DeleteIcon />
400
+ </IconButton>
401
+ );
402
+ };
403
+
404
+ const OrganizationGeneralTab = () => (
405
+ <Box>
406
+ <TextInput source="name" validate={[required()]} fullWidth />
407
+ <TextInput source="display_name" fullWidth />
408
+ <TextInput source="description" multiline fullWidth />
409
+ </Box>
410
+ );
411
+
412
+ const OrganizationMembersTab = () => {
413
+ const record = useRecordContext();
414
+ const redirect = useRedirect();
415
+
416
+ if (!record?.id) {
417
+ return (
418
+ <Typography>Save the organization first to manage members.</Typography>
419
+ );
420
+ }
421
+
422
+ return (
423
+ <Box>
424
+ <Typography variant="h6" gutterBottom>
425
+ Organization Members
426
+ </Typography>
427
+
428
+ <AddOrganizationMemberButton />
429
+
430
+ <ReferenceManyField
431
+ reference="organization-members"
432
+ target="organization_id"
433
+ pagination={<Pagination />}
434
+ >
435
+ <Datagrid bulkActionButtons={false}>
436
+ <FunctionField
437
+ label="User ID"
438
+ render={(record) => (
439
+ <Button
440
+ variant="text"
441
+ onClick={() => redirect("edit", "users", record.user_id)}
442
+ sx={{
443
+ textTransform: "none",
444
+ p: 0,
445
+ minWidth: "auto",
446
+ color: "primary.main",
447
+ textDecoration: "underline",
448
+ "&:hover": {
449
+ textDecoration: "underline",
450
+ backgroundColor: "transparent",
451
+ },
452
+ }}
453
+ >
454
+ {record.user_id}
455
+ </Button>
456
+ )}
457
+ />
458
+ <TextField source="email" label="Email" />
459
+ <FunctionField
460
+ label="Actions"
461
+ render={(record) => <RemoveMemberButton record={record} />}
462
+ />
463
+ </Datagrid>
464
+ </ReferenceManyField>
465
+ </Box>
466
+ );
467
+ };
468
+
469
+ const CreateInviteButton = () => {
470
+ const [open, setOpen] = useState(false);
471
+ const [inviteData, setInviteData] = useState({
472
+ inviterName: "",
473
+ inviteeEmail: "",
474
+ clientId: "",
475
+ });
476
+ const dataProvider = useDataProvider();
477
+ const notify = useNotify();
478
+ const refresh = useRefresh();
479
+ const { id: organizationId } = useParams();
480
+
481
+ const handleOpen = () => {
482
+ setOpen(true);
483
+ };
484
+
485
+ const handleClose = () => {
486
+ setOpen(false);
487
+ setInviteData({
488
+ inviterName: "",
489
+ inviteeEmail: "",
490
+ clientId: "",
491
+ });
492
+ };
493
+
494
+ const handleCreateInvite = async () => {
495
+ if (!organizationId) return;
496
+
497
+ try {
498
+ await dataProvider.create("organization-invitations", {
499
+ data: {
500
+ organization_id: organizationId,
501
+ inviter: { name: inviteData.inviterName },
502
+ invitee: { email: inviteData.inviteeEmail },
503
+ client_id: inviteData.clientId,
504
+ },
505
+ });
506
+
507
+ notify("Invitation created successfully", { type: "success" });
508
+ refresh();
509
+ handleClose();
510
+ } catch (error) {
511
+ notify("Error creating invitation", { type: "error" });
512
+ }
513
+ };
514
+
515
+ return (
516
+ <>
517
+ <Button
518
+ variant="contained"
519
+ startIcon={<AddIcon />}
520
+ onClick={handleOpen}
521
+ sx={{ mb: 2 }}
522
+ >
523
+ Create Invitation
524
+ </Button>
525
+
526
+ <Dialog open={open} onClose={handleClose} maxWidth="sm" fullWidth>
527
+ <DialogTitle>Create Organization Invitation</DialogTitle>
528
+ <DialogContent>
529
+ <DialogContentText sx={{ mb: 2 }}>
530
+ Create an invitation to invite a user to join this organization.
531
+ </DialogContentText>
532
+
533
+ <MuiTextField
534
+ fullWidth
535
+ label="Inviter Name"
536
+ value={inviteData.inviterName}
537
+ onChange={(e) =>
538
+ setInviteData({ ...inviteData, inviterName: e.target.value })
539
+ }
540
+ sx={{ mb: 2, mt: 1 }}
541
+ required
542
+ />
543
+
544
+ <MuiTextField
545
+ fullWidth
546
+ label="Invitee Email"
547
+ type="email"
548
+ value={inviteData.inviteeEmail}
549
+ onChange={(e) =>
550
+ setInviteData({ ...inviteData, inviteeEmail: e.target.value })
551
+ }
552
+ sx={{ mb: 2 }}
553
+ required
554
+ />
555
+
556
+ <MuiTextField
557
+ fullWidth
558
+ label="Client ID"
559
+ value={inviteData.clientId}
560
+ onChange={(e) =>
561
+ setInviteData({ ...inviteData, clientId: e.target.value })
562
+ }
563
+ required
564
+ />
565
+ </DialogContent>
566
+ <DialogActions>
567
+ <Button onClick={handleClose}>Cancel</Button>
568
+ <Button
569
+ onClick={handleCreateInvite}
570
+ variant="contained"
571
+ disabled={
572
+ !inviteData.inviterName ||
573
+ !inviteData.inviteeEmail ||
574
+ !inviteData.clientId
575
+ }
576
+ >
577
+ Create Invitation
578
+ </Button>
579
+ </DialogActions>
580
+ </Dialog>
581
+ </>
582
+ );
583
+ };
584
+
585
+ const DeleteInviteButton = ({ record }: { record: any }) => {
586
+ const dataProvider = useDataProvider();
587
+ const notify = useNotify();
588
+ const refresh = useRefresh();
589
+ const { id: organizationId } = useParams();
590
+
591
+ const handleDeleteInvite = async () => {
592
+ if (!organizationId || !record?.id) return;
593
+
594
+ try {
595
+ await dataProvider.delete("organization-invitations", {
596
+ id: record.id,
597
+ previousData: record,
598
+ });
599
+
600
+ notify("Invitation deleted successfully", { type: "success" });
601
+ refresh();
602
+ } catch (error) {
603
+ notify("Error deleting invitation", { type: "error" });
604
+ }
605
+ };
606
+
607
+ return (
608
+ <IconButton
609
+ onClick={handleDeleteInvite}
610
+ color="error"
611
+ size="small"
612
+ title="Delete invitation"
613
+ >
614
+ <DeleteIcon />
615
+ </IconButton>
616
+ );
617
+ };
618
+
619
+ const OrganizationInvitesTab = () => {
620
+ const record = useRecordContext();
621
+
622
+ if (!record?.id) {
623
+ return (
624
+ <Typography>
625
+ Save the organization first to manage invitations.
626
+ </Typography>
627
+ );
628
+ }
629
+
630
+ return (
631
+ <Box>
632
+ <Typography variant="h6" gutterBottom>
633
+ Organization Invitations
634
+ </Typography>
635
+
636
+ <CreateInviteButton />
637
+
638
+ <ReferenceManyField
639
+ reference="organization-invitations"
640
+ target="organization_id"
641
+ pagination={<Pagination />}
642
+ >
643
+ <Datagrid bulkActionButtons={false}>
644
+ <TextField source="id" label="Invitation ID" />
645
+ <FunctionField
646
+ label="Invitee Email"
647
+ render={(record) => record.invitee?.email || "-"}
648
+ />
649
+ <FunctionField
650
+ label="Inviter Name"
651
+ render={(record) => record.inviter?.name || "-"}
652
+ />
653
+ <TextField source="client_id" label="Client ID" />
654
+ <TextField source="created_at" label="Created At" />
655
+ <TextField source="expires_at" label="Expires At" />
656
+ <FunctionField
657
+ label="Actions"
658
+ render={(record) => <DeleteInviteButton record={record} />}
659
+ />
660
+ </Datagrid>
661
+ </ReferenceManyField>
662
+ </Box>
663
+ );
664
+ };
665
+
666
+ export function OrganizationEdit() {
667
+ return (
668
+ <Edit>
669
+ <TabbedForm>
670
+ <TabbedForm.Tab label="General">
671
+ <OrganizationGeneralTab />
672
+ </TabbedForm.Tab>
673
+ <TabbedForm.Tab label="Members">
674
+ <OrganizationMembersTab />
675
+ </TabbedForm.Tab>
676
+ <TabbedForm.Tab label="Invites">
677
+ <OrganizationInvitesTab />
678
+ </TabbedForm.Tab>
679
+ </TabbedForm>
680
+ </Edit>
681
+ );
682
+ }
@@ -0,0 +1,3 @@
1
+ export * from "./list";
2
+ export * from "./create";
3
+ export * from "./edit";
@@ -0,0 +1,21 @@
1
+ import { List, Datagrid, TextField, DateField, TextInput } from "react-admin";
2
+ import { PostListActions } from "../listActions/PostListActions";
3
+
4
+ export function OrganizationList() {
5
+ const filters = [
6
+ <TextInput key="search" label="Search" source="q" alwaysOn />,
7
+ ];
8
+
9
+ return (
10
+ <List actions={<PostListActions />} filters={filters}>
11
+ <Datagrid rowClick="edit" bulkActionButtons={false}>
12
+ <TextField source="id" />
13
+ <TextField source="name" />
14
+ <TextField source="display_name" />
15
+ <TextField source="description" />
16
+ <DateField source="created_at" showTime={true} />
17
+ <DateField source="updated_at" showTime={true} />
18
+ </Datagrid>
19
+ </List>
20
+ );
21
+ }