@rebasepro/core 0.0.1-canary.f81da60 → 0.1.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/dist/components/BootstrapAdminBanner.d.ts +4 -0
- package/dist/components/LoginView/LoginView.d.ts +22 -0
- package/dist/components/index.d.ts +1 -0
- package/dist/hooks/index.d.ts +1 -0
- package/dist/hooks/useCollapsedGroups.d.ts +16 -1
- package/dist/hooks/useResolvedComponent.d.ts +47 -0
- package/dist/index.es.js +214 -42
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +212 -40
- package/dist/index.umd.js.map +1 -1
- package/dist/vitePlugin.d.ts +28 -1
- package/dist/vitePlugin.js +42 -0
- package/package.json +6 -6
- package/src/components/BootstrapAdminBanner.tsx +66 -0
- package/src/components/LoginView/LoginView.tsx +48 -20
- package/src/components/common/useDataTableController.tsx +15 -3
- package/src/components/index.tsx +1 -1
- package/src/core/Rebase.tsx +20 -14
- package/src/hooks/index.tsx +1 -0
- package/src/hooks/useCollapsedGroups.ts +48 -6
- package/src/hooks/useRebaseContext.tsx +11 -6
- package/src/hooks/useResolvedComponent.tsx +157 -0
- package/src/locales/de.ts +1 -0
- package/src/locales/en.ts +1 -0
- package/src/locales/es.ts +1 -0
- package/src/locales/fr.ts +1 -0
- package/src/locales/hi.ts +1 -0
- package/src/locales/it.ts +1 -0
- package/src/locales/pt.ts +1 -0
- package/src/util/previews.ts +15 -6
- package/src/vitePlugin.ts +87 -1
package/dist/vitePlugin.d.ts
CHANGED
|
@@ -7,10 +7,37 @@ export interface RebaseCollectionsPluginOptions {
|
|
|
7
7
|
}
|
|
8
8
|
/**
|
|
9
9
|
* A Vite plugin that dynamically loads and automatically wires Rebase collections.
|
|
10
|
-
*
|
|
10
|
+
*
|
|
11
|
+
* It provides two capabilities:
|
|
12
|
+
* 1. A **virtual module** `"virtual:rebase-collections"` that statically exports
|
|
13
|
+
* the resolved collections array.
|
|
14
|
+
* 2. A **transform hook** that converts string-based component references
|
|
15
|
+
* (e.g. `Field: "../../components/MyField"`) into `LazyComponentRef` objects
|
|
16
|
+
* (`{ __rebaseLazy: true, load: () => import(...) }`), enabling code-splitting
|
|
17
|
+
* and preventing the backend from loading React-dependent modules.
|
|
11
18
|
*/
|
|
12
19
|
export declare function rebaseCollectionsPlugin(options: RebaseCollectionsPluginOptions): {
|
|
13
20
|
name: string;
|
|
21
|
+
configResolved(config: {
|
|
22
|
+
root: string;
|
|
23
|
+
}): void;
|
|
14
24
|
resolveId(id: string): string | null;
|
|
15
25
|
load(id: string): string | null;
|
|
26
|
+
/**
|
|
27
|
+
* Transform collection files to convert string component references
|
|
28
|
+
* into lazy-loading `LazyComponentRef` objects.
|
|
29
|
+
*
|
|
30
|
+
* Example transform:
|
|
31
|
+
* ```
|
|
32
|
+
* // Input
|
|
33
|
+
* Field: "../../frontend/src/components/MyField"
|
|
34
|
+
*
|
|
35
|
+
* // Output
|
|
36
|
+
* Field: { __rebaseLazy: true, load: () => import("../../frontend/src/components/MyField") }
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
39
|
+
transform(code: string, id: string): {
|
|
40
|
+
code: string;
|
|
41
|
+
map: null;
|
|
42
|
+
} | null;
|
|
16
43
|
};
|
package/dist/vitePlugin.js
CHANGED
|
@@ -1,8 +1,22 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
const LAZY_COMPONENT_KEYS = ["Field", "Preview", "Builder"];
|
|
3
|
+
function buildTransformRegex() {
|
|
4
|
+
const keys = LAZY_COMPONENT_KEYS.join("|");
|
|
5
|
+
return new RegExp(
|
|
6
|
+
`((?:${keys})\\s*:\\s*)(['"])(\\.\\.?\\/[^'"]+)\\2`,
|
|
7
|
+
"g"
|
|
8
|
+
);
|
|
9
|
+
}
|
|
1
10
|
function rebaseCollectionsPlugin(options) {
|
|
2
11
|
const virtualModuleId = "virtual:rebase-collections";
|
|
3
12
|
const resolvedVirtualModuleId = "\0" + virtualModuleId;
|
|
13
|
+
let resolvedCollectionsDir;
|
|
14
|
+
const transformRegex = buildTransformRegex();
|
|
4
15
|
return {
|
|
5
16
|
name: "rebase-collections-plugin",
|
|
17
|
+
configResolved(config) {
|
|
18
|
+
resolvedCollectionsDir = path.isAbsolute(options.collectionsDir) ? options.collectionsDir : path.resolve(config.root, options.collectionsDir);
|
|
19
|
+
},
|
|
6
20
|
resolveId(id) {
|
|
7
21
|
if (id === virtualModuleId) {
|
|
8
22
|
return resolvedVirtualModuleId;
|
|
@@ -21,6 +35,34 @@ function rebaseCollectionsPlugin(options) {
|
|
|
21
35
|
`;
|
|
22
36
|
}
|
|
23
37
|
return null;
|
|
38
|
+
},
|
|
39
|
+
/**
|
|
40
|
+
* Transform collection files to convert string component references
|
|
41
|
+
* into lazy-loading `LazyComponentRef` objects.
|
|
42
|
+
*
|
|
43
|
+
* Example transform:
|
|
44
|
+
* ```
|
|
45
|
+
* // Input
|
|
46
|
+
* Field: "../../frontend/src/components/MyField"
|
|
47
|
+
*
|
|
48
|
+
* // Output
|
|
49
|
+
* Field: { __rebaseLazy: true, load: () => import("../../frontend/src/components/MyField") }
|
|
50
|
+
* ```
|
|
51
|
+
*/
|
|
52
|
+
transform(code, id) {
|
|
53
|
+
if (!resolvedCollectionsDir) return null;
|
|
54
|
+
if (!id.startsWith(resolvedCollectionsDir)) return null;
|
|
55
|
+
if (!/\.tsx?$/.test(id)) return null;
|
|
56
|
+
transformRegex.lastIndex = 0;
|
|
57
|
+
if (!transformRegex.test(code)) return null;
|
|
58
|
+
transformRegex.lastIndex = 0;
|
|
59
|
+
const transformed = code.replace(
|
|
60
|
+
transformRegex,
|
|
61
|
+
(_match, prefix, quote, importPath) => {
|
|
62
|
+
return `${prefix}{ __rebaseLazy: true, load: () => import(${quote}${importPath}${quote}) }`;
|
|
63
|
+
}
|
|
64
|
+
);
|
|
65
|
+
return { code: transformed, map: null };
|
|
24
66
|
}
|
|
25
67
|
};
|
|
26
68
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rebasepro/core",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.1.0",
|
|
5
5
|
"description": "Rebase core — framework-agnostic runtime for data-driven admin panels",
|
|
6
6
|
"funding": {
|
|
7
7
|
"url": "https://github.com/sponsors/rebaseco"
|
|
@@ -52,11 +52,11 @@
|
|
|
52
52
|
"i18next": "^23.16.4",
|
|
53
53
|
"notistack": "^3.0.2",
|
|
54
54
|
"react-i18next": "^14.1.3",
|
|
55
|
-
"@rebasepro/
|
|
56
|
-
"@rebasepro/
|
|
57
|
-
"@rebasepro/
|
|
58
|
-
"@rebasepro/
|
|
59
|
-
"@rebasepro/
|
|
55
|
+
"@rebasepro/utils": "0.1.0",
|
|
56
|
+
"@rebasepro/common": "0.1.0",
|
|
57
|
+
"@rebasepro/formex": "0.1.0",
|
|
58
|
+
"@rebasepro/types": "0.1.0",
|
|
59
|
+
"@rebasepro/ui": "0.1.0"
|
|
60
60
|
},
|
|
61
61
|
"peerDependencies": {
|
|
62
62
|
"react": ">=19.0.0",
|
|
@@ -0,0 +1,66 @@
|
|
|
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
|
+
const { users, loading: delegateLoading, bootstrapAdmin, usersError } = userManagement;
|
|
31
|
+
const hasAdmin = users.some(u => u.roles?.includes("admin"));
|
|
32
|
+
|
|
33
|
+
if (delegateLoading || hasAdmin || usersError || !bootstrapAdmin) {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const handleBootstrap = async () => {
|
|
38
|
+
if (!bootstrapAdmin) return;
|
|
39
|
+
setBootstrapping(true);
|
|
40
|
+
try {
|
|
41
|
+
await bootstrapAdmin();
|
|
42
|
+
snackbarController.open({ type: "success", message: t("bootstrap_admin_success") || "Admin successfully created" });
|
|
43
|
+
window.location.reload();
|
|
44
|
+
} catch (error: unknown) {
|
|
45
|
+
snackbarController.open({ type: "error", message: error instanceof Error ? error.message : t("failed_to_bootstrap_admin") || "Failed to bootstrap admin" });
|
|
46
|
+
} finally {
|
|
47
|
+
setBootstrapping(false);
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<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 || ""}`}>
|
|
53
|
+
<div>
|
|
54
|
+
<Typography variant="label" className="text-yellow-800 dark:text-yellow-200">
|
|
55
|
+
{t("no_users_or_roles_defined") || "No admins found. Click to add your user as admin."}
|
|
56
|
+
</Typography>
|
|
57
|
+
</div>
|
|
58
|
+
<Button
|
|
59
|
+
onClick={handleBootstrap}
|
|
60
|
+
disabled={bootstrapping}
|
|
61
|
+
>
|
|
62
|
+
{bootstrapping ? <CircularProgress size="small"/> : (t("add_logged_user_as_admin") || "Add logged user as admin")}
|
|
63
|
+
</Button>
|
|
64
|
+
</div>
|
|
65
|
+
);
|
|
66
|
+
}
|
|
@@ -1,6 +1,23 @@
|
|
|
1
|
-
|
|
2
1
|
import React, { ReactNode, useEffect, useRef, useState } from "react";
|
|
3
2
|
|
|
3
|
+
/** Google Identity Services SDK — injected by the GIS <script> tag. */
|
|
4
|
+
declare global {
|
|
5
|
+
interface Window {
|
|
6
|
+
google?: {
|
|
7
|
+
accounts: {
|
|
8
|
+
oauth2: {
|
|
9
|
+
initCodeClient(config: {
|
|
10
|
+
client_id: string;
|
|
11
|
+
scope: string;
|
|
12
|
+
ux_mode: "popup" | "redirect";
|
|
13
|
+
callback: (response: { code?: string; error?: string }) => void;
|
|
14
|
+
}): { requestCode(): void };
|
|
15
|
+
};
|
|
16
|
+
};
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
4
21
|
import { Button, cls, IconButton, LoadingButton, Menu, MenuItem, TextField, Typography, iconSize } from "@rebasepro/ui";
|
|
5
22
|
import { ArrowLeftIcon, MailIcon, MoonIcon, SunIcon, SunMoonIcon } from "lucide-react";
|
|
6
23
|
import { AuthControllerExtended, User } from "@rebasepro/types";
|
|
@@ -119,7 +136,9 @@ export function LoginView({
|
|
|
119
136
|
|
|
120
137
|
// Resolve capabilities — explicit props override authController.capabilities
|
|
121
138
|
const caps = authController.capabilities ?? {};
|
|
122
|
-
const isBootstrapMode = needsSetup
|
|
139
|
+
const isBootstrapMode = needsSetup
|
|
140
|
+
?? ("needsSetup" in authController && !!(authController as { needsSetup?: boolean }).needsSetup)
|
|
141
|
+
?? false;
|
|
123
142
|
const canRegister = registrationEnabled ?? caps.registration ?? false;
|
|
124
143
|
const hasGoogleLogin = googleEnabled ?? caps.googleLogin ?? false;
|
|
125
144
|
const hasPasswordReset = caps.passwordReset ?? !!authController.forgotPassword;
|
|
@@ -243,15 +262,18 @@ export function LoginView({
|
|
|
243
262
|
/>
|
|
244
263
|
)}
|
|
245
264
|
{showRegistration && (
|
|
246
|
-
<
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
265
|
+
<div className="mt-2 text-center">
|
|
266
|
+
<Typography variant="body2" color="secondary">
|
|
267
|
+
Don't have an account?{" "}
|
|
268
|
+
<button
|
|
269
|
+
type="button"
|
|
270
|
+
className="font-semibold hover:underline cursor-pointer text-primary-600 dark:text-primary-400"
|
|
271
|
+
onClick={() => switchMode("register")}
|
|
272
|
+
>
|
|
273
|
+
Create one
|
|
274
|
+
</button>
|
|
275
|
+
</Typography>
|
|
276
|
+
</div>
|
|
255
277
|
)}
|
|
256
278
|
</div>
|
|
257
279
|
)}
|
|
@@ -342,24 +364,30 @@ function GoogleLoginButton({
|
|
|
342
364
|
googleClientId: string,
|
|
343
365
|
authController: AuthControllerExtended
|
|
344
366
|
}) {
|
|
345
|
-
const
|
|
367
|
+
const codeClientRef = useRef<{ requestCode(): void } | null>(null);
|
|
346
368
|
|
|
347
369
|
useEffect(() => {
|
|
348
370
|
if (!authController.googleLogin) return;
|
|
349
371
|
|
|
350
|
-
const google =
|
|
351
|
-
if (!google ||
|
|
372
|
+
const google = window.google;
|
|
373
|
+
if (!google || codeClientRef.current) return;
|
|
352
374
|
|
|
353
|
-
|
|
375
|
+
codeClientRef.current = google.accounts.oauth2.initCodeClient({
|
|
354
376
|
client_id: googleClientId,
|
|
355
377
|
scope: "openid email profile",
|
|
356
|
-
|
|
357
|
-
|
|
378
|
+
ux_mode: "popup",
|
|
379
|
+
callback: async (response: { code?: string; error?: string }) => {
|
|
380
|
+
if (response.error || !response.code) {
|
|
358
381
|
console.error("Google login error:", response.error);
|
|
359
382
|
return;
|
|
360
383
|
}
|
|
361
384
|
try {
|
|
362
|
-
|
|
385
|
+
// Send the authorization code to the backend.
|
|
386
|
+
// redirectUri "postmessage" is required when using popup ux_mode.
|
|
387
|
+
await authController.googleLogin!({
|
|
388
|
+
code: response.code,
|
|
389
|
+
redirectUri: "postmessage"
|
|
390
|
+
});
|
|
363
391
|
} catch (err: unknown) {
|
|
364
392
|
console.error("Google login error:", err);
|
|
365
393
|
}
|
|
@@ -368,11 +396,11 @@ function GoogleLoginButton({
|
|
|
368
396
|
}, [googleClientId, authController]);
|
|
369
397
|
|
|
370
398
|
const handleClick = () => {
|
|
371
|
-
if (!
|
|
399
|
+
if (!codeClientRef.current) {
|
|
372
400
|
console.error("Google Sign-In not loaded");
|
|
373
401
|
return;
|
|
374
402
|
}
|
|
375
|
-
|
|
403
|
+
codeClientRef.current.requestCode();
|
|
376
404
|
};
|
|
377
405
|
|
|
378
406
|
return (
|
|
@@ -81,7 +81,16 @@ export function useDataTableController<M extends Record<string, any> = any, USER
|
|
|
81
81
|
const paginationEnabled = collection.pagination === undefined || Boolean(collection.pagination);
|
|
82
82
|
const pageSize = typeof collection.pagination === "number" ? collection.pagination : DEFAULT_PAGE_SIZE;
|
|
83
83
|
|
|
84
|
-
const
|
|
84
|
+
const location = useLocation();
|
|
85
|
+
|
|
86
|
+
const [searchString, setSearchString] = React.useState<string | undefined>(() => {
|
|
87
|
+
if (updateUrl) {
|
|
88
|
+
const params = new URLSearchParams(location.search);
|
|
89
|
+
const urlSearch = params.get("search");
|
|
90
|
+
return urlSearch ? decodeURIComponent(urlSearch) : undefined;
|
|
91
|
+
}
|
|
92
|
+
return undefined;
|
|
93
|
+
});
|
|
85
94
|
|
|
86
95
|
const checkFilterCombination = useCallback((filterValues: FilterValues<any>,
|
|
87
96
|
sortBy?: [string, "asc" | "desc"]) => {
|
|
@@ -97,8 +106,6 @@ export function useDataTableController<M extends Record<string, any> = any, USER
|
|
|
97
106
|
return sort;
|
|
98
107
|
}, [sort, fixedFilter]);
|
|
99
108
|
|
|
100
|
-
const location = useLocation();
|
|
101
|
-
|
|
102
109
|
const {
|
|
103
110
|
filterValues: filterUrl,
|
|
104
111
|
sortBy: sortUrl
|
|
@@ -128,6 +135,11 @@ export function useDataTableController<M extends Record<string, any> = any, USER
|
|
|
128
135
|
} else {
|
|
129
136
|
setSortBy(urlSortBy as [Extract<keyof M, string> | (string & {}), "asc" | "desc"] | undefined);
|
|
130
137
|
}
|
|
138
|
+
|
|
139
|
+
// Sync search string from URL
|
|
140
|
+
const urlParams = new URLSearchParams(location.search);
|
|
141
|
+
const urlSearch = urlParams.get("search");
|
|
142
|
+
setSearchString(urlSearch ? decodeURIComponent(urlSearch) : undefined);
|
|
131
143
|
}, [location.search, updateUrl, fixedFilter, checkFilterCombination]);
|
|
132
144
|
|
|
133
145
|
useUpdateUrl(filterValues, sortBy, searchString, updateUrl);
|
package/src/components/index.tsx
CHANGED
package/src/core/Rebase.tsx
CHANGED
|
@@ -116,20 +116,21 @@ export function Rebase<USER extends User>(props: RebaseProps<USER>) {
|
|
|
116
116
|
}
|
|
117
117
|
|
|
118
118
|
// 2. Auto-derive from the client's WebSocket connection (Rebase backend)
|
|
119
|
-
const ws =
|
|
120
|
-
if (ws && typeof ws.executeSql === "function") {
|
|
119
|
+
const ws = client?.ws;
|
|
120
|
+
if (ws && typeof (ws as Record<string, unknown>).executeSql === "function") {
|
|
121
|
+
const wsAdmin = ws as import("@rebasepro/types").DatabaseAdmin;
|
|
121
122
|
return {
|
|
122
|
-
executeSql:
|
|
123
|
-
fetchAvailableDatabases:
|
|
124
|
-
fetchAvailableRoles:
|
|
125
|
-
fetchCurrentDatabase:
|
|
126
|
-
fetchUnmappedTables:
|
|
127
|
-
fetchTableMetadata:
|
|
123
|
+
executeSql: wsAdmin.executeSql!.bind(wsAdmin),
|
|
124
|
+
fetchAvailableDatabases: wsAdmin.fetchAvailableDatabases?.bind(wsAdmin),
|
|
125
|
+
fetchAvailableRoles: wsAdmin.fetchAvailableRoles?.bind(wsAdmin),
|
|
126
|
+
fetchCurrentDatabase: wsAdmin.fetchCurrentDatabase?.bind(wsAdmin),
|
|
127
|
+
fetchUnmappedTables: wsAdmin.fetchUnmappedTables?.bind(wsAdmin),
|
|
128
|
+
fetchTableMetadata: wsAdmin.fetchTableMetadata?.bind(wsAdmin),
|
|
128
129
|
// Branch admin capabilities
|
|
129
|
-
...(typeof
|
|
130
|
-
createBranch:
|
|
131
|
-
deleteBranch:
|
|
132
|
-
listBranches:
|
|
130
|
+
...(typeof wsAdmin.createBranch === "function" ? {
|
|
131
|
+
createBranch: wsAdmin.createBranch.bind(wsAdmin),
|
|
132
|
+
deleteBranch: wsAdmin.deleteBranch!.bind(wsAdmin),
|
|
133
|
+
listBranches: wsAdmin.listBranches!.bind(wsAdmin)
|
|
133
134
|
} : {})
|
|
134
135
|
};
|
|
135
136
|
}
|
|
@@ -224,7 +225,7 @@ export function Rebase<USER extends User>(props: RebaseProps<USER>) {
|
|
|
224
225
|
</RebaseI18nProvider>
|
|
225
226
|
);
|
|
226
227
|
|
|
227
|
-
const resolvedApiUrl = apiUrl ??
|
|
228
|
+
const resolvedApiUrl = apiUrl ?? client?.baseUrl;
|
|
228
229
|
|
|
229
230
|
if (resolvedApiUrl) {
|
|
230
231
|
return (
|
|
@@ -255,7 +256,12 @@ function RebaseInternal({
|
|
|
255
256
|
}) : children;
|
|
256
257
|
|
|
257
258
|
const plugins = customizationController.plugins;
|
|
258
|
-
|
|
259
|
+
const authController = context.authController;
|
|
260
|
+
const authReady = !loading
|
|
261
|
+
&& !authController.authLoading
|
|
262
|
+
&& (Boolean(authController.user) || authController.loginSkipped);
|
|
263
|
+
|
|
264
|
+
if (authReady && plugins && plugins.length > 0) {
|
|
259
265
|
return (
|
|
260
266
|
<PluginProviderStack
|
|
261
267
|
plugins={plugins}
|
package/src/hooks/index.tsx
CHANGED
|
@@ -7,10 +7,18 @@ const STORAGE_KEY_PREFIX = "rebase-collapsed-groups";
|
|
|
7
7
|
* with localStorage persistence. Automatically cleans up stale group entries
|
|
8
8
|
* when groups are removed from the navigation.
|
|
9
9
|
*
|
|
10
|
+
* Groups that have never been toggled by the user fall back to
|
|
11
|
+
* `defaults[groupName]` (driven by `collapsedByDefault` in config).
|
|
12
|
+
*
|
|
10
13
|
* @param groupNames - Array of group names to track
|
|
11
14
|
* @param namespace - Namespace for localStorage key (e.g., "home", "drawer") to allow independent state
|
|
15
|
+
* @param defaults - Optional map of group name → collapsed boolean from config
|
|
12
16
|
*/
|
|
13
|
-
export function useCollapsedGroups(
|
|
17
|
+
export function useCollapsedGroups(
|
|
18
|
+
groupNames: string[],
|
|
19
|
+
namespace = "default",
|
|
20
|
+
defaults?: Record<string, boolean>
|
|
21
|
+
) {
|
|
14
22
|
const storageKey = `${STORAGE_KEY_PREFIX}-${namespace}`;
|
|
15
23
|
|
|
16
24
|
// Load collapsed groups from localStorage on mount
|
|
@@ -57,13 +65,23 @@ export function useCollapsedGroups(groupNames: string[], namespace = "default")
|
|
|
57
65
|
}, [groupNames]);
|
|
58
66
|
|
|
59
67
|
const isGroupCollapsed = useCallback((name: string) => {
|
|
60
|
-
|
|
61
|
-
|
|
68
|
+
// If the user has explicitly toggled this group, use that value
|
|
69
|
+
if (name in collapsedGroups) {
|
|
70
|
+
return collapsedGroups[name];
|
|
71
|
+
}
|
|
72
|
+
// Otherwise fall back to the config default
|
|
73
|
+
return defaults?.[name] ?? false;
|
|
74
|
+
}, [collapsedGroups, defaults]);
|
|
62
75
|
|
|
63
76
|
const toggleGroupCollapsed = useCallback((name: string) => {
|
|
64
|
-
setCollapsedGroups(prev =>
|
|
65
|
-
|
|
66
|
-
|
|
77
|
+
setCollapsedGroups(prev => {
|
|
78
|
+
// Resolve current effective state (explicit or default)
|
|
79
|
+
const currentlyCollapsed = name in prev
|
|
80
|
+
? prev[name]
|
|
81
|
+
: (defaults?.[name] ?? false);
|
|
82
|
+
return { ...prev, [name]: !currentlyCollapsed };
|
|
83
|
+
});
|
|
84
|
+
}, [defaults]);
|
|
67
85
|
|
|
68
86
|
return useMemo(() => ({
|
|
69
87
|
isGroupCollapsed,
|
|
@@ -71,3 +89,27 @@ export function useCollapsedGroups(groupNames: string[], namespace = "default")
|
|
|
71
89
|
}), [isGroupCollapsed, toggleGroupCollapsed]);
|
|
72
90
|
}
|
|
73
91
|
|
|
92
|
+
/**
|
|
93
|
+
* Build a defaults map from navigationGroupMappings for a given namespace.
|
|
94
|
+
* Returns a Record<groupName, collapsed> that can be passed to useCollapsedGroups.
|
|
95
|
+
*/
|
|
96
|
+
export function buildCollapsedDefaults(
|
|
97
|
+
mappings: Array<{ name: string; collapsedByDefault?: boolean | { drawer?: boolean; home?: boolean } }> | undefined,
|
|
98
|
+
namespace: "drawer" | "home"
|
|
99
|
+
): Record<string, boolean> {
|
|
100
|
+
if (!mappings) return {};
|
|
101
|
+
const result: Record<string, boolean> = {};
|
|
102
|
+
for (const mapping of mappings) {
|
|
103
|
+
const val = mapping.collapsedByDefault;
|
|
104
|
+
if (val === undefined) continue;
|
|
105
|
+
if (typeof val === "boolean") {
|
|
106
|
+
result[mapping.name] = val;
|
|
107
|
+
} else {
|
|
108
|
+
const ns = val[namespace];
|
|
109
|
+
if (ns !== undefined) {
|
|
110
|
+
result[mapping.name] = ns;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return result;
|
|
115
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { AuthController, RebaseContext, User } from "@rebasepro/types";
|
|
2
2
|
import { useAuthController } from "./useAuthController";
|
|
3
|
+
import { useRebaseClient } from "./useRebaseClient";
|
|
3
4
|
import { useData } from "./data/useData";
|
|
4
5
|
import { useStorageSource } from "./useStorageSource";
|
|
5
6
|
import { useSnackbarController } from "./useSnackbarController";
|
|
@@ -11,9 +12,7 @@ import { useEffectiveRoleController } from "./useEffectiveRoleController";
|
|
|
11
12
|
import React, { useEffect, useContext } from "react";
|
|
12
13
|
import { useInternalUserManagementController } from "./useInternalUserManagementController";
|
|
13
14
|
|
|
14
|
-
//
|
|
15
|
-
// Wait, `databaseAdmin` hasn't been added to a context yet. Let's add it to a context in Rebase.tsx later.
|
|
16
|
-
// Let's create a placeholder for databaseAdmin context access.
|
|
15
|
+
// DatabaseAdmin is provided by <Rebase> via DatabaseAdminContext.
|
|
17
16
|
import { DatabaseAdminContext } from "../contexts/DatabaseAdminContext";
|
|
18
17
|
|
|
19
18
|
/**
|
|
@@ -27,6 +26,8 @@ import { DatabaseAdminContext } from "../contexts/DatabaseAdminContext";
|
|
|
27
26
|
*/
|
|
28
27
|
export const useRebaseContext = <USER extends User = User, AuthControllerType extends AuthController<USER> = AuthController<USER>>(): RebaseContext<USER, AuthControllerType> => {
|
|
29
28
|
|
|
29
|
+
const client = useRebaseClient<any>();
|
|
30
|
+
|
|
30
31
|
const authController = useAuthController<USER, AuthControllerType>();
|
|
31
32
|
const data = useData();
|
|
32
33
|
const storageSource = useStorageSource();
|
|
@@ -52,7 +53,8 @@ export const useRebaseContext = <USER extends User = User, AuthControllerType ex
|
|
|
52
53
|
analyticsController,
|
|
53
54
|
userManagement,
|
|
54
55
|
effectiveRoleController,
|
|
55
|
-
databaseAdmin
|
|
56
|
+
databaseAdmin,
|
|
57
|
+
client: client! // Client should be provided
|
|
56
58
|
});
|
|
57
59
|
|
|
58
60
|
React.useEffect(() => {
|
|
@@ -67,9 +69,12 @@ export const useRebaseContext = <USER extends User = User, AuthControllerType ex
|
|
|
67
69
|
analyticsController,
|
|
68
70
|
userManagement,
|
|
69
71
|
effectiveRoleController,
|
|
70
|
-
databaseAdmin
|
|
72
|
+
databaseAdmin,
|
|
73
|
+
client: client!
|
|
71
74
|
};
|
|
72
|
-
}, [authController,
|
|
75
|
+
}, [authController, data, storageSource, snackbarController, userConfigPersistence,
|
|
76
|
+
dialogsController, customizationController, analyticsController, userManagement,
|
|
77
|
+
effectiveRoleController, databaseAdmin, client]);
|
|
73
78
|
|
|
74
79
|
return rebaseContextRef.current;
|
|
75
80
|
}
|