@rebasepro/admin 0.2.3 → 0.2.5

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 (225) hide show
  1. package/dist/{CollectionEditorDialog-CmGXXSY9.js → CollectionEditorDialog-Cn8-tGyL.js} +89 -79
  2. package/dist/CollectionEditorDialog-Cn8-tGyL.js.map +1 -0
  3. package/dist/{CollectionsStudioView-DcLHT5bU.js → CollectionsStudioView-C-Ts1rZt.js} +5 -4
  4. package/dist/{CollectionsStudioView-DcLHT5bU.js.map → CollectionsStudioView-C-Ts1rZt.js.map} +1 -1
  5. package/dist/{ExportCollectionAction-BfN34eWX.js → ExportCollectionAction-BRdKM3DF.js} +4 -3
  6. package/dist/ExportCollectionAction-BRdKM3DF.js.map +1 -0
  7. package/dist/{ImportCollectionAction-SZrInjhx.js → ImportCollectionAction-U-v7lGxO.js} +3 -2
  8. package/dist/{ImportCollectionAction-SZrInjhx.js.map → ImportCollectionAction-U-v7lGxO.js.map} +1 -1
  9. package/dist/{PropertyEditView-Cvryrb3B.js → PropertyEditView-BDNYkfNf.js} +128 -121
  10. package/dist/PropertyEditView-BDNYkfNf.js.map +1 -0
  11. package/dist/collection_editor/ConfigControllerProvider.d.ts +0 -5
  12. package/dist/collection_editor/index.d.ts +0 -1
  13. package/dist/collection_editor/types/collection_editor_controller.d.ts +0 -2
  14. package/dist/collection_editor/ui/collection_editor/CollectionPropertiesEditorForm.d.ts +3 -3
  15. package/dist/collection_editor_ui.js +3 -3
  16. package/dist/components/ArrayContainer.d.ts +2 -2
  17. package/dist/components/DefaultAppBar.d.ts +18 -1
  18. package/dist/components/DefaultDrawer.d.ts +51 -3
  19. package/dist/components/EntityCollectionTable/fields/TableStorageUpload.d.ts +2 -2
  20. package/dist/components/EntityCollectionTable/internal/EntityTableCell.d.ts +2 -2
  21. package/dist/components/EntityCollectionTable/table_bindings.d.ts +4 -3
  22. package/dist/components/EntityCollectionView/hooks/useKanbanDragAndDrop.d.ts +4 -3
  23. package/dist/components/EntityEditView.d.ts +2 -1
  24. package/dist/components/HomePage/HomePageDnD.d.ts +3 -3
  25. package/dist/components/PropertyCollectionView.d.ts +1 -1
  26. package/dist/components/PropertyIdCopyTooltip.d.ts +1 -1
  27. package/dist/components/RebaseRouteDefs.d.ts +1 -1
  28. package/dist/components/SelectableTable/SelectionStore.d.ts +4 -1
  29. package/dist/components/SelectableTable/filters/BooleanFilterField.d.ts +2 -2
  30. package/dist/components/SelectableTable/filters/DateTimeFilterField.d.ts +2 -2
  31. package/dist/components/SelectableTable/filters/ReferenceFilterField.d.ts +2 -2
  32. package/dist/components/SelectableTable/filters/StringNumberFilterField.d.ts +2 -2
  33. package/dist/components/admin/index.d.ts +1 -3
  34. package/dist/components/app/Drawer.d.ts +8 -1
  35. package/dist/data_export/export/export.d.ts +3 -3
  36. package/dist/editor/components/editor-bubble.d.ts +5 -1
  37. package/dist/editor/components/image-bubble.d.ts +5 -1
  38. package/dist/editor/components/index.d.ts +3 -3
  39. package/dist/editor/components/table-bubble.d.ts +5 -1
  40. package/dist/editor/nodeViews/ReactNodeView.d.ts +4 -1
  41. package/dist/editor/useProseMirror.d.ts +2 -2
  42. package/dist/editor/utils/remove_classes.d.ts +1 -1
  43. package/dist/editor.js +15 -14
  44. package/dist/editor.js.map +1 -1
  45. package/dist/form/EntityForm.d.ts +2 -2
  46. package/dist/form/components/StorageUploadProgress.d.ts +2 -2
  47. package/dist/form/field_bindings/MultiSelectFieldBinding.d.ts +1 -1
  48. package/dist/form/field_bindings/StorageUploadFieldBinding.d.ts +1 -1
  49. package/dist/form/validation.d.ts +3 -3
  50. package/dist/hooks/navigation/useBuildNavigationStateController.d.ts +1 -1
  51. package/dist/hooks/navigation/useResolvedCollections.d.ts +6 -0
  52. package/dist/hooks/navigation/useResolvedViews.d.ts +3 -7
  53. package/dist/{index-DjduZG1T.js → index-DHaOV-7A.js} +3 -3
  54. package/dist/index-DHaOV-7A.js.map +1 -0
  55. package/dist/{index-MKPc70-v.js → index-DJSL_SCr.js} +3 -3
  56. package/dist/index-DJSL_SCr.js.map +1 -0
  57. package/dist/{index-PLIQXpTt.js → index-XMII4H3d.js} +3 -2
  58. package/dist/{index-PLIQXpTt.js.map → index-XMII4H3d.js.map} +1 -1
  59. package/dist/index.d.ts +1 -3
  60. package/dist/index.js +2688 -452
  61. package/dist/index.js.map +1 -1
  62. package/dist/{markdown-z2Ir7Cgo.js → markdown-DD2JDU1X.js} +2 -2
  63. package/dist/markdown-DD2JDU1X.js.map +1 -0
  64. package/dist/preview/components/UrlComponentPreview.d.ts +1 -0
  65. package/dist/types/components/EntityFormActionsProps.d.ts +1 -1
  66. package/dist/types/components/EntityFormProps.d.ts +2 -2
  67. package/dist/types/fields.d.ts +1 -1
  68. package/dist/{util-DbWax_sV.js → util-0GYaJqL_.js} +1505 -2043
  69. package/dist/util-0GYaJqL_.js.map +1 -0
  70. package/package.json +8 -8
  71. package/src/collection_editor/ConfigControllerProvider.tsx +3 -13
  72. package/src/collection_editor/index.ts +1 -3
  73. package/src/collection_editor/pgColumnToProperty.ts +19 -2
  74. package/src/collection_editor/types/collection_editor_controller.tsx +0 -3
  75. package/src/collection_editor/ui/EditorCollectionAction.tsx +1 -6
  76. package/src/collection_editor/ui/EditorCollectionActionStart.tsx +1 -6
  77. package/src/collection_editor/ui/EditorEntityAction.tsx +1 -6
  78. package/src/collection_editor/ui/HomePageEditorCollectionAction.tsx +7 -14
  79. package/src/collection_editor/ui/NewCollectionCard.tsx +1 -5
  80. package/src/collection_editor/ui/PropertyAddColumnComponent.tsx +3 -8
  81. package/src/collection_editor/ui/collection_editor/CollectionJsonImportDialog.tsx +8 -12
  82. package/src/collection_editor/ui/collection_editor/CollectionPropertiesEditorForm.tsx +21 -21
  83. package/src/collection_editor/ui/collection_editor/CollectionRLSTab.tsx +4 -4
  84. package/src/collection_editor/ui/collection_editor/EnumForm.tsx +1 -1
  85. package/src/collection_editor/ui/collection_editor/properties/BlockPropertyField.tsx +3 -3
  86. package/src/collection_editor/ui/collection_editor/properties/CommonPropertyFields.tsx +3 -3
  87. package/src/collection_editor/ui/collection_editor/properties/DateTimePropertyField.tsx +8 -8
  88. package/src/collection_editor/ui/collection_editor/properties/EnumPropertyField.tsx +5 -5
  89. package/src/collection_editor/ui/collection_editor/properties/MapPropertyField.tsx +2 -2
  90. package/src/collection_editor/ui/collection_editor/properties/MarkdownPropertyField.tsx +5 -5
  91. package/src/collection_editor/ui/collection_editor/properties/NumberPropertyField.tsx +5 -5
  92. package/src/collection_editor/ui/collection_editor/properties/ReferencePropertyField.tsx +2 -2
  93. package/src/collection_editor/ui/collection_editor/properties/RepeatPropertyField.tsx +2 -2
  94. package/src/collection_editor/ui/collection_editor/properties/StoragePropertyField.tsx +8 -8
  95. package/src/collection_editor/ui/collection_editor/properties/StringPropertyField.tsx +5 -5
  96. package/src/collection_editor/ui/collection_editor/properties/UrlPropertyField.tsx +3 -2
  97. package/src/collection_editor/ui/collection_editor/properties/VectorPropertyField.tsx +2 -2
  98. package/src/collection_editor/ui/collection_editor/properties/validation/ArrayPropertyValidation.tsx +2 -2
  99. package/src/collection_editor/ui/collection_editor/properties/validation/GeneralPropertyValidation.tsx +1 -1
  100. package/src/collection_editor/ui/collection_editor/properties/validation/NumberPropertyValidation.tsx +4 -7
  101. package/src/collection_editor/ui/collection_editor/properties/validation/StringPropertyValidation.tsx +4 -4
  102. package/src/components/ArrayContainer.tsx +3 -3
  103. package/src/components/DefaultAppBar.tsx +52 -31
  104. package/src/components/DefaultDrawer.tsx +280 -67
  105. package/src/components/DrawerNavigationItem.tsx +1 -1
  106. package/src/components/EntityCollectionTable/EntityCollectionTable.tsx +6 -5
  107. package/src/components/EntityCollectionTable/PropertyTableCell.tsx +9 -7
  108. package/src/components/EntityCollectionTable/fields/TableStorageUpload.tsx +5 -5
  109. package/src/components/EntityCollectionTable/fields/VirtualTableNumberInput.tsx +12 -9
  110. package/src/components/EntityCollectionTable/internal/EntityTableCell.tsx +2 -2
  111. package/src/components/EntityCollectionTable/internal/popup_field/PopupFormField.tsx +1 -1
  112. package/src/components/EntityCollectionTable/table_bindings.tsx +5 -4
  113. package/src/components/EntityCollectionView/EntityCollectionCardView.tsx +4 -4
  114. package/src/components/EntityCollectionView/EntityCollectionListView.tsx +7 -0
  115. package/src/components/EntityCollectionView/EntityCollectionView.tsx +10 -5
  116. package/src/components/EntityCollectionView/hooks/useCollectionInlineEditor.ts +1 -1
  117. package/src/components/EntityCollectionView/hooks/useKanbanDragAndDrop.ts +7 -6
  118. package/src/components/EntityDetailView.tsx +46 -24
  119. package/src/components/EntityEditView.tsx +51 -28
  120. package/src/components/EntityEditViewFormActions.tsx +4 -4
  121. package/src/components/EntityPreview.tsx +9 -4
  122. package/src/components/HomePage/HomePageDnD.tsx +3 -2
  123. package/src/components/PropertyCollectionView.tsx +1 -1
  124. package/src/components/PropertyIdCopyTooltip.tsx +1 -1
  125. package/src/components/RebaseLayout.tsx +5 -1
  126. package/src/components/RebaseNavigation.tsx +2 -2
  127. package/src/components/RebaseRouteDefs.tsx +6 -11
  128. package/src/components/RebaseShell.tsx +16 -13
  129. package/src/components/SearchIconsView.tsx +1 -8
  130. package/src/components/SelectableTable/SelectableTable.tsx +8 -11
  131. package/src/components/SelectableTable/SelectionStore.ts +1 -1
  132. package/src/components/SelectableTable/filters/BooleanFilterField.tsx +3 -3
  133. package/src/components/SelectableTable/filters/DateTimeFilterField.tsx +3 -3
  134. package/src/components/SelectableTable/filters/ReferenceFilterField.tsx +5 -5
  135. package/src/components/SelectableTable/filters/StringNumberFilterField.tsx +3 -3
  136. package/src/components/SideEntityProvider.tsx +2 -1
  137. package/src/components/admin/index.ts +1 -3
  138. package/src/components/app/Drawer.tsx +9 -1
  139. package/src/components/app/Scaffold.tsx +5 -1
  140. package/src/components/index.ts +1 -3
  141. package/src/data_export/export/export.ts +17 -17
  142. package/src/data_import/components/DataNewPropertiesMapping.tsx +1 -1
  143. package/src/editor/components/editor-bubble.tsx +32 -9
  144. package/src/editor/components/image-bubble.tsx +27 -11
  145. package/src/editor/components/index.ts +3 -3
  146. package/src/editor/components/table-bubble.tsx +79 -17
  147. package/src/editor/extensions/HighlightDecorationExtension.ts +3 -2
  148. package/src/editor/nodeViews/ReactNodeView.tsx +1 -1
  149. package/src/editor/nodeViews/TaskItemComponent.tsx +9 -8
  150. package/src/editor/schema.ts +135 -59
  151. package/src/editor/selectors/link-selector.tsx +8 -5
  152. package/src/editor/useProseMirror.ts +2 -2
  153. package/src/editor/utils/remove_classes.ts +6 -5
  154. package/src/form/EntityForm.tsx +15 -15
  155. package/src/form/EntityFormActions.tsx +2 -2
  156. package/src/form/PropertyFieldBinding.tsx +64 -64
  157. package/src/form/components/FieldHelperText.tsx +4 -4
  158. package/src/form/components/StorageUploadProgress.tsx +2 -2
  159. package/src/form/field_bindings/ArrayCustomShapedFieldBinding.tsx +1 -1
  160. package/src/form/field_bindings/ArrayOfReferencesFieldBinding.tsx +1 -1
  161. package/src/form/field_bindings/BlockFieldBinding.tsx +54 -53
  162. package/src/form/field_bindings/KeyValueFieldBinding.tsx +290 -289
  163. package/src/form/field_bindings/MapFieldBinding.tsx +2 -2
  164. package/src/form/field_bindings/MultiSelectFieldBinding.tsx +2 -2
  165. package/src/form/field_bindings/MultipleRelationFieldBinding.tsx +1 -1
  166. package/src/form/field_bindings/ReferenceAsStringFieldBinding.tsx +1 -1
  167. package/src/form/field_bindings/ReferenceFieldBinding.tsx +8 -6
  168. package/src/form/field_bindings/RelationFieldBinding.tsx +4 -4
  169. package/src/form/field_bindings/RepeatFieldBinding.tsx +1 -1
  170. package/src/form/field_bindings/SelectFieldBinding.tsx +1 -1
  171. package/src/form/field_bindings/StorageUploadFieldBinding.tsx +84 -84
  172. package/src/form/field_bindings/SwitchFieldBinding.tsx +16 -16
  173. package/src/form/field_bindings/TextFieldBinding.tsx +77 -73
  174. package/src/form/field_bindings/UserSelectFieldBinding.tsx +17 -17
  175. package/src/form/validation.ts +43 -43
  176. package/src/hooks/navigation/useBuildNavigationStateController.tsx +4 -7
  177. package/src/hooks/navigation/useResolvedCollections.ts +27 -7
  178. package/src/hooks/navigation/useResolvedViews.tsx +8 -70
  179. package/src/index.ts +4 -3
  180. package/src/preview/PropertyPreview.tsx +2 -2
  181. package/src/preview/components/ImagePreview.tsx +2 -1
  182. package/src/preview/components/UrlComponentPreview.tsx +11 -2
  183. package/src/preview/components/UserPreview.tsx +1 -1
  184. package/src/preview/property_previews/ArrayOfMapsPreview.tsx +2 -2
  185. package/src/preview/property_previews/ArrayOfReferencesPreview.tsx +4 -4
  186. package/src/preview/property_previews/ArrayOfRelationsPreview.tsx +3 -3
  187. package/src/preview/property_previews/ArrayOfStorageComponentsPreview.tsx +3 -3
  188. package/src/preview/property_previews/ArrayOfStringsPreview.tsx +3 -2
  189. package/src/preview/property_previews/ArrayOneOfPreview.tsx +6 -8
  190. package/src/preview/property_previews/ArrayPropertyEnumPreview.tsx +1 -1
  191. package/src/preview/property_previews/ArrayPropertyPreview.tsx +3 -3
  192. package/src/preview/property_previews/MapPropertyPreview.tsx +4 -3
  193. package/src/preview/property_previews/NumberPropertyPreview.tsx +5 -3
  194. package/src/preview/property_previews/StringPropertyPreview.tsx +10 -8
  195. package/src/types/components/EntityFormActionsProps.tsx +1 -1
  196. package/src/types/components/EntityFormProps.tsx +2 -2
  197. package/src/types/fields.tsx +2 -2
  198. package/src/util/previews.ts +9 -1
  199. package/dist/CollectionEditorDialog-CmGXXSY9.js.map +0 -1
  200. package/dist/ContentHomePage-C7vFqKSe.js +0 -1784
  201. package/dist/ContentHomePage-C7vFqKSe.js.map +0 -1
  202. package/dist/ExportCollectionAction-BfN34eWX.js.map +0 -1
  203. package/dist/PropertyEditView-Cvryrb3B.js.map +0 -1
  204. package/dist/RoleChip-QtUFXeTp.js +0 -67
  205. package/dist/RoleChip-QtUFXeTp.js.map +0 -1
  206. package/dist/RolesView-BCb7qwWs.js +0 -437
  207. package/dist/RolesView-BCb7qwWs.js.map +0 -1
  208. package/dist/UsersView-Cex24r8H.js +0 -408
  209. package/dist/UsersView-Cex24r8H.js.map +0 -1
  210. package/dist/collection_editor/types/config_permissions.d.ts +0 -19
  211. package/dist/components/admin/RoleChip.d.ts +0 -4
  212. package/dist/components/admin/RolesFilterSelect.d.ts +0 -2
  213. package/dist/components/admin/RolesView.d.ts +0 -4
  214. package/dist/components/admin/UserRolesSelectField.d.ts +0 -2
  215. package/dist/components/admin/UsersView.d.ts +0 -4
  216. package/dist/index-DjduZG1T.js.map +0 -1
  217. package/dist/index-MKPc70-v.js.map +0 -1
  218. package/dist/markdown-z2Ir7Cgo.js.map +0 -1
  219. package/dist/util-DbWax_sV.js.map +0 -1
  220. package/src/collection_editor/types/config_permissions.ts +0 -20
  221. package/src/components/admin/RoleChip.tsx +0 -23
  222. package/src/components/admin/RolesFilterSelect.tsx +0 -45
  223. package/src/components/admin/RolesView.tsx +0 -465
  224. package/src/components/admin/UserRolesSelectField.tsx +0 -50
  225. package/src/components/admin/UsersView.tsx +0 -687
@@ -1,687 +0,0 @@
1
-
2
- import React, { useState, useEffect, useCallback, useRef } from "react";
3
- import { User } from "@rebasepro/types";
4
- import { useSnackbarController, useAuthController, useTranslation } from "@rebasepro/core";
5
- import { useBreadcrumbsController } from "../../index";
6
- import {
7
- Alert,
8
- Button,
9
- CenteredView,
10
- CheckCircleIcon,
11
- ChevronLeftIcon,
12
- ChevronRightIcon,
13
- CircularProgress,
14
- Container,
15
- CopyIcon,
16
- Dialog,
17
- DialogActions,
18
- DialogContent,
19
- DialogTitle,
20
- IconButton,
21
- iconSize,
22
- KeyRoundIcon,
23
- LoadingButton,
24
- MailIcon,
25
- MultiSelect,
26
- MultiSelectItem,
27
- PlusIcon,
28
- SearchBar,
29
- Select,
30
- SelectItem,
31
- Skeleton,
32
- Table,
33
- TableBody,
34
- TableCell,
35
- TableHeader,
36
- TableRow,
37
- TextField,
38
- Tooltip,
39
- Trash2Icon,
40
- Typography
41
- } from "@rebasepro/ui";
42
- import { RoleChip } from "./RoleChip";
43
- import { UserManagementDelegate, Role, UserCreationResult } from "@rebasepro/types";
44
- import { ConfirmationDialog, BootstrapAdminBanner } from "@rebasepro/core";
45
- import { CreationResultDialog } from "./CreationResultDialog";
46
-
47
-
48
- const PAGE_SIZE = 25;
49
-
50
- // ============================================
51
- // UsersView Component
52
- // ============================================
53
- export function UsersView({ userManagement }: {
54
- userManagement: UserManagementDelegate;
55
- }) {
56
- const { roles, saveUser, createUser, deleteUser, resetPassword, loading: delegateLoading, bootstrapAdmin, usersError } = userManagement;
57
- const snackbarController = useSnackbarController();
58
- const { user: loggedInUser } = useAuthController();
59
- const { t } = useTranslation();
60
- const breadcrumbs = useBreadcrumbsController();
61
-
62
- React.useEffect(() => {
63
- breadcrumbs.set({
64
- breadcrumbs: [{ title: t("users"),
65
- url: "/users" }]
66
- });
67
-
68
- }, []);
69
-
70
- const [dialogOpen, setDialogOpen] = useState(false);
71
- const [selectedUser, setSelectedUser] = useState<User | undefined>();
72
- const [deleteConfirmOpen, setDeleteConfirmOpen] = useState(false);
73
- const [userToDelete, setUserToDelete] = useState<User | undefined>();
74
- const [deleteInProgress, setDeleteInProgress] = useState(false);
75
- const [formKey, setFormKey] = useState(0);
76
- const [bootstrapping, setBootstrapping] = useState(false);
77
-
78
- // Creation result state
79
- const [creationResult, setCreationResult] = useState<UserCreationResult | null>(null);
80
-
81
- // Reset password
82
- const [resetConfirmOpen, setResetConfirmOpen] = useState(false);
83
- const [userToReset, setUserToReset] = useState<User | undefined>();
84
- const [resetInProgress, setResetInProgress] = useState(false);
85
-
86
- // Check if server-side search is available
87
- const hasServerSearch = !!userManagement.searchUsers;
88
-
89
- // ---- Server-side pagination state ----
90
- const [searchQuery, setSearchQuery] = useState("");
91
- const [roleFilter, setRoleFilter] = useState<string>("");
92
- const [page, setPage] = useState(0);
93
- const [paginatedUsers, setPaginatedUsers] = useState<User[]>([]);
94
- const [totalUsers, setTotalUsers] = useState(0);
95
- const [tableLoading, setTableLoading] = useState(hasServerSearch);
96
-
97
- // Debounce timer ref for search
98
- const searchTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
99
-
100
- // Fallback: use in-memory users if no searchUsers
101
- const allUsers = userManagement.users;
102
-
103
- /**
104
- * Fetch a page of users from the server.
105
- * Only shows the loading skeleton when we have no data yet (initial load).
106
- * Subsequent re-fetches (pagination, search) update in-place without flashing.
107
- */
108
- const fetchPage = useCallback(async (pageNum: number, search: string, filterRole: string, forceLoading = false) => {
109
- if (!userManagement.searchUsers) return;
110
-
111
- // Only show skeleton on initial load or explicit requests (search/filter/page change).
112
- // This avoids flashing skeletons when the effect re-fires from dep changes.
113
- if (forceLoading) {
114
- setTableLoading(true);
115
- }
116
- try {
117
- const result = await userManagement.searchUsers({
118
- search: search || undefined,
119
- roleId: filterRole || undefined,
120
- limit: PAGE_SIZE,
121
- offset: pageNum * PAGE_SIZE,
122
- orderBy: "createdAt",
123
- orderDir: "desc"
124
- });
125
- setPaginatedUsers(result.users);
126
- setTotalUsers(result.total);
127
- } catch (error: unknown) {
128
- console.error("Failed to fetch users:", error);
129
- snackbarController.open({ type: "error",
130
- message: error instanceof Error ? error.message : "Failed to load users" });
131
- } finally {
132
- setTableLoading(false);
133
- }
134
- }, [userManagement.searchUsers, snackbarController]);
135
-
136
- // Stable ref for fetchPage so the initial-load effect doesn't re-fire
137
- // every time fetchPage's reference changes (which happens on parent re-renders).
138
- const fetchPageRef = useRef(fetchPage);
139
- fetchPageRef.current = fetchPage;
140
- const initialFetchDone = useRef(false);
141
-
142
- // Load initial page when delegate finishes loading — runs exactly once.
143
- useEffect(() => {
144
- if (!delegateLoading && !usersError && hasServerSearch && !initialFetchDone.current) {
145
- initialFetchDone.current = true;
146
- fetchPageRef.current(0, "", roleFilter, true);
147
- }
148
- }, [delegateLoading, usersError, hasServerSearch, roleFilter]);
149
-
150
- // Handle search changes (debounced)
151
- const handleSearch = useCallback((value: string) => {
152
- setSearchQuery(value);
153
- setPage(0);
154
-
155
- if (searchTimerRef.current) {
156
- clearTimeout(searchTimerRef.current);
157
- }
158
-
159
- if (hasServerSearch) {
160
- searchTimerRef.current = setTimeout(() => {
161
- fetchPage(0, value, roleFilter, true);
162
- }, 300);
163
- }
164
- }, [hasServerSearch, fetchPage, roleFilter]);
165
-
166
- const handleRoleFilterChange = useCallback((newRole: string) => {
167
- setRoleFilter(newRole);
168
- setPage(0);
169
- if (hasServerSearch) {
170
- fetchPage(0, searchQuery, newRole, true);
171
- }
172
- }, [hasServerSearch, fetchPage, searchQuery]);
173
-
174
- // Handle page change
175
- const handlePageChange = useCallback((newPage: number) => {
176
- setPage(newPage);
177
- if (hasServerSearch) {
178
- fetchPage(newPage, searchQuery, roleFilter, true);
179
- }
180
- }, [hasServerSearch, fetchPage, searchQuery, roleFilter]);
181
-
182
- // Refresh current page (after create/update/delete)
183
- const refreshCurrentPage = useCallback(() => {
184
- if (hasServerSearch) {
185
- fetchPage(page, searchQuery, roleFilter);
186
- }
187
- }, [hasServerSearch, fetchPage, page, searchQuery, roleFilter]);
188
-
189
- // Determine which users to show
190
- let displayUsers: User[];
191
- let displayTotal: number;
192
-
193
- if (hasServerSearch) {
194
- displayUsers = paginatedUsers;
195
- displayTotal = totalUsers;
196
- } else {
197
- // Fallback: local filtering for backward compat
198
- const filtered = allUsers.filter(u => {
199
- let matches = true;
200
- if (searchQuery) {
201
- const q = searchQuery.toLowerCase();
202
- matches = !!(u.email?.toLowerCase().includes(q) || u.displayName?.toLowerCase().includes(q));
203
- }
204
- if (matches && roleFilter) {
205
- matches = !!u.roles?.includes(roleFilter);
206
- }
207
- return matches;
208
- });
209
- displayTotal = filtered.length;
210
- displayUsers = filtered.slice(page * PAGE_SIZE, (page + 1) * PAGE_SIZE);
211
- }
212
-
213
- const totalPages = Math.max(1, Math.ceil(displayTotal / PAGE_SIZE));
214
-
215
- // Check if any admin exists
216
- const hasAdmin = allUsers.some(u => u.roles?.includes("admin"));
217
-
218
- const handleBootstrap = async () => {
219
- if (!bootstrapAdmin) return;
220
- setBootstrapping(true);
221
- try {
222
- await bootstrapAdmin();
223
- snackbarController.open({ type: "success",
224
- message: t("bootstrap_admin_success") });
225
- window.location.reload();
226
- } catch (error: unknown) {
227
- snackbarController.open({ type: "error",
228
- message: error instanceof Error ? error.message : t("failed_to_bootstrap_admin") });
229
- } finally {
230
- setBootstrapping(false);
231
- }
232
- };
233
-
234
- const handleAddUser = () => {
235
- setSelectedUser(undefined);
236
- setFormKey(k => k + 1);
237
- setDialogOpen(true);
238
- };
239
-
240
- const handleEditUser = (user: User) => {
241
- setSelectedUser(user);
242
- setDialogOpen(true);
243
- };
244
-
245
- const handleClose = () => {
246
- setDialogOpen(false);
247
- setSelectedUser(undefined);
248
- };
249
-
250
- const handleDelete = async () => {
251
- if (!userToDelete || !deleteUser) return;
252
- setDeleteInProgress(true);
253
- try {
254
- await deleteUser(userToDelete);
255
- snackbarController.open({ type: "success",
256
- message: t("user_deleted_successfully") });
257
- setDeleteConfirmOpen(false);
258
- setUserToDelete(undefined);
259
- refreshCurrentPage();
260
- } catch (error: unknown) {
261
- snackbarController.open({ type: "error",
262
- message: error instanceof Error ? error.message : t("error_deleting_user") });
263
- } finally {
264
- setDeleteInProgress(false);
265
- }
266
- };
267
-
268
- const handleResetPassword = async () => {
269
- if (!userToReset || !resetPassword) return;
270
- setResetInProgress(true);
271
- try {
272
- const result = await resetPassword(userToReset);
273
- setResetConfirmOpen(false);
274
- setUserToReset(undefined);
275
- setCreationResult(result);
276
- snackbarController.open({ type: "success",
277
- message: t("reset_password_success") });
278
- } catch (error: unknown) {
279
- snackbarController.open({ type: "error",
280
- message: error instanceof Error ? error.message : t("error_resetting_password") });
281
- } finally {
282
- setResetInProgress(false);
283
- }
284
- };
285
-
286
- return (
287
- <Container className="w-full flex flex-col py-4 gap-4" maxWidth={"6xl"}>
288
- <BootstrapAdminBanner className="mb-4" />
289
-
290
- <div className="flex items-center mt-12 mb-4 gap-4">
291
- <Typography gutterBottom variant="h4" className="grow mb-0" component="h4">
292
- {t("users")}
293
- </Typography>
294
- {roles && roles.length > 0 && (
295
- <Select
296
- value={roleFilter || "__all__"}
297
- onValueChange={(v) => handleRoleFilterChange(v === "__all__" ? "" : v)}
298
- placeholder={t("all_roles") || "All Roles"}
299
- size="small"
300
- className="w-48"
301
- >
302
- <SelectItem value="__all__">{t("all_roles") || "All Roles"}</SelectItem>
303
- {roles.map(role => (
304
- <SelectItem key={role.id} value={role.id}>{role.name}</SelectItem>
305
- ))}
306
- </Select>
307
- )}
308
- <SearchBar
309
- placeholder={t("search_users")}
310
- onTextSearch={(v) => handleSearch(v || "")}
311
- size="small"
312
- expandable
313
- />
314
- <Button startIcon={<PlusIcon/>} onClick={handleAddUser} disabled={!saveUser}>
315
- {t("add_user")}
316
- </Button>
317
- </div>
318
-
319
- <div className="overflow-auto">
320
- <Table className="w-full">
321
- <TableHeader>
322
- <TableCell header className="w-48">{t("id") || "ID"}</TableCell>
323
- <TableCell header>{t("email")}</TableCell>
324
- <TableCell header>{t("name")}</TableCell>
325
- <TableCell header>{t("roles")}</TableCell>
326
- <TableCell header className="whitespace-nowrap">{t("created")}</TableCell>
327
- <TableCell header className="w-24 text-right">{t("actions")}</TableCell>
328
- </TableHeader>
329
- <TableBody>
330
- {(tableLoading || delegateLoading) ? (
331
- [
332
- { email: "w-48",
333
- name: "w-32",
334
- roles: ["w-16", "w-20"] },
335
- { email: "w-32",
336
- name: "w-24",
337
- roles: ["w-24"] },
338
- { email: "w-40",
339
- name: "w-36",
340
- roles: ["w-16", "w-16"] }
341
- ].map((row, i) => (
342
- <TableRow key={`skeleton-${i}`}>
343
- <TableCell className="font-mono text-xs"><Skeleton className="h-3 w-40"/></TableCell>
344
- <TableCell><Skeleton className={`h-4 ${row.email}`}/></TableCell>
345
- <TableCell className="font-medium"><Skeleton className={`h-4 ${row.name}`}/></TableCell>
346
- <TableCell>
347
- <div className="flex flex-wrap gap-2">
348
- {row.roles.map((w, j) => (
349
- <Skeleton key={j} className={`h-6 ${w} rounded-full`}/>
350
- ))}
351
- </div>
352
- </TableCell>
353
- <TableCell className="whitespace-nowrap text-sm">
354
- <Skeleton className="h-4 w-20"/>
355
- </TableCell>
356
- <TableCell className="text-right whitespace-nowrap">
357
- <div className="flex justify-end items-center gap-1">
358
- <Skeleton className="h-7 w-7 rounded-md"/>
359
- <Skeleton className="h-7 w-7 rounded-md"/>
360
- </div>
361
- </TableCell>
362
- </TableRow>
363
- ))
364
- ) : (
365
- displayUsers.map(user => (
366
- <TableRow key={user.uid} onClick={() => saveUser && handleEditUser(user)}>
367
- <TableCell className="font-mono text-xs">{user.uid}</TableCell>
368
- <TableCell>{user.email}</TableCell>
369
- <TableCell className="font-medium">{user.displayName}</TableCell>
370
- <TableCell>
371
- <div className="flex flex-wrap gap-2">
372
- {user.roles?.map((roleId: string) => {
373
- const role = roles?.find(r => r.id === roleId);
374
- return role ? <RoleChip key={roleId} role={role}/> : <span key={roleId}>{roleId}</span>;
375
- })}
376
- </div>
377
- </TableCell>
378
- <TableCell className="whitespace-nowrap text-sm text-surface-accent-600 dark:text-surface-accent-400">
379
- {user.createdAt ? new Date(user.createdAt).toLocaleDateString() : "-"}
380
- </TableCell>
381
- <TableCell className="text-right whitespace-nowrap">
382
- <div className="flex justify-end items-center gap-1">
383
- {resetPassword && (
384
- <Tooltip asChild title={t("reset_password")}>
385
- <IconButton
386
- size="small"
387
- onClick={(e) => {
388
- e.stopPropagation();
389
- setUserToReset(user);
390
- setResetConfirmOpen(true);
391
- }}>
392
- <KeyRoundIcon size={iconSize.small}/>
393
- </IconButton>
394
- </Tooltip>
395
- )}
396
- {deleteUser && (
397
- <Tooltip asChild title={t("delete_this_user")}>
398
- <IconButton
399
- size="small"
400
- onClick={(e) => {
401
- e.stopPropagation();
402
- setUserToDelete(user);
403
- setDeleteConfirmOpen(true);
404
- }}>
405
- <Trash2Icon size={iconSize.small}/>
406
- </IconButton>
407
- </Tooltip>
408
- )}
409
- </div>
410
- </TableCell>
411
- </TableRow>
412
- )))}
413
-
414
- {displayUsers.length === 0 && !tableLoading && !delegateLoading && (
415
- <TableRow>
416
- <TableCell colspan={6}>
417
- <CenteredView className="flex flex-col gap-4 my-8 items-center">
418
- <Typography variant="label">
419
- {usersError
420
- ? t("no_permission_to_view_users")
421
- : searchQuery ? t("no_users_found") : t("no_users_yet")}
422
- </Typography>
423
- {usersError && (
424
- <Typography variant="caption" color="secondary">
425
- {t("no_permission_description")}
426
- </Typography>
427
- )}
428
- </CenteredView>
429
- </TableCell>
430
- </TableRow>
431
- )}
432
- </TableBody>
433
- </Table>
434
- </div>
435
-
436
- {/* Pagination */}
437
- {displayTotal > PAGE_SIZE && (
438
- <div className="flex items-center justify-between px-2 py-3">
439
- <Typography variant="body2" className="text-surface-accent-500 dark:text-surface-accent-400">
440
- {`${page * PAGE_SIZE + 1}–${Math.min((page + 1) * PAGE_SIZE, displayTotal)} / ${displayTotal}`}
441
- </Typography>
442
- <div className="flex items-center gap-1">
443
- <IconButton
444
- size="small"
445
- disabled={page === 0}
446
- onClick={() => handlePageChange(page - 1)}>
447
- <ChevronLeftIcon size={iconSize.smallest}/>
448
- </IconButton>
449
- <Typography variant="body2" className="px-3 text-surface-accent-600 dark:text-surface-accent-300">
450
- {page + 1} / {totalPages}
451
- </Typography>
452
- <IconButton
453
- size="small"
454
- disabled={page >= totalPages - 1}
455
- onClick={() => handlePageChange(page + 1)}>
456
- <ChevronRightIcon size={iconSize.smallest}/>
457
- </IconButton>
458
- </div>
459
- </div>
460
- )}
461
-
462
- {/* User Edit Dialog */}
463
- {saveUser && (
464
- <UserDetailsForm
465
- key={selectedUser?.uid ?? `new-${formKey}`}
466
- open={dialogOpen}
467
- user={selectedUser}
468
- roles={roles}
469
- saveUser={saveUser}
470
- createUser={createUser}
471
- handleClose={handleClose}
472
- onCreationResult={setCreationResult}
473
- onSaved={refreshCurrentPage}
474
- />
475
- )}
476
-
477
- {/* Creation Result Dialog */}
478
- {creationResult && (
479
- <CreationResultDialog
480
- result={creationResult}
481
- onClose={() => setCreationResult(null)}
482
- />
483
- )}
484
-
485
- {/* Delete Confirmation */}
486
- <ConfirmationDialog
487
- open={deleteConfirmOpen}
488
- loading={deleteInProgress}
489
- onAccept={handleDelete}
490
- onCancel={() => { setDeleteConfirmOpen(false); setUserToDelete(undefined); }}
491
- title={<>{t("delete_confirmation_title")}</>}
492
- body={<>{t("delete_user_confirmation")}</>}
493
- />
494
-
495
- {/* Reset Password Confirmation */}
496
- <ConfirmationDialog
497
- open={resetConfirmOpen}
498
- loading={resetInProgress}
499
- onAccept={handleResetPassword}
500
- onCancel={() => { setResetConfirmOpen(false); setUserToReset(undefined); }}
501
- title={<>{t("reset_password")}</>}
502
- body={<>{t("reset_password_confirmation")}</>}
503
- />
504
- </Container>
505
- );
506
- }
507
-
508
- // ============================================
509
- // UserDetailsForm Component
510
- // ============================================
511
- function UserDetailsForm({
512
- open,
513
- user: userProp,
514
- roles,
515
- saveUser,
516
- createUser,
517
- handleClose,
518
- onCreationResult,
519
- onSaved
520
- }: {
521
- open: boolean;
522
- user?: User;
523
- roles?: Role[];
524
- saveUser: (user: User) => Promise<User>;
525
- createUser?: (user: User) => Promise<UserCreationResult>;
526
- handleClose: () => void;
527
- onCreationResult?: (result: UserCreationResult) => void;
528
- onSaved?: () => void;
529
- }) {
530
- const snackbarController = useSnackbarController();
531
- const { t } = useTranslation();
532
- const isNewUser = !userProp;
533
-
534
- const [displayName, setDisplayName] = useState(userProp?.displayName || "");
535
- const [email, setEmail] = useState(userProp?.email || "");
536
- const [selectedRoleIds, setSelectedRoleIds] = useState<string[]>(
537
- userProp?.roles || []
538
- );
539
- const [isSubmitting, setIsSubmitting] = useState(false);
540
- const [errors, setErrors] = useState<{ displayName?: string; email?: string; roles?: string }>({});
541
- const [submitCount, setSubmitCount] = useState(0);
542
-
543
- const validate = () => {
544
- const newErrors: typeof errors = {};
545
- if (!displayName) newErrors.displayName = "Required";
546
- if (!email) newErrors.email = "Required";
547
- else if (!/\S+@\S+\.\S+/.test(email)) newErrors.email = "Invalid email";
548
- setErrors(newErrors);
549
- return Object.keys(newErrors).length === 0;
550
- };
551
-
552
- const handleSubmit = async (e: React.FormEvent) => {
553
- e.preventDefault();
554
- setSubmitCount(c => c + 1);
555
-
556
- if (!validate()) return;
557
-
558
- setIsSubmitting(true);
559
- try {
560
- const userRoles = selectedRoleIds;
561
- const userToSave: User = {
562
- uid: userProp?.uid || crypto.randomUUID(),
563
- email,
564
- displayName: displayName || null,
565
- photoURL: userProp?.photoURL || null,
566
- providerId: userProp?.providerId || "custom",
567
- isAnonymous: userProp?.isAnonymous || false,
568
- roles: userRoles
569
- };
570
-
571
- if (isNewUser && createUser && onCreationResult) {
572
- // Use createUser for new users to get invitation/password info
573
- const result = await createUser(userToSave);
574
- handleClose();
575
- onCreationResult(result);
576
- } else {
577
- await saveUser(userToSave);
578
- handleClose();
579
- }
580
- onSaved?.();
581
- } catch (error: unknown) {
582
- snackbarController.open({ type: "error",
583
- message: error instanceof Error ? error.message : "Failed to save user" });
584
- } finally {
585
- setIsSubmitting(false);
586
- }
587
- };
588
-
589
- const dirty = isNewUser ||
590
- displayName !== (userProp?.displayName || "") ||
591
- email !== (userProp?.email || "") ||
592
- (() => {
593
- const prev = userProp?.roles || [];
594
- if (selectedRoleIds.length !== prev.length) return true;
595
- const set = new Set(prev);
596
- return selectedRoleIds.some(id => !set.has(id));
597
- })();
598
-
599
- return (
600
- <Dialog open={open} onOpenChange={(open) => !open ? handleClose() : undefined} maxWidth="4xl">
601
- <form onSubmit={handleSubmit} autoComplete="off" noValidate
602
- style={{ display: "flex",
603
- flexDirection: "column",
604
- position: "relative",
605
- height: "100%" }}>
606
-
607
- <DialogTitle variant="h4" gutterBottom={false}>
608
- {t("user")}
609
- </DialogTitle>
610
-
611
- <DialogContent className="h-full grow">
612
- <div className="grid grid-cols-12 gap-4">
613
- {!isNewUser && (
614
- <div className="col-span-12">
615
- <TextField
616
- name="uid"
617
- value={userProp?.uid || ""}
618
- label={t("id") || "ID"}
619
- disabled
620
- />
621
- </div>
622
- )}
623
- <div className="col-span-12">
624
- <TextField
625
- name="displayName"
626
- required
627
- error={submitCount > 0 && Boolean(errors.displayName)}
628
- value={displayName}
629
- onChange={(e) => setDisplayName(e.target.value)}
630
- label={t("name")}
631
- />
632
- {submitCount > 0 && errors.displayName && (
633
- <Typography variant="caption" color="error">{errors.displayName}</Typography>
634
- )}
635
- </div>
636
-
637
- <div className="col-span-12">
638
- <TextField
639
- required
640
- error={submitCount > 0 && Boolean(errors.email)}
641
- name="email"
642
- value={email}
643
- onChange={(e) => setEmail(e.target.value)}
644
- label={t("email")}
645
- disabled={!isNewUser}
646
- />
647
- {submitCount > 0 && errors.email && (
648
- <Typography variant="caption" color="error">{errors.email}</Typography>
649
- )}
650
- </div>
651
-
652
- {roles && roles.length > 0 && (
653
- <div className="col-span-12">
654
- <MultiSelect
655
- className="w-full"
656
- label={t("roles")}
657
- value={selectedRoleIds}
658
- onValueChange={(value: string[]) => setSelectedRoleIds(value)}
659
- >
660
- {roles.map(role => (
661
- <MultiSelectItem key={role.id} value={role.id}>
662
- <RoleChip role={role}/>
663
- </MultiSelectItem>
664
- ))}
665
- </MultiSelect>
666
- </div>
667
- )}
668
- </div>
669
- </DialogContent>
670
-
671
- <DialogActions>
672
- <Button variant="text" onClick={handleClose}>
673
- {t("cancel")}
674
- </Button>
675
- <LoadingButton
676
- variant="filled"
677
- type="submit"
678
- disabled={!dirty}
679
- loading={isSubmitting}
680
- >
681
- {isNewUser ? t("create_user") : t("update")}
682
- </LoadingButton>
683
- </DialogActions>
684
- </form>
685
- </Dialog>
686
- );
687
- }