@rebasepro/core 0.0.1-canary.eae7889 → 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/common/useDataTableController.d.ts +3 -3
- package/dist/components/index.d.ts +1 -0
- package/dist/hooks/data/useRelationSelector.d.ts +2 -2
- 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 +333 -121
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +330 -118
- package/dist/index.umd.js.map +1 -1
- package/dist/vitePlugin.d.ts +28 -1
- package/dist/vitePlugin.js +42 -0
- package/package.json +7 -8
- package/src/components/BootstrapAdminBanner.tsx +66 -0
- package/src/components/LoginView/LoginView.tsx +73 -42
- package/src/components/UserSelectPopover.tsx +8 -34
- package/src/components/common/useColumnsIds.tsx +16 -16
- package/src/components/common/useDataTableController.tsx +30 -18
- package/src/components/index.tsx +1 -1
- package/src/core/Rebase.tsx +20 -14
- package/src/hooks/data/useRelationSelector.tsx +6 -6
- 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/hooks/useStudioBridge.tsx +2 -1
- package/src/locales/de.ts +4 -0
- package/src/locales/en.ts +6 -0
- package/src/locales/es.ts +4 -0
- package/src/locales/fr.ts +4 -0
- package/src/locales/hi.ts +4 -0
- package/src/locales/it.ts +4 -0
- package/src/locales/pt.ts +4 -0
- package/src/util/previews.ts +16 -7
- 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"
|
|
@@ -47,16 +47,16 @@
|
|
|
47
47
|
},
|
|
48
48
|
"dependencies": {
|
|
49
49
|
"compressorjs": "^1.2.1",
|
|
50
|
-
"fast-equals": "
|
|
50
|
+
"fast-equals": "6.0.0",
|
|
51
51
|
"fuse.js": "^7.1.0",
|
|
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",
|
|
@@ -82,7 +82,6 @@
|
|
|
82
82
|
"npm-run-all": "^4.1.5",
|
|
83
83
|
"react-router-dom": "^7.0.0",
|
|
84
84
|
"ts-jest": "^29.4.5",
|
|
85
|
-
"ts-node": "^10.9.2",
|
|
86
85
|
"tsd": "^0.31.2",
|
|
87
86
|
"typescript": "^5.9.3",
|
|
88
87
|
"vite": "^7.2.4"
|
|
@@ -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;
|
|
@@ -171,7 +190,7 @@ export function LoginView({
|
|
|
171
190
|
return (
|
|
172
191
|
<div
|
|
173
192
|
className={cls(
|
|
174
|
-
"relative flex items-center justify-center h-screen w-screen p-4 transition-opacity duration-500 bg-white dark:bg-surface-
|
|
193
|
+
"relative flex items-center justify-center h-screen w-screen p-4 transition-opacity duration-500 bg-white dark:bg-surface-900",
|
|
175
194
|
fadeIn ? "opacity-100" : "opacity-0"
|
|
176
195
|
)}>
|
|
177
196
|
|
|
@@ -323,6 +342,19 @@ function LoginButton({
|
|
|
323
342
|
);
|
|
324
343
|
}
|
|
325
344
|
|
|
345
|
+
const GoogleIcon = () => (
|
|
346
|
+
<svg viewBox="0 0 24 24" width="20" height="20">
|
|
347
|
+
<path fill="#4285F4"
|
|
348
|
+
d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"/>
|
|
349
|
+
<path fill="#34A853"
|
|
350
|
+
d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"/>
|
|
351
|
+
<path fill="#FBBC05"
|
|
352
|
+
d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"/>
|
|
353
|
+
<path fill="#EA4335"
|
|
354
|
+
d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"/>
|
|
355
|
+
</svg>
|
|
356
|
+
);
|
|
357
|
+
|
|
326
358
|
function GoogleLoginButton({
|
|
327
359
|
disabled,
|
|
328
360
|
googleClientId,
|
|
@@ -332,54 +364,52 @@ function GoogleLoginButton({
|
|
|
332
364
|
googleClientId: string,
|
|
333
365
|
authController: AuthControllerExtended
|
|
334
366
|
}) {
|
|
335
|
-
const
|
|
336
|
-
if (!authController.googleLogin) return;
|
|
367
|
+
const codeClientRef = useRef<{ requestCode(): void } | null>(null);
|
|
337
368
|
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
if (!google) {
|
|
341
|
-
console.error("Google Sign-In not loaded");
|
|
342
|
-
return;
|
|
343
|
-
}
|
|
369
|
+
useEffect(() => {
|
|
370
|
+
if (!authController.googleLogin) return;
|
|
344
371
|
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
372
|
+
const google = window.google;
|
|
373
|
+
if (!google || codeClientRef.current) return;
|
|
374
|
+
|
|
375
|
+
codeClientRef.current = google.accounts.oauth2.initCodeClient({
|
|
376
|
+
client_id: googleClientId,
|
|
377
|
+
scope: "openid email profile",
|
|
378
|
+
ux_mode: "popup",
|
|
379
|
+
callback: async (response: { code?: string; error?: string }) => {
|
|
380
|
+
if (response.error || !response.code) {
|
|
381
|
+
console.error("Google login error:", response.error);
|
|
382
|
+
return;
|
|
353
383
|
}
|
|
354
|
-
|
|
384
|
+
try {
|
|
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
|
+
});
|
|
391
|
+
} catch (err: unknown) {
|
|
392
|
+
console.error("Google login error:", err);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
});
|
|
396
|
+
}, [googleClientId, authController]);
|
|
355
397
|
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
console.error("Google
|
|
398
|
+
const handleClick = () => {
|
|
399
|
+
if (!codeClientRef.current) {
|
|
400
|
+
console.error("Google Sign-In not loaded");
|
|
401
|
+
return;
|
|
359
402
|
}
|
|
403
|
+
codeClientRef.current.requestCode();
|
|
360
404
|
};
|
|
361
405
|
|
|
362
406
|
return (
|
|
363
|
-
<
|
|
407
|
+
<LoginButton
|
|
364
408
|
disabled={disabled}
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
<div className="flex items-center justify-center w-full gap-3 py-1">
|
|
370
|
-
<svg viewBox="0 0 24 24" width="20" height="20">
|
|
371
|
-
<path fill="#4285F4"
|
|
372
|
-
d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"/>
|
|
373
|
-
<path fill="#34A853"
|
|
374
|
-
d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"/>
|
|
375
|
-
<path fill="#FBBC05"
|
|
376
|
-
d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"/>
|
|
377
|
-
<path fill="#EA4335"
|
|
378
|
-
d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"/>
|
|
379
|
-
</svg>
|
|
380
|
-
<Typography variant="button">Continue with Google</Typography>
|
|
381
|
-
</div>
|
|
382
|
-
</Button>
|
|
409
|
+
text="Sign in with Google"
|
|
410
|
+
icon={<GoogleIcon/>}
|
|
411
|
+
onClick={handleClick}
|
|
412
|
+
/>
|
|
383
413
|
);
|
|
384
414
|
}
|
|
385
415
|
|
|
@@ -544,6 +574,7 @@ function LoginForm({
|
|
|
544
574
|
<LoadingButton
|
|
545
575
|
type="submit"
|
|
546
576
|
variant="filled"
|
|
577
|
+
color="primary"
|
|
547
578
|
className="w-full mt-1"
|
|
548
579
|
size="large"
|
|
549
580
|
loading={authController.authLoading}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
|
|
2
2
|
import React, { useCallback, useMemo, useRef, useState } from "react";
|
|
3
|
-
import { cls, defaultBorderMixin, Popover, Typography, CircularProgress, IconButton , iconSize } from "@rebasepro/ui";
|
|
3
|
+
import { cls, defaultBorderMixin, Popover, Typography, CircularProgress, IconButton , iconSize, SearchBar } from "@rebasepro/ui";
|
|
4
4
|
import { CircleUserIcon, SearchIcon, XIcon } from "lucide-react";
|
|
5
5
|
import { User } from "@rebasepro/types";
|
|
6
6
|
|
|
@@ -228,40 +228,14 @@ export function UserSelectPopover({
|
|
|
228
228
|
)}
|
|
229
229
|
</div>
|
|
230
230
|
|
|
231
|
-
{/*
|
|
231
|
+
{/* Search input */}
|
|
232
232
|
<div className={cls("px-2 py-1.5 border-b shrink-0", defaultBorderMixin)}>
|
|
233
|
-
<
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
ref={inputRef}
|
|
240
|
-
type="text"
|
|
241
|
-
value={searchText}
|
|
242
|
-
onChange={(e) => setSearchText(e.target.value)}
|
|
243
|
-
placeholder="SearchIcon by name, email, or role…"
|
|
244
|
-
className={cls(
|
|
245
|
-
"w-full pl-7 pr-7 py-1.5 text-xs rounded-md",
|
|
246
|
-
"bg-surface-100 dark:bg-surface-950 border",
|
|
247
|
-
defaultBorderMixin,
|
|
248
|
-
"outline-none focus:ring-1 focus:ring-primary/40",
|
|
249
|
-
"placeholder-text-disabled dark:placeholder-text-disabled-dark",
|
|
250
|
-
"text-text-primary dark:text-text-primary-dark"
|
|
251
|
-
)}
|
|
252
|
-
/>
|
|
253
|
-
{searchText && (
|
|
254
|
-
<button
|
|
255
|
-
onClick={() => {
|
|
256
|
-
setSearchText("");
|
|
257
|
-
inputRef.current?.focus();
|
|
258
|
-
}}
|
|
259
|
-
className="absolute right-1.5 top-1/2 -translate-y-1/2 p-0.5 rounded hover:bg-surface-200 dark:hover:bg-surface-700 text-text-disabled"
|
|
260
|
-
>
|
|
261
|
-
<XIcon size={iconSize.smallest}/>
|
|
262
|
-
</button>
|
|
263
|
-
)}
|
|
264
|
-
</div>
|
|
233
|
+
<SearchBar
|
|
234
|
+
inputRef={inputRef}
|
|
235
|
+
size="smallest"
|
|
236
|
+
placeholder="Search by name, email, or role…"
|
|
237
|
+
onTextSearch={(val) => setSearchText(val ?? "")}
|
|
238
|
+
/>
|
|
265
239
|
</div>
|
|
266
240
|
|
|
267
241
|
{/* User list */}
|
|
@@ -49,7 +49,7 @@ function hideAndExpandKeys<M extends Record<string, any>>(collection: EntityColl
|
|
|
49
49
|
if (key.includes(".")) {
|
|
50
50
|
const rootKey = key.split(".")[0];
|
|
51
51
|
const rootProperty = collection.properties[rootKey];
|
|
52
|
-
if (rootProperty && rootProperty.type === "map" && rootProperty.spreadChildren && rootProperty.properties) {
|
|
52
|
+
if (rootProperty && rootProperty.type === "map" && rootProperty.ui?.spreadChildren && rootProperty.properties) {
|
|
53
53
|
rootsWithExplicitChildren.add(rootKey);
|
|
54
54
|
}
|
|
55
55
|
}
|
|
@@ -66,12 +66,12 @@ function hideAndExpandKeys<M extends Record<string, any>>(collection: EntityColl
|
|
|
66
66
|
const property = collection.properties[key];
|
|
67
67
|
if (property) {
|
|
68
68
|
processedPropertyKeys.add(key);
|
|
69
|
-
if (property.hideFromCollection)
|
|
69
|
+
if (property.ui?.hideFromCollection)
|
|
70
70
|
return [null];
|
|
71
|
-
if (property.disabled && typeof property.disabled === "object" && property.disabled.hidden)
|
|
71
|
+
if (property.ui?.disabled && typeof property.ui?.disabled === "object" && property.ui?.disabled.hidden)
|
|
72
72
|
return [null];
|
|
73
73
|
|
|
74
|
-
if (property.type === "map" && property.spreadChildren && property.properties) {
|
|
74
|
+
if (property.type === "map" && property.ui?.spreadChildren && property.properties) {
|
|
75
75
|
// Check if this spread map has explicit child keys in propertiesOrder
|
|
76
76
|
if (rootsWithExplicitChildren.has(key)) {
|
|
77
77
|
// DON'T auto-expand - the children are explicitly listed elsewhere
|
|
@@ -84,7 +84,7 @@ function hideAndExpandKeys<M extends Record<string, any>>(collection: EntityColl
|
|
|
84
84
|
}
|
|
85
85
|
return [{
|
|
86
86
|
key,
|
|
87
|
-
disabled: Boolean(property.disabled) || Boolean(property.readOnly)
|
|
87
|
+
disabled: Boolean(property.ui?.disabled) || Boolean(property.ui?.readOnly)
|
|
88
88
|
}];
|
|
89
89
|
}
|
|
90
90
|
|
|
@@ -100,15 +100,15 @@ function hideAndExpandKeys<M extends Record<string, any>>(collection: EntityColl
|
|
|
100
100
|
// Mark root as seen
|
|
101
101
|
processedPropertyKeys.add(rootKey);
|
|
102
102
|
|
|
103
|
-
if (nestedProperty.hideFromCollection)
|
|
103
|
+
if (nestedProperty.ui?.hideFromCollection)
|
|
104
104
|
return [null];
|
|
105
|
-
if (nestedProperty.disabled && typeof nestedProperty.disabled === "object" && nestedProperty.disabled.hidden)
|
|
105
|
+
if (nestedProperty.ui?.disabled && typeof nestedProperty.ui?.disabled === "object" && nestedProperty.ui?.disabled.hidden)
|
|
106
106
|
return [null];
|
|
107
107
|
|
|
108
108
|
return [{
|
|
109
109
|
key,
|
|
110
|
-
disabled: Boolean(rootProperty.disabled) || Boolean(rootProperty.readOnly) ||
|
|
111
|
-
Boolean(nestedProperty.disabled) || Boolean(nestedProperty.readOnly)
|
|
110
|
+
disabled: Boolean(rootProperty.ui?.disabled) || Boolean(rootProperty.ui?.readOnly) ||
|
|
111
|
+
Boolean(nestedProperty.ui?.disabled) || Boolean(nestedProperty.ui?.readOnly)
|
|
112
112
|
}];
|
|
113
113
|
}
|
|
114
114
|
}
|
|
@@ -147,10 +147,10 @@ function hideAndExpandKeys<M extends Record<string, any>>(collection: EntityColl
|
|
|
147
147
|
|
|
148
148
|
const property = collection.properties[propKey];
|
|
149
149
|
if (!property) continue;
|
|
150
|
-
if (property.hideFromCollection) continue;
|
|
151
|
-
if (property.disabled && typeof property.disabled === "object" && property.disabled.hidden) continue;
|
|
150
|
+
if (property.ui?.hideFromCollection) continue;
|
|
151
|
+
if (property.ui?.disabled && typeof property.ui?.disabled === "object" && property.ui?.disabled.hidden) continue;
|
|
152
152
|
|
|
153
|
-
if (property.type === "map" && property.spreadChildren && property.properties) {
|
|
153
|
+
if (property.type === "map" && property.ui?.spreadChildren && property.properties) {
|
|
154
154
|
// For spread maps, add all children that weren't already added
|
|
155
155
|
const allChildConfigs = getColumnKeysForProperty(property as MapProperty, propKey);
|
|
156
156
|
for (const childConfig of allChildConfigs) {
|
|
@@ -162,7 +162,7 @@ function hideAndExpandKeys<M extends Record<string, any>>(collection: EntityColl
|
|
|
162
162
|
} else {
|
|
163
163
|
result.push({
|
|
164
164
|
key: propKey,
|
|
165
|
-
disabled: Boolean(property.disabled) || Boolean(property.readOnly)
|
|
165
|
+
disabled: Boolean(property.ui?.disabled) || Boolean(property.ui?.readOnly)
|
|
166
166
|
});
|
|
167
167
|
processedPropertyKeys.add(propKey);
|
|
168
168
|
}
|
|
@@ -193,17 +193,17 @@ function getDefaultColumnKeys<M extends Record<string, any> = any>(collection: E
|
|
|
193
193
|
}
|
|
194
194
|
|
|
195
195
|
export function getColumnKeysForProperty(property: Property, key: string, disabled?: boolean): PropertyColumnConfig[] {
|
|
196
|
-
if (property.type === "map" && property.spreadChildren && property.properties) {
|
|
196
|
+
if (property.type === "map" && property.ui?.spreadChildren && property.properties) {
|
|
197
197
|
return Object.entries(property.properties)
|
|
198
198
|
.flatMap(([childKey, childProperty]) => getColumnKeysForProperty(
|
|
199
199
|
childProperty as Readonly<Property>,
|
|
200
200
|
`${key}.${childKey}`,
|
|
201
|
-
disabled || Boolean(property.disabled) || Boolean(property.readOnly))
|
|
201
|
+
disabled || Boolean(property.ui?.disabled) || Boolean(property.ui?.readOnly))
|
|
202
202
|
);
|
|
203
203
|
}
|
|
204
204
|
return [{
|
|
205
205
|
key,
|
|
206
|
-
disabled: disabled || Boolean(property.disabled) || Boolean(property.readOnly)
|
|
206
|
+
disabled: disabled || Boolean(property.ui?.disabled) || Boolean(property.ui?.readOnly)
|
|
207
207
|
}];
|
|
208
208
|
}
|
|
209
209
|
|
|
@@ -30,7 +30,7 @@ export type DataTableControllerProps<M extends Record<string, any> = any> = {
|
|
|
30
30
|
/**
|
|
31
31
|
* Force filter to be applied to the table.
|
|
32
32
|
*/
|
|
33
|
-
|
|
33
|
+
fixedFilter?: FilterValues<string>;
|
|
34
34
|
|
|
35
35
|
scrollRestoration?: ScrollRestorationController;
|
|
36
36
|
|
|
@@ -53,7 +53,7 @@ export type DataTableControllerProps<M extends Record<string, any> = any> = {
|
|
|
53
53
|
* @param scrollRestoration
|
|
54
54
|
* @param entitiesDisplayedFirst
|
|
55
55
|
* @param lastDeleteTimestamp
|
|
56
|
-
* @param
|
|
56
|
+
* @param fixedFilterFromProps
|
|
57
57
|
* @param updateUrl
|
|
58
58
|
*/
|
|
59
59
|
export function useDataTableController<M extends Record<string, any> = any, USER extends User = User>(
|
|
@@ -63,25 +63,34 @@ export function useDataTableController<M extends Record<string, any> = any, USER
|
|
|
63
63
|
scrollRestoration,
|
|
64
64
|
entitiesDisplayedFirst,
|
|
65
65
|
lastDeleteTimestamp: _lastDeleteTimestamp,
|
|
66
|
-
|
|
66
|
+
fixedFilter: fixedFilterFromProps,
|
|
67
67
|
updateUrl
|
|
68
68
|
}: DataTableControllerProps<M>)
|
|
69
69
|
: EntityTableController<M> {
|
|
70
70
|
|
|
71
71
|
const {
|
|
72
|
-
|
|
72
|
+
defaultFilter,
|
|
73
73
|
sort,
|
|
74
|
-
|
|
74
|
+
fixedFilter: fixedFilterFromCollection
|
|
75
75
|
} = collection;
|
|
76
76
|
|
|
77
77
|
const [popupCell, setPopupCell] = React.useState<SelectedCellProps<M> | undefined>(undefined);
|
|
78
78
|
const dataClient = useData();
|
|
79
79
|
|
|
80
|
-
const
|
|
80
|
+
const fixedFilter = fixedFilterFromProps ?? fixedFilterFromCollection;
|
|
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"]) => {
|
|
@@ -90,21 +99,19 @@ export function useDataTableController<M extends Record<string, any> = any, USER
|
|
|
90
99
|
}, []);
|
|
91
100
|
|
|
92
101
|
const sortInternal = useMemo(() => {
|
|
93
|
-
if (sort &&
|
|
102
|
+
if (sort && fixedFilter && !checkFilterCombination(fixedFilter, sort)) {
|
|
94
103
|
console.warn("Initial sort is not compatible with the force filter. Ignoring initial sort");
|
|
95
104
|
return undefined;
|
|
96
105
|
}
|
|
97
106
|
return sort;
|
|
98
|
-
}, [sort,
|
|
99
|
-
|
|
100
|
-
const location = useLocation();
|
|
107
|
+
}, [sort, fixedFilter]);
|
|
101
108
|
|
|
102
109
|
const {
|
|
103
110
|
filterValues: filterUrl,
|
|
104
111
|
sortBy: sortUrl
|
|
105
112
|
} = parseFilterAndSort(location.search);
|
|
106
113
|
|
|
107
|
-
const [filterValues, setFilterValues] = React.useState<FilterValues<Extract<keyof M, string> | (string & {})> | undefined>(
|
|
114
|
+
const [filterValues, setFilterValues] = React.useState<FilterValues<Extract<keyof M, string> | (string & {})> | undefined>(fixedFilter ?? (updateUrl ? filterUrl : undefined) ?? defaultFilter ?? undefined);
|
|
108
115
|
const [sortBy, setSortBy] = React.useState<[Extract<keyof M, string> | (string & {}), "asc" | "desc"] | undefined>((updateUrl ? sortUrl : undefined) ?? sortInternal);
|
|
109
116
|
|
|
110
117
|
// Sync filter/sort state from URL on browser navigation (back/forward).
|
|
@@ -120,15 +127,20 @@ export function useDataTableController<M extends Record<string, any> = any, USER
|
|
|
120
127
|
initialSearchRef.current = null;
|
|
121
128
|
|
|
122
129
|
const { filterValues: urlFilterValues, sortBy: urlSortBy } = parseFilterAndSort(location.search);
|
|
123
|
-
if (!
|
|
130
|
+
if (!fixedFilter) {
|
|
124
131
|
setFilterValues(urlFilterValues as FilterValues<Extract<keyof M, string> | (string & {})> | undefined);
|
|
125
132
|
}
|
|
126
|
-
if (urlSortBy &&
|
|
133
|
+
if (urlSortBy && fixedFilter && !checkFilterCombination(fixedFilter, urlSortBy)) {
|
|
127
134
|
console.warn("URL sort is not compatible with the force filter.");
|
|
128
135
|
} else {
|
|
129
136
|
setSortBy(urlSortBy as [Extract<keyof M, string> | (string & {}), "asc" | "desc"] | undefined);
|
|
130
137
|
}
|
|
131
|
-
|
|
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);
|
|
143
|
+
}, [location.search, updateUrl, fixedFilter, checkFilterCombination]);
|
|
132
144
|
|
|
133
145
|
useUpdateUrl(filterValues, sortBy, searchString, updateUrl);
|
|
134
146
|
|
|
@@ -174,10 +186,10 @@ export function useDataTableController<M extends Record<string, any> = any, USER
|
|
|
174
186
|
const [dataLoadingError, setDataLoadingError] = useState<Error | undefined>();
|
|
175
187
|
const [noMoreToLoad, setNoMoreToLoad] = useState<boolean>(false);
|
|
176
188
|
|
|
177
|
-
const clearFilter = useCallback(() => setFilterValues(
|
|
189
|
+
const clearFilter = useCallback(() => setFilterValues(fixedFilter ?? undefined), [fixedFilter]);
|
|
178
190
|
|
|
179
191
|
const updateFilterValues = useCallback((updatedFilter: FilterValues<Extract<keyof M, string> | (string & {})> | undefined) => {
|
|
180
|
-
if (
|
|
192
|
+
if (fixedFilter) {
|
|
181
193
|
console.warn("Filter is not compatible with the force filter. Ignoring filter");
|
|
182
194
|
return;
|
|
183
195
|
}
|
|
@@ -186,7 +198,7 @@ export function useDataTableController<M extends Record<string, any> = any, USER
|
|
|
186
198
|
} else {
|
|
187
199
|
setFilterValues(updatedFilter);
|
|
188
200
|
}
|
|
189
|
-
}, [
|
|
201
|
+
}, [fixedFilter]);
|
|
190
202
|
|
|
191
203
|
useEffect(() => {
|
|
192
204
|
|
package/src/components/index.tsx
CHANGED