@rebasepro/core 0.5.0 → 0.6.1
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/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 +12748 -15004
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +13062 -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 -21
- package/src/components/Debug/UIReferenceView.tsx +109 -201
- package/src/components/ErrorView.tsx +16 -6
- package/src/components/LoginView/LoginView.tsx +15 -17
- package/src/components/SchemaDriftBanner.tsx +102 -0
- package/src/components/common/useDataTableController.tsx +1 -1
- 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
|
@@ -88,7 +88,6 @@ export interface LoginViewProps {
|
|
|
88
88
|
notAllowedError?: string | Error;
|
|
89
89
|
|
|
90
90
|
|
|
91
|
-
|
|
92
91
|
/**
|
|
93
92
|
* Google client ID for Google OAuth.
|
|
94
93
|
* Required when Google login is enabled via ID token flow.
|
|
@@ -208,7 +207,7 @@ export function LoginView({
|
|
|
208
207
|
// Clear URL search params without page reload
|
|
209
208
|
const cleanUrl = window.location.origin + window.location.pathname;
|
|
210
209
|
window.history.replaceState({}, document.title, cleanUrl);
|
|
211
|
-
|
|
210
|
+
|
|
212
211
|
if (authController.oauthLogin) {
|
|
213
212
|
authController.oauthLogin(provider, {
|
|
214
213
|
code,
|
|
@@ -220,18 +219,7 @@ export function LoginView({
|
|
|
220
219
|
}
|
|
221
220
|
}, [authController]);
|
|
222
221
|
|
|
223
|
-
|
|
224
|
-
if (!authController.authProviderError) return null;
|
|
225
|
-
if (authController.user != null) return null;
|
|
226
|
-
const errorMsg = authController.authProviderError instanceof Error
|
|
227
|
-
? authController.authProviderError.message
|
|
228
|
-
: String(authController.authProviderError);
|
|
229
|
-
return (
|
|
230
|
-
<div className="w-full">
|
|
231
|
-
<ErrorView error={errorMsg}/>
|
|
232
|
-
</div>
|
|
233
|
-
);
|
|
234
|
-
}
|
|
222
|
+
|
|
235
223
|
|
|
236
224
|
let logoComponent;
|
|
237
225
|
if (logo) {
|
|
@@ -297,12 +285,11 @@ export function LoginView({
|
|
|
297
285
|
</div>
|
|
298
286
|
)}
|
|
299
287
|
|
|
300
|
-
{mode !== "forgot" && buildErrorView()}
|
|
301
|
-
|
|
302
288
|
<div className={cls(
|
|
303
289
|
"w-full transition-opacity duration-150",
|
|
304
290
|
viewVisible ? "opacity-100" : "opacity-0"
|
|
305
291
|
)}>
|
|
292
|
+
|
|
306
293
|
{/* Bootstrap mode: show setup form directly */}
|
|
307
294
|
{isBootstrapMode && !authController.user && (
|
|
308
295
|
<LoginForm
|
|
@@ -421,7 +408,7 @@ export function LoginView({
|
|
|
421
408
|
)}
|
|
422
409
|
</div>
|
|
423
410
|
|
|
424
|
-
{additionalComponent && (
|
|
411
|
+
{additionalComponent && mode === "buttons" && (
|
|
425
412
|
<div className="w-full">
|
|
426
413
|
{additionalComponent}
|
|
427
414
|
</div>
|
|
@@ -673,6 +660,17 @@ function LoginForm({
|
|
|
673
660
|
</div>
|
|
674
661
|
)}
|
|
675
662
|
|
|
663
|
+
{(() => {
|
|
664
|
+
const err = authController.authProviderError;
|
|
665
|
+
if (!err || authController.user) return null;
|
|
666
|
+
const msg: string = err instanceof Error ? err.message : String(err);
|
|
667
|
+
return (
|
|
668
|
+
<div className="w-full mb-2">
|
|
669
|
+
<ErrorView error={msg}/>
|
|
670
|
+
</div>
|
|
671
|
+
);
|
|
672
|
+
})()}
|
|
673
|
+
|
|
676
674
|
<Typography variant="h6" className="mb-0.5">
|
|
677
675
|
{title}
|
|
678
676
|
</Typography>
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import React, { createContext, useCallback, useContext, useMemo, useState } from "react";
|
|
2
|
+
import { Alert, Typography } from "@rebasepro/ui";
|
|
3
|
+
|
|
4
|
+
// ── Schema Drift Context ────────────────────────────────────────────────
|
|
5
|
+
|
|
6
|
+
interface SchemaDriftContextValue {
|
|
7
|
+
/** Report a schema drift error (call from any data-loading hook). */
|
|
8
|
+
reportSchemaDrift: (message: string) => void;
|
|
9
|
+
/** The most recent schema drift message, or null. */
|
|
10
|
+
schemaDriftMessage: string | null;
|
|
11
|
+
/** Dismiss the banner for this session. */
|
|
12
|
+
dismiss: () => void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const SchemaDriftContext = createContext<SchemaDriftContextValue>({
|
|
16
|
+
reportSchemaDrift: () => {},
|
|
17
|
+
schemaDriftMessage: null,
|
|
18
|
+
dismiss: () => {}
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
export function useSchemaDriftContext(): SchemaDriftContextValue {
|
|
22
|
+
return useContext(SchemaDriftContext);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Detects schema drift from an error object.
|
|
27
|
+
* Checks both the error code (from `RebaseApiError`) and the message
|
|
28
|
+
* for schema drift indicators.
|
|
29
|
+
*/
|
|
30
|
+
export function isSchemaDriftError(error: unknown): boolean {
|
|
31
|
+
if (!error || typeof error !== "object") return false;
|
|
32
|
+
// RebaseApiError from the SDK has a `code` property
|
|
33
|
+
if ("code" in error && (error as { code?: string }).code === "SCHEMA_DRIFT") {
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
// Fallback: check the message for the telltale phrase
|
|
37
|
+
if (error instanceof Error && error.message.includes("Schema drift:")) {
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// ── Provider ─────────────────────────────────────────────────────────────
|
|
44
|
+
|
|
45
|
+
export function SchemaDriftProvider({ children }: { children: React.ReactNode }) {
|
|
46
|
+
const [schemaDriftMessage, setSchemaDriftMessage] = useState<string | null>(null);
|
|
47
|
+
const [dismissed, setDismissed] = useState(false);
|
|
48
|
+
|
|
49
|
+
const reportSchemaDrift = useCallback((message: string) => {
|
|
50
|
+
setSchemaDriftMessage(prev => prev ?? message);
|
|
51
|
+
}, []);
|
|
52
|
+
|
|
53
|
+
const dismiss = useCallback(() => {
|
|
54
|
+
setDismissed(true);
|
|
55
|
+
}, []);
|
|
56
|
+
|
|
57
|
+
const value = useMemo<SchemaDriftContextValue>(() => ({
|
|
58
|
+
reportSchemaDrift,
|
|
59
|
+
schemaDriftMessage: dismissed ? null : schemaDriftMessage,
|
|
60
|
+
dismiss
|
|
61
|
+
}), [reportSchemaDrift, schemaDriftMessage, dismissed, dismiss]);
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
<SchemaDriftContext.Provider value={value}>
|
|
65
|
+
{children}
|
|
66
|
+
</SchemaDriftContext.Provider>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// ── Banner Component ─────────────────────────────────────────────────────
|
|
71
|
+
|
|
72
|
+
export interface SchemaDriftBannerProps {
|
|
73
|
+
className?: string;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Persistent banner shown when a schema drift error is detected.
|
|
78
|
+
*/
|
|
79
|
+
export function SchemaDriftBanner({ className }: SchemaDriftBannerProps) {
|
|
80
|
+
const { schemaDriftMessage, dismiss } = useSchemaDriftContext();
|
|
81
|
+
|
|
82
|
+
if (!schemaDriftMessage) return null;
|
|
83
|
+
|
|
84
|
+
return (
|
|
85
|
+
<Alert
|
|
86
|
+
color="warning"
|
|
87
|
+
size="small"
|
|
88
|
+
onDismiss={dismiss}
|
|
89
|
+
className={className}
|
|
90
|
+
>
|
|
91
|
+
<div className="flex flex-col gap-1">
|
|
92
|
+
<Typography variant="label" className="text-amber-800 dark:text-amber-200">
|
|
93
|
+
Schema drift detected
|
|
94
|
+
</Typography>
|
|
95
|
+
<Typography variant="body2" className="text-amber-700 dark:text-amber-300">
|
|
96
|
+
{schemaDriftMessage}{" "}
|
|
97
|
+
Run <code className="bg-amber-200 dark:bg-amber-800 px-1 rounded text-xs">pnpm db:push</code> in your backend terminal to sync.
|
|
98
|
+
</Typography>
|
|
99
|
+
</div>
|
|
100
|
+
</Alert>
|
|
101
|
+
);
|
|
102
|
+
}
|
|
@@ -235,7 +235,7 @@ export function useDataTableController<M extends Record<string, any> = any, USER
|
|
|
235
235
|
const onError = (error: Error) => {
|
|
236
236
|
console.error("ERROR", error);
|
|
237
237
|
setDataLoading(false);
|
|
238
|
-
setRawData([]);
|
|
238
|
+
setRawData((prev) => prev && prev.length > 0 ? prev : []);
|
|
239
239
|
setDataLoadingError(error);
|
|
240
240
|
};
|
|
241
241
|
|
package/src/components/index.tsx
CHANGED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import React, { createContext, useContext, useMemo } from "react";
|
|
2
|
+
import type { ComponentOverrideMap } from "@rebasepro/types";
|
|
3
|
+
|
|
4
|
+
/** Stable empty reference to avoid re-creating context values on every render. */
|
|
5
|
+
const EMPTY_OVERRIDES: ComponentOverrideMap = {};
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Internal state for the component override resolution chain.
|
|
9
|
+
* Holds both global overrides (set on `<Rebase>`) and collection-scoped
|
|
10
|
+
* overrides (set on individual collections).
|
|
11
|
+
*
|
|
12
|
+
* Resolution priority: collection > global > default.
|
|
13
|
+
*
|
|
14
|
+
* @internal
|
|
15
|
+
*/
|
|
16
|
+
interface ComponentOverrideState {
|
|
17
|
+
globalOverrides: ComponentOverrideMap;
|
|
18
|
+
collectionOverrides: ComponentOverrideMap;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const ComponentOverrideContext = createContext<ComponentOverrideState>({
|
|
22
|
+
globalOverrides: EMPTY_OVERRIDES,
|
|
23
|
+
collectionOverrides: EMPTY_OVERRIDES
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Provider set at the `<Rebase>` root level to supply global component overrides.
|
|
28
|
+
*
|
|
29
|
+
* @internal — Used by the Rebase component. End users set overrides via
|
|
30
|
+
* the `components` prop on `<Rebase>`.
|
|
31
|
+
*/
|
|
32
|
+
export function GlobalComponentOverrideProvider({
|
|
33
|
+
overrides,
|
|
34
|
+
children
|
|
35
|
+
}: {
|
|
36
|
+
overrides?: ComponentOverrideMap;
|
|
37
|
+
children: React.ReactNode;
|
|
38
|
+
}) {
|
|
39
|
+
const value = useMemo<ComponentOverrideState>(() => ({
|
|
40
|
+
globalOverrides: overrides ?? EMPTY_OVERRIDES,
|
|
41
|
+
collectionOverrides: EMPTY_OVERRIDES
|
|
42
|
+
}), [overrides]);
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<ComponentOverrideContext.Provider value={value}>
|
|
46
|
+
{children}
|
|
47
|
+
</ComponentOverrideContext.Provider>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Provider set at the collection level to layer collection-scoped overrides
|
|
53
|
+
* on top of global overrides.
|
|
54
|
+
*
|
|
55
|
+
* When a collection defines `components`, this provider is mounted
|
|
56
|
+
* around the collection's view subtree. Components within the subtree
|
|
57
|
+
* will resolve overrides in order: collection → global → default.
|
|
58
|
+
*
|
|
59
|
+
* @internal — Used by EntityCollectionView when a collection has
|
|
60
|
+
* `components` defined.
|
|
61
|
+
*/
|
|
62
|
+
export function CollectionComponentOverrideProvider({
|
|
63
|
+
overrides,
|
|
64
|
+
children
|
|
65
|
+
}: {
|
|
66
|
+
overrides?: ComponentOverrideMap;
|
|
67
|
+
children: React.ReactNode;
|
|
68
|
+
}) {
|
|
69
|
+
const parent = useContext(ComponentOverrideContext);
|
|
70
|
+
|
|
71
|
+
const value = useMemo<ComponentOverrideState>(() => ({
|
|
72
|
+
globalOverrides: parent.globalOverrides,
|
|
73
|
+
collectionOverrides: overrides ?? EMPTY_OVERRIDES
|
|
74
|
+
}), [parent.globalOverrides, overrides]);
|
|
75
|
+
|
|
76
|
+
return (
|
|
77
|
+
<ComponentOverrideContext.Provider value={value}>
|
|
78
|
+
{children}
|
|
79
|
+
</ComponentOverrideContext.Provider>
|
|
80
|
+
);
|
|
81
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* Context that exposes the full RebaseClient instance
|
|
4
|
+
* Context that exposes the full RebaseClient instance.
|
|
5
5
|
* Used by the JS Editor to give developer scripts access to `client.data`, `client.auth`, etc.
|
|
6
6
|
*/
|
|
7
7
|
export const RebaseClientInstanceContext = React.createContext<any>(undefined);
|
package/src/contexts/index.ts
CHANGED
|
@@ -8,6 +8,6 @@ export * from "./AnalyticsContext";
|
|
|
8
8
|
export * from "./StorageSourceContext";
|
|
9
9
|
export * from "./UserConfigurationPersistenceContext";
|
|
10
10
|
export * from "./DialogsProvider";
|
|
11
|
-
export * from "./InternalUserManagementContext";
|
|
12
11
|
export * from "./RebaseClientInstanceContext";
|
|
13
12
|
export * from "./CustomizationControllerContext";
|
|
13
|
+
export * from "./ComponentOverrideContext";
|
package/src/core/Rebase.tsx
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import type { RebaseProps } from "./RebaseProps";
|
|
3
3
|
import type { CustomizationController, RebasePlugin, SlotContribution } from "@rebasepro/types";
|
|
4
|
+
import type { ComponentOverrideMap } from "@rebasepro/types";
|
|
4
5
|
|
|
5
6
|
import React, { useMemo } from "react";
|
|
6
7
|
import { CenteredView, Typography } from "@rebasepro/ui";
|
|
7
|
-
import { RebaseContext, User,
|
|
8
|
+
import { RebaseContext, User, CollectionRegistryController } from "@rebasepro/types";
|
|
8
9
|
import { PluginProviderStack } from "./PluginProviderStack";
|
|
9
10
|
import { PluginLifecycleManager } from "./PluginLifecycleManager";
|
|
10
11
|
import { AuthControllerContext } from "../contexts";
|
|
@@ -18,6 +19,8 @@ import { DatabaseAdminContext } from "../contexts/DatabaseAdminContext";
|
|
|
18
19
|
import { ModeControllerProvider, AdminModeControllerProvider, SnackbarProvider } from "../contexts";
|
|
19
20
|
import { RebaseI18nProvider } from "../i18n/RebaseI18nProvider";
|
|
20
21
|
import { RebaseRegistryProvider } from "../hooks/useRebaseRegistry";
|
|
22
|
+
import { SchemaDriftProvider } from "../components/SchemaDriftBanner";
|
|
23
|
+
import { GlobalComponentOverrideProvider } from "../contexts/ComponentOverrideContext";
|
|
21
24
|
import { useBuildModeController } from "../hooks/useBuildModeController";
|
|
22
25
|
import { useBuildAdminModeController } from "../hooks/useBuildAdminModeController";
|
|
23
26
|
import { RebaseClientInstanceContext } from "../contexts/RebaseClientInstanceContext";
|
|
@@ -25,7 +28,7 @@ import { DialogsProvider } from "../contexts/DialogsProvider";
|
|
|
25
28
|
import { buildRebaseData, CollectionRegistry } from "@rebasepro/common";
|
|
26
29
|
import { CustomizationControllerContext } from "../contexts/CustomizationControllerContext";
|
|
27
30
|
import { AnalyticsContext } from "../contexts/AnalyticsContext";
|
|
28
|
-
|
|
31
|
+
|
|
29
32
|
import { EffectiveRoleControllerContext } from "../contexts/EffectiveRoleController";
|
|
30
33
|
import { useBuildEffectiveRoleController } from "../hooks/useBuildEffectiveRoleController";
|
|
31
34
|
|
|
@@ -60,12 +63,12 @@ export function Rebase<USER extends User>(props: RebaseProps<USER>) {
|
|
|
60
63
|
propertyConfigs,
|
|
61
64
|
entityViews,
|
|
62
65
|
entityActions,
|
|
63
|
-
components,
|
|
64
66
|
apiKey,
|
|
65
|
-
|
|
67
|
+
|
|
66
68
|
effectiveRoleController,
|
|
67
69
|
apiUrl,
|
|
68
|
-
translations
|
|
70
|
+
translations,
|
|
71
|
+
components: componentsProp
|
|
69
72
|
} = props;
|
|
70
73
|
|
|
71
74
|
const plugins = pluginsProp;
|
|
@@ -84,13 +87,6 @@ export function Rebase<USER extends User>(props: RebaseProps<USER>) {
|
|
|
84
87
|
...((plugins ?? []).flatMap((p) => p.slots ?? []))
|
|
85
88
|
], [directSlots, plugins]);
|
|
86
89
|
|
|
87
|
-
const userManagement = useMemo(() => plugins?.find((p) => p.userManagement)?.userManagement
|
|
88
|
-
?? _userManagement
|
|
89
|
-
?? {
|
|
90
|
-
loading: false,
|
|
91
|
-
users: [],
|
|
92
|
-
getUser: (uid: string) => null
|
|
93
|
-
} as UserManagementDelegate<USER>, [plugins, _userManagement]);
|
|
94
90
|
|
|
95
91
|
// Auth fallback logic
|
|
96
92
|
const clientAuthController = useAuthSubscription(authControllerProp ? undefined : client?.auth);
|
|
@@ -159,8 +155,8 @@ export function Rebase<USER extends User>(props: RebaseProps<USER>) {
|
|
|
159
155
|
entityViews: entityViews ?? [],
|
|
160
156
|
entityActions: entityActions ?? [],
|
|
161
157
|
propertyConfigs: propertyConfigs ?? {},
|
|
162
|
-
components
|
|
163
|
-
}), [dateTimeFormat, locale, entityLinkBuilder, plugins, resolvedSlots, entityViews, entityActions, propertyConfigs,
|
|
158
|
+
components: componentsProp
|
|
159
|
+
}), [dateTimeFormat, locale, entityLinkBuilder, plugins, resolvedSlots, entityViews, entityActions, propertyConfigs, componentsProp]);
|
|
164
160
|
|
|
165
161
|
const analyticsController = useMemo(() => ({
|
|
166
162
|
onAnalyticsEvent
|
|
@@ -184,6 +180,7 @@ export function Rebase<USER extends User>(props: RebaseProps<USER>) {
|
|
|
184
180
|
|
|
185
181
|
const content = (
|
|
186
182
|
<RebaseI18nProvider locale={locale} translations={translations}>
|
|
183
|
+
<GlobalComponentOverrideProvider overrides={componentsProp}>
|
|
187
184
|
<SnackbarProvider>
|
|
188
185
|
<ModeControllerProvider value={modeController}>
|
|
189
186
|
<AdminModeControllerProvider value={adminModeController}>
|
|
@@ -200,18 +197,18 @@ export function Rebase<USER extends User>(props: RebaseProps<USER>) {
|
|
|
200
197
|
value={resolvedDatabaseAdmin}>
|
|
201
198
|
<AuthControllerContext.Provider
|
|
202
199
|
value={authController}>
|
|
203
|
-
<InternalUserManagementContext.Provider value={userManagement}>
|
|
204
200
|
<EffectiveRoleControllerContext.Provider value={activeEffectiveRoleController}>
|
|
205
201
|
<DialogsProvider>
|
|
202
|
+
<SchemaDriftProvider>
|
|
206
203
|
<RebaseRegistryProvider>
|
|
207
204
|
<RebaseInternal
|
|
208
205
|
loading={loading}>
|
|
209
206
|
{children}
|
|
210
207
|
</RebaseInternal>
|
|
211
208
|
</RebaseRegistryProvider>
|
|
209
|
+
</SchemaDriftProvider>
|
|
212
210
|
</DialogsProvider>
|
|
213
211
|
</EffectiveRoleControllerContext.Provider>
|
|
214
|
-
</InternalUserManagementContext.Provider>
|
|
215
212
|
</AuthControllerContext.Provider>
|
|
216
213
|
</DatabaseAdminContext.Provider>
|
|
217
214
|
</RebaseDataContext.Provider>
|
|
@@ -223,6 +220,7 @@ export function Rebase<USER extends User>(props: RebaseProps<USER>) {
|
|
|
223
220
|
</AdminModeControllerProvider>
|
|
224
221
|
</ModeControllerProvider>
|
|
225
222
|
</SnackbarProvider>
|
|
223
|
+
</GlobalComponentOverrideProvider>
|
|
226
224
|
</RebaseI18nProvider>
|
|
227
225
|
);
|
|
228
226
|
|
|
@@ -262,13 +260,13 @@ function RebaseInternal({
|
|
|
262
260
|
&& !authController.authLoading
|
|
263
261
|
&& (Boolean(authController.user) || authController.loginSkipped);
|
|
264
262
|
|
|
265
|
-
if (
|
|
263
|
+
if (plugins && plugins.length > 0) {
|
|
266
264
|
return (
|
|
267
265
|
<PluginProviderStack
|
|
268
266
|
plugins={plugins}
|
|
269
267
|
scope="root"
|
|
270
268
|
scopeProps={{ context }}>
|
|
271
|
-
<PluginLifecycleManager plugins={plugins} context={context}/>
|
|
269
|
+
{authReady && <PluginLifecycleManager plugins={plugins} context={context}/>}
|
|
272
270
|
{childrenResult}
|
|
273
271
|
</PluginProviderStack>
|
|
274
272
|
);
|
package/src/core/RebaseProps.tsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
-
import { Locale, User, AuthController, AnalyticsEvent, DataDriver, StorageSource, UserConfigurationPersistence, CollectionRegistryController, DatabaseAdmin, UrlController, NavigationStateController, RebaseData, RebaseClient, RebaseContext,
|
|
2
|
+
import { Locale, User, AuthController, AnalyticsEvent, DataDriver, StorageSource, UserConfigurationPersistence, CollectionRegistryController, DatabaseAdmin, UrlController, NavigationStateController, RebaseData, RebaseClient, RebaseContext, EntityLinkBuilder, RebasePlugin, SlotContribution, PropertyConfig, EntityCustomView, EntityAction, RebaseTranslations, ComponentOverrideMap } from "@rebasepro/types";
|
|
3
3
|
|
|
4
4
|
/** DeepPartial helper — allows partial overrides at any nesting level */
|
|
5
5
|
type DeepPartial<T> = T extends object
|
|
@@ -111,19 +111,6 @@ export type RebaseProps<USER extends User> = {
|
|
|
111
111
|
*/
|
|
112
112
|
entityLinkBuilder?: EntityLinkBuilder;
|
|
113
113
|
|
|
114
|
-
/**
|
|
115
|
-
* You can use this props to provide your own user management implementation.
|
|
116
|
-
* Note that this will not affect the UI, but it will be used to show user information
|
|
117
|
-
* in various places of the CMS, for example, to show who created or modified an entity,
|
|
118
|
-
* or to assign ownership of an entity.
|
|
119
|
-
*
|
|
120
|
-
* You can also use this data to be retrieved in your custom properties,
|
|
121
|
-
* for example, to show a list of users in a dropdown.
|
|
122
|
-
*
|
|
123
|
-
* If you are using the Rebase user management plugin, this
|
|
124
|
-
* prop will be implemented automatically.
|
|
125
|
-
*/
|
|
126
|
-
userManagement?: UserManagementDelegate<USER>;
|
|
127
114
|
|
|
128
115
|
/**
|
|
129
116
|
* Plugins loaded in the CMS
|
|
@@ -150,17 +137,6 @@ export type RebaseProps<USER extends User> = {
|
|
|
150
137
|
*/
|
|
151
138
|
entityActions?: EntityAction[];
|
|
152
139
|
|
|
153
|
-
components?: {
|
|
154
|
-
|
|
155
|
-
/**
|
|
156
|
-
* Component to render when a reference is missing
|
|
157
|
-
*/
|
|
158
|
-
missingReference?: React.ComponentType<{
|
|
159
|
-
path: string,
|
|
160
|
-
}>;
|
|
161
|
-
|
|
162
|
-
};
|
|
163
|
-
|
|
164
140
|
/**
|
|
165
141
|
* Controller to simulate different roles when dev mode is active.
|
|
166
142
|
*/
|
|
@@ -173,5 +149,28 @@ export type RebaseProps<USER extends User> = {
|
|
|
173
149
|
[locale: string]: DeepPartial<RebaseTranslations>;
|
|
174
150
|
};
|
|
175
151
|
|
|
152
|
+
/**
|
|
153
|
+
* Override built-in UI components with custom implementations.
|
|
154
|
+
*
|
|
155
|
+
* Keys are component names from {@link OverridableComponentName}.
|
|
156
|
+
* Values specify the replacement component and an optional `wrap`
|
|
157
|
+
* flag for the wrapping pattern.
|
|
158
|
+
*
|
|
159
|
+
* @example
|
|
160
|
+
* ```tsx
|
|
161
|
+
* <Rebase
|
|
162
|
+
* client={client}
|
|
163
|
+
* components={{
|
|
164
|
+
* "Shell.AppBar": { Component: MyCustomAppBar },
|
|
165
|
+
* "Entity.FormActions": {
|
|
166
|
+
* Component: MyFormActions,
|
|
167
|
+
* wrap: true
|
|
168
|
+
* }
|
|
169
|
+
* }}
|
|
170
|
+
* >
|
|
171
|
+
* ```
|
|
172
|
+
*/
|
|
173
|
+
components?: ComponentOverrideMap;
|
|
174
|
+
|
|
176
175
|
};
|
|
177
176
|
|
|
@@ -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
|
+
}
|