@rebasepro/core 0.2.3 → 0.2.4

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 (36) hide show
  1. package/dist/components/LoginView/LoginView.d.ts +17 -1
  2. package/dist/components/common/types.d.ts +10 -7
  3. package/dist/components/common/useDebouncedData.d.ts +1 -1
  4. package/dist/core/RebaseProps.d.ts +13 -2
  5. package/dist/core/RebaseRouter.d.ts +1 -1
  6. package/dist/hooks/index.d.ts +0 -1
  7. package/dist/index.es.js +499 -418
  8. package/dist/index.es.js.map +1 -1
  9. package/dist/index.umd.js +499 -418
  10. package/dist/index.umd.js.map +1 -1
  11. package/dist/util/entity_cache.d.ts +0 -5
  12. package/dist/util/index.d.ts +0 -2
  13. package/dist/util/useStorageUploadController.d.ts +2 -2
  14. package/package.json +6 -6
  15. package/src/components/BootstrapAdminBanner.tsx +12 -3
  16. package/src/components/LoginView/LoginView.tsx +151 -6
  17. package/src/components/UserSettingsView.tsx +95 -2
  18. package/src/components/common/types.tsx +7 -7
  19. package/src/components/common/useDebouncedData.ts +2 -2
  20. package/src/core/Rebase.tsx +3 -2
  21. package/src/core/RebaseProps.tsx +15 -2
  22. package/src/core/RebaseRouter.tsx +1 -1
  23. package/src/hooks/index.tsx +0 -1
  24. package/src/hooks/useResolvedComponent.tsx +4 -3
  25. package/src/locales/en.ts +13 -0
  26. package/src/locales/es.ts +11 -1
  27. package/src/util/entity_cache.ts +1 -27
  28. package/src/util/icon_list.ts +2 -2
  29. package/src/util/index.ts +2 -2
  30. package/src/util/useStorageUploadController.tsx +4 -4
  31. package/dist/hooks/useValidateAuthenticator.d.ts +0 -21
  32. package/dist/util/icon_synonyms.d.ts +0 -1
  33. package/dist/util/useTraceUpdate.d.ts +0 -2
  34. package/src/hooks/useValidateAuthenticator.tsx +0 -116
  35. package/src/util/icon_synonyms.ts +0 -1
  36. package/src/util/useTraceUpdate.tsx +0 -24
@@ -7,7 +7,6 @@ export declare function saveEntityToCache(path: string, data: object): void;
7
7
  export declare function removeEntityFromMemoryCache(path: string): void;
8
8
  export declare function saveEntityToMemoryCache(path: string, data: object): void;
9
9
  export declare function getEntityFromMemoryCache(path: string): object | undefined;
10
- export declare function hasEntityInCache(path: string): boolean;
11
10
  /**
12
11
  * Retrieves an entity from the in-memory cache or `sessionStorage`.
13
12
  * If the entity is not in the cache but exists in `sessionStorage`, it loads it into the cache.
@@ -20,8 +19,4 @@ export declare function getEntityFromCache(path: string): object | undefined;
20
19
  * @param path - The unique path/key for the entity to remove.
21
20
  */
22
21
  export declare function removeEntityFromCache(path: string): void;
23
- /**
24
- * Clears the entire in-memory cache and removes all related entities from `sessionStorage`.
25
- */
26
- export declare function clearEntityCache(): void;
27
22
  export declare function flattenKeys(obj: Record<string, unknown> | unknown[], prefix?: string, result?: string[]): string[];
@@ -1,10 +1,8 @@
1
1
  export * from "./icon_list";
2
- export * from "./icon_synonyms";
3
2
  export * from "./icons";
4
3
  export * from "./createFormexStub";
5
4
  export * from "./entity_cache";
6
5
  export * from "./useStorageUploadController";
7
- export * from "./useTraceUpdate";
8
6
  export * from "./previews";
9
7
  export * from "./enums";
10
8
  export * from "./constants";
@@ -12,7 +12,7 @@ export interface StorageFieldItem {
12
12
  storagePathOrDownloadUrl?: string;
13
13
  file?: File;
14
14
  fileName?: string;
15
- metadata?: any;
15
+ metadata?: Record<string, unknown>;
16
16
  size: StorageFieldSize;
17
17
  }
18
18
  export declare function useStorageUploadController<M extends Record<string, unknown>>({ entityId, entityValues, path, value, property, propertyKey, storageSource, disabled, onChange }: {
@@ -31,7 +31,7 @@ export declare function useStorageUploadController<M extends Record<string, unkn
31
31
  storage: StorageConfig;
32
32
  fileNameBuilder: (file: File) => Promise<string>;
33
33
  storagePathBuilder: (file: File) => string;
34
- onFileUploadComplete: (uploadedPath: string, entry: StorageFieldItem, metadata?: any, uploadedUrl?: string) => Promise<void>;
34
+ onFileUploadComplete: (uploadedPath: string, entry: StorageFieldItem, metadata?: Record<string, unknown>, uploadedUrl?: string) => Promise<void>;
35
35
  onFileUploadError: (entry: StorageFieldItem) => void;
36
36
  onFilesAdded: (acceptedFiles: File[]) => Promise<void>;
37
37
  multipleFilesSupported: boolean;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@rebasepro/core",
3
3
  "type": "module",
4
- "version": "0.2.3",
4
+ "version": "0.2.4",
5
5
  "description": "Rebase core — framework-agnostic runtime for data-driven admin panels",
6
6
  "funding": {
7
7
  "url": "https://github.com/sponsors/rebaseco"
@@ -53,11 +53,11 @@
53
53
  "notistack": "^3.0.2",
54
54
  "react-compiler-runtime": "1.0.0",
55
55
  "react-i18next": "^14.1.3",
56
- "@rebasepro/formex": "0.2.3",
57
- "@rebasepro/types": "0.2.3",
58
- "@rebasepro/ui": "0.2.3",
59
- "@rebasepro/common": "0.2.3",
60
- "@rebasepro/utils": "0.2.3"
56
+ "@rebasepro/common": "0.2.4",
57
+ "@rebasepro/types": "0.2.4",
58
+ "@rebasepro/formex": "0.2.4",
59
+ "@rebasepro/utils": "0.2.4",
60
+ "@rebasepro/ui": "0.2.4"
61
61
  },
62
62
  "peerDependencies": {
63
63
  "react": ">=19.0.0",
@@ -27,10 +27,19 @@ export function BootstrapAdminBanner({
27
27
  return null;
28
28
  }
29
29
 
30
- const { users, loading: delegateLoading, bootstrapAdmin, usersError } = userManagement;
31
- const hasAdmin = users.some(u => u.roles?.includes("admin"));
30
+ // Non-admin users don't load the users list (admin API is skipped to
31
+ // avoid 403s), so `users` would be empty and falsely trigger this banner.
32
+ // Only admin users (or users with no roles yet, during initial bootstrap)
33
+ // should ever see this prompt.
34
+ const loggedInUserRoles = loggedInUser.roles ?? [];
35
+ const isLoggedInUserAdmin = loggedInUserRoles.length === 0 || loggedInUserRoles.some(r => r === "admin");
36
+ if (!isLoggedInUserAdmin) {
37
+ return null;
38
+ }
39
+
40
+ const { hasAdminUsers, loading: delegateLoading, bootstrapAdmin, usersError } = userManagement;
32
41
 
33
- if (delegateLoading || hasAdmin || usersError || !bootstrapAdmin) {
42
+ if (delegateLoading || hasAdminUsers || usersError || !bootstrapAdmin) {
34
43
  return null;
35
44
  }
36
45
 
@@ -95,6 +95,26 @@ export interface LoginViewProps {
95
95
  */
96
96
  googleClientId?: string;
97
97
 
98
+ /**
99
+ * GitHub client ID for GitHub OAuth.
100
+ */
101
+ githubClientId?: string;
102
+
103
+ /**
104
+ * LinkedIn client ID for LinkedIn OAuth.
105
+ */
106
+ linkedinClientId?: string;
107
+
108
+ /**
109
+ * Optional custom title shown above options
110
+ */
111
+ title?: string;
112
+
113
+ /**
114
+ * Optional custom subtitle shown above options
115
+ */
116
+ subtitle?: string;
117
+
98
118
  /**
99
119
  * When true, shows bootstrap/setup UI (first-user creation).
100
120
  * If not set, derived from `authController` if it exposes `needsSetup`.
@@ -123,8 +143,13 @@ export function LoginView({
123
143
  disabled = false,
124
144
  notAllowedError,
125
145
  googleClientId,
146
+ githubClientId,
147
+ linkedinClientId,
148
+ title,
149
+ subtitle,
126
150
  needsSetup,
127
- registrationEnabled
151
+ registrationEnabled,
152
+ additionalComponent
128
153
  }: LoginViewProps) {
129
154
 
130
155
  const modeState = useModeController();
@@ -150,6 +175,8 @@ export function LoginView({
150
175
  ?? false;
151
176
  const canRegister = registrationEnabled ?? caps.registration ?? false;
152
177
  const hasGoogleLogin = googleClientId && (caps.enabledProviders?.includes("google") ?? caps.googleLogin ?? false);
178
+ const hasGitHubLogin = githubClientId && (caps.enabledProviders?.includes("github") ?? false);
179
+ const hasLinkedinLogin = linkedinClientId && (caps.enabledProviders?.includes("linkedin") ?? false);
153
180
  const hasPasswordReset = caps.passwordReset ?? !!authController.forgotPassword;
154
181
 
155
182
  const showRegistration = !disableSignupScreen && canRegister;
@@ -159,6 +186,28 @@ export function LoginView({
159
186
  return () => clearTimeout(timer);
160
187
  }, []);
161
188
 
189
+ // Effect to handle incoming redirect OAuth codes (GitHub, LinkedIn, etc.)
190
+ useEffect(() => {
191
+ const params = new URLSearchParams(window.location.search);
192
+ const code = params.get("code");
193
+ const provider = localStorage.getItem("rebase_oauth_provider");
194
+ if (code && provider) {
195
+ localStorage.removeItem("rebase_oauth_provider");
196
+ // Clear URL search params without page reload
197
+ const cleanUrl = window.location.origin + window.location.pathname;
198
+ window.history.replaceState({}, document.title, cleanUrl);
199
+
200
+ if (authController.oauthLogin) {
201
+ authController.oauthLogin(provider, {
202
+ code,
203
+ redirectUri: cleanUrl
204
+ }).catch((err) => {
205
+ console.error(`${provider} login failed:`, err);
206
+ });
207
+ }
208
+ }
209
+ }, [authController]);
210
+
162
211
  function buildErrorView() {
163
212
  if (!authController.authProviderError) return null;
164
213
  if (authController.user != null) return null;
@@ -192,17 +241,21 @@ export function LoginView({
192
241
  } else if (notAllowedError instanceof Error) {
193
242
  notAllowedMessage = notAllowedError.message;
194
243
  } else {
195
- notAllowedMessage = "It looks like you don't have access, based on the specified Authenticator configuration";
244
+ notAllowedMessage = "It looks like you don't have access, based on the specified access configuration";
196
245
  }
197
246
  }
198
247
 
199
248
  return (
200
249
  <div
201
250
  className={cls(
202
- "relative flex items-center justify-center h-screen w-screen p-4 transition-opacity duration-500 bg-surface-50 dark:bg-surface-800",
251
+ "relative flex items-center justify-center h-screen w-screen p-4 transition-opacity duration-500 bg-surface-50 dark:bg-surface-950 overflow-hidden",
203
252
  fadeIn ? "opacity-100" : "opacity-0"
204
253
  )}>
205
254
 
255
+ {/* Glowing background blobs */}
256
+ <div className="absolute top-[-10%] left-[-10%] w-[50%] h-[50%] rounded-full bg-primary-500/10 blur-[120px] pointer-events-none" />
257
+ <div className="absolute bottom-[-10%] right-[-10%] w-[50%] h-[50%] rounded-full bg-indigo-500/10 blur-[120px] pointer-events-none" />
258
+
206
259
  {/* Top-right controls */}
207
260
  <div className="absolute top-4 right-4 flex items-center gap-1 z-10">
208
261
  <LanguageToggle/>
@@ -220,9 +273,9 @@ export function LoginView({
220
273
  </Menu>
221
274
  </div>
222
275
 
223
- <div className="flex flex-col items-center w-[480px] max-w-full p-8 sm:p-10">
276
+ <div className="relative flex flex-col items-center w-[440px] max-w-full p-8 sm:p-10 bg-white/70 dark:bg-surface-900/60 backdrop-blur-xl border border-surface-200/50 dark:border-surface-800/50 rounded-2xl shadow-2xl z-10 transition-all duration-300 hover:shadow-primary-500/5">
224
277
  {/* Logo */}
225
- <div className="w-32 h-32 m-2 mb-6">
278
+ <div className="w-24 h-24 m-2 mb-4 drop-shadow-md">
226
279
  {logoComponent}
227
280
  </div>
228
281
 
@@ -257,6 +310,20 @@ export function LoginView({
257
310
  {/* Provider buttons screen */}
258
311
  {mode === "buttons" && (
259
312
  <div className="w-full flex flex-col gap-3 mt-2">
313
+ {(title || subtitle) && (
314
+ <div className="text-center mb-2">
315
+ {title && (
316
+ <Typography variant="h6" className="mb-0.5 font-bold">
317
+ {title}
318
+ </Typography>
319
+ )}
320
+ {subtitle && (
321
+ <Typography variant="body2" color="secondary" className="mb-4">
322
+ {subtitle}
323
+ </Typography>
324
+ )}
325
+ </div>
326
+ )}
260
327
  <LoginButton
261
328
  disabled={disabled}
262
329
  text={"Sign in with email"}
@@ -270,6 +337,18 @@ export function LoginView({
270
337
  authController={authController}
271
338
  />
272
339
  )}
340
+ {hasGitHubLogin && githubClientId && (
341
+ <GitHubLoginButton
342
+ disabled={disabled}
343
+ githubClientId={githubClientId}
344
+ />
345
+ )}
346
+ {hasLinkedinLogin && linkedinClientId && (
347
+ <LinkedInLoginButton
348
+ disabled={disabled}
349
+ linkedinClientId={linkedinClientId}
350
+ />
351
+ )}
273
352
  {showRegistration && (
274
353
  <div className="mt-2 text-center">
275
354
  <Typography variant="body2" color="secondary">
@@ -323,6 +402,12 @@ export function LoginView({
323
402
  </>
324
403
  )}
325
404
  </div>
405
+
406
+ {additionalComponent && (
407
+ <div className="w-full">
408
+ {additionalComponent}
409
+ </div>
410
+ )}
326
411
  </div>
327
412
  </div>
328
413
  );
@@ -337,7 +422,7 @@ function LoginButton({
337
422
  return (
338
423
  <Button
339
424
  disabled={disabled}
340
- className="w-full"
425
+ className="w-full transition-transform duration-200 active:scale-[0.98]"
341
426
  variant="outlined"
342
427
  size="large"
343
428
  onClick={onClick}>
@@ -422,6 +507,66 @@ function GoogleLoginButton({
422
507
  );
423
508
  }
424
509
 
510
+ const GitHubIcon = () => (
511
+ <svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor">
512
+ <path d="M12 2C6.477 2 2 6.477 2 12c0 4.42 2.865 8.166 6.839 9.489.5.092.682-.217.682-.482 0-.237-.008-.866-.013-1.7-2.782.603-3.369-1.34-3.369-1.34-.454-1.156-1.11-1.464-1.11-1.464-.908-.62.069-.608.069-.608 1.003.07 1.531 1.03 1.531 1.03.892 1.529 2.341 1.087 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.11-4.555-4.943 0-1.091.39-1.984 1.029-2.683-.103-.253-.446-1.27.098-2.647 0 0 .84-.269 2.75 1.025A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.294 2.747-1.025 2.747-1.025.546 1.377.203 2.394.1 2.647.64.699 1.028 1.592 1.028 2.683 0 3.842-2.339 4.687-4.566 4.935.359.309.678.919.678 1.852 0 1.336-.012 2.415-.012 2.743 0 .267.18.579.688.481C19.137 20.162 22 16.418 22 12c0-5.523-4.477-10-10-10z"/>
513
+ </svg>
514
+ );
515
+
516
+ function GitHubLoginButton({
517
+ disabled,
518
+ githubClientId
519
+ }: {
520
+ disabled?: boolean,
521
+ githubClientId: string
522
+ }) {
523
+ const handleClick = () => {
524
+ localStorage.setItem("rebase_oauth_provider", "github");
525
+ const redirectUri = encodeURIComponent(window.location.origin + window.location.pathname);
526
+ const scope = "read:user,user:email";
527
+ window.location.href = `https://github.com/login/oauth/authorize?client_id=${githubClientId}&redirect_uri=${redirectUri}&scope=${scope}`;
528
+ };
529
+
530
+ return (
531
+ <LoginButton
532
+ disabled={disabled}
533
+ text="Sign in with GitHub"
534
+ icon={<GitHubIcon/>}
535
+ onClick={handleClick}
536
+ />
537
+ );
538
+ }
539
+
540
+ const LinkedInIcon = () => (
541
+ <svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor">
542
+ <path d="M19 0h-14c-2.761 0-5 2.239-5 5v14c0 2.761 2.239 5 5 5h14c2.762 0 5-2.239 5-5v-14c0-2.761-2.238-5-5-5zm-11 19h-3v-11h3v11zm-1.5-12.268c-.966 0-1.75-.79-1.75-1.764s.784-1.764 1.75-1.764 1.75.79 1.75 1.764-.783 1.764-1.75 1.764zm13.5 12.268h-3v-5.604c0-3.368-4-3.113-4 0v5.604h-3v-11h3v1.765c1.396-2.586 7-2.777 7 2.476v6.759z"/>
543
+ </svg>
544
+ );
545
+
546
+ function LinkedInLoginButton({
547
+ disabled,
548
+ linkedinClientId
549
+ }: {
550
+ disabled?: boolean,
551
+ linkedinClientId: string
552
+ }) {
553
+ const handleClick = () => {
554
+ localStorage.setItem("rebase_oauth_provider", "linkedin");
555
+ const redirectUri = encodeURIComponent(window.location.origin + window.location.pathname);
556
+ const scope = "openid profile email";
557
+ window.location.href = `https://www.linkedin.com/oauth/v2/authorization?response_type=code&client_id=${linkedinClientId}&redirect_uri=${redirectUri}&scope=${scope}`;
558
+ };
559
+
560
+ return (
561
+ <LoginButton
562
+ disabled={disabled}
563
+ text="Sign in with LinkedIn"
564
+ icon={<LinkedInIcon/>}
565
+ onClick={handleClick}
566
+ />
567
+ );
568
+ }
569
+
425
570
  function LoginForm({
426
571
  onClose,
427
572
  onForgotPassword,
@@ -24,18 +24,22 @@ interface SessionInfo {
24
24
  interface ExtendedAuthController {
25
25
  user: { displayName?: string | null; photoURL?: string | null; email?: string | null } | null;
26
26
  updateProfile?: (displayName: string, photoURL: string) => Promise<void>;
27
+ changePassword?: (oldPassword: string, newPassword: string) => Promise<void>;
27
28
  fetchSessions?: () => Promise<SessionInfo[]>;
28
29
  revokeSession?: (id: string) => Promise<void>;
29
30
  revokeAllSessions?: () => Promise<void>;
30
31
  signOut: () => Promise<void>;
31
32
  }
32
33
 
34
+ type ActiveTab = "profile" | "security" | "sessions";
35
+
33
36
  export function UserSettingsView() {
34
37
  const authController = useAuthController() as ExtendedAuthController;
35
38
  const user = authController.user;
36
39
  const { t } = useTranslation();
37
40
 
38
- const [activeTab, setActiveTab] = useState<"profile" | "sessions">("profile");
41
+ const hasPasswordChange = !!authController.changePassword;
42
+ const [activeTab, setActiveTab] = useState<ActiveTab>("profile");
39
43
 
40
44
  // Profile state
41
45
  const [displayName, setDisplayName] = useState(user?.displayName || "");
@@ -43,6 +47,14 @@ export function UserSettingsView() {
43
47
  const [savingProfile, setSavingProfile] = useState(false);
44
48
  const [profileError, setProfileError] = useState<string | null>(null);
45
49
 
50
+ // Password change state
51
+ const [currentPassword, setCurrentPassword] = useState("");
52
+ const [newPassword, setNewPassword] = useState("");
53
+ const [confirmPassword, setConfirmPassword] = useState("");
54
+ const [changingPassword, setChangingPassword] = useState(false);
55
+ const [passwordError, setPasswordError] = useState<string | null>(null);
56
+ const [passwordSuccess, setPasswordSuccess] = useState<string | null>(null);
57
+
46
58
  // Sessions state
47
59
  const [sessions, setSessions] = useState<SessionInfo[]>([]);
48
60
  const [loadingSessions, setLoadingSessions] = useState(false);
@@ -74,6 +86,41 @@ export function UserSettingsView() {
74
86
  }
75
87
  };
76
88
 
89
+ const handleChangePassword = async () => {
90
+ setPasswordError(null);
91
+ setPasswordSuccess(null);
92
+
93
+ // Validate
94
+ if (newPassword.length < 8) {
95
+ setPasswordError(t("password_too_short"));
96
+ return;
97
+ }
98
+ if (newPassword !== confirmPassword) {
99
+ setPasswordError(t("passwords_dont_match"));
100
+ return;
101
+ }
102
+
103
+ setChangingPassword(true);
104
+ try {
105
+ if (authController.changePassword) {
106
+ await authController.changePassword(currentPassword, newPassword);
107
+ setPasswordSuccess(t("password_changed"));
108
+ setCurrentPassword("");
109
+ setNewPassword("");
110
+ setConfirmPassword("");
111
+ // Backend invalidates all sessions on password change,
112
+ // so the user will be logged out shortly
113
+ setTimeout(() => {
114
+ authController.signOut();
115
+ }, 2000);
116
+ }
117
+ } catch (e: unknown) {
118
+ setPasswordError(e instanceof Error ? e.message : String(e));
119
+ } finally {
120
+ setChangingPassword(false);
121
+ }
122
+ };
123
+
77
124
  const loadSessions = async () => {
78
125
  setLoadingSessions(true);
79
126
  setSessionsError(null);
@@ -134,8 +181,9 @@ export function UserSettingsView() {
134
181
  <div className="flex-grow max-w-4xl w-full mx-auto p-4 sm:p-6 md:p-12">
135
182
  <Typography variant="h4" className="mb-8">{t("account_settings")}</Typography>
136
183
 
137
- <Tabs value={activeTab} onValueChange={(v) => setActiveTab(v as "profile" | "sessions")} className="mb-8">
184
+ <Tabs value={activeTab} onValueChange={(v) => setActiveTab(v as ActiveTab)} className="mb-8">
138
185
  <Tab value="profile">{t("profile")}</Tab>
186
+ {hasPasswordChange && <Tab value="security">{t("security")}</Tab>}
139
187
  <Tab value="sessions">{t("sessions")}</Tab>
140
188
  </Tabs>
141
189
 
@@ -165,6 +213,51 @@ export function UserSettingsView() {
165
213
  </div>
166
214
  )}
167
215
 
216
+ {activeTab === "security" && hasPasswordChange && (
217
+ <div className="flex flex-col gap-6 max-w-xl">
218
+ <Typography variant="h6" className="mb-2">{t("change_password")}</Typography>
219
+
220
+ <TextField
221
+ label={t("current_password")}
222
+ type="password"
223
+ value={currentPassword}
224
+ onChange={(e) => setCurrentPassword(e.target.value)}
225
+ autoComplete="current-password"
226
+ />
227
+ <TextField
228
+ label={t("new_password")}
229
+ type="password"
230
+ value={newPassword}
231
+ onChange={(e) => setNewPassword(e.target.value)}
232
+ autoComplete="new-password"
233
+ />
234
+ <TextField
235
+ label={t("confirm_password")}
236
+ type="password"
237
+ value={confirmPassword}
238
+ onChange={(e) => setConfirmPassword(e.target.value)}
239
+ autoComplete="new-password"
240
+ />
241
+
242
+ {passwordError && (
243
+ <Typography color="error">{passwordError}</Typography>
244
+ )}
245
+ {passwordSuccess && (
246
+ <Typography className="text-emerald-600 dark:text-emerald-400">{passwordSuccess}</Typography>
247
+ )}
248
+
249
+ <div className="mt-4">
250
+ <Button
251
+ variant="filled"
252
+ onClick={handleChangePassword}
253
+ disabled={changingPassword || !currentPassword || !newPassword || !confirmPassword}
254
+ >
255
+ {changingPassword ? t("changing_password") : t("change_password")}
256
+ </Button>
257
+ </div>
258
+ </div>
259
+ )}
260
+
168
261
  {activeTab === "sessions" && (
169
262
  <div className="flex flex-col gap-4 max-w-3xl">
170
263
  {loadingSessions ? (
@@ -1,16 +1,16 @@
1
1
  import type { Property } from "@rebasepro/types";
2
2
  import { CollectionSize, SelectedCellProps } from "@rebasepro/types";
3
3
 
4
- export type EntityCollectionTableController<M extends Record<string, any>> = {
4
+ export type EntityCollectionTableController<M extends Record<string, unknown>> = {
5
5
 
6
6
  /**
7
7
  * This cell is displayed as selected
8
8
  */
9
- selectedCell?: SelectedCellProps<any>;
9
+ selectedCell?: SelectedCellProps;
10
10
  /**
11
11
  * Store used to sync selection state across cells efficiently.
12
12
  */
13
- selectionStore?: any;
13
+ selectionStore?: { getSnapshot: () => SelectedCellProps | undefined; subscribe: (cb: () => void) => () => void };
14
14
  /**
15
15
  * Select a table cell
16
16
  * @param cell
@@ -25,7 +25,7 @@ export type EntityCollectionTableController<M extends Record<string, any>> = {
25
25
  * Callback used when the value of a cell has changed.
26
26
  * @param params
27
27
  */
28
- onValueChange?: (params: OnCellValueChangeParams<any, M>) => void;
28
+ onValueChange?: (params: OnCellValueChangeParams<unknown, M>) => void;
29
29
  /**
30
30
  * Size of the elements in the collection
31
31
  */
@@ -36,7 +36,7 @@ export type EntityCollectionTableController<M extends Record<string, any>> = {
36
36
  * Props passed in a callback when the content of a cell in a table has been edited
37
37
  * @group Collection components
38
38
  */
39
- export interface OnCellValueChangeParams<T = any, D = any> {
39
+ export interface OnCellValueChangeParams<T = unknown, D = unknown> {
40
40
  value: T,
41
41
  propertyKey: string,
42
42
  data?: D,
@@ -49,7 +49,7 @@ export interface OnCellValueChangeParams<T = any, D = any> {
49
49
  */
50
50
  export type UniqueFieldValidator = (props: {
51
51
  name: string,
52
- value: any,
52
+ value: unknown,
53
53
  property: Property,
54
54
  entityId?: string | number
55
55
  }) => Promise<boolean>;
@@ -58,7 +58,7 @@ export type UniqueFieldValidator = (props: {
58
58
  * Callback when a cell has changed in a table
59
59
  * @group Collection components
60
60
  */
61
- export type OnCellValueChange<T, M extends Record<string, any>> = (params: OnCellValueChangeParams<T, M>) => Promise<void> | void;
61
+ export type OnCellValueChange<T, M extends Record<string, unknown>> = (params: OnCellValueChangeParams<T, M>) => Promise<void> | void;
62
62
 
63
63
  /**
64
64
  * @group Collection components
@@ -9,7 +9,7 @@ import { deepEqual as equal } from "fast-equals";
9
9
  * @param deps
10
10
  * @param timeoutMs
11
11
  */
12
- export function useDebouncedData<T>(data: T[], deps: any, timeoutMs = 5000) {
12
+ export function useDebouncedData<T>(data: T[], deps: unknown, timeoutMs = 5000) {
13
13
 
14
14
  const [deferredData, setDeferredData] = React.useState(data);
15
15
  const dataLength = React.useRef(deferredData.length ?? 0);
@@ -32,7 +32,7 @@ export function useDebouncedData<T>(data: T[], deps: any, timeoutMs = 5000) {
32
32
 
33
33
  pendingUpdate.current = true;
34
34
 
35
- let handler: any;
35
+ let handler: ReturnType<typeof setTimeout> | undefined;
36
36
  if (immediateUpdate)
37
37
  performUpdate()
38
38
  else
@@ -64,7 +64,8 @@ export function Rebase<USER extends User>(props: RebaseProps<USER>) {
64
64
  apiKey,
65
65
  userManagement: _userManagement,
66
66
  effectiveRoleController,
67
- apiUrl
67
+ apiUrl,
68
+ translations
68
69
  } = props;
69
70
 
70
71
  const plugins = pluginsProp;
@@ -182,7 +183,7 @@ export function Rebase<USER extends User>(props: RebaseProps<USER>) {
182
183
  }
183
184
 
184
185
  const content = (
185
- <RebaseI18nProvider locale={locale}>
186
+ <RebaseI18nProvider locale={locale} translations={translations}>
186
187
  <SnackbarProvider>
187
188
  <ModeControllerProvider value={modeController}>
188
189
  <AdminModeControllerProvider value={adminModeController}>
@@ -1,5 +1,10 @@
1
1
  import React from "react";
2
- import { Locale, User, AuthController, AnalyticsEvent, DataDriver, StorageSource, UserConfigurationPersistence, CollectionRegistryController, DatabaseAdmin, UrlController, NavigationStateController, RebaseData, RebaseClient, RebaseContext, UserManagementDelegate, EntityLinkBuilder, RebasePlugin, SlotContribution, PropertyConfig, EntityCustomView, EntityAction } from "@rebasepro/types";
2
+ import { Locale, User, AuthController, AnalyticsEvent, DataDriver, StorageSource, UserConfigurationPersistence, CollectionRegistryController, DatabaseAdmin, UrlController, NavigationStateController, RebaseData, RebaseClient, RebaseContext, UserManagementDelegate, EntityLinkBuilder, RebasePlugin, SlotContribution, PropertyConfig, EntityCustomView, EntityAction, RebaseTranslations } from "@rebasepro/types";
3
+
4
+ /** DeepPartial helper — allows partial overrides at any nesting level */
5
+ type DeepPartial<T> = T extends object
6
+ ? { [K in keyof T]?: DeepPartial<T[K]> }
7
+ : T;
3
8
 
4
9
  /**
5
10
  * Controller to simulate different roles when dev mode is active.
@@ -138,7 +143,7 @@ export type RebaseProps<USER extends User> = {
138
143
  /**
139
144
  * Entity Views
140
145
  */
141
- entityViews?: EntityCustomView<any>[];
146
+ entityViews?: EntityCustomView[];
142
147
 
143
148
  /**
144
149
  * Entity Actions
@@ -161,4 +166,12 @@ export type RebaseProps<USER extends User> = {
161
166
  */
162
167
  effectiveRoleController?: EffectiveRoleController;
163
168
 
169
+ /**
170
+ * Override or extend any Rebase UI string, keyed by locale.
171
+ */
172
+ translations?: {
173
+ [locale: string]: DeepPartial<RebaseTranslations>;
174
+ };
175
+
164
176
  };
177
+
@@ -5,7 +5,7 @@ export function RebaseRouter({
5
5
  children,
6
6
  basePath
7
7
  }: {
8
- children: any,
8
+ children: React.ReactNode,
9
9
  basePath?: string;
10
10
  }) {
11
11
  return <RouterProvider router={createBrowserRouter([
@@ -33,7 +33,6 @@ export * from "./useCustomizationController";
33
33
  export * from "./useBuildLocalConfigurationPersistence";
34
34
  export * from "./useBuildModeController";
35
35
 
36
- export * from "./useValidateAuthenticator";
37
36
  export * from "./useRebaseRegistry";
38
37
  export * from "./useBackendStorageSource";
39
38
  export * from "./usePermissions";
@@ -13,7 +13,8 @@ import { isLazyComponentRef } from "@rebasepro/types";
13
13
  * to the `React.lazy()` wrapper it produced. Strings are keyed by a separate
14
14
  * plain Map since they can't be WeakMap keys.
15
15
  */
16
- const lazyCache = new WeakMap<object | ((..._args: any[]) => any), React.ComponentType<any>>();
16
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
17
+ const lazyCache = new WeakMap<object | Function, React.ComponentType<any>>();
17
18
 
18
19
  /**
19
20
  * Resolves a `ComponentRef` into a renderable `React.ComponentType`.
@@ -62,7 +63,7 @@ export function useResolvedComponent<P = unknown>(
62
63
  * same loader always returns the same lazy component identity.
63
64
  */
64
65
  function getOrCreateLazy<P>(
65
- key: object | ((..._args: any[]) => any),
66
+ key: object | Function,
66
67
  loader: () => Promise<{ default: React.ComponentType<P> }>
67
68
  ): React.ComponentType<P> {
68
69
  const cached = lazyCache.get(key);
@@ -108,7 +109,7 @@ export function resolveComponentRef<P = unknown>(
108
109
 
109
110
  // 3. Function — either a React component or a lazy import loader.
110
111
  if (typeof ref === "function") {
111
- const fn = ref as (..._args: any[]) => any;
112
+ const fn = ref as Function;
112
113
 
113
114
  // Class components (React.Component / PureComponent) have this flag
114
115
  if (fn.prototype?.isReactComponent) {