@rebasepro/core 0.4.0 → 0.6.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 (66) hide show
  1. package/README.md +142 -210
  2. package/dist/components/AIIcon.d.ts +3 -2
  3. package/dist/components/ConfirmationDialog.d.ts +1 -1
  4. package/dist/components/Debug/UIReferenceView.d.ts +13 -1
  5. package/dist/components/Debug/UIStyleGuide.d.ts +2 -1
  6. package/dist/components/ErrorTooltip.d.ts +2 -1
  7. package/dist/components/LanguageToggle.d.ts +2 -1
  8. package/dist/components/LoginView/LoginView.d.ts +2 -2
  9. package/dist/components/NotFoundPage.d.ts +2 -1
  10. package/dist/components/RebaseLogo.d.ts +1 -1
  11. package/dist/components/SchemaDriftBanner.d.ts +27 -0
  12. package/dist/components/UnsavedChangesDialog.d.ts +1 -1
  13. package/dist/components/UserDisplay.d.ts +1 -1
  14. package/dist/components/UserSelectPopover.d.ts +2 -1
  15. package/dist/components/UserSettingsView.d.ts +2 -1
  16. package/dist/components/index.d.ts +1 -1
  17. package/dist/contexts/ComponentOverrideContext.d.ts +42 -0
  18. package/dist/contexts/RebaseClientInstanceContext.d.ts +1 -1
  19. package/dist/contexts/index.d.ts +1 -1
  20. package/dist/core/Rebase.d.ts +2 -1
  21. package/dist/core/RebaseProps.d.ts +23 -22
  22. package/dist/core/RebaseRouter.d.ts +1 -1
  23. package/dist/core/RebaseRoutes.d.ts +1 -1
  24. package/dist/hooks/ApiConfigContext.d.ts +1 -1
  25. package/dist/hooks/index.d.ts +1 -2
  26. package/dist/hooks/useComponentOverride.d.ts +32 -0
  27. package/dist/hooks/useRebaseRegistry.d.ts +1 -1
  28. package/dist/hooks/useStudioBridge.d.ts +4 -16
  29. package/dist/i18n/RebaseI18nProvider.d.ts +2 -2
  30. package/dist/index.es.js +12878 -15004
  31. package/dist/index.es.js.map +1 -1
  32. package/dist/index.umd.js +13192 -15144
  33. package/dist/index.umd.js.map +1 -1
  34. package/dist/util/icons.d.ts +2 -2
  35. package/dist/vitePlugin.js +4 -1
  36. package/package.json +21 -22
  37. package/src/components/ErrorView.tsx +16 -6
  38. package/src/components/LoginView/LoginView.tsx +14 -16
  39. package/src/components/SchemaDriftBanner.tsx +102 -0
  40. package/src/components/common/useDataTableController.tsx +3 -3
  41. package/src/components/index.tsx +1 -1
  42. package/src/contexts/ComponentOverrideContext.tsx +81 -0
  43. package/src/contexts/RebaseClientInstanceContext.tsx +1 -1
  44. package/src/contexts/index.ts +1 -1
  45. package/src/core/Rebase.tsx +16 -18
  46. package/src/core/RebaseProps.tsx +24 -25
  47. package/src/hooks/data/useCollectionFetch.tsx +10 -2
  48. package/src/hooks/data/useRelationSelector.tsx +4 -2
  49. package/src/hooks/index.tsx +1 -3
  50. package/src/hooks/useCollapsedGroups.ts +2 -1
  51. package/src/hooks/useComponentOverride.tsx +59 -0
  52. package/src/hooks/useRebaseContext.tsx +3 -5
  53. package/src/hooks/useResolvedComponent.tsx +1 -1
  54. package/src/hooks/useStudioBridge.tsx +5 -13
  55. package/src/locales/en.ts +3 -0
  56. package/src/util/entity_cache.ts +0 -2
  57. package/src/util/previews.ts +1 -1
  58. package/src/vitePlugin.ts +2 -1
  59. package/dist/components/BootstrapAdminBanner.d.ts +0 -4
  60. package/dist/contexts/InternalUserManagementContext.d.ts +0 -3
  61. package/dist/hooks/data/useUserSelector.d.ts +0 -31
  62. package/dist/hooks/useInternalUserManagementController.d.ts +0 -12
  63. package/src/components/BootstrapAdminBanner.tsx +0 -75
  64. package/src/contexts/InternalUserManagementContext.tsx +0 -4
  65. package/src/hooks/data/useUserSelector.tsx +0 -157
  66. package/src/hooks/useInternalUserManagementController.tsx +0 -17
@@ -2,6 +2,7 @@ import type { EntityCollection } from "@rebasepro/types";
2
2
  import { useEffect, useState, useMemo } from "react";
3
3
  import { Entity, FilterValues, User } from "@rebasepro/types";
4
4
  import { useData } from "./useData";
5
+ import { isSchemaDriftError, useSchemaDriftContext } from "../../components/SchemaDriftBanner";
5
6
  /**
6
7
  * @group Hooks and utilities
7
8
  */
@@ -83,6 +84,7 @@ export function useCollectionFetch<M extends Record<string, any>, USER extends U
83
84
  searchString
84
85
  }: CollectionFetchProps<M>): CollectionFetchResult<M> {
85
86
  const dataClient = useData();
87
+ const { reportSchemaDrift } = useSchemaDriftContext();
86
88
 
87
89
  const sortByProperty = sortBy ? sortBy[0] : undefined;
88
90
  const currentSort = sortBy ? sortBy[1] : undefined;
@@ -137,6 +139,10 @@ export function useCollectionFetch<M extends Record<string, any>, USER extends U
137
139
  setData([]);
138
140
  setDataLoadingError(error);
139
141
  setTotalCount(undefined);
142
+ // Report schema drift to the global banner context
143
+ if (isSchemaDriftError(error)) {
144
+ reportSchemaDrift(error.message);
145
+ }
140
146
  };
141
147
 
142
148
  const accessor = dataClient.collection(path);
@@ -156,7 +162,8 @@ export function useCollectionFetch<M extends Record<string, any>, USER extends U
156
162
  orderBy: orderByParams,
157
163
  searchString,
158
164
  include: includeParams
159
- }, (res) => onEntitiesUpdate({ data: res.data as Entity<M>[], meta: res.meta }), onError);
165
+ }, (res) => onEntitiesUpdate({ data: res.data as Entity<M>[],
166
+ meta: res.meta }), onError);
160
167
  } else {
161
168
  accessor.find({
162
169
  where: whereParams,
@@ -167,7 +174,8 @@ export function useCollectionFetch<M extends Record<string, any>, USER extends U
167
174
  searchString,
168
175
  include: includeParams
169
176
  })
170
- .then((res) => onEntitiesUpdate({ data: res.data as Entity<M>[], meta: res.meta }))
177
+ .then((res) => onEntitiesUpdate({ data: res.data as Entity<M>[],
178
+ meta: res.meta }))
171
179
  .catch(onError);
172
180
  return () => {
173
181
  };
@@ -181,7 +181,8 @@ export function useRelationSelector<M extends Record<string, any> = any>(
181
181
  limit: limit,
182
182
  orderBy: undefined,
183
183
  searchString: currentSearch
184
- }, (res) => onEntitiesUpdate({ data: res.data as Entity<M>[], meta: res.meta }), onErrorUpdate);
184
+ }, (res) => onEntitiesUpdate({ data: res.data as Entity<M>[],
185
+ meta: res.meta }), onErrorUpdate);
185
186
  } else {
186
187
  accessor.find({
187
188
  where: whereParams,
@@ -190,7 +191,8 @@ export function useRelationSelector<M extends Record<string, any> = any>(
190
191
  orderBy: undefined,
191
192
  searchString: currentSearch
192
193
  })
193
- .then((res) => onEntitiesUpdate({ data: res.data as Entity<M>[], meta: res.meta }))
194
+ .then((res) => onEntitiesUpdate({ data: res.data as Entity<M>[],
195
+ meta: res.meta }))
194
196
  .catch(onErrorUpdate);
195
197
  unsubscribe = () => {};
196
198
  }
@@ -2,7 +2,6 @@ export * from "./data/useData";
2
2
  export * from "./data/useCollectionFetch";
3
3
  export * from "./data/useEntityFetch";
4
4
  export * from "./data/useRelationSelector";
5
- export * from "./data/useUserSelector";
6
5
  export * from "./data/save";
7
6
  export * from "./data/delete";
8
7
 
@@ -24,8 +23,6 @@ export * from "./useClipboard";
24
23
  export * from "./useLargeLayout";
25
24
  export * from "./useCollapsedGroups";
26
25
 
27
- export * from "./useInternalUserManagementController";
28
-
29
26
  export * from "./useBrowserTitleAndIcon";
30
27
  export * from "./useSlot";
31
28
  export * from "./useCustomizationController";
@@ -45,3 +42,4 @@ export * from "./useRebaseClient";
45
42
  export * from "./useAnalyticsController";
46
43
  export * from "./useUserConfigurationPersistence";
47
44
  export * from "./useResolvedComponent";
45
+ export * from "./useComponentOverride";
@@ -79,7 +79,8 @@ export function useCollapsedGroups(
79
79
  const currentlyCollapsed = name in prev
80
80
  ? prev[name]
81
81
  : (defaults?.[name] ?? false);
82
- return { ...prev, [name]: !currentlyCollapsed };
82
+ return { ...prev,
83
+ [name]: !currentlyCollapsed };
83
84
  });
84
85
  }, [defaults]);
85
86
 
@@ -0,0 +1,59 @@
1
+ import React, { useContext, useMemo } from "react";
2
+ import type { OverridableComponentName } from "@rebasepro/types";
3
+ import { ComponentOverrideContext } from "../contexts/ComponentOverrideContext";
4
+
5
+ /**
6
+ * Resolves a potentially overridden component.
7
+ *
8
+ * Resolution order:
9
+ * 1. Collection-scoped override (highest priority)
10
+ * 2. Global override
11
+ * 3. Default component (fallback)
12
+ *
13
+ * Supports two override modes:
14
+ * - **Eject** (default): The override component fully replaces the default.
15
+ * - **Wrap** (`wrap: true`): The override component wraps the default.
16
+ * The default is passed as `OriginalComponent` in props.
17
+ *
18
+ * @param name - The overridable component name (e.g. `"Entity.Form"`)
19
+ * @param DefaultComponent - The built-in default component
20
+ * @returns The resolved component — either the default, a full replacement, or a wrapper
21
+ *
22
+ * @example
23
+ * ```tsx
24
+ * import { useComponentOverride } from "@rebasepro/core";
25
+ *
26
+ * function EntityFormWrapper(props: EntityFormProps) {
27
+ * const ResolvedForm = useComponentOverride("Entity.Form", DefaultEntityForm);
28
+ * return <ResolvedForm {...props} />;
29
+ * }
30
+ * ```
31
+ *
32
+ * @group Hooks
33
+ */
34
+ export function useComponentOverride<P>(
35
+ name: OverridableComponentName,
36
+ DefaultComponent: React.ComponentType<P>
37
+ ): React.ComponentType<P> {
38
+ const { globalOverrides, collectionOverrides } = useContext(ComponentOverrideContext);
39
+
40
+ return useMemo(() => {
41
+ // Collection-level overrides have highest priority
42
+ const override = collectionOverrides[name] ?? globalOverrides[name];
43
+
44
+ if (!override) return DefaultComponent;
45
+
46
+ if (override.wrap) {
47
+ // Wrapping mode: inject OriginalComponent as a prop
48
+ const UserComponent = override.Component;
49
+ const Wrapper = (props: P) => (
50
+ <UserComponent {...(props as Record<string, unknown>)} OriginalComponent={DefaultComponent} />
51
+ );
52
+ Wrapper.displayName = `Wrapped(${name})`;
53
+ return Wrapper as React.ComponentType<P>;
54
+ }
55
+
56
+ // Eject mode: full replacement
57
+ return override.Component as React.ComponentType<P>;
58
+ }, [collectionOverrides, globalOverrides, name, DefaultComponent]);
59
+ }
@@ -10,7 +10,7 @@ import { useCustomizationController } from "./useCustomizationController";
10
10
  import { useAnalyticsController } from "./useAnalyticsController";
11
11
  import { useEffectiveRoleController } from "./useEffectiveRoleController";
12
12
  import React, { useEffect, useContext } from "react";
13
- import { useInternalUserManagementController } from "./useInternalUserManagementController";
13
+
14
14
 
15
15
  // DatabaseAdmin is provided by <Rebase> via DatabaseAdminContext.
16
16
  import { DatabaseAdminContext } from "../contexts/DatabaseAdminContext";
@@ -37,7 +37,7 @@ export const useRebaseContext = <USER extends User = User, AuthControllerType ex
37
37
  const customizationController = useCustomizationController();
38
38
  const analyticsController = useAnalyticsController();
39
39
  const effectiveRoleController = useEffectiveRoleController();
40
- const userManagement = useInternalUserManagementController<USER>();
40
+
41
41
 
42
42
  // Will get `databaseAdmin` from context
43
43
  const databaseAdmin = useContext(DatabaseAdminContext);
@@ -51,7 +51,6 @@ export const useRebaseContext = <USER extends User = User, AuthControllerType ex
51
51
  dialogsController,
52
52
  customizationController,
53
53
  analyticsController,
54
- userManagement,
55
54
  effectiveRoleController,
56
55
  databaseAdmin,
57
56
  client: client! // Client should be provided
@@ -67,13 +66,12 @@ export const useRebaseContext = <USER extends User = User, AuthControllerType ex
67
66
  dialogsController,
68
67
  customizationController,
69
68
  analyticsController,
70
- userManagement,
71
69
  effectiveRoleController,
72
70
  databaseAdmin,
73
71
  client: client!
74
72
  };
75
73
  }, [authController, data, storageSource, snackbarController, userConfigPersistence,
76
- dialogsController, customizationController, analyticsController, userManagement,
74
+ dialogsController, customizationController, analyticsController,
77
75
  effectiveRoleController, databaseAdmin, client]);
78
76
 
79
77
  return rebaseContextRef.current;
@@ -13,7 +13,7 @@ import { isLazyComponentRef } from "@rebasepro/types";
13
13
  * to the `React.lazy()` wrapper it produced. Strings are keyed by a separate
14
14
  * plain Map since they can't be WeakMap keys.
15
15
  */
16
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
16
+
17
17
  const lazyCache = new WeakMap<object | ((...args: any[]) => any), React.ComponentType<any>>();
18
18
 
19
19
  /**
@@ -3,23 +3,15 @@ import type {
3
3
  CollectionRegistryController,
4
4
  SideEntityController,
5
5
  UrlController,
6
- NavigationStateController
6
+ NavigationStateController,
7
+ BreadcrumbEntry,
8
+ BreadcrumbsController
7
9
  } from "@rebasepro/types";
8
10
 
9
- // ─── Breadcrumbs (defined here so studio doesn't need CMS) ──────────
11
+ export type { BreadcrumbEntry, BreadcrumbsController };
10
12
 
11
- export interface BreadcrumbEntry {
12
- title: string;
13
- url: string;
14
- count?: number | null;
15
- id?: string;
16
- }
13
+ // ─── Breadcrumbs ──────────────────────────────────────────────────
17
14
 
18
- export interface BreadcrumbsController {
19
- breadcrumbs: BreadcrumbEntry[];
20
- set: (props: { breadcrumbs: BreadcrumbEntry[] }) => void;
21
- updateCount: (id: string, count: number | null | undefined) => void;
22
- }
23
15
 
24
16
  // ─── Bridge interface ───────────────────────────────────────────────
25
17
 
package/src/locales/en.ts CHANGED
@@ -170,6 +170,9 @@ export const en: RebaseTranslations = {
170
170
 
171
171
  // ─── Error states ─────────────────────────────────────────────
172
172
  error: "Error",
173
+ error_loading_data: "Could not load data",
174
+ error_check_server_logs: "Check server logs for details.",
175
+ error_technical_details: "Technical details",
173
176
  error_uploading_file: "Error uploading file",
174
177
  error_deleting: "Error deleting",
175
178
  error_before_delete: "Error before delete",
@@ -155,7 +155,6 @@ export function getEntityFromMemoryCache(path: string): object | undefined {
155
155
  }
156
156
 
157
157
 
158
-
159
158
  /**
160
159
  * Retrieves an entity from the in-memory cache or `sessionStorage`.
161
160
  * If the entity is not in the cache but exists in `sessionStorage`, it loads it into the cache.
@@ -211,7 +210,6 @@ export function removeEntityFromCache(path: string): void {
211
210
  }
212
211
 
213
212
 
214
-
215
213
  export function flattenKeys(obj: Record<string, unknown> | unknown[], prefix = "", result: string[] = []): string[] {
216
214
 
217
215
  if (isObject(obj) || Array.isArray(obj)) {
@@ -59,7 +59,7 @@ export function getEntityTitlePropertyKey<M extends Record<string, any>>(collect
59
59
  if (collection.titleProperty) {
60
60
  return collection.titleProperty as string;
61
61
  }
62
-
62
+
63
63
  const orderToSearch = (collection.propertiesOrder as string[]) || Object.keys(collection.properties);
64
64
  let firstStringCandidate: string | undefined;
65
65
 
package/src/vitePlugin.ts CHANGED
@@ -126,7 +126,8 @@ export function rebaseCollectionsPlugin(options: RebaseCollectionsPluginOptions)
126
126
  }
127
127
  );
128
128
 
129
- return { code: transformed, map: null };
129
+ return { code: transformed,
130
+ map: null };
130
131
  }
131
132
  };
132
133
  }
@@ -1,4 +0,0 @@
1
- export interface BootstrapAdminBannerProps {
2
- className?: string;
3
- }
4
- export declare function BootstrapAdminBanner({ className }: BootstrapAdminBannerProps): import("react/jsx-runtime").JSX.Element | null;
@@ -1,3 +0,0 @@
1
- import React from "react";
2
- import { UserManagementDelegate } from "@rebasepro/types";
3
- export declare const InternalUserManagementContext: React.Context<UserManagementDelegate<any> | undefined>;
@@ -1,31 +0,0 @@
1
- import { User } from "@rebasepro/types";
2
- export interface UserSelectorItem {
3
- uid: string;
4
- label: string;
5
- description?: string;
6
- user: User;
7
- }
8
- export interface UseUserSelectorProps {
9
- /**
10
- * Page size for pagination. Defaults to 10.
11
- */
12
- pageSize?: number;
13
- }
14
- export interface UserSelectorController {
15
- items: UserSelectorItem[];
16
- isLoading: boolean;
17
- error: Error | undefined;
18
- search: (searchString: string) => void;
19
- loadMore: () => void;
20
- hasMore: boolean;
21
- getUser: (uid: string) => User | null;
22
- }
23
- /**
24
- * Hook to manage user selection with server-side search and pagination.
25
- * Similar to useRelationSelector but for the UserManagementDelegate.
26
- *
27
- * If the delegate provides `searchUsers`, this hook uses server-side
28
- * search/pagination. Otherwise it falls back to client-side filtering
29
- * over the in-memory `users` array.
30
- */
31
- export declare function useUserSelector({ pageSize }?: UseUserSelectorProps): UserSelectorController;
@@ -1,12 +0,0 @@
1
- import { User, UserManagementDelegate } from "@rebasepro/types";
2
- /**
3
- * Use this hook to get the internal user management of the app.
4
- * Note that this is different from the user management plugin controller.
5
- * This controller will be eventually replaced by the one provided
6
- * by the user management plugin.
7
- *
8
- * Use at your own risk!
9
- *
10
- * @group Hooks and utilities
11
- */
12
- export declare function useInternalUserManagementController<USER extends User>(): UserManagementDelegate<USER> | undefined;
@@ -1,75 +0,0 @@
1
- import React, { useState } from "react";
2
- import { useInternalUserManagementController } from "../hooks/useInternalUserManagementController";
3
- import { useAuthController } from "../hooks/useAuthController";
4
- import { useTranslation } from "../hooks/useTranslation";
5
- import { useSnackbarController } from "../hooks/useSnackbarController";
6
- import { Button, CircularProgress, Typography } from "@rebasepro/ui";
7
-
8
- export interface BootstrapAdminBannerProps {
9
- className?: string;
10
- }
11
-
12
- export function BootstrapAdminBanner({
13
- className
14
- }: BootstrapAdminBannerProps) {
15
- const userManagement = useInternalUserManagementController();
16
- const { user: loggedInUser } = useAuthController();
17
- const { t } = useTranslation();
18
- const snackbarController = useSnackbarController();
19
- const [bootstrapping, setBootstrapping] = useState(false);
20
-
21
- // If we're not running locally, don't render anything
22
- if (typeof window !== "undefined" && window.location.hostname !== "localhost" && window.location.hostname !== "127.0.0.1") {
23
- return null;
24
- }
25
-
26
- if (!userManagement || !loggedInUser) {
27
- return null;
28
- }
29
-
30
- // Non-admin users don't load the users list (admin API is skipped to
31
- // avoid 403s), so `users` would be empty and falsely trigger this banner.
32
- // Only admin users (or users with no roles yet, during initial bootstrap)
33
- // should ever see this prompt.
34
- const loggedInUserRoles = loggedInUser.roles ?? [];
35
- const isLoggedInUserAdmin = loggedInUserRoles.length === 0 || loggedInUserRoles.some(r => r === "admin");
36
- if (!isLoggedInUserAdmin) {
37
- return null;
38
- }
39
-
40
- const { hasAdminUsers, loading: delegateLoading, bootstrapAdmin, usersError } = userManagement;
41
-
42
- if (delegateLoading || hasAdminUsers || usersError || !bootstrapAdmin) {
43
- return null;
44
- }
45
-
46
- const handleBootstrap = async () => {
47
- if (!bootstrapAdmin) return;
48
- setBootstrapping(true);
49
- try {
50
- await bootstrapAdmin();
51
- snackbarController.open({ type: "success", message: t("bootstrap_admin_success") || "Admin successfully created" });
52
- window.location.reload();
53
- } catch (error: unknown) {
54
- snackbarController.open({ type: "error", message: error instanceof Error ? error.message : t("failed_to_bootstrap_admin") || "Failed to bootstrap admin" });
55
- } finally {
56
- setBootstrapping(false);
57
- }
58
- };
59
-
60
- return (
61
- <div className={`bg-yellow-100 dark:bg-yellow-900 border border-yellow-400 dark:border-yellow-700 rounded p-4 flex items-center justify-between ${className || ""}`}>
62
- <div>
63
- <Typography variant="label" className="text-yellow-800 dark:text-yellow-200">
64
- {t("no_users_or_roles_defined") || "No admins found. Click to add your user as admin."}
65
- </Typography>
66
- </div>
67
- <Button
68
- onClick={handleBootstrap}
69
- disabled={bootstrapping}
70
- >
71
- {bootstrapping ? <CircularProgress size="small"/> : (t("add_logged_user_as_admin") || "Add logged user as admin")}
72
- </Button>
73
- </div>
74
- );
75
- }
@@ -1,4 +0,0 @@
1
- import React from "react";
2
- import { UserManagementDelegate } from "@rebasepro/types";
3
-
4
- export const InternalUserManagementContext = React.createContext<UserManagementDelegate<any> | undefined>(undefined);
@@ -1,157 +0,0 @@
1
-
2
- import { useCallback, useEffect, useRef, useState, useMemo } from "react";
3
- import { User, UserManagementDelegate } from "@rebasepro/types";
4
- import { useInternalUserManagementController } from "../useInternalUserManagementController";
5
-
6
- export interface UserSelectorItem {
7
- uid: string;
8
- label: string;
9
- description?: string;
10
- user: User;
11
- }
12
-
13
- export interface UseUserSelectorProps {
14
- /**
15
- * Page size for pagination. Defaults to 10.
16
- */
17
- pageSize?: number;
18
- }
19
-
20
- export interface UserSelectorController {
21
- items: UserSelectorItem[];
22
- isLoading: boolean;
23
- error: Error | undefined;
24
- search: (searchString: string) => void;
25
- loadMore: () => void;
26
- hasMore: boolean;
27
- getUser: (uid: string) => User | null;
28
- }
29
-
30
- const DEFAULT_PAGE_SIZE = 10;
31
-
32
- /**
33
- * Hook to manage user selection with server-side search and pagination.
34
- * Similar to useRelationSelector but for the UserManagementDelegate.
35
- *
36
- * If the delegate provides `searchUsers`, this hook uses server-side
37
- * search/pagination. Otherwise it falls back to client-side filtering
38
- * over the in-memory `users` array.
39
- */
40
- export function useUserSelector(
41
- { pageSize = DEFAULT_PAGE_SIZE }: UseUserSelectorProps = {}
42
- ): UserSelectorController {
43
-
44
- const userManagement = useInternalUserManagementController<User>();
45
-
46
- const [items, setItems] = useState<UserSelectorItem[]>([]);
47
- const [isLoading, setIsLoading] = useState(false);
48
- const isLoadingRef = useRef(false);
49
- const [error, setError] = useState<Error | undefined>();
50
- const [hasMore, setHasMore] = useState(true);
51
- const [currentSearch, setCurrentSearch] = useState<string>("");
52
- const [limit, setLimit] = useState<number>(pageSize);
53
-
54
- const searchTimeoutRef = useRef<NodeJS.Timeout | null>(null);
55
-
56
- const setLoadingState = useCallback((loading: boolean) => {
57
- isLoadingRef.current = loading;
58
- setIsLoading(loading);
59
- }, []);
60
-
61
- const userToItem = useCallback((user: User): UserSelectorItem => {
62
- return {
63
- uid: user.uid,
64
- label: user.displayName || user.email || user.uid,
65
- description: user.displayName && user.email ? user.email : undefined,
66
- user
67
- };
68
- }, []);
69
-
70
- const fetchData = useCallback(() => {
71
- if (!userManagement) return;
72
-
73
- setError(undefined);
74
- setLoadingState(true);
75
-
76
- if (userManagement.searchUsers) {
77
- // Server-side search + pagination
78
- userManagement.searchUsers({
79
- search: currentSearch || undefined,
80
- limit,
81
- offset: 0
82
- }).then(({ users, total }) => {
83
- setItems(users.map(userToItem));
84
- setHasMore(users.length < total);
85
- setLoadingState(false);
86
- }).catch((err: unknown) => {
87
- console.error("useUserSelector: Error fetching users:", err);
88
- setError(err instanceof Error ? err : new Error(String(err)));
89
- setLoadingState(false);
90
- });
91
- } else {
92
- // Client-side fallback: filter in-memory users list
93
- const allUsers = userManagement.users ?? [];
94
- const searchLower = currentSearch.toLowerCase();
95
- const filtered = currentSearch
96
- ? allUsers.filter((u: User) => {
97
- const name = (u.displayName || "").toLowerCase();
98
- const email = (u.email || "").toLowerCase();
99
- return name.includes(searchLower) || email.includes(searchLower);
100
- })
101
- : allUsers;
102
-
103
- const page = filtered.slice(0, limit);
104
- setItems(page.map(userToItem));
105
- setHasMore(page.length < filtered.length);
106
- setLoadingState(false);
107
- }
108
- }, [userManagement, currentSearch, limit, userToItem, setLoadingState]);
109
-
110
- // Search function with debouncing
111
- const search = useCallback((searchString: string) => {
112
- if (searchTimeoutRef.current) {
113
- clearTimeout(searchTimeoutRef.current);
114
- }
115
-
116
- searchTimeoutRef.current = setTimeout(() => {
117
- setLimit(pageSize);
118
- setCurrentSearch(searchString);
119
- }, searchString.trim() ? 300 : 0);
120
- }, [pageSize]);
121
-
122
- // Load more function
123
- const loadMore = useCallback(() => {
124
- if (!isLoadingRef.current && hasMore && items.length > 0) {
125
- setLoadingState(true);
126
- setLimit(prev => prev + pageSize);
127
- }
128
- }, [hasMore, items.length, pageSize, setLoadingState]);
129
-
130
- // Fetch when search/limit changes
131
- useEffect(() => {
132
- fetchData();
133
- }, [fetchData]);
134
-
135
- // Cleanup debounce timer
136
- useEffect(() => {
137
- return () => {
138
- if (searchTimeoutRef.current) {
139
- clearTimeout(searchTimeoutRef.current);
140
- }
141
- };
142
- }, []);
143
-
144
- const getUser = useCallback((uid: string): User | null => {
145
- return userManagement?.getUser?.(uid) ?? null;
146
- }, [userManagement]);
147
-
148
- return useMemo(() => ({
149
- items,
150
- isLoading,
151
- error,
152
- search,
153
- loadMore,
154
- hasMore,
155
- getUser
156
- }), [items, isLoading, error, search, loadMore, hasMore, getUser]);
157
- }
@@ -1,17 +0,0 @@
1
- import { useContext } from "react";
2
- import { InternalUserManagementContext } from "../contexts/InternalUserManagementContext";
3
- import { User, UserManagementDelegate } from "@rebasepro/types";
4
-
5
- /**
6
- * Use this hook to get the internal user management of the app.
7
- * Note that this is different from the user management plugin controller.
8
- * This controller will be eventually replaced by the one provided
9
- * by the user management plugin.
10
- *
11
- * Use at your own risk!
12
- *
13
- * @group Hooks and utilities
14
- */
15
- export function useInternalUserManagementController<USER extends User>(): UserManagementDelegate<USER> | undefined {
16
- return useContext(InternalUserManagementContext);
17
- }