@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.
- package/README.md +142 -210
- package/dist/components/AIIcon.d.ts +3 -2
- package/dist/components/ConfirmationDialog.d.ts +1 -1
- package/dist/components/Debug/UIReferenceView.d.ts +13 -1
- package/dist/components/Debug/UIStyleGuide.d.ts +2 -1
- package/dist/components/ErrorTooltip.d.ts +2 -1
- package/dist/components/LanguageToggle.d.ts +2 -1
- package/dist/components/LoginView/LoginView.d.ts +2 -2
- package/dist/components/NotFoundPage.d.ts +2 -1
- package/dist/components/RebaseLogo.d.ts +1 -1
- package/dist/components/SchemaDriftBanner.d.ts +27 -0
- package/dist/components/UnsavedChangesDialog.d.ts +1 -1
- package/dist/components/UserDisplay.d.ts +1 -1
- package/dist/components/UserSelectPopover.d.ts +2 -1
- package/dist/components/UserSettingsView.d.ts +2 -1
- package/dist/components/index.d.ts +1 -1
- package/dist/contexts/ComponentOverrideContext.d.ts +42 -0
- package/dist/contexts/RebaseClientInstanceContext.d.ts +1 -1
- package/dist/contexts/index.d.ts +1 -1
- package/dist/core/Rebase.d.ts +2 -1
- package/dist/core/RebaseProps.d.ts +23 -22
- package/dist/core/RebaseRouter.d.ts +1 -1
- package/dist/core/RebaseRoutes.d.ts +1 -1
- package/dist/hooks/ApiConfigContext.d.ts +1 -1
- package/dist/hooks/index.d.ts +1 -2
- package/dist/hooks/useComponentOverride.d.ts +32 -0
- package/dist/hooks/useRebaseRegistry.d.ts +1 -1
- package/dist/hooks/useStudioBridge.d.ts +4 -16
- package/dist/i18n/RebaseI18nProvider.d.ts +2 -2
- package/dist/index.es.js +12878 -15004
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +13192 -15144
- package/dist/index.umd.js.map +1 -1
- package/dist/util/icons.d.ts +2 -2
- package/dist/vitePlugin.js +4 -1
- package/package.json +21 -22
- package/src/components/ErrorView.tsx +16 -6
- package/src/components/LoginView/LoginView.tsx +14 -16
- package/src/components/SchemaDriftBanner.tsx +102 -0
- package/src/components/common/useDataTableController.tsx +3 -3
- package/src/components/index.tsx +1 -1
- package/src/contexts/ComponentOverrideContext.tsx +81 -0
- package/src/contexts/RebaseClientInstanceContext.tsx +1 -1
- package/src/contexts/index.ts +1 -1
- package/src/core/Rebase.tsx +16 -18
- package/src/core/RebaseProps.tsx +24 -25
- package/src/hooks/data/useCollectionFetch.tsx +10 -2
- package/src/hooks/data/useRelationSelector.tsx +4 -2
- package/src/hooks/index.tsx +1 -3
- package/src/hooks/useCollapsedGroups.ts +2 -1
- package/src/hooks/useComponentOverride.tsx +59 -0
- package/src/hooks/useRebaseContext.tsx +3 -5
- package/src/hooks/useResolvedComponent.tsx +1 -1
- package/src/hooks/useStudioBridge.tsx +5 -13
- package/src/locales/en.ts +3 -0
- package/src/util/entity_cache.ts +0 -2
- package/src/util/previews.ts +1 -1
- package/src/vitePlugin.ts +2 -1
- package/dist/components/BootstrapAdminBanner.d.ts +0 -4
- package/dist/contexts/InternalUserManagementContext.d.ts +0 -3
- package/dist/hooks/data/useUserSelector.d.ts +0 -31
- package/dist/hooks/useInternalUserManagementController.d.ts +0 -12
- package/src/components/BootstrapAdminBanner.tsx +0 -75
- package/src/contexts/InternalUserManagementContext.tsx +0 -4
- package/src/hooks/data/useUserSelector.tsx +0 -157
- 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>[],
|
|
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>[],
|
|
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>[],
|
|
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>[],
|
|
194
|
+
.then((res) => onEntitiesUpdate({ data: res.data as Entity<M>[],
|
|
195
|
+
meta: res.meta }))
|
|
194
196
|
.catch(onErrorUpdate);
|
|
195
197
|
unsubscribe = () => {};
|
|
196
198
|
}
|
package/src/hooks/index.tsx
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
11
|
+
export type { BreadcrumbEntry, BreadcrumbsController };
|
|
10
12
|
|
|
11
|
-
|
|
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",
|
package/src/util/entity_cache.ts
CHANGED
|
@@ -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)) {
|
package/src/util/previews.ts
CHANGED
|
@@ -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
|
@@ -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,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
|
-
}
|