@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.
Files changed (37) hide show
  1. package/dist/components/BootstrapAdminBanner.d.ts +4 -0
  2. package/dist/components/LoginView/LoginView.d.ts +22 -0
  3. package/dist/components/common/useDataTableController.d.ts +3 -3
  4. package/dist/components/index.d.ts +1 -0
  5. package/dist/hooks/data/useRelationSelector.d.ts +2 -2
  6. package/dist/hooks/index.d.ts +1 -0
  7. package/dist/hooks/useCollapsedGroups.d.ts +16 -1
  8. package/dist/hooks/useResolvedComponent.d.ts +47 -0
  9. package/dist/index.es.js +333 -121
  10. package/dist/index.es.js.map +1 -1
  11. package/dist/index.umd.js +330 -118
  12. package/dist/index.umd.js.map +1 -1
  13. package/dist/vitePlugin.d.ts +28 -1
  14. package/dist/vitePlugin.js +42 -0
  15. package/package.json +7 -8
  16. package/src/components/BootstrapAdminBanner.tsx +66 -0
  17. package/src/components/LoginView/LoginView.tsx +73 -42
  18. package/src/components/UserSelectPopover.tsx +8 -34
  19. package/src/components/common/useColumnsIds.tsx +16 -16
  20. package/src/components/common/useDataTableController.tsx +30 -18
  21. package/src/components/index.tsx +1 -1
  22. package/src/core/Rebase.tsx +20 -14
  23. package/src/hooks/data/useRelationSelector.tsx +6 -6
  24. package/src/hooks/index.tsx +1 -0
  25. package/src/hooks/useCollapsedGroups.ts +48 -6
  26. package/src/hooks/useRebaseContext.tsx +11 -6
  27. package/src/hooks/useResolvedComponent.tsx +157 -0
  28. package/src/hooks/useStudioBridge.tsx +2 -1
  29. package/src/locales/de.ts +4 -0
  30. package/src/locales/en.ts +6 -0
  31. package/src/locales/es.ts +4 -0
  32. package/src/locales/fr.ts +4 -0
  33. package/src/locales/hi.ts +4 -0
  34. package/src/locales/it.ts +4 -0
  35. package/src/locales/pt.ts +4 -0
  36. package/src/util/previews.ts +16 -7
  37. package/src/vitePlugin.ts +87 -1
@@ -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
- * It provides a virtual module "virtual:rebase-collections" that statically exports the resolved collections array.
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
  };
@@ -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.0.1-canary.eae7889",
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": "5.3.0",
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/formex": "0.0.1-canary.eae7889",
56
- "@rebasepro/types": "0.0.1-canary.eae7889",
57
- "@rebasepro/common": "0.0.1-canary.eae7889",
58
- "@rebasepro/ui": "0.0.1-canary.eae7889",
59
- "@rebasepro/utils": "0.0.1-canary.eae7889"
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 ?? (authController as any).needsSetup ?? false;
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-950",
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 handleGoogleLogin = async () => {
336
- if (!authController.googleLogin) return;
367
+ const codeClientRef = useRef<{ requestCode(): void } | null>(null);
337
368
 
338
- try {
339
- const google = (window as unknown as { google?: { accounts: { id: { initialize: (config: { client_id: string; callback: (response: { credential: string }) => void }) => void; prompt: () => void } } } }).google;
340
- if (!google) {
341
- console.error("Google Sign-In not loaded");
342
- return;
343
- }
369
+ useEffect(() => {
370
+ if (!authController.googleLogin) return;
344
371
 
345
- google.accounts.id.initialize({
346
- client_id: googleClientId,
347
- callback: async (response: { credential: string }) => {
348
- try {
349
- await authController.googleLogin!(response.credential);
350
- } catch (err: unknown) {
351
- console.error("Google login error:", err);
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
- google.accounts.id.prompt();
357
- } catch (err: unknown) {
358
- console.error("Google login error:", err);
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
- <Button
407
+ <LoginButton
364
408
  disabled={disabled}
365
- className="w-full"
366
- variant="outlined"
367
- size="large"
368
- onClick={handleGoogleLogin}>
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
- {/* SearchIcon input */}
231
+ {/* Search input */}
232
232
  <div className={cls("px-2 py-1.5 border-b shrink-0", defaultBorderMixin)}>
233
- <div className="relative">
234
- <SearchIcon
235
- size={"smallest"}
236
- className="absolute left-2 top-1/2 -translate-y-1/2 text-text-disabled dark:text-text-disabled-dark pointer-events-none"
237
- />
238
- <input
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
- forceFilter?: FilterValues<string>;
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 forceFilterFromProps
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
- forceFilter: forceFilterFromProps,
66
+ fixedFilter: fixedFilterFromProps,
67
67
  updateUrl
68
68
  }: DataTableControllerProps<M>)
69
69
  : EntityTableController<M> {
70
70
 
71
71
  const {
72
- filter,
72
+ defaultFilter,
73
73
  sort,
74
- forceFilter: forceFilterFromCollection
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 forceFilter = forceFilterFromProps ?? forceFilterFromCollection;
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 [searchString, setSearchString] = React.useState<string | undefined>();
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 && forceFilter && !checkFilterCombination(forceFilter, 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, forceFilter]);
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>(forceFilter ?? (updateUrl ? filterUrl : undefined) ?? filter ?? 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 (!forceFilter) {
130
+ if (!fixedFilter) {
124
131
  setFilterValues(urlFilterValues as FilterValues<Extract<keyof M, string> | (string & {})> | undefined);
125
132
  }
126
- if (urlSortBy && forceFilter && !checkFilterCombination(forceFilter, 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
- }, [location.search, updateUrl, forceFilter, checkFilterCombination]);
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(forceFilter ?? undefined), [forceFilter]);
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 (forceFilter) {
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
- }, [forceFilter]);
201
+ }, [fixedFilter]);
190
202
 
191
203
  useEffect(() => {
192
204
 
@@ -22,4 +22,4 @@ export * from "./LoginView";
22
22
 
23
23
  export * from "./RebaseAuth";
24
24
 
25
-
25
+ export * from "./BootstrapAdminBanner";