@rebasepro/app 0.22.0 → 0.23.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/index.es.js CHANGED
@@ -1,15 +1,15 @@
1
1
  import React, { createContext, useCallback, useContext, useEffect, useId, useLayoutEffect, useMemo, useRef, useState } from "react";
2
- import { Alert, AlertTriangleIcon, ArrowLeftIcon, Avatar, Button, Card, CenteredView, CheckIcon, Checkbox, Chip, CircleUserIcon, CircularProgress, Dialog, DialogActions, DialogContent, DialogTitle, ErrorBoundary, IconButton, LanguagesIcon, LoadingButton, LucideIconByName, MailIcon, Menu, MenuItem, MoonIcon, Popover, SearchBar, SunIcon, SunMoonIcon, Tab, Tabs, TextField, Tooltip, Trash2Icon, Typography, Wand2Icon, cls, colorClassesMapping, coolIconKeys, defaultBorderMixin, getColorSchemeForKey, getColorSchemeForSeed, iconKeys, iconSize, lazyChunk } from "@rebasepro/ui";
3
- import { ALL_WHERE_FILTER_OPS, DEFAULT_DATA_SOURCE_KEY, DEFAULT_FILTERABLE_RELATION_KINDS, DEFAULT_STORAGE_SOURCE_KEY, EntityReference, EntityRelation, GeoPoint, RebaseApiError, Vector, getDataSourceCapabilities, isLazyComponentRef, isRelationalCollectionConfig, resourceKeyOf } from "@rebasepro/types";
2
+ import { Alert, AlertTriangleIcon, ArrowLeftIcon, Avatar, Button, Card, CenteredView, CheckCircle2Icon, CheckIcon, Checkbox, Chip, CircleUserIcon, CircularProgress, Dialog, DialogActions, DialogContent, DialogTitle, ErrorBoundary, IconButton, LanguagesIcon, LoadingButton, LucideIconByName, MailIcon, Menu, MenuItem, MoonIcon, Popover, SearchBar, Select, SelectItem, SunIcon, SunMoonIcon, Tab, Tabs, TextField, Tooltip, Trash2Icon, Typography, Wand2Icon, cls, colorClassesMapping, coolIconKeys, defaultBorderMixin, getColorSchemeForKey, getColorSchemeForSeed, iconKeys, iconSize, lazyChunk } from "@rebasepro/ui";
3
+ import { ALL_WHERE_FILTER_OPS, DEFAULT_DATA_SOURCE_KEY, DEFAULT_FILTERABLE_RELATION_KINDS, DEFAULT_STORAGE_SOURCE_KEY, EntityReference, EntityRelation, GeoPoint, RebaseApiError, Vector, getDataSourceCapabilities, isLazyComponentRef, isPublicStoragePath, isRelationalCollectionConfig, resourceKeyOf } from "@rebasepro/types";
4
4
  import { UNRENDERED_SLOTS, resolveAdminCollection } from "@rebasepro/cms-types";
5
5
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
6
6
  import { SnackbarProvider as SnackbarProvider$1, useSnackbar } from "notistack";
7
7
  import { buildRebaseData, canCreateEntity, canDeleteEntity, canEditEntity, canReadCollection, evaluateCondition, getChildViewDeclaringProperties, getChildViewRelationPropertyKeys, getEntityChildViews, getLabelOrConfigFrom, getPrimaryKeys, getSubcollections, isPropertyBuilder, normalizeOrderBy, resolveStorageFilenameString, resolveStoragePathString, resolveStorageSource, serializeOrderBy, stripCollectionPath, wrapAsEntityData, wrapAsSdkData } from "@rebasepro/common";
8
8
  import { getTitlePropertyKey as getTitlePropertyKey$1, removeInitialAndTrailingSlashes as removeInitialAndTrailingSlashes$1 } from "@rebasepro/app";
9
+ import { deepEqual } from "fast-equals";
9
10
  import { Link, Routes, createBrowserRouter, useBlocker, useLocation } from "react-router";
10
11
  import { generateForeignKeyName, hashString, isArrayValue, isObject, isPlainObject, isRecordValue, mergeDeep, randomString, readStoredJson, slugify, writeStoredJson } from "@rebasepro/utils";
11
12
  import { I18nextProvider, initReactI18next, useTranslation as useTranslation$1 } from "react-i18next";
12
- import { deepEqual } from "fast-equals";
13
13
  import i18next from "i18next";
14
14
  import { RouterProvider } from "react-router/dom";
15
15
  import Fuse from "fuse.js";
@@ -40,28 +40,39 @@ function PluginProviderStack({ plugins, scope, scopeProps, children }) {
40
40
  /**
41
41
  * Render-less component that manages plugin lifecycle hooks.
42
42
  *
43
- * - Calls `lifecycle.onMount(context)` when plugins mount.
44
- * - Calls `lifecycle.onUnmount()` when plugins unmount.
45
- * - Subscribes to auth state changes and calls `lifecycle.onAuthStateChange`.
43
+ * - Calls `lifecycle.onMount(context)` once, the first time auth is ready.
44
+ * - Calls `lifecycle.onAuthStateChange(user)` on every change of user after
45
+ * that — `null` on sign-out, the new user on the next sign-in.
46
+ * - Calls `lifecycle.onUnmount()` when the Rebase tree unmounts.
47
+ *
48
+ * Mounted for as long as there are plugins, signed in or not. It used to be
49
+ * mounted only while a user was signed in, so a sign-out unmounted it and the
50
+ * next sign-in mounted a fresh one — `onAuthStateChange` could never see the
51
+ * user change, and never fired.
46
52
  *
47
53
  * Mount this component inside the Rebase tree, below PluginProviderStack,
48
54
  * so that the RebaseContext is fully available.
49
55
  *
50
56
  * @internal
51
57
  */
52
- function PluginLifecycleManager({ plugins, context }) {
58
+ function PluginLifecycleManager({ plugins, context, authReady }) {
53
59
  const mountedRef = useRef(false);
54
- const prevUserRef = useRef(void 0);
60
+ const prevUidRef = useRef(null);
61
+ const currentUser = context.authController?.user ?? null;
55
62
  useEffect(() => {
56
- if (mountedRef.current) return;
63
+ if (!authReady || mountedRef.current) return;
57
64
  mountedRef.current = true;
65
+ prevUidRef.current = currentUser?.uid ?? null;
58
66
  for (const plugin of plugins) if (plugin.lifecycle?.onMount) try {
59
67
  const result = plugin.lifecycle.onMount(context);
60
68
  if (result instanceof Promise) result.catch((err) => console.error(`[Rebase] Plugin "${plugin.key}" onMount error:`, err));
61
69
  } catch (err) {
62
70
  console.error(`[Rebase] Plugin "${plugin.key}" onMount error:`, err);
63
71
  }
72
+ }, [authReady]);
73
+ useEffect(() => {
64
74
  return () => {
75
+ if (!mountedRef.current) return;
65
76
  mountedRef.current = false;
66
77
  for (const plugin of plugins) if (plugin.lifecycle?.onUnmount) try {
67
78
  plugin.lifecycle.onUnmount();
@@ -70,14 +81,11 @@ function PluginLifecycleManager({ plugins, context }) {
70
81
  }
71
82
  };
72
83
  }, []);
73
- const currentUser = context.authController?.user ?? null;
74
84
  useEffect(() => {
75
- if (prevUserRef.current === void 0) {
76
- prevUserRef.current = currentUser;
77
- return;
78
- }
79
- if (prevUserRef.current?.uid === currentUser?.uid) return;
80
- prevUserRef.current = currentUser;
85
+ if (!mountedRef.current) return;
86
+ const currUid = currentUser?.uid ?? null;
87
+ if (prevUidRef.current === currUid) return;
88
+ prevUidRef.current = currUid;
81
89
  for (const plugin of plugins) if (plugin.lifecycle?.onAuthStateChange) try {
82
90
  plugin.lifecycle.onAuthStateChange(currentUser);
83
91
  } catch (err) {
@@ -646,6 +654,21 @@ function getEntityTitlePropertyKey(collection, propertyConfigs) {
646
654
  return getTitlePropertyKey$1(collection);
647
655
  }
648
656
  //#endregion
657
+ //#region src/hooks/data/useStableFilterValues.ts
658
+ /**
659
+ * The same filter object for as long as its contents stay the same.
660
+ *
661
+ * Callers write filters inline — the hooks guide does — so every render hands
662
+ * a data hook a new object. Used as an effect dependency by identity, that
663
+ * re-subscribed on every render, and every delivery rendered again: a
664
+ * subscribe loop against the server for as long as the component was mounted.
665
+ */
666
+ function useStableFilterValues(filterValues) {
667
+ const ref = useRef(filterValues);
668
+ if (!deepEqual(ref.current, filterValues)) ref.current = filterValues;
669
+ return ref.current;
670
+ }
671
+ //#endregion
649
672
  //#region src/hooks/data/useCollection.tsx
650
673
  /**
651
674
  * This hook is used to fetch collections using a given collection
@@ -665,7 +688,7 @@ function useCollection({ path, collection, filterValues, sortBy, itemCount, offs
665
688
  const sortByProperty = sortBy ? sortBy[0] : void 0;
666
689
  const currentSort = sortBy ? sortBy[1] : void 0;
667
690
  const orderByParams = sortBy ? [String(sortBy[0]), sortBy[1]] : void 0;
668
- const whereParams = filterValues && Object.keys(filterValues).length > 0 ? filterValues : void 0;
691
+ const whereParams = useStableFilterValues(filterValues && Object.keys(filterValues).length > 0 ? filterValues : void 0);
669
692
  const [data, setData] = useState([]);
670
693
  const [dataLoading, setDataLoading] = useState(false);
671
694
  const [dataLoadingError, setDataLoadingError] = useState();
@@ -734,7 +757,7 @@ function useCollection({ path, collection, filterValues, sortBy, itemCount, offs
734
757
  page,
735
758
  currentSort,
736
759
  sortByProperty,
737
- filterValues,
760
+ whereParams,
738
761
  searchString,
739
762
  dataClient,
740
763
  collection
@@ -917,7 +940,7 @@ var useRebaseContext = () => {
917
940
  const analyticsController = useAnalyticsController();
918
941
  const effectiveRoleController = useEffectiveRoleController();
919
942
  const databaseAdmin = useContext(DatabaseAdminContext);
920
- const rebaseContextRef = React.useRef({
943
+ return React.useMemo(() => ({
921
944
  authController,
922
945
  data,
923
946
  storageSource,
@@ -929,22 +952,7 @@ var useRebaseContext = () => {
929
952
  effectiveRoleController,
930
953
  databaseAdmin,
931
954
  client
932
- });
933
- React.useEffect(() => {
934
- rebaseContextRef.current = {
935
- authController,
936
- data,
937
- storageSource,
938
- snackbarController,
939
- userConfigPersistence,
940
- dialogsController,
941
- customizationController,
942
- analyticsController,
943
- effectiveRoleController,
944
- databaseAdmin,
945
- client
946
- };
947
- }, [
955
+ }), [
948
956
  authController,
949
957
  data,
950
958
  storageSource,
@@ -957,7 +965,6 @@ var useRebaseContext = () => {
957
965
  databaseAdmin,
958
966
  client
959
967
  ]);
960
- return rebaseContextRef.current;
961
968
  };
962
969
  //#endregion
963
970
  //#region src/hooks/data/useFetch.tsx
@@ -1061,6 +1068,7 @@ var DEFAULT_PAGE_SIZE$1 = 10;
1061
1068
  */
1062
1069
  function useRelationSelector({ path, collection, fixedFilter, pageSize = DEFAULT_PAGE_SIZE$1, getLabelFromEntity, getDescriptionFromEntity, descriptionProperty, enabled = true }) {
1063
1070
  const dataClient = useData();
1071
+ const stableFixedFilter = useStableFilterValues(fixedFilter);
1064
1072
  const [items, setItems] = useState([]);
1065
1073
  const [isLoading, setIsLoading] = useState(false);
1066
1074
  const isLoadingRef = useRef(false);
@@ -1116,7 +1124,7 @@ function useRelationSelector({ path, collection, fixedFilter, pageSize = DEFAULT
1116
1124
  cleanupSubscription();
1117
1125
  setError(void 0);
1118
1126
  setLoading(true);
1119
- const whereParams = fixedFilter && Object.keys(fixedFilter).length > 0 ? fixedFilter : void 0;
1127
+ const whereParams = stableFixedFilter && Object.keys(stableFixedFilter).length > 0 ? stableFixedFilter : void 0;
1120
1128
  const onEntitiesUpdate = (res) => {
1121
1129
  const newItems = res.data.map((e) => entityToRelationItem(e));
1122
1130
  hasLoadedRef.current = true;
@@ -1169,7 +1177,7 @@ function useRelationSelector({ path, collection, fixedFilter, pageSize = DEFAULT
1169
1177
  }, [
1170
1178
  dataClient,
1171
1179
  path,
1172
- fixedFilter,
1180
+ stableFixedFilter,
1173
1181
  limit,
1174
1182
  currentSearch,
1175
1183
  entityToRelationItem,
@@ -1522,222 +1530,686 @@ function useUnsavedChangesDialog(when, onOk) {
1522
1530
  ]);
1523
1531
  }
1524
1532
  //#endregion
1525
- //#region src/hooks/useAuthSubscription.ts
1526
- function useAuthSubscription(authClient) {
1527
- const currentSession = authClient?.getSession();
1528
- const [user, setUser] = useState(currentSession?.user ?? null);
1529
- const [authLoading, setAuthLoading] = useState(false);
1530
- const [authError, setAuthError] = useState();
1531
- const [loginSkipped, setLoginSkipped] = useState(false);
1532
- const [extra, setExtra] = useState();
1533
- /**
1534
- * Whether asking the server "who am I?" could possibly answer.
1535
- *
1536
- * A client configured with `persistSession: false` and JSON auth — the
1537
- * shape used when one client borrows another's credential, as the hosted
1538
- * Studio does — has no stored session and no auth cookie, so the probe
1539
- * below is guaranteed to 401. Sending it anyway put three failing
1540
- * `GET /auth/me` calls on the wire per mount, each one landing in the
1541
- * *customer's* request log as an authentication failure against their own
1542
- * backend.
1543
- *
1544
- * A client that does not implement the capability is treated as "might
1545
- * work" — the historical behaviour.
1546
- */
1547
- const mayHaveRestorableSession = authClient?.canRestoreSession?.() ?? true;
1548
- /**
1549
- * `true` only while a probe is genuinely outstanding.
1550
- *
1551
- * It used to be seeded from `!currentSession` alone, which left a client
1552
- * that cannot restore a session — and therefore never runs the probe that
1553
- * clears the flag — reporting "still loading" forever. Anything gated on
1554
- * `initialLoading` (a spinner, a redirect to the login view) would never
1555
- * resolve.
1556
- */
1557
- const [initialLoading, setInitialLoading] = useState(!currentSession && mayHaveRestorableSession);
1558
- useEffect(() => {
1559
- if (!authClient) return;
1560
- if (!currentSession && mayHaveRestorableSession) {
1561
- setInitialLoading(true);
1562
- authClient.getUser().then((user) => {
1563
- if (user) setUser(user);
1564
- setAuthError(void 0);
1565
- }).catch((e) => {
1566
- const status = e?.status;
1567
- if (status === 401 || status === 403) {
1568
- setAuthError(void 0);
1569
- return;
1570
- }
1571
- setAuthError(e instanceof Error ? e : new Error(String(e)));
1572
- }).finally(() => {
1573
- setInitialLoading(false);
1574
- });
1575
- }
1576
- }, [
1577
- authClient,
1578
- currentSession,
1579
- mayHaveRestorableSession
1580
- ]);
1581
- useEffect(() => {
1582
- if (!authClient) return;
1583
- return authClient.onAuthStateChange((event, session) => {
1584
- if (event === "SIGNED_IN" || event === "USER_UPDATED" || event === "TOKEN_REFRESHED") setUser(session?.user ?? null);
1585
- else if (event === "SIGNED_OUT") setUser(null);
1586
- });
1587
- }, [authClient]);
1588
- const signOut = useCallback(async () => {
1589
- if (!authClient) return;
1590
- setAuthLoading(true);
1591
- try {
1592
- await authClient.signOut();
1593
- } finally {
1594
- setAuthLoading(false);
1595
- }
1596
- }, [authClient]);
1597
- const getAuthToken = useCallback(async () => {
1598
- if (!authClient) return "";
1599
- const session = authClient.getSession();
1600
- if (!session) return "";
1601
- if (session.expiresAt < Date.now()) try {
1602
- return (await authClient.refreshSession()).accessToken;
1603
- } catch (e) {
1604
- return "";
1605
- }
1606
- return session.accessToken;
1607
- }, [authClient]);
1608
- return useMemo(() => ({
1609
- user,
1610
- initialLoading: authClient ? initialLoading : false,
1611
- authLoading,
1612
- signOut,
1613
- authError,
1614
- getAuthToken,
1615
- loginSkipped,
1616
- extra,
1617
- setExtra
1618
- }), [
1619
- user,
1620
- initialLoading,
1621
- authLoading,
1622
- signOut,
1623
- authError,
1624
- getAuthToken,
1625
- loginSkipped,
1626
- extra,
1627
- setExtra,
1628
- authClient
1629
- ]);
1630
- }
1631
- //#endregion
1632
- //#region src/hooks/useAdminModeController.tsx
1633
- /**
1634
- * Hook to retrieve the current admin mode (`"cms"` | `"studio"`), and `setMode`
1635
- * to change it.
1636
- *
1637
- * Consider that in order to use this hook you need to have a parent
1638
- * `Rebase`
1639
- *
1640
- * @see AdminModeController
1641
- * @group Hooks and utilities
1642
- */
1643
- var useAdminModeController = () => useContext(AdminModeControllerContext);
1644
- //#endregion
1645
- //#region src/util/local_storage.ts
1533
+ //#region src/components/common/useScrollRestoration.tsx
1534
+ var collectionScrollCache = /* @__PURE__ */ new Map();
1646
1535
  /**
1647
- * localStorage that tolerates not being there.
1648
- *
1649
- * `typeof window !== "undefined"` was the guard these call sites used, and it
1650
- * answers the wrong question: it says "am I in a browser", not "can I read
1651
- * storage". Three things make that distinction real rather than theoretical:
1652
- *
1653
- * - A browser can have storage **disabled**. Safari in private mode, a blocked
1654
- * third-party cookie policy, and a sandboxed iframe all give you a `window`
1655
- * whose `localStorage` property *throws on access* — a SecurityError raised
1656
- * before any method is called, which no amount of null-checking the result
1657
- * catches.
1658
- * - A test environment can be **half torn down**. jsdom is disposed at the end
1659
- * of a file while React is still finishing a concurrent render, so `window`
1660
- * survives as a husk and `localStorage.getItem` is no longer a function.
1661
- * That is how this was found: `TypeError: localStorage.getItem is not a
1662
- * function`, thrown out of a render, failing a suite in which every one of
1663
- * 709 tests passed.
1664
- * - Server rendering has no `window` at all, which is the only case the old
1665
- * guard actually covered.
1536
+ * Forget every collection's scroll position and the rows kept with it.
1666
1537
  *
1667
- * Reading a UI preference is never worth a crash. Every function here fails to
1668
- * `null` / no-op, so a user with storage switched off gets the default
1669
- * appearance instead of a blank page.
1538
+ * The rows are what the signed-in user was allowed to read, and a table seeds
1539
+ * its first render from them, so they must not outlive that user's session.
1670
1540
  */
1671
- /** The Storage object, or null when it is absent, blocked, or not usable. */
1672
- function usableStorage() {
1673
- try {
1674
- if (typeof window === "undefined") return null;
1675
- const candidate = window.localStorage;
1676
- return typeof candidate?.getItem === "function" ? candidate : null;
1677
- } catch {
1678
- return null;
1679
- }
1680
- }
1681
- /** The stored string for `key`, or null if it is unset or unreadable. */
1682
- function readStoredString(key) {
1683
- try {
1684
- return usableStorage()?.getItem(key) ?? null;
1685
- } catch {
1686
- return null;
1687
- }
1688
- }
1689
- /** Persist `value` under `key`. A no-op when storage is unavailable. */
1690
- function writeStoredString(key, value) {
1691
- try {
1692
- usableStorage()?.setItem(key, value);
1693
- } catch {}
1541
+ function clearCollectionScrollCache() {
1542
+ collectionScrollCache.clear();
1694
1543
  }
1695
- /** Remove `key`. A no-op when storage is unavailable. */
1696
- function removeStoredString(key) {
1697
- try {
1698
- usableStorage()?.removeItem(key);
1699
- } catch {}
1544
+ function useScrollRestoration() {
1545
+ const updateCollectionScroll = ({ path, filters, scrollOffset, data }) => {
1546
+ collectionScrollCache.set(createCacheKey(path, filters), {
1547
+ scrollOffset,
1548
+ data
1549
+ });
1550
+ };
1551
+ const getCollectionScroll = (path, filters) => {
1552
+ return collectionScrollCache.get(createCacheKey(path, filters));
1553
+ };
1554
+ return {
1555
+ getCollectionScroll,
1556
+ updateCollectionScroll
1557
+ };
1700
1558
  }
1701
- //#endregion
1702
- //#region src/hooks/useBuildAdminModeController.tsx
1703
- /**
1704
- * Use this hook to build an admin mode controller that determines
1705
- * whether the UI shows Developer or Editor tools.
1706
- */
1707
- function useBuildAdminModeController() {
1708
- const stored = readStoredString("rebase-admin-mode");
1709
- const migrated = stored === "content" ? "cms" : stored;
1710
- const [mode, setMode] = useState((migrated === "cms" || migrated === "studio" ? migrated : null) ?? "cms");
1711
- const setModeInternal = useCallback((newMode) => {
1712
- writeStoredString("rebase-admin-mode", newMode);
1713
- setMode(newMode);
1714
- }, []);
1715
- return useMemo(() => ({
1716
- mode,
1717
- setMode: setModeInternal
1718
- }), [mode, setModeInternal]);
1559
+ function createCacheKey(path, filters) {
1560
+ if (!filters) return path;
1561
+ return `${path}?${filters ? Object.keys(filters).map((key) => {
1562
+ return `${key}=${JSON.stringify(filters[key])}`;
1563
+ }).join("&") : ""}`;
1719
1564
  }
1720
1565
  //#endregion
1721
- //#region src/hooks/useBuildEffectiveRoleController.tsx
1566
+ //#region src/util/entity_cache.ts
1567
+ var LOCAL_STORAGE_PREFIX = "entity_cache::";
1722
1568
  /**
1723
- * Use this hook to build an effective role controller that determines
1724
- * what role is simulated in Editor mode when Dev mode is active.
1569
+ * The in-session handoff channel: an edit in flight, parked here by the layout
1570
+ * that is being replaced and picked up by the one taking over — the split's
1571
+ * "hide list", full screen's "show list", the side panel's "open full screen".
1572
+ * A record loaded from it opens *dirty*, because that is what it is for.
1725
1573
  *
1726
- * It uses localStorage to persist the simulated role across reloads.
1574
+ * Deliberately **not** hydrated from `sessionStorage`, and it is the one thing
1575
+ * here that isn't. The persisted half of this file is the local-changes backup:
1576
+ * a draft that outlives a reload, offered back through the banner or applied on
1577
+ * open, according to the collection's `localChangesBackup`. Loading those
1578
+ * drafts into this map on startup made every one of them look like a handoff —
1579
+ * the first visit to the record after a reload silently opened dirty carrying a
1580
+ * draft the user had walked away from, and the banner that exists to ask about
1581
+ * it never appeared.
1727
1582
  */
1728
- function useBuildEffectiveRoleController() {
1729
- const [effectiveRole, setEffectiveRole] = useState(readStoredString("rebase-effective-role") ?? null);
1730
- const setRoleInternal = useCallback((newRole) => {
1731
- if (newRole) writeStoredString("rebase-effective-role", newRole);
1732
- else removeStoredString("rebase-effective-role");
1733
- setEffectiveRole(newRole);
1734
- }, []);
1735
- return useMemo(() => ({
1736
- effectiveRole,
1737
- setEffectiveRole: setRoleInternal
1738
- }), [effectiveRole, setRoleInternal]);
1739
- }
1740
- //#endregion
1583
+ var entityCache = /* @__PURE__ */ new Map();
1584
+ var isSessionStorageAvailable = typeof sessionStorage !== "undefined";
1585
+ function customReplacer(key) {
1586
+ const value = this[key];
1587
+ if (value instanceof Date) return {
1588
+ __type: "Date",
1589
+ value: value.toISOString()
1590
+ };
1591
+ if (value instanceof EntityReference) return {
1592
+ __type: "EntityReference",
1593
+ id: value.id,
1594
+ path: value.path,
1595
+ driver: value.driver,
1596
+ databaseId: value.databaseId
1597
+ };
1598
+ if (value instanceof EntityRelation) return {
1599
+ __type: "EntityRelation",
1600
+ id: value.id,
1601
+ path: value.path,
1602
+ data: value.data
1603
+ };
1604
+ if (value instanceof GeoPoint) return {
1605
+ __type: "GeoPoint",
1606
+ latitude: value.latitude,
1607
+ longitude: value.longitude
1608
+ };
1609
+ if (value instanceof Vector) return {
1610
+ __type: "Vector",
1611
+ value: value.value
1612
+ };
1613
+ return value;
1614
+ }
1615
+ function customReviver(_key, value) {
1616
+ if (value && typeof value === "object" && "__type" in value) {
1617
+ const record = value;
1618
+ switch (record.__type) {
1619
+ case "date":
1620
+ case "Date": return new Date(record.value);
1621
+ case "reference":
1622
+ case "EntityReference": return new EntityReference({
1623
+ id: record.id,
1624
+ path: record.path,
1625
+ driver: record.driver,
1626
+ databaseId: record.databaseId
1627
+ });
1628
+ case "relation":
1629
+ case "EntityRelation": return new EntityRelation(record.id, record.path, record.data);
1630
+ case "GeoPoint": return new GeoPoint(record.latitude, record.longitude);
1631
+ case "Vector": return new Vector(record.value);
1632
+ default: return value;
1633
+ }
1634
+ }
1635
+ return value;
1636
+ }
1637
+ /**
1638
+ * Saves data to the local-changes backup in `sessionStorage`.
1639
+ *
1640
+ * The backup only — a draft that survives a reload is not an edit in flight.
1641
+ * {@link saveEntityToMemoryCache} is the other channel; see {@link entityCache}
1642
+ * for what separates them.
1643
+ * @param path - The unique path/key for the data.
1644
+ * @param data - The data to cache and persist.
1645
+ */
1646
+ function saveEntityToCache(path, data) {
1647
+ if (isSessionStorageAvailable) try {
1648
+ const key = LOCAL_STORAGE_PREFIX + path;
1649
+ const entityString = JSON.stringify(data, customReplacer);
1650
+ sessionStorage.setItem(key, entityString);
1651
+ } catch (error) {
1652
+ console.error(`Failed to save entity for path "${path}" to sessionStorage:`, error);
1653
+ }
1654
+ }
1655
+ /**
1656
+ * Consumes a handoff. The channel is one-shot: whoever picks an edit up owns it
1657
+ * from then on, and an edit left behind here is one that reopens a record dirty
1658
+ * long after the user stopped looking at it.
1659
+ */
1660
+ function removeEntityFromMemoryCache(path) {
1661
+ entityCache.delete(path);
1662
+ }
1663
+ /**
1664
+ * Parks an edit in flight for the layout about to take over. See
1665
+ * {@link entityCache}: this is the handoff, not the backup.
1666
+ */
1667
+ function saveEntityToMemoryCache(path, data) {
1668
+ entityCache.set(path, data);
1669
+ }
1670
+ function getEntityFromMemoryCache(path) {
1671
+ return entityCache.get(path);
1672
+ }
1673
+ /**
1674
+ * Drops every edit in flight. They were parked by a user who is no longer
1675
+ * signed in, so nobody is taking them over.
1676
+ */
1677
+ function clearEntityMemoryCache() {
1678
+ entityCache.clear();
1679
+ }
1680
+ /**
1681
+ * Drops the whole local-changes backup: every draft in `sessionStorage`, for
1682
+ * every record. For when the drafts belong to somebody other than the user now
1683
+ * signed in.
1684
+ */
1685
+ function clearEntityDrafts() {
1686
+ if (!isSessionStorageAvailable) return;
1687
+ try {
1688
+ const keys = [];
1689
+ for (let i = 0; i < sessionStorage.length; i++) {
1690
+ const key = sessionStorage.key(i);
1691
+ if (key?.startsWith(LOCAL_STORAGE_PREFIX)) keys.push(key);
1692
+ }
1693
+ for (const key of keys) sessionStorage.removeItem(key);
1694
+ } catch (error) {
1695
+ console.error("Failed to clear the local-changes backup from sessionStorage:", error);
1696
+ }
1697
+ }
1698
+ /**
1699
+ * Retrieves a entity from the local-changes backup in `sessionStorage`.
1700
+ *
1701
+ * The backup only — the handoff channel is read through
1702
+ * {@link getEntityFromMemoryCache}, and the two mean different things to the
1703
+ * form that receives them.
1704
+ * @param path - The unique path/key for the entity.
1705
+ * @returns The cached entity or `undefined` if not found.
1706
+ */
1707
+ function getEntityFromCache(path) {
1708
+ if (isSessionStorageAvailable) try {
1709
+ const key = LOCAL_STORAGE_PREFIX + path;
1710
+ const entityString = sessionStorage.getItem(key);
1711
+ if (entityString) return JSON.parse(entityString, customReviver);
1712
+ } catch (error) {
1713
+ console.error(`Failed to load entity for path "${path}" from sessionStorage:`, error);
1714
+ }
1715
+ }
1716
+ /**
1717
+ * Removes a entity from both the in-memory cache and `sessionStorage`.
1718
+ * @param path - The unique path/key for the entity to remove.
1719
+ */
1720
+ function removeEntityFromCache(path) {
1721
+ console.debug("Removing entity from cache", path);
1722
+ entityCache.delete(path);
1723
+ if (isSessionStorageAvailable) try {
1724
+ const key = LOCAL_STORAGE_PREFIX + path;
1725
+ sessionStorage.removeItem(key);
1726
+ } catch (error) {
1727
+ console.error(`Failed to remove entity for path "${path}" from sessionStorage:`, error);
1728
+ }
1729
+ }
1730
+ function flattenKeys(obj, prefix = "", result = []) {
1731
+ if (isObject(obj) || Array.isArray(obj)) if (!isPlainObject(obj) && prefix) result.push(prefix);
1732
+ else {
1733
+ const record = obj;
1734
+ for (const key in record) if (Object.prototype.hasOwnProperty.call(record, key)) {
1735
+ const newKey = prefix ? Array.isArray(obj) ? `${prefix}[${key}]` : `${prefix}.${key}` : key;
1736
+ const val = record[key];
1737
+ if (isObject(val) || Array.isArray(val)) flattenKeys(val, newKey, result);
1738
+ else result.push(newKey);
1739
+ }
1740
+ }
1741
+ return result;
1742
+ }
1743
+ //#endregion
1744
+ //#region src/collections/entity-display-cache.ts
1745
+ /** The identity of one role of one record, as a cache key. */
1746
+ function entityDisplayKey(path, entityId, role) {
1747
+ return `${role} ${path} ${entityId ?? ""}`;
1748
+ }
1749
+ /**
1750
+ * One shared store, so every surface agrees on a record and resolves it once.
1751
+ *
1752
+ * Module-level rather than a provider: two app instances on one page share a
1753
+ * cache keyed by collection path and record id, which *is* the same record.
1754
+ * A resolved title is what the signed-in user was allowed to read, though, so
1755
+ * the store is emptied with the rest of the session's caches when that user
1756
+ * signs out (`clearSessionCaches`).
1757
+ *
1758
+ * Built on first use rather than at module scope. Importing a module should not
1759
+ * construct anything: it runs during SSR where there is nothing to cache, and it
1760
+ * made the modules that read this store unimportable from any test that stubs
1761
+ * `@rebasepro/app`.
1762
+ */
1763
+ var sharedCache;
1764
+ function getSharedEntityDisplayCache() {
1765
+ if (!sharedCache) sharedCache = new EntityDisplayCache();
1766
+ return sharedCache;
1767
+ }
1768
+ var EntityDisplayCache = class {
1769
+ entries = /* @__PURE__ */ new Map();
1770
+ listeners = /* @__PURE__ */ new Set();
1771
+ /**
1772
+ * The resolved value, or `undefined` when this pair has not been resolved
1773
+ * yet. `null` is a resolved absence, and the two must stay distinct: a
1774
+ * caller that reads "not yet" as "nothing" flickers its fallback in on every
1775
+ * mount.
1776
+ */
1777
+ peek(key) {
1778
+ const entry = this.entries.get(key);
1779
+ if (!entry || "promise" in entry) return void 0;
1780
+ return entry.value;
1781
+ }
1782
+ /** True while a resolution for this pair is in flight. */
1783
+ isLoading(key) {
1784
+ const entry = this.entries.get(key);
1785
+ return Boolean(entry && "promise" in entry);
1786
+ }
1787
+ /**
1788
+ * Resolve once per record and role. Concurrent callers share the first
1789
+ * call's promise; later callers get the cached value with no promise at all.
1790
+ *
1791
+ * A resolver that throws is recorded as "nothing" rather than retried: the
1792
+ * alternative is every render re-running a call that just failed. And it is
1793
+ * reported here, which is a correction.
1794
+ *
1795
+ * It used to say "the caller that saw the rejection is the one that logs
1796
+ * it", and `useEntityDisplay` duly attached a `.catch()` that warned. But
1797
+ * both failure paths below swallow and return a *resolved* promise, so that
1798
+ * catch could never run — the two halves each did the reasonable thing and
1799
+ * between them the log was unreachable. A resolver that blew up produced a
1800
+ * blank chip and total silence, which is the failure mode
1801
+ * `EntityDisplayResolver`'s own contract ("treated as `undefined` and logged
1802
+ * once") exists to rule out.
1803
+ *
1804
+ * Reporting belongs here for the reason the caller could not do it: this is
1805
+ * the one place that runs exactly once per key, so "once" is a property of
1806
+ * the code rather than a hope about how many components mount.
1807
+ */
1808
+ resolve(key, resolver) {
1809
+ const entry = this.entries.get(key);
1810
+ if (entry) return "promise" in entry ? entry.promise : Promise.resolve(entry.value);
1811
+ let produced;
1812
+ try {
1813
+ produced = resolver();
1814
+ } catch (error) {
1815
+ this.fail(key, error);
1816
+ return Promise.resolve(null);
1817
+ }
1818
+ if (!isPromise(produced)) {
1819
+ const value = normalize(produced);
1820
+ this.set(key, value);
1821
+ return Promise.resolve(value);
1822
+ }
1823
+ const promise = produced.then((resolved) => {
1824
+ const value = normalize(resolved);
1825
+ this.set(key, value);
1826
+ return value;
1827
+ }).catch((error) => {
1828
+ this.fail(key, error);
1829
+ return null;
1830
+ });
1831
+ this.entries.set(key, { promise });
1832
+ return promise;
1833
+ }
1834
+ /**
1835
+ * Drop what is known about a record, so the next ask resolves again. Called
1836
+ * after a write: the row that just saved may be called something else now.
1837
+ */
1838
+ invalidate(path, entityId) {
1839
+ const suffix = ` ${path} ${entityId ?? ""}`;
1840
+ let changed = false;
1841
+ for (const key of [...this.entries.keys()]) if (entityId === void 0 ? key.includes(` ${path} `) || key.endsWith(` ${path} `) : key.endsWith(suffix)) {
1842
+ this.entries.delete(key);
1843
+ changed = true;
1844
+ }
1845
+ if (changed) this.emit();
1846
+ }
1847
+ /** Drop everything. The user signed out, or the app swapped datasource. */
1848
+ clear() {
1849
+ if (this.entries.size === 0) return;
1850
+ this.entries.clear();
1851
+ this.emit();
1852
+ }
1853
+ subscribe(listener) {
1854
+ this.listeners.add(listener);
1855
+ return () => {
1856
+ this.listeners.delete(listener);
1857
+ };
1858
+ }
1859
+ set(key, value) {
1860
+ this.entries.set(key, { value });
1861
+ this.emit();
1862
+ }
1863
+ /**
1864
+ * Record a failed resolution as "nothing", and say so once.
1865
+ *
1866
+ * The key is the message: it is `<role> <path> <id>`, which is exactly what a
1867
+ * reader needs to find the resolver that blew up. A warning with no key would
1868
+ * tell them a display resolver failed somewhere in a list of fifty rows.
1869
+ *
1870
+ * `console.warn` rather than a thrown error, because this runs while a row is
1871
+ * rendering: the contract is that a title which cannot be fetched must not
1872
+ * take down the row that shows it.
1873
+ */
1874
+ fail(key, error) {
1875
+ console.warn(`[rebase] Could not resolve display value for ${key}:`, error);
1876
+ this.set(key, null);
1877
+ }
1878
+ emit() {
1879
+ for (const listener of [...this.listeners]) listener();
1880
+ }
1881
+ };
1882
+ /**
1883
+ * Empty strings and empty arrays are absences, not values — a title of `" "`
1884
+ * would otherwise beat the derived one and render as a blank heading.
1885
+ */
1886
+ function normalize(value) {
1887
+ if (value === void 0 || value === null) return null;
1888
+ if (typeof value === "string") {
1889
+ const trimmed = value.trim();
1890
+ return trimmed.length > 0 ? trimmed : null;
1891
+ }
1892
+ if (Array.isArray(value)) return value.length > 0 ? value : null;
1893
+ return value;
1894
+ }
1895
+ function isPromise(value) {
1896
+ return typeof value?.then === "function";
1897
+ }
1898
+ //#endregion
1899
+ //#region src/auth/session_caches.ts
1900
+ /**
1901
+ * Who the tab's cached state was read or written as. Kept in `sessionStorage`
1902
+ * as well as in memory because the local-changes backup is: a reload keeps the
1903
+ * drafts, so it has to keep the name of the user they belong to.
1904
+ */
1905
+ var OWNER_STORAGE_KEY = "rebase::session_cache_owner";
1906
+ var ownerInMemory;
1907
+ function readOwner() {
1908
+ try {
1909
+ if (typeof sessionStorage !== "undefined") return sessionStorage.getItem(OWNER_STORAGE_KEY) ?? ownerInMemory;
1910
+ } catch {}
1911
+ return ownerInMemory;
1912
+ }
1913
+ function writeOwner(userId) {
1914
+ ownerInMemory = userId;
1915
+ try {
1916
+ if (typeof sessionStorage !== "undefined") sessionStorage.setItem(OWNER_STORAGE_KEY, userId);
1917
+ } catch {}
1918
+ }
1919
+ /**
1920
+ * Empty every module-level cache that holds data read as the signed-in user:
1921
+ * fetched records, each table's scroll position and the rows kept with it,
1922
+ * edits parked for a layout handoff, and resolved display values (the titles
1923
+ * on relation chips and breadcrumbs).
1924
+ *
1925
+ * All of them are keyed by path and id only, with no user in the key, and the
1926
+ * views seed their first render from them — so without this the next user to
1927
+ * sign in to the tab is shown what the last one could read. Called by both auth
1928
+ * controllers on `SIGNED_OUT`.
1929
+ *
1930
+ * The local-changes backup is not cleared here: a sign-out is also how a
1931
+ * rejected refresh token ends a session, and the backup exists for that user
1932
+ * coming back. {@link bindSessionCachesToUser} drops it when somebody else
1933
+ * signs in instead.
1934
+ */
1935
+ function clearSessionCaches() {
1936
+ clearFetchCache();
1937
+ clearCollectionScrollCache();
1938
+ clearEntityMemoryCache();
1939
+ getSharedEntityDisplayCache().clear();
1940
+ }
1941
+ /**
1942
+ * Record that the tab now acts as `userId`. When that is not who the caches
1943
+ * were filled for, everything goes — the session caches, and the drafts the
1944
+ * other user left in the local-changes backup.
1945
+ *
1946
+ * Call it before the new user is rendered: the views read these caches in
1947
+ * their first render, before any effect of the auth controller would run.
1948
+ */
1949
+ function bindSessionCachesToUser(userId) {
1950
+ const owner = readOwner();
1951
+ if (owner !== void 0 && owner !== userId) {
1952
+ clearSessionCaches();
1953
+ clearEntityDrafts();
1954
+ }
1955
+ if (owner !== userId) writeOwner(userId);
1956
+ }
1957
+ //#endregion
1958
+ //#region src/hooks/useAuthSubscription.ts
1959
+ /**
1960
+ * Sets the user, first making sure the tab's caches were filled for them.
1961
+ * Before the state update, not in an effect: the views read those caches in
1962
+ * the very render the new user arrives in.
1963
+ */
1964
+ function adoptUser(user) {
1965
+ if (user) bindSessionCachesToUser(user.uid);
1966
+ return user;
1967
+ }
1968
+ function useAuthSubscription(authClient) {
1969
+ const currentSession = authClient?.getSession();
1970
+ const [user, setUser] = useState(() => adoptUser(currentSession?.user ?? null));
1971
+ const [authLoading, setAuthLoading] = useState(false);
1972
+ const [authError, setAuthError] = useState();
1973
+ const [loginSkipped, setLoginSkipped] = useState(false);
1974
+ const [extra, setExtra] = useState();
1975
+ /**
1976
+ * Whether asking the server "who am I?" could possibly answer.
1977
+ *
1978
+ * A client configured with `persistSession: false` and JSON auth — the
1979
+ * shape used when one client borrows another's credential, as the hosted
1980
+ * Studio does — has no stored session and no auth cookie, so the probe
1981
+ * below is guaranteed to 401. Sending it anyway put three failing
1982
+ * `GET /auth/me` calls on the wire per mount, each one landing in the
1983
+ * *customer's* request log as an authentication failure against their own
1984
+ * backend.
1985
+ *
1986
+ * A client that does not implement the capability is treated as "might
1987
+ * work" — the historical behaviour.
1988
+ */
1989
+ const mayHaveRestorableSession = authClient?.canRestoreSession?.() ?? true;
1990
+ /**
1991
+ * `true` only while a probe is genuinely outstanding.
1992
+ *
1993
+ * It used to be seeded from `!currentSession` alone, which left a client
1994
+ * that cannot restore a session — and therefore never runs the probe that
1995
+ * clears the flag — reporting "still loading" forever. Anything gated on
1996
+ * `initialLoading` (a spinner, a redirect to the login view) would never
1997
+ * resolve.
1998
+ */
1999
+ const [initialLoading, setInitialLoading] = useState(!currentSession && mayHaveRestorableSession);
2000
+ /**
2001
+ * `true` until the client has finished restoring a session on its own.
2002
+ *
2003
+ * The first `getSession()` above is provisional while that runs: the SDK
2004
+ * holds an expired stored session during its boot refresh, and when the
2005
+ * refresh is rejected it drops the session without emitting anything. So
2006
+ * the user is read again once the restore settles, and the app does not
2007
+ * render as signed in before then.
2008
+ */
2009
+ const [restoring, setRestoring] = useState(Boolean(authClient?.isInitialized));
2010
+ useEffect(() => {
2011
+ if (!authClient?.isInitialized) {
2012
+ setRestoring(false);
2013
+ return;
2014
+ }
2015
+ let cancelled = false;
2016
+ setRestoring(true);
2017
+ authClient.isInitialized().catch(() => {}).then(() => {
2018
+ if (cancelled) return;
2019
+ setUser(adoptUser(authClient.getSession()?.user ?? null));
2020
+ setRestoring(false);
2021
+ });
2022
+ return () => {
2023
+ cancelled = true;
2024
+ };
2025
+ }, [authClient]);
2026
+ useEffect(() => {
2027
+ if (!authClient) return;
2028
+ if (!currentSession && mayHaveRestorableSession) {
2029
+ setInitialLoading(true);
2030
+ authClient.getUser().then((user) => {
2031
+ if (user) setUser(adoptUser(user));
2032
+ setAuthError(void 0);
2033
+ }).catch((e) => {
2034
+ const status = e?.status;
2035
+ if (status === 401 || status === 403) {
2036
+ setAuthError(void 0);
2037
+ return;
2038
+ }
2039
+ setAuthError(e instanceof Error ? e : new Error(String(e)));
2040
+ }).finally(() => {
2041
+ setInitialLoading(false);
2042
+ });
2043
+ }
2044
+ }, [
2045
+ authClient,
2046
+ currentSession,
2047
+ mayHaveRestorableSession
2048
+ ]);
2049
+ useEffect(() => {
2050
+ if (!authClient) return;
2051
+ return authClient.onAuthStateChange((event, session) => {
2052
+ if (event === "SIGNED_IN" || event === "USER_UPDATED" || event === "TOKEN_REFRESHED") setUser(adoptUser(session?.user ?? null));
2053
+ else if (event === "SIGNED_OUT") {
2054
+ clearSessionCaches();
2055
+ setUser(null);
2056
+ }
2057
+ });
2058
+ }, [authClient]);
2059
+ const signOut = useCallback(async () => {
2060
+ if (!authClient) return;
2061
+ setAuthLoading(true);
2062
+ try {
2063
+ await authClient.signOut();
2064
+ } finally {
2065
+ setAuthLoading(false);
2066
+ }
2067
+ }, [authClient]);
2068
+ const getAuthToken = useCallback(async () => {
2069
+ if (!authClient) return "";
2070
+ const session = authClient.getSession();
2071
+ if (!session) return "";
2072
+ if (session.expiresAt < Date.now()) try {
2073
+ return (await authClient.refreshSession()).accessToken;
2074
+ } catch (e) {
2075
+ return "";
2076
+ }
2077
+ return session.accessToken;
2078
+ }, [authClient]);
2079
+ return useMemo(() => ({
2080
+ user,
2081
+ initialLoading: authClient ? initialLoading || restoring : false,
2082
+ authLoading,
2083
+ signOut,
2084
+ authError,
2085
+ getAuthToken,
2086
+ loginSkipped,
2087
+ extra,
2088
+ setExtra
2089
+ }), [
2090
+ user,
2091
+ initialLoading,
2092
+ restoring,
2093
+ authLoading,
2094
+ signOut,
2095
+ authError,
2096
+ getAuthToken,
2097
+ loginSkipped,
2098
+ extra,
2099
+ setExtra,
2100
+ authClient
2101
+ ]);
2102
+ }
2103
+ //#endregion
2104
+ //#region src/hooks/useAdminModeController.tsx
2105
+ /**
2106
+ * Hook to retrieve the current admin mode (`"cms"` | `"studio"`), and `setMode`
2107
+ * to change it.
2108
+ *
2109
+ * Consider that in order to use this hook you need to have a parent
2110
+ * `Rebase`
2111
+ *
2112
+ * @see AdminModeController
2113
+ * @group Hooks and utilities
2114
+ */
2115
+ var useAdminModeController = () => useContext(AdminModeControllerContext);
2116
+ //#endregion
2117
+ //#region src/util/local_storage.ts
2118
+ /**
2119
+ * localStorage that tolerates not being there.
2120
+ *
2121
+ * `typeof window !== "undefined"` was the guard these call sites used, and it
2122
+ * answers the wrong question: it says "am I in a browser", not "can I read
2123
+ * storage". Three things make that distinction real rather than theoretical:
2124
+ *
2125
+ * - A browser can have storage **disabled**. Safari in private mode, a blocked
2126
+ * third-party cookie policy, and a sandboxed iframe all give you a `window`
2127
+ * whose `localStorage` property *throws on access* — a SecurityError raised
2128
+ * before any method is called, which no amount of null-checking the result
2129
+ * catches.
2130
+ * - A test environment can be **half torn down**. jsdom is disposed at the end
2131
+ * of a file while React is still finishing a concurrent render, so `window`
2132
+ * survives as a husk and `localStorage.getItem` is no longer a function.
2133
+ * That is how this was found: `TypeError: localStorage.getItem is not a
2134
+ * function`, thrown out of a render, failing a suite in which every one of
2135
+ * 709 tests passed.
2136
+ * - Server rendering has no `window` at all, which is the only case the old
2137
+ * guard actually covered.
2138
+ *
2139
+ * Reading a UI preference is never worth a crash. Every function here fails to
2140
+ * `null` / no-op, so a user with storage switched off gets the default
2141
+ * appearance instead of a blank page.
2142
+ */
2143
+ /** The Storage object, or null when it is absent, blocked, or not usable. */
2144
+ function usableStorage() {
2145
+ try {
2146
+ if (typeof window === "undefined") return null;
2147
+ const candidate = window.localStorage;
2148
+ return typeof candidate?.getItem === "function" ? candidate : null;
2149
+ } catch {
2150
+ return null;
2151
+ }
2152
+ }
2153
+ /** The stored string for `key`, or null if it is unset or unreadable. */
2154
+ function readStoredString(key) {
2155
+ try {
2156
+ return usableStorage()?.getItem(key) ?? null;
2157
+ } catch {
2158
+ return null;
2159
+ }
2160
+ }
2161
+ /** Persist `value` under `key`. A no-op when storage is unavailable. */
2162
+ function writeStoredString(key, value) {
2163
+ try {
2164
+ usableStorage()?.setItem(key, value);
2165
+ } catch {}
2166
+ }
2167
+ /** Remove `key`. A no-op when storage is unavailable. */
2168
+ function removeStoredString(key) {
2169
+ try {
2170
+ usableStorage()?.removeItem(key);
2171
+ } catch {}
2172
+ }
2173
+ //#endregion
2174
+ //#region src/hooks/useBuildAdminModeController.tsx
2175
+ /**
2176
+ * Use this hook to build an admin mode controller that determines
2177
+ * whether the UI shows Developer or Editor tools.
2178
+ */
2179
+ function useBuildAdminModeController() {
2180
+ const stored = readStoredString("rebase-admin-mode");
2181
+ const migrated = stored === "content" ? "cms" : stored;
2182
+ const [mode, setMode] = useState((migrated === "cms" || migrated === "studio" ? migrated : null) ?? "cms");
2183
+ const setModeInternal = useCallback((newMode) => {
2184
+ writeStoredString("rebase-admin-mode", newMode);
2185
+ setMode(newMode);
2186
+ }, []);
2187
+ return useMemo(() => ({
2188
+ mode,
2189
+ setMode: setModeInternal
2190
+ }), [mode, setModeInternal]);
2191
+ }
2192
+ //#endregion
2193
+ //#region src/hooks/useBuildEffectiveRoleController.tsx
2194
+ /**
2195
+ * Use this hook to build an effective role controller that determines
2196
+ * what role is simulated in Editor mode when Dev mode is active.
2197
+ *
2198
+ * It uses localStorage to persist the simulated role across reloads.
2199
+ */
2200
+ function useBuildEffectiveRoleController() {
2201
+ const [effectiveRole, setEffectiveRole] = useState(readStoredString("rebase-effective-role") ?? null);
2202
+ const setRoleInternal = useCallback((newRole) => {
2203
+ if (newRole) writeStoredString("rebase-effective-role", newRole);
2204
+ else removeStoredString("rebase-effective-role");
2205
+ setEffectiveRole(newRole);
2206
+ }, []);
2207
+ return useMemo(() => ({
2208
+ effectiveRole,
2209
+ setEffectiveRole: setRoleInternal
2210
+ }), [effectiveRole, setRoleInternal]);
2211
+ }
2212
+ //#endregion
1741
2213
  //#region src/hooks/useModeController.tsx
1742
2214
  /**
1743
2215
  * Hook to retrieve the current mode ("light" | "dark"), and `setMode`
@@ -2246,6 +2718,14 @@ function ApiConfigProvider({ apiUrl, apiPath = DEFAULT_API_PATH, getAuthToken, c
2246
2718
  * React hook for using backend storage API as a StorageSource
2247
2719
  */
2248
2720
  /**
2721
+ * A storage key as it goes into a URL path: each segment encoded, the `/`
2722
+ * between them kept. A raw key with `#`, `?` or `%` in it cut the URL short or
2723
+ * made the server's decode throw.
2724
+ */
2725
+ function encodeStorageKey(key) {
2726
+ return key.split("/").map(encodeURIComponent).join("/");
2727
+ }
2728
+ /**
2249
2729
  * Hook to create a StorageSource that uses the backend storage REST API.
2250
2730
  * Use this for self-hosted Rebase with local or S3 storage.
2251
2731
  *
@@ -2303,7 +2783,7 @@ function useBackendStorageSource({ apiUrl, apiPath = DEFAULT_API_PATH, getAuthTo
2303
2783
  const getSignedUrl = useCallback(async (keyOrUrl, bucket) => {
2304
2784
  const cacheKey = bucket ? `${bucket}/${keyOrUrl}` : keyOrUrl;
2305
2785
  const cached = urlsCache.get(cacheKey);
2306
- if (cached) return cached;
2786
+ if (cached && (cached.expiresAt === void 0 || cached.expiresAt > Date.now())) return cached.config;
2307
2787
  let filePath = keyOrUrl;
2308
2788
  if (filePath && (filePath.startsWith("local://") || filePath.startsWith("s3://"))) filePath = filePath.substring(filePath.indexOf("://") + 3);
2309
2789
  if (bucket && filePath && !filePath.startsWith(bucket)) filePath = `${bucket}/${filePath}`;
@@ -2311,7 +2791,13 @@ function useBackendStorageSource({ apiUrl, apiPath = DEFAULT_API_PATH, getAuthTo
2311
2791
  url: null,
2312
2792
  fileNotFound: true
2313
2793
  };
2314
- const response = await fetchWithAuth(`${storageBasePath}/metadata/${filePath}`);
2794
+ const fileUrl = `${storageBasePath}/file/${encodeStorageKey(filePath)}`;
2795
+ if (isPublicStoragePath(filePath)) {
2796
+ const publicConfig = { url: fileUrl };
2797
+ urlsCache.set(cacheKey, { config: publicConfig });
2798
+ return publicConfig;
2799
+ }
2800
+ const response = await fetchWithAuth(`${storageBasePath}/metadata/${encodeStorageKey(filePath)}`);
2315
2801
  if (response.status === 404) return {
2316
2802
  url: null,
2317
2803
  fileNotFound: true
@@ -2321,13 +2807,23 @@ function useBackendStorageSource({ apiUrl, apiPath = DEFAULT_API_PATH, getAuthTo
2321
2807
  throw new Error(error.error || "Failed to get download URL");
2322
2808
  }
2323
2809
  const result = await response.json();
2324
- const token = await getAuthToken();
2325
- const tokenQuery = token ? `?token=${token}` : "";
2810
+ if (result.data.public) {
2811
+ const publicConfig = {
2812
+ url: fileUrl,
2813
+ metadata: result.data
2814
+ };
2815
+ urlsCache.set(cacheKey, { config: publicConfig });
2816
+ return publicConfig;
2817
+ }
2818
+ const scopedToken = result.data.token;
2326
2819
  const downloadConfig = {
2327
- url: `${storageBasePath}/file/${filePath}${tokenQuery}`,
2820
+ url: scopedToken ? `${fileUrl}?token=${encodeURIComponent(scopedToken)}` : fileUrl,
2328
2821
  metadata: result.data
2329
2822
  };
2330
- urlsCache.set(cacheKey, downloadConfig);
2823
+ urlsCache.set(cacheKey, {
2824
+ config: downloadConfig,
2825
+ expiresAt: result.data.tokenExpiresIn ? Date.now() + (result.data.tokenExpiresIn - 10) * 1e3 : void 0
2826
+ });
2331
2827
  return downloadConfig;
2332
2828
  }, [
2333
2829
  fetchWithAuth,
@@ -2338,17 +2834,15 @@ function useBackendStorageSource({ apiUrl, apiPath = DEFAULT_API_PATH, getAuthTo
2338
2834
  * Get file as a File object
2339
2835
  */
2340
2836
  const getObject = useCallback(async (key, bucket) => {
2341
- let filePath = key;
2342
- if (filePath && (filePath.startsWith("local://") || filePath.startsWith("s3://"))) filePath = filePath.substring(filePath.indexOf("://") + 3);
2343
- if (bucket && filePath && !filePath.startsWith(bucket)) filePath = `${bucket}/${filePath}`;
2344
- if (!filePath || filePath.trim() === "" || filePath === "/") return null;
2345
- const response = await fetchWithAuth(`${storageBasePath}/file/${filePath}`);
2837
+ const downloadConfig = await getSignedUrl(key, bucket);
2838
+ if (downloadConfig.fileNotFound || !downloadConfig.url) return null;
2839
+ const response = await fetch(downloadConfig.url);
2346
2840
  if (response.status === 404) return null;
2347
2841
  if (!response.ok) throw new Error("Failed to get file");
2348
2842
  const blob = await response.blob();
2349
- const fileName = filePath.split("/").pop() || "file";
2843
+ const fileName = (bucket ? `${bucket}/${key}` : key).split("/").pop() || "file";
2350
2844
  return new File([blob], fileName, { type: blob.type });
2351
- }, [fetchWithAuth, storageBasePath]);
2845
+ }, [getSignedUrl]);
2352
2846
  /**
2353
2847
  * Delete a file
2354
2848
  */
@@ -2357,7 +2851,7 @@ function useBackendStorageSource({ apiUrl, apiPath = DEFAULT_API_PATH, getAuthTo
2357
2851
  if (filePath && (filePath.startsWith("local://") || filePath.startsWith("s3://"))) filePath = filePath.substring(filePath.indexOf("://") + 3);
2358
2852
  if (bucket && filePath && !filePath.startsWith(bucket)) filePath = `${bucket}/${filePath}`;
2359
2853
  if (!filePath || filePath.trim() === "" || filePath === "/") return;
2360
- const response = await fetchWithAuth(`${storageBasePath}/file/${filePath}`, { method: "DELETE" });
2854
+ const response = await fetchWithAuth(`${storageBasePath}/file/${encodeStorageKey(filePath)}`, { method: "DELETE" });
2361
2855
  if (!response.ok && response.status !== 404) {
2362
2856
  const error = await response.json().catch(() => ({ error: "Failed to delete file" }));
2363
2857
  throw new Error(error.error || "Failed to delete file");
@@ -3091,7 +3585,7 @@ var DEFAULT_PAGE_SIZE = 50;
3091
3585
  * @param fixedFilterFromProps
3092
3586
  * @param updateUrl
3093
3587
  */
3094
- function useDataTableController({ path, collection, scrollRestoration, entitiesDisplayedFirst, lastDeleteTimestamp: _lastDeleteTimestamp, fixedFilter: fixedFilterFromProps, updateUrl }) {
3588
+ function useDataTableController({ path, collection, scrollRestoration, entitiesDisplayedFirst, lastDeleteTimestamp, fixedFilter: fixedFilterFromProps, updateUrl }) {
3095
3589
  const { defaultFilter, sort, fixedFilter: fixedFilterFromCollection } = collection;
3096
3590
  const [popupCell, setPopupCell] = React.useState(void 0);
3097
3591
  const dataClient = useData();
@@ -3100,10 +3594,7 @@ function useDataTableController({ path, collection, scrollRestoration, entitiesD
3100
3594
  const pageSize = typeof collection.pagination === "number" && collection.pagination > 0 ? collection.pagination : 50;
3101
3595
  const location = useLocation();
3102
3596
  const [searchString, setSearchString] = React.useState(() => {
3103
- if (updateUrl) {
3104
- const urlSearch = new URLSearchParams(location.search).get("search");
3105
- return urlSearch ? decodeURIComponent(urlSearch) : void 0;
3106
- }
3597
+ if (updateUrl) return parseSearchString(location.search);
3107
3598
  });
3108
3599
  const checkFilterCombination = useCallback((filterValues, sortBy) => {
3109
3600
  return true;
@@ -3120,23 +3611,26 @@ function useDataTableController({ path, collection, scrollRestoration, entitiesD
3120
3611
  const [filterValues, setFilterValues] = React.useState(fixedFilter ?? (updateUrl ? filterUrl : void 0) ?? defaultFilter ?? void 0);
3121
3612
  const [sortBy, setSortBy] = React.useState((updateUrl ? sortUrl : void 0) ?? sortInternal);
3122
3613
  const lastSyncedSearchRef = React.useRef(location.search);
3614
+ const lastWrittenListStateRef = React.useRef(void 0);
3123
3615
  useEffect(() => {
3124
3616
  if (!updateUrl) return;
3125
3617
  if (lastSyncedSearchRef.current === location.search) return;
3126
3618
  lastSyncedSearchRef.current = location.search;
3127
3619
  const { filterValues: urlFilterValues, sortBy: urlSortBy } = parseFilterAndSort(location.search);
3620
+ const urlSearchString = parseSearchString(location.search);
3621
+ if (encodeListState(urlFilterValues, urlSortBy, urlSearchString) === lastWrittenListStateRef.current) return;
3128
3622
  if (!fixedFilter) setFilterValues(urlFilterValues ?? defaultFilter);
3129
3623
  if (urlSortBy && fixedFilter && !checkFilterCombination(fixedFilter, urlSortBy)) console.warn("URL sort is not compatible with the force filter.");
3130
- else setSortBy(urlSortBy);
3131
- const urlSearch = new URLSearchParams(location.search).get("search");
3132
- setSearchString(urlSearch ? decodeURIComponent(urlSearch) : void 0);
3624
+ else setSortBy(urlSortBy ?? sortInternal);
3625
+ setSearchString(urlSearchString);
3133
3626
  }, [
3134
3627
  location.search,
3135
3628
  updateUrl,
3136
3629
  fixedFilter,
3137
- checkFilterCombination
3630
+ checkFilterCombination,
3631
+ sortInternal
3138
3632
  ]);
3139
- useUpdateUrl(filterValues, sortBy, searchString, updateUrl);
3633
+ useUpdateUrl(filterValues, sortBy, searchString, updateUrl, lastWrittenListStateRef);
3140
3634
  const collectionScroll = scrollRestoration?.getCollectionScroll(path, filterValues);
3141
3635
  /**
3142
3636
  * How many rows to ask for on mount: as many as were loaded last time this
@@ -3216,6 +3710,7 @@ function useDataTableController({ path, collection, scrollRestoration, entitiesD
3216
3710
  else setFilterValues(updatedFilter);
3217
3711
  }, [fixedFilter]);
3218
3712
  useEffect(() => {
3713
+ let cancelled = false;
3219
3714
  setDataLoading(true);
3220
3715
  const onEntitiesUpdate = async (entities) => {
3221
3716
  if (collection.browserCallbacks?.afterRead) try {
@@ -3237,6 +3732,7 @@ function useDataTableController({ path, collection, scrollRestoration, entitiesD
3237
3732
  } catch (_e) {
3238
3733
  console.error(_e);
3239
3734
  }
3735
+ if (cancelled) return;
3240
3736
  setDataLoading(false);
3241
3737
  setDataLoadingError(void 0);
3242
3738
  setRawData(entities.map((e) => ({ ...e })));
@@ -3244,6 +3740,7 @@ function useDataTableController({ path, collection, scrollRestoration, entitiesD
3244
3740
  populateFetchCache(path, entities);
3245
3741
  };
3246
3742
  const onError = (error) => {
3743
+ if (cancelled) return;
3247
3744
  console.error("ERROR", error);
3248
3745
  setDataLoading(false);
3249
3746
  setRawData((prev) => prev && prev.length > 0 ? prev : []);
@@ -3261,24 +3758,25 @@ function useDataTableController({ path, collection, scrollRestoration, entitiesD
3261
3758
  searchString,
3262
3759
  include: includeParams
3263
3760
  }), (res) => onEntitiesUpdate(res.data), onError);
3264
- else {
3265
- accessor.find(toFindParams({
3266
- where: whereParams,
3267
- limit: itemCount,
3268
- orderBy: orderByParams,
3269
- searchString,
3270
- include: includeParams
3271
- })).then((res) => onEntitiesUpdate(res.data)).catch(onError);
3272
- unsubscribe = () => void 0;
3273
- }
3274
- return unsubscribe;
3761
+ else accessor.find(toFindParams({
3762
+ where: whereParams,
3763
+ limit: itemCount,
3764
+ orderBy: orderByParams,
3765
+ searchString,
3766
+ include: includeParams
3767
+ })).then((res) => onEntitiesUpdate(res.data)).catch(onError);
3768
+ return () => {
3769
+ cancelled = true;
3770
+ unsubscribe?.();
3771
+ };
3275
3772
  }, [
3276
3773
  dataClient,
3277
3774
  path,
3278
3775
  itemCount,
3279
3776
  sortKey,
3280
3777
  filterValues,
3281
- searchString
3778
+ searchString,
3779
+ Boolean(dataClient.collection(path).listen) ? void 0 : lastDeleteTimestamp
3282
3780
  ]);
3283
3781
  const data = useDataOrder({
3284
3782
  data: rawData,
@@ -3324,7 +3822,7 @@ function useDataTableController({ path, collection, scrollRestoration, entitiesD
3324
3822
  popupCell
3325
3823
  ]);
3326
3824
  }
3327
- function useUpdateUrl(filterValues, sortBy, searchString, updateUrl) {
3825
+ function useUpdateUrl(filterValues, sortBy, searchString, updateUrl, lastWrittenListStateRef) {
3328
3826
  useEffect(() => {
3329
3827
  if (updateUrl) {
3330
3828
  const existingParams = new URLSearchParams(window.location.search);
@@ -3332,16 +3830,12 @@ function useUpdateUrl(filterValues, sortBy, searchString, updateUrl) {
3332
3830
  existingParams.forEach((value, key) => {
3333
3831
  if (key.startsWith("__") && key !== "__sort" && key !== "__sort_order") preservedParams.set(key, value);
3334
3832
  });
3335
- const filterSortState = encodeFilterAndSort(filterValues, sortBy);
3336
- const search = searchString ? `search=${encodeURIComponent(searchString)}` : "";
3337
- const state = [
3338
- preservedParams.toString(),
3339
- filterSortState,
3340
- search
3341
- ].filter(Boolean).join("&");
3833
+ const listState = encodeListState(filterValues, sortBy, searchString);
3834
+ lastWrittenListStateRef.current = listState;
3835
+ const state = [preservedParams.toString(), listState].filter(Boolean).join("&");
3342
3836
  const hash = window.location.hash;
3343
- if (state === "") window.history.replaceState({}, "", `${window.location.pathname}${hash}`);
3344
- else window.history.replaceState({}, "", `?${state}${hash}`);
3837
+ if (state === "") window.history.replaceState(window.history.state, "", `${window.location.pathname}${hash}`);
3838
+ else window.history.replaceState(window.history.state, "", `?${state}${hash}`);
3345
3839
  }
3346
3840
  }, [
3347
3841
  filterValues,
@@ -3350,16 +3844,31 @@ function useUpdateUrl(filterValues, sortBy, searchString, updateUrl) {
3350
3844
  updateUrl
3351
3845
  ]);
3352
3846
  }
3847
+ /**
3848
+ * The part of the query string this hook owns — filters, sort and search — in
3849
+ * the one canonical spelling, so two states can be compared by what they would
3850
+ * write.
3851
+ */
3852
+ function encodeListState(filterValues, sortBy, searchString) {
3853
+ const search = searchString ? `search=${encodeURIComponent(searchString)}` : "";
3854
+ return [encodeFilterAndSort(filterValues, sortBy), search].filter(Boolean).join("&");
3855
+ }
3856
+ function parseSearchString(search) {
3857
+ return new URLSearchParams(search).get("search") || void 0;
3858
+ }
3859
+ var OP_SUFFIX = "_op";
3860
+ var VALUE_SUFFIX = "_value";
3353
3861
  function encodeFilterAndSort(filterValues, sortBy) {
3354
- const entries = {};
3862
+ const entries = [];
3355
3863
  if (sortBy && sortBy.length > 0) {
3356
- entries["__sort"] = sortBy.map(([field]) => encodeURIComponent(field)).join(",");
3357
- entries["__sort_order"] = sortBy.map(([, direction]) => direction).join(",");
3864
+ entries.push(["__sort", sortBy.map(([field]) => encodeURIComponent(field)).join(",")]);
3865
+ entries.push(["__sort_order", sortBy.map(([, direction]) => direction).join(",")]);
3358
3866
  }
3359
3867
  if (filterValues) Object.entries(filterValues).forEach(([key, value]) => {
3360
3868
  if (value) {
3361
- const [op, val] = (Array.isArray(value[0]) ? value : [value])[0] || [];
3362
- if (op) {
3869
+ const conditions = Array.isArray(value[0]) ? value : [value];
3870
+ for (const [op, val] of conditions) {
3871
+ if (!op) continue;
3363
3872
  let encodedValue = val;
3364
3873
  try {
3365
3874
  if (typeof val === "object") {
@@ -3376,14 +3885,13 @@ function encodeFilterAndSort(filterValues, sortBy) {
3376
3885
  encodedValue = val;
3377
3886
  }
3378
3887
  if (encodedValue !== void 0) {
3379
- entries[encodeURIComponent(`${key}_op`)] = encodeURIComponent(op);
3380
- entries[encodeURIComponent(`${key}_value`)] = encodedValue ? encodeURIComponent(String(encodedValue)) : "null";
3888
+ entries.push([encodeURIComponent(`${key}${OP_SUFFIX}`), encodeURIComponent(op)]);
3889
+ entries.push([encodeURIComponent(`${key}${VALUE_SUFFIX}`), encodedValue === null ? "null" : encodeURIComponent(String(encodedValue))]);
3381
3890
  }
3382
3891
  }
3383
3892
  }
3384
3893
  });
3385
- if (!Object.keys(entries).length) return "";
3386
- return Object.entries(entries).map(([key, value]) => `${key}=${value}`).join("&");
3894
+ return entries.map(([key, value]) => `${key}=${value}`).join("&");
3387
3895
  }
3388
3896
  function parseFilterAndSort(search) {
3389
3897
  const entries = new URLSearchParams(search);
@@ -3394,11 +3902,13 @@ function parseFilterAndSort(search) {
3394
3902
  const directions = (entries.get("__sort_order") ?? "").split(",");
3395
3903
  const keys = value.split(",").map((field) => decodeURIComponent(field).trim()).filter(Boolean).map((field, index) => [field, directions[index] === "desc" ? "desc" : "asc"]);
3396
3904
  sortBy = keys.length > 0 ? keys : void 0;
3397
- } else if (key.endsWith("_op")) {
3398
- const field = key.replace("_op", "");
3399
- const filterOp = decodeURIComponent(value);
3400
- const filterValStr = entries.get(`${field}_value`);
3401
- if (filterValStr !== null) filterValues[field] = [filterOp, decodeString(filterValStr)];
3905
+ } else if (key.endsWith(OP_SUFFIX) && !(key.slice(0, -3) in filterValues)) {
3906
+ const field = key.slice(0, -3);
3907
+ const ops = entries.getAll(key);
3908
+ const values = entries.getAll(`${field}${VALUE_SUFFIX}`);
3909
+ const conditions = ops.slice(0, values.length).map((op, index) => [decodeURIComponent(op), decodeString(values[index])]);
3910
+ if (conditions.length === 1) filterValues[field] = conditions[0];
3911
+ else if (conditions.length > 1) filterValues[field] = conditions;
3402
3912
  }
3403
3913
  });
3404
3914
  return {
@@ -3455,30 +3965,6 @@ function decodeString(val) {
3455
3965
  return parsedFilterVal;
3456
3966
  }
3457
3967
  //#endregion
3458
- //#region src/components/common/useScrollRestoration.tsx
3459
- var collectionScrollCache = /* @__PURE__ */ new Map();
3460
- function useScrollRestoration() {
3461
- const updateCollectionScroll = ({ path, filters, scrollOffset, data }) => {
3462
- collectionScrollCache.set(createCacheKey(path, filters), {
3463
- scrollOffset,
3464
- data
3465
- });
3466
- };
3467
- const getCollectionScroll = (path, filters) => {
3468
- return collectionScrollCache.get(createCacheKey(path, filters));
3469
- };
3470
- return {
3471
- getCollectionScroll,
3472
- updateCollectionScroll
3473
- };
3474
- }
3475
- function createCacheKey(path, filters) {
3476
- if (!filters) return path;
3477
- return `${path}?${filters ? Object.keys(filters).map((key) => {
3478
- return `${key}=${JSON.stringify(filters[key])}`;
3479
- }).join("&") : ""}`;
3480
- }
3481
- //#endregion
3482
3968
  //#region src/components/common/table_height.tsx
3483
3969
  function getRowHeight(size) {
3484
3970
  switch (size) {
@@ -3861,6 +4347,15 @@ function UserSettingsView() {
3861
4347
  const authController = useAuthController();
3862
4348
  const user = authController.user;
3863
4349
  const { t } = useTranslation();
4350
+ const authControllerRef = useRef(authController);
4351
+ authControllerRef.current = authController;
4352
+ const mountedRef = useRef(true);
4353
+ useEffect(() => {
4354
+ mountedRef.current = true;
4355
+ return () => {
4356
+ mountedRef.current = false;
4357
+ };
4358
+ }, []);
3864
4359
  const hasPasswordChange = !!authController.changePassword;
3865
4360
  const [activeTab, setActiveTab] = useState("profile");
3866
4361
  const [displayName, setDisplayName] = useState(user?.displayName || "");
@@ -3915,7 +4410,7 @@ function UserSettingsView() {
3915
4410
  setNewPassword("");
3916
4411
  setConfirmPassword("");
3917
4412
  setTimeout(() => {
3918
- authController.signOut();
4413
+ if (mountedRef.current && authControllerRef.current.user) authControllerRef.current.signOut();
3919
4414
  }, 2e3);
3920
4415
  }
3921
4416
  } catch (e) {
@@ -4656,6 +5151,74 @@ function authErrorMessage(error, t) {
4656
5151
  return error instanceof Error ? error.message : String(error);
4657
5152
  }
4658
5153
  //#endregion
5154
+ //#region src/components/LoginView/email-link.ts
5155
+ var ACTIONS = ["reset-password", "verify-email"];
5156
+ function readEmailLinkAction(location) {
5157
+ const token = new URLSearchParams(location.search).get("token");
5158
+ if (!token) return null;
5159
+ const lastSegment = location.pathname.replace(/\/+$/, "").split("/").pop();
5160
+ const kind = ACTIONS.find((action) => action === lastSegment);
5161
+ return kind ? {
5162
+ kind,
5163
+ token
5164
+ } : null;
5165
+ }
5166
+ /**
5167
+ * Where the app lives: the link's own address without the action segment and
5168
+ * its token. `/admin/reset-password?token=…` becomes `/admin/`.
5169
+ */
5170
+ function appAddressOfEmailLink(location) {
5171
+ const withoutAction = location.pathname.replace(/\/+$/, "").replace(/\/[^/]*$/, "");
5172
+ return `${location.origin}${withoutAction}/`;
5173
+ }
5174
+ //#endregion
5175
+ //#region src/components/LoginView/mfa-required.ts
5176
+ function isFactor(value) {
5177
+ return typeof value === "object" && value !== null && "id" in value && typeof value.id === "string" && value.id.length > 0 && "factorType" in value && typeof value.factorType === "string";
5178
+ }
5179
+ /**
5180
+ * The pending sign-in an `MFA_REQUIRED` refusal describes, or `null` for any
5181
+ * other error — including an `MFA_REQUIRED` without the token or a factor to
5182
+ * answer with, which no code step could finish.
5183
+ */
5184
+ function readMfaRequired(error) {
5185
+ if (!(error instanceof Error) || !("code" in error) || error.code !== "MFA_REQUIRED") return null;
5186
+ const details = "details" in error ? error.details : void 0;
5187
+ if (typeof details !== "object" || details === null) return null;
5188
+ const mfaToken = "mfaToken" in details ? details.mfaToken : void 0;
5189
+ const factors = "factors" in details && Array.isArray(details.factors) ? details.factors.filter(isFactor) : [];
5190
+ if (typeof mfaToken !== "string" || !mfaToken || factors.length === 0) return null;
5191
+ return {
5192
+ mfaToken,
5193
+ factors
5194
+ };
5195
+ }
5196
+ /** Whether this controller can take a sign-in through its second step. */
5197
+ function canAnswerMfa(authController) {
5198
+ return Boolean(authController.startMfaChallenge && authController.verifyMfaChallenge);
5199
+ }
5200
+ /**
5201
+ * What a refused challenge means for the code step.
5202
+ *
5203
+ * - `invalid-code`: the code was wrong; the same challenge takes another.
5204
+ * - `exhausted`: the challenge has had its five attempts; the next code needs
5205
+ * a new one.
5206
+ * - `expired`: the sign-in itself is over. The pending token lasts five
5207
+ * minutes from the password, and once it has lapsed both routes refuse the
5208
+ * caller as unauthenticated; a challenge that lapsed, or a sign-in revoked
5209
+ * meanwhile, ends the same way. Only a new sign-in gets a new token.
5210
+ */
5211
+ function classifyMfaRefusal(error) {
5212
+ if (!(error instanceof Error)) return "other";
5213
+ const code = "code" in error && typeof error.code === "string" ? error.code : void 0;
5214
+ const status = "status" in error && typeof error.status === "number" ? error.status : void 0;
5215
+ if (code === "INVALID_CODE") return "invalid-code";
5216
+ if (code === "CHALLENGE_EXHAUSTED") return "exhausted";
5217
+ if (code === "INVALID_CHALLENGE" || code === "SESSION_REVOKED") return "expired";
5218
+ if (status === 401 && (code === void 0 || code === "UNAUTHORIZED")) return "expired";
5219
+ return "other";
5220
+ }
5221
+ //#endregion
4659
5222
  //#region src/components/LoginView/LoginView.tsx
4660
5223
  /**
4661
5224
  * The shared field background mixin (`dark:bg-black/30`) is invisible on the
@@ -4695,6 +5258,18 @@ function isUserCancellation(code) {
4695
5258
  ].includes(code);
4696
5259
  }
4697
5260
  /**
5261
+ * The controller's last refused sign-in, as a sign-in screen should show it:
5262
+ * not at all once a user is present — a stale failure must not sit over a
5263
+ * screen that has since succeeded — and not at all for an `MFA_REQUIRED` this
5264
+ * view can take to its code step, where it is a cue rather than a failure.
5265
+ */
5266
+ function signInRefusal(authController) {
5267
+ const error = authController.authProviderError;
5268
+ if (!error || authController.user) return null;
5269
+ if (canAnswerMfa(authController) && readMfaRequired(error)) return null;
5270
+ return error;
5271
+ }
5272
+ /**
4698
5273
  * Generic login view component that works with any AuthControllerExtended.
4699
5274
  * Feature-detects capabilities to show/hide login methods.
4700
5275
  * @group Core
@@ -4702,11 +5277,25 @@ function isUserCancellation(code) {
4702
5277
  function LoginView({ logo, authController, noUserComponent, disableSignupScreen = false, disabled = false, notAllowedError, googleClientId, githubClientId, linkedinClientId, title, subtitle, needsSetup, registrationEnabled, additionalComponent, topComponent, defaultEmail, defaultPassword, onNewsletterOptIn }) {
4703
5278
  const { mode: colorMode, setMode: setColorMode } = useModeController();
4704
5279
  const { t } = useTranslation();
4705
- const [mode, setMode] = useState("buttons");
5280
+ const [emailLink] = useState(() => typeof window === "undefined" ? null : readEmailLinkAction(window.location));
5281
+ const [mode, setMode] = useState(emailLink?.kind ?? "buttons");
4706
5282
  const [fadeIn, setFadeIn] = useState(false);
4707
5283
  const [viewVisible, setViewVisible] = useState(true);
4708
5284
  const [providerError, setProviderError] = useState(null);
4709
5285
  const [newsletterOptIn, setNewsletterOptIn] = useState(false);
5286
+ const [pendingMfa, setPendingMfa] = useState(null);
5287
+ const mfaAnswerable = canAnswerMfa(authController);
5288
+ useEffect(() => {
5289
+ if (!mfaAnswerable || authController.user) return;
5290
+ const pending = readMfaRequired(authController.authProviderError);
5291
+ if (!pending) return;
5292
+ setProviderError(null);
5293
+ setPendingMfa(pending);
5294
+ }, [
5295
+ authController.authProviderError,
5296
+ authController.user,
5297
+ mfaAnswerable
5298
+ ]);
4710
5299
  const authControllerRef = useRef(authController);
4711
5300
  authControllerRef.current = authController;
4712
5301
  /**
@@ -4760,7 +5349,7 @@ function LoginView({ logo, authController, noUserComponent, disableSignupScreen
4760
5349
  const result = consumeOAuthCallback(window.location.search);
4761
5350
  if (result.status === "none") return;
4762
5351
  const cleanUrl = window.location.origin + window.location.pathname;
4763
- window.history.replaceState({}, document.title, cleanUrl);
5352
+ window.history.replaceState(window.history.state, document.title, cleanUrl);
4764
5353
  if (result.status === "error") {
4765
5354
  console.error(`OAuth sign-in failed: ${result.error}`);
4766
5355
  if (!isUserCancellation(result.error)) setProviderError(`Sign-in failed: ${result.error}`);
@@ -4790,7 +5379,8 @@ function LoginView({ logo, authController, noUserComponent, disableSignupScreen
4790
5379
  alt: "Logo"
4791
5380
  });
4792
5381
  else logoComponent = /* @__PURE__ */ jsx(RebaseLogo, {});
4793
- const controllerError = authController.authProviderError && !authController.user ? authErrorMessage(authController.authProviderError, t) : null;
5382
+ const refusal = signInRefusal(authController);
5383
+ const controllerError = refusal ? authErrorMessage(refusal, t) : null;
4794
5384
  const buttonsErrorMessage = providerError ?? controllerError;
4795
5385
  let notAllowedMessage;
4796
5386
  if (notAllowedError) if (typeof notAllowedError === "string") notAllowedMessage = notAllowedError;
@@ -4846,125 +5436,144 @@ function LoginView({ logo, authController, noUserComponent, disableSignupScreen
4846
5436
  }),
4847
5437
  /* @__PURE__ */ jsxs("div", {
4848
5438
  className: cls("w-full transition-opacity duration-150", viewVisible ? "opacity-100" : "opacity-0"),
4849
- children: [isBootstrapMode && !authController.user && /* @__PURE__ */ jsx(LoginForm, {
4850
- authController,
4851
- registrationMode: true,
4852
- onClose: () => {},
4853
- onForgotPassword: () => {},
4854
- disableSignupScreen: false,
4855
- bootstrapMode: true,
4856
- defaultEmail,
4857
- defaultPassword,
4858
- onSignedIn: subscribeIfOptedIn,
4859
- newsletterOptIn,
4860
- setNewsletterOptIn,
4861
- showNewsletterOptIn: Boolean(onNewsletterOptIn)
4862
- }), !isBootstrapMode && /* @__PURE__ */ jsxs(Fragment, { children: [
4863
- mode === "buttons" && /* @__PURE__ */ jsxs("div", {
4864
- className: "w-full flex flex-col gap-3 mt-2",
4865
- children: [
4866
- buttonsErrorMessage && /* @__PURE__ */ jsx(ErrorView, { error: buttonsErrorMessage }),
4867
- (title || subtitle) && /* @__PURE__ */ jsxs("div", {
4868
- className: "text-center mb-2",
4869
- children: [title && /* @__PURE__ */ jsx(Typography, {
4870
- variant: "h6",
4871
- className: "mb-0.5 font-semibold",
4872
- children: title
4873
- }), subtitle && /* @__PURE__ */ jsx(Typography, {
4874
- variant: "body2",
4875
- color: "secondary",
4876
- className: "mb-4",
4877
- children: subtitle
4878
- })]
4879
- }),
4880
- topComponent && /* @__PURE__ */ jsx("div", {
4881
- className: "w-full",
4882
- children: topComponent
4883
- }),
4884
- onNewsletterOptIn && /* @__PURE__ */ jsxs("label", {
4885
- className: cls("flex items-center gap-2 cursor-pointer", (topComponent || title || subtitle) && "-mt-3"),
4886
- children: [/* @__PURE__ */ jsx(Checkbox, {
4887
- checked: newsletterOptIn,
4888
- onCheckedChange: (checked) => setNewsletterOptIn(checked === true),
4889
- size: "small"
4890
- }), /* @__PURE__ */ jsx(Typography, {
4891
- variant: "caption",
4892
- color: "secondary",
4893
- className: "select-none",
4894
- children: t("join_newsletter")
4895
- })]
4896
- }),
4897
- /* @__PURE__ */ jsx(LoginButton, {
4898
- disabled,
4899
- text: "Sign in with email",
4900
- icon: /* @__PURE__ */ jsx(MailIcon, {}),
4901
- onClick: () => switchMode("login")
4902
- }),
4903
- hasGoogleLogin && googleClientId && /* @__PURE__ */ jsx(GoogleLoginButton, {
4904
- disabled,
4905
- googleClientId,
4906
- authController,
4907
- onSignedIn: () => subscribeIfOptedIn(authControllerRef.current.user?.email),
4908
- onError: setProviderError
4909
- }),
4910
- hasGitHubLogin && githubClientId && /* @__PURE__ */ jsx(GitHubLoginButton, {
4911
- disabled,
4912
- githubClientId
4913
- }),
4914
- hasLinkedinLogin && linkedinClientId && /* @__PURE__ */ jsx(LinkedInLoginButton, {
4915
- disabled,
4916
- linkedinClientId
4917
- }),
4918
- showRegistration && /* @__PURE__ */ jsx("div", {
4919
- className: "mt-2 text-center",
4920
- children: /* @__PURE__ */ jsxs(Typography, {
4921
- variant: "body2",
4922
- color: "secondary",
4923
- children: [
4924
- "Don't have an account?",
4925
- " ",
4926
- /* @__PURE__ */ jsx("button", {
4927
- type: "button",
4928
- className: "font-semibold hover:underline cursor-pointer text-primary dark:text-primary-light",
4929
- onClick: () => switchMode("register"),
4930
- children: "Create one"
4931
- })
4932
- ]
4933
- })
4934
- })
4935
- ]
4936
- }),
4937
- mode === "login" && /* @__PURE__ */ jsx(LoginForm, {
4938
- authController,
4939
- registrationMode: false,
4940
- onClose: () => switchMode("buttons"),
4941
- onForgotPassword: hasPasswordReset ? () => switchMode("forgot") : void 0,
4942
- noUserComponent,
4943
- disableSignupScreen,
4944
- switchToRegister: showRegistration ? () => switchMode("register") : void 0,
4945
- defaultEmail,
4946
- defaultPassword,
4947
- onSignedIn: subscribeIfOptedIn
4948
- }),
4949
- mode === "register" && /* @__PURE__ */ jsx(LoginForm, {
5439
+ children: [
5440
+ isBootstrapMode && !authController.user && /* @__PURE__ */ jsx(LoginForm, {
4950
5441
  authController,
4951
5442
  registrationMode: true,
4952
- onClose: () => switchMode("buttons"),
4953
- onForgotPassword: hasPasswordReset ? () => switchMode("forgot") : void 0,
4954
- noUserComponent,
4955
- disableSignupScreen,
4956
- switchToLogin: () => switchMode("login"),
5443
+ onClose: () => {},
5444
+ onForgotPassword: () => {},
5445
+ disableSignupScreen: false,
5446
+ bootstrapMode: true,
4957
5447
  defaultEmail,
4958
5448
  defaultPassword,
4959
- onSignedIn: subscribeIfOptedIn
5449
+ onSignedIn: subscribeIfOptedIn,
5450
+ newsletterOptIn,
5451
+ setNewsletterOptIn,
5452
+ showNewsletterOptIn: Boolean(onNewsletterOptIn)
4960
5453
  }),
4961
- mode === "forgot" && authController.forgotPassword && /* @__PURE__ */ jsx(ForgotPasswordForm, {
5454
+ !isBootstrapMode && pendingMfa && /* @__PURE__ */ jsx(MfaChallengeForm, {
5455
+ pending: pendingMfa,
4962
5456
  authController,
4963
- onClose: () => switchMode("login")
4964
- })
4965
- ] })]
5457
+ onBack: () => setPendingMfa(null),
5458
+ onSignedIn: () => subscribeIfOptedIn(authControllerRef.current.user?.email)
5459
+ }, pendingMfa.mfaToken),
5460
+ !isBootstrapMode && !pendingMfa && /* @__PURE__ */ jsxs(Fragment, { children: [
5461
+ mode === "buttons" && /* @__PURE__ */ jsxs("div", {
5462
+ className: "w-full flex flex-col gap-3 mt-2",
5463
+ children: [
5464
+ buttonsErrorMessage && /* @__PURE__ */ jsx(ErrorView, { error: buttonsErrorMessage }),
5465
+ (title || subtitle) && /* @__PURE__ */ jsxs("div", {
5466
+ className: "text-center mb-2",
5467
+ children: [title && /* @__PURE__ */ jsx(Typography, {
5468
+ variant: "h6",
5469
+ className: "mb-0.5 font-semibold",
5470
+ children: title
5471
+ }), subtitle && /* @__PURE__ */ jsx(Typography, {
5472
+ variant: "body2",
5473
+ color: "secondary",
5474
+ className: "mb-4",
5475
+ children: subtitle
5476
+ })]
5477
+ }),
5478
+ topComponent && /* @__PURE__ */ jsx("div", {
5479
+ className: "w-full",
5480
+ children: topComponent
5481
+ }),
5482
+ onNewsletterOptIn && /* @__PURE__ */ jsxs("label", {
5483
+ className: cls("flex items-center gap-2 cursor-pointer", (topComponent || title || subtitle) && "-mt-3"),
5484
+ children: [/* @__PURE__ */ jsx(Checkbox, {
5485
+ checked: newsletterOptIn,
5486
+ onCheckedChange: (checked) => setNewsletterOptIn(checked === true),
5487
+ size: "small"
5488
+ }), /* @__PURE__ */ jsx(Typography, {
5489
+ variant: "caption",
5490
+ color: "secondary",
5491
+ className: "select-none",
5492
+ children: t("join_newsletter")
5493
+ })]
5494
+ }),
5495
+ /* @__PURE__ */ jsx(LoginButton, {
5496
+ disabled,
5497
+ text: "Sign in with email",
5498
+ icon: /* @__PURE__ */ jsx(MailIcon, {}),
5499
+ onClick: () => switchMode("login")
5500
+ }),
5501
+ hasGoogleLogin && googleClientId && /* @__PURE__ */ jsx(GoogleLoginButton, {
5502
+ disabled,
5503
+ googleClientId,
5504
+ authController,
5505
+ onSignedIn: () => subscribeIfOptedIn(authControllerRef.current.user?.email),
5506
+ onError: setProviderError
5507
+ }),
5508
+ hasGitHubLogin && githubClientId && /* @__PURE__ */ jsx(GitHubLoginButton, {
5509
+ disabled,
5510
+ githubClientId
5511
+ }),
5512
+ hasLinkedinLogin && linkedinClientId && /* @__PURE__ */ jsx(LinkedInLoginButton, {
5513
+ disabled,
5514
+ linkedinClientId
5515
+ }),
5516
+ showRegistration && /* @__PURE__ */ jsx("div", {
5517
+ className: "mt-2 text-center",
5518
+ children: /* @__PURE__ */ jsxs(Typography, {
5519
+ variant: "body2",
5520
+ color: "secondary",
5521
+ children: [
5522
+ "Don't have an account?",
5523
+ " ",
5524
+ /* @__PURE__ */ jsx("button", {
5525
+ type: "button",
5526
+ className: "font-semibold hover:underline cursor-pointer text-primary dark:text-primary-light",
5527
+ onClick: () => switchMode("register"),
5528
+ children: "Create one"
5529
+ })
5530
+ ]
5531
+ })
5532
+ })
5533
+ ]
5534
+ }),
5535
+ mode === "login" && /* @__PURE__ */ jsx(LoginForm, {
5536
+ authController,
5537
+ registrationMode: false,
5538
+ onClose: () => switchMode("buttons"),
5539
+ onForgotPassword: hasPasswordReset ? () => switchMode("forgot") : void 0,
5540
+ noUserComponent,
5541
+ disableSignupScreen,
5542
+ switchToRegister: showRegistration ? () => switchMode("register") : void 0,
5543
+ defaultEmail,
5544
+ defaultPassword,
5545
+ onSignedIn: subscribeIfOptedIn
5546
+ }),
5547
+ mode === "register" && /* @__PURE__ */ jsx(LoginForm, {
5548
+ authController,
5549
+ registrationMode: true,
5550
+ onClose: () => switchMode("buttons"),
5551
+ onForgotPassword: hasPasswordReset ? () => switchMode("forgot") : void 0,
5552
+ noUserComponent,
5553
+ disableSignupScreen,
5554
+ switchToLogin: () => switchMode("login"),
5555
+ defaultEmail,
5556
+ defaultPassword,
5557
+ onSignedIn: subscribeIfOptedIn
5558
+ }),
5559
+ mode === "forgot" && authController.forgotPassword && /* @__PURE__ */ jsx(ForgotPasswordForm, {
5560
+ authController,
5561
+ onClose: () => switchMode("login")
5562
+ }),
5563
+ mode === "reset-password" && emailLink?.kind === "reset-password" && /* @__PURE__ */ jsx(ResetPasswordForm, {
5564
+ token: emailLink.token,
5565
+ authController,
5566
+ onDone: leaveEmailLink
5567
+ }),
5568
+ mode === "verify-email" && emailLink?.kind === "verify-email" && /* @__PURE__ */ jsx(VerifyEmailView, {
5569
+ token: emailLink.token,
5570
+ authController,
5571
+ onDone: leaveEmailLink
5572
+ })
5573
+ ] })
5574
+ ]
4966
5575
  }),
4967
- additionalComponent && !isBootstrapMode && mode === "buttons" && /* @__PURE__ */ jsx("div", {
5576
+ additionalComponent && !isBootstrapMode && mode === "buttons" && !pendingMfa && /* @__PURE__ */ jsx("div", {
4968
5577
  className: "w-full",
4969
5578
  children: additionalComponent
4970
5579
  })
@@ -5020,10 +5629,15 @@ function GoogleLoginButton({ disabled, googleClientId, authController, onSignedI
5020
5629
  onSignedInRef.current = onSignedIn;
5021
5630
  const onErrorRef = useRef(onError);
5022
5631
  onErrorRef.current = onError;
5023
- useEffect(() => {
5024
- if (!authController.googleLogin) return;
5632
+ /**
5633
+ * Build Google's code client, if its script has arrived. Google's script
5634
+ * tag is `async`, so it routinely lands after this button has mounted:
5635
+ * the click tries again rather than trusting whatever the mount saw.
5636
+ */
5637
+ const ensureCodeClient = useCallback(() => {
5638
+ if (codeClientRef.current || !authController.googleLogin) return codeClientRef.current;
5025
5639
  const google = window.google;
5026
- if (!google || codeClientRef.current) return;
5640
+ if (!google) return null;
5027
5641
  codeClientRef.current = google.accounts.oauth2.initCodeClient({
5028
5642
  client_id: googleClientId,
5029
5643
  scope: "openid email profile",
@@ -5046,15 +5660,20 @@ function GoogleLoginButton({ disabled, googleClientId, authController, onSignedI
5046
5660
  }
5047
5661
  }
5048
5662
  });
5663
+ return codeClientRef.current;
5049
5664
  }, [googleClientId, authController]);
5665
+ useEffect(() => {
5666
+ ensureCodeClient();
5667
+ }, [ensureCodeClient]);
5050
5668
  const handleClick = () => {
5051
- if (!codeClientRef.current) {
5669
+ const codeClient = ensureCodeClient();
5670
+ if (!codeClient) {
5052
5671
  console.error("Google Sign-In not loaded");
5053
5672
  onErrorRef.current?.("Google Sign-In could not be loaded. Check your connection or any script blockers, then try again.");
5054
5673
  return;
5055
5674
  }
5056
5675
  onErrorRef.current?.(null);
5057
- codeClientRef.current.requestCode();
5676
+ codeClient.requestCode();
5058
5677
  };
5059
5678
  return /* @__PURE__ */ jsx(LoginButton, {
5060
5679
  disabled,
@@ -5159,8 +5778,8 @@ function LoginForm({ onClose, onForgotPassword, authController, registrationMode
5159
5778
  })
5160
5779
  }),
5161
5780
  (() => {
5162
- const err = authController.authProviderError;
5163
- if (!err || authController.user) return null;
5781
+ const err = signInRefusal(authController);
5782
+ if (!err) return null;
5164
5783
  return /* @__PURE__ */ jsx("div", {
5165
5784
  className: "w-full mb-2",
5166
5785
  children: /* @__PURE__ */ jsx(ErrorView, { error: authErrorMessage(err, t) })
@@ -5318,6 +5937,396 @@ function LoginForm({ onClose, onForgotPassword, authController, registrationMode
5318
5937
  ]
5319
5938
  });
5320
5939
  }
5940
+ /** How a factor is named in the picker: the name its owner gave it, if any. */
5941
+ function factorName(factor, index, t) {
5942
+ if (factor.friendlyName) return factor.friendlyName;
5943
+ return `${factor.factorType === "totp" ? t("auth_mfa_factor_totp") : factor.factorType} ${index + 1}`;
5944
+ }
5945
+ /**
5946
+ * The second step of a sign-in refused with `MFA_REQUIRED`: a code from the
5947
+ * authenticator app, or one of the recovery codes, in the same field — the
5948
+ * server tries the one and then the other.
5949
+ *
5950
+ * The challenge is opened on the first submit and answered by every code
5951
+ * after it until it is spent, which is after five wrong ones: the next code
5952
+ * then opens a new one. The pending token behind both lasts five minutes from
5953
+ * the password; once it has lapsed nothing here can help, so the step says so
5954
+ * and sends the visitor back to sign in again.
5955
+ */
5956
+ function MfaChallengeForm({ pending, authController, onBack, onSignedIn }) {
5957
+ const { t } = useTranslation();
5958
+ const factorFieldId = useId();
5959
+ const codeId = useId();
5960
+ const [factorId, setFactorId] = useState(pending.factors[0].id);
5961
+ const [code, setCode] = useState("");
5962
+ const [challengeId, setChallengeId] = useState(null);
5963
+ const [submitting, setSubmitting] = useState(false);
5964
+ const [error, setError] = useState(null);
5965
+ const [expired, setExpired] = useState(false);
5966
+ const chooseFactor = (id) => {
5967
+ setFactorId(id);
5968
+ setChallengeId(null);
5969
+ setError(null);
5970
+ };
5971
+ const handleSubmit = async (event) => {
5972
+ event.preventDefault();
5973
+ const answer = code.trim();
5974
+ const { startMfaChallenge, verifyMfaChallenge } = authController;
5975
+ if (!answer || submitting || expired || !startMfaChallenge || !verifyMfaChallenge) return;
5976
+ setError(null);
5977
+ setSubmitting(true);
5978
+ try {
5979
+ const id = challengeId ?? await startMfaChallenge(pending.mfaToken, factorId);
5980
+ setChallengeId(id);
5981
+ await verifyMfaChallenge(pending.mfaToken, id, answer);
5982
+ onSignedIn?.();
5983
+ } catch (err) {
5984
+ switch (classifyMfaRefusal(err)) {
5985
+ case "invalid-code":
5986
+ setCode("");
5987
+ setError(t("auth_mfa_code_invalid"));
5988
+ break;
5989
+ case "exhausted":
5990
+ setCode("");
5991
+ setChallengeId(null);
5992
+ setError(t("auth_mfa_challenge_exhausted"));
5993
+ break;
5994
+ case "expired":
5995
+ setExpired(true);
5996
+ setError(t("auth_mfa_expired"));
5997
+ break;
5998
+ default: setError(authErrorMessage(err, t));
5999
+ }
6000
+ } finally {
6001
+ setSubmitting(false);
6002
+ }
6003
+ };
6004
+ return /* @__PURE__ */ jsxs("form", {
6005
+ onSubmit: handleSubmit,
6006
+ className: "flex flex-col w-full gap-1 mt-2",
6007
+ children: [
6008
+ /* @__PURE__ */ jsx("div", {
6009
+ className: "w-full mb-2 -ml-2.5",
6010
+ children: /* @__PURE__ */ jsx(IconButton, {
6011
+ onClick: onBack,
6012
+ "aria-label": t("back"),
6013
+ children: /* @__PURE__ */ jsx(ArrowLeftIcon, {})
6014
+ })
6015
+ }),
6016
+ /* @__PURE__ */ jsx(Typography, {
6017
+ variant: "h6",
6018
+ className: "mb-0.5",
6019
+ children: t("auth_mfa_title")
6020
+ }),
6021
+ /* @__PURE__ */ jsx(Typography, {
6022
+ variant: "body2",
6023
+ color: "secondary",
6024
+ className: "mb-5",
6025
+ children: t("auth_mfa_subtitle")
6026
+ }),
6027
+ error && /* @__PURE__ */ jsx("div", {
6028
+ className: "w-full mb-3",
6029
+ children: /* @__PURE__ */ jsx(ErrorView, { error })
6030
+ }),
6031
+ pending.factors.length > 1 && /* @__PURE__ */ jsxs("div", {
6032
+ className: "w-full mb-3",
6033
+ children: [/* @__PURE__ */ jsx(Typography, {
6034
+ variant: "label",
6035
+ component: "label",
6036
+ color: "secondary",
6037
+ className: "mb-1",
6038
+ htmlFor: factorFieldId,
6039
+ children: t("auth_mfa_factor_label")
6040
+ }), /* @__PURE__ */ jsx(Select, {
6041
+ id: factorFieldId,
6042
+ "aria-label": t("auth_mfa_factor_label"),
6043
+ value: factorId,
6044
+ onValueChange: chooseFactor,
6045
+ disabled: submitting || expired,
6046
+ size: "medium",
6047
+ fullWidth: true,
6048
+ children: pending.factors.map((factor, index) => /* @__PURE__ */ jsx(SelectItem, {
6049
+ value: factor.id,
6050
+ children: factorName(factor, index, t)
6051
+ }, factor.id))
6052
+ })]
6053
+ }),
6054
+ /* @__PURE__ */ jsxs("div", {
6055
+ className: "w-full mb-3",
6056
+ children: [/* @__PURE__ */ jsx(Typography, {
6057
+ variant: "label",
6058
+ component: "label",
6059
+ color: "secondary",
6060
+ className: "mb-1",
6061
+ htmlFor: codeId,
6062
+ children: t("auth_mfa_code_label")
6063
+ }), /* @__PURE__ */ jsx(TextField, {
6064
+ id: codeId,
6065
+ className: loginFieldClasses,
6066
+ autoFocus: true,
6067
+ autoComplete: "one-time-code",
6068
+ autoCapitalize: "none",
6069
+ spellCheck: false,
6070
+ value: code,
6071
+ disabled: expired,
6072
+ type: "text",
6073
+ size: "medium",
6074
+ onChange: (event) => setCode(event.target.value)
6075
+ })]
6076
+ }),
6077
+ expired ? /* @__PURE__ */ jsx(Button, {
6078
+ onClick: onBack,
6079
+ variant: "filled",
6080
+ color: "primary",
6081
+ size: "large",
6082
+ className: "w-full mt-1",
6083
+ children: t("auth_mfa_sign_in_again")
6084
+ }) : /* @__PURE__ */ jsx(LoadingButton, {
6085
+ type: "submit",
6086
+ variant: "filled",
6087
+ color: "primary",
6088
+ className: "w-full mt-1",
6089
+ size: "large",
6090
+ loading: submitting,
6091
+ disabled: submitting || !code.trim(),
6092
+ children: t("auth_mfa_verify")
6093
+ })
6094
+ ]
6095
+ });
6096
+ }
6097
+ /**
6098
+ * Leave an email link for the app's own address, with a fresh load.
6099
+ *
6100
+ * The router, when there is one, still stands on `/reset-password`: rewriting
6101
+ * the address under it would leave it rendering a route that does not exist
6102
+ * the moment the user signs in. A load starts it where the app starts, and the
6103
+ * spent token leaves the address bar and the history entry with it.
6104
+ */
6105
+ function leaveEmailLink() {
6106
+ window.location.replace(appAddressOfEmailLink(window.location));
6107
+ }
6108
+ /** What to say about a refused email-link token, or any other failure. */
6109
+ function emailLinkErrorMessage(error, t) {
6110
+ if (error instanceof Error && "code" in error && error.code === "INVALID_TOKEN") return t("auth_link_invalid_or_expired");
6111
+ return authErrorMessage(error, t);
6112
+ }
6113
+ /**
6114
+ * The step a reset link opens: a new password for the account the token was
6115
+ * issued to. An invitation sends the same link, so this is also where an
6116
+ * invited user — created without a password — sets their first one.
6117
+ */
6118
+ function ResetPasswordForm({ token, authController, onDone }) {
6119
+ const { t } = useTranslation();
6120
+ const passwordId = useId();
6121
+ const confirmationId = useId();
6122
+ const [password, setPassword] = useState("");
6123
+ const [confirmation, setConfirmation] = useState("");
6124
+ const [submitting, setSubmitting] = useState(false);
6125
+ const [error, setError] = useState(null);
6126
+ const [changed, setChanged] = useState(false);
6127
+ const handleSubmit = async (event) => {
6128
+ event.preventDefault();
6129
+ setError(null);
6130
+ if (!authController.resetPassword) {
6131
+ setError(t("auth_password_reset_unavailable"));
6132
+ return;
6133
+ }
6134
+ if (password !== confirmation) {
6135
+ setError(t("passwords_dont_match"));
6136
+ return;
6137
+ }
6138
+ setSubmitting(true);
6139
+ try {
6140
+ await authController.resetPassword(token, password);
6141
+ setChanged(true);
6142
+ } catch (err) {
6143
+ setError(emailLinkErrorMessage(err, t));
6144
+ } finally {
6145
+ setSubmitting(false);
6146
+ }
6147
+ };
6148
+ if (changed) return /* @__PURE__ */ jsxs("div", {
6149
+ className: "flex flex-col w-full gap-4 mt-2",
6150
+ children: [/* @__PURE__ */ jsxs("div", {
6151
+ className: "flex flex-col items-center text-center rounded-xl p-6 bg-surface-raised",
6152
+ children: [
6153
+ /* @__PURE__ */ jsx(CheckCircle2Icon, {
6154
+ size: iconSize.large,
6155
+ className: "mb-3 text-primary"
6156
+ }),
6157
+ /* @__PURE__ */ jsx(Typography, {
6158
+ variant: "subtitle1",
6159
+ className: "mb-2",
6160
+ children: t("auth_password_changed_title")
6161
+ }),
6162
+ /* @__PURE__ */ jsx(Typography, {
6163
+ variant: "body2",
6164
+ color: "secondary",
6165
+ children: t("auth_password_changed_body")
6166
+ })
6167
+ ]
6168
+ }), /* @__PURE__ */ jsx(Button, {
6169
+ onClick: onDone,
6170
+ variant: "filled",
6171
+ color: "primary",
6172
+ size: "large",
6173
+ className: "w-full",
6174
+ children: t("auth_continue_to_sign_in")
6175
+ })]
6176
+ });
6177
+ return /* @__PURE__ */ jsxs("form", {
6178
+ onSubmit: handleSubmit,
6179
+ className: "flex flex-col w-full gap-1 mt-2",
6180
+ children: [
6181
+ /* @__PURE__ */ jsx("div", {
6182
+ className: "w-full mb-2 -ml-2.5",
6183
+ children: /* @__PURE__ */ jsx(IconButton, {
6184
+ onClick: onDone,
6185
+ "aria-label": t("back"),
6186
+ children: /* @__PURE__ */ jsx(ArrowLeftIcon, {})
6187
+ })
6188
+ }),
6189
+ /* @__PURE__ */ jsx(Typography, {
6190
+ variant: "h6",
6191
+ className: "mb-0.5",
6192
+ children: t("auth_new_password_title")
6193
+ }),
6194
+ /* @__PURE__ */ jsx(Typography, {
6195
+ variant: "body2",
6196
+ color: "secondary",
6197
+ className: "mb-5",
6198
+ children: t("auth_new_password_subtitle")
6199
+ }),
6200
+ error && /* @__PURE__ */ jsx("div", {
6201
+ className: "w-full mb-3",
6202
+ children: /* @__PURE__ */ jsx(ErrorView, { error })
6203
+ }),
6204
+ /* @__PURE__ */ jsxs("div", {
6205
+ className: "w-full mb-3",
6206
+ children: [/* @__PURE__ */ jsx(Typography, {
6207
+ variant: "label",
6208
+ component: "label",
6209
+ color: "secondary",
6210
+ className: "mb-1",
6211
+ htmlFor: passwordId,
6212
+ children: t("new_password")
6213
+ }), /* @__PURE__ */ jsx(TextField, {
6214
+ id: passwordId,
6215
+ className: loginFieldClasses,
6216
+ autoFocus: true,
6217
+ autoComplete: "new-password",
6218
+ value: password,
6219
+ type: "password",
6220
+ size: "medium",
6221
+ onChange: (event) => setPassword(event.target.value)
6222
+ })]
6223
+ }),
6224
+ /* @__PURE__ */ jsxs("div", {
6225
+ className: "w-full mb-1",
6226
+ children: [/* @__PURE__ */ jsx(Typography, {
6227
+ variant: "label",
6228
+ component: "label",
6229
+ color: "secondary",
6230
+ className: "mb-1",
6231
+ htmlFor: confirmationId,
6232
+ children: t("confirm_password")
6233
+ }), /* @__PURE__ */ jsx(TextField, {
6234
+ id: confirmationId,
6235
+ className: loginFieldClasses,
6236
+ autoComplete: "new-password",
6237
+ value: confirmation,
6238
+ type: "password",
6239
+ size: "medium",
6240
+ onChange: (event) => setConfirmation(event.target.value)
6241
+ })]
6242
+ }),
6243
+ /* @__PURE__ */ jsx(Typography, {
6244
+ variant: "caption",
6245
+ color: "secondary",
6246
+ className: "mb-3",
6247
+ children: t("auth_password_requirements")
6248
+ }),
6249
+ /* @__PURE__ */ jsx(LoadingButton, {
6250
+ type: "submit",
6251
+ variant: "filled",
6252
+ color: "primary",
6253
+ className: "w-full mt-1",
6254
+ size: "large",
6255
+ loading: submitting,
6256
+ disabled: submitting || !password || !confirmation,
6257
+ children: t("auth_set_password")
6258
+ })
6259
+ ]
6260
+ });
6261
+ }
6262
+ /**
6263
+ * The step a verification link opens. There is nothing to ask: the token is
6264
+ * spent as soon as the screen opens, and the result is what it shows.
6265
+ */
6266
+ function VerifyEmailView({ token, authController, onDone }) {
6267
+ const { t } = useTranslation();
6268
+ const [status, setStatus] = useState("verifying");
6269
+ const [error, setError] = useState(null);
6270
+ const requestedRef = useRef(false);
6271
+ useEffect(() => {
6272
+ if (requestedRef.current) return;
6273
+ requestedRef.current = true;
6274
+ if (!authController.verifyEmail) {
6275
+ setError(t("auth_email_verification_unavailable"));
6276
+ setStatus("failed");
6277
+ return;
6278
+ }
6279
+ authController.verifyEmail(token).then(() => setStatus("verified")).catch((err) => {
6280
+ setError(emailLinkErrorMessage(err, t));
6281
+ setStatus("failed");
6282
+ });
6283
+ }, [
6284
+ authController,
6285
+ token,
6286
+ t
6287
+ ]);
6288
+ if (status === "verifying") return /* @__PURE__ */ jsxs("div", {
6289
+ className: "flex flex-col items-center w-full gap-4 mt-6 mb-4",
6290
+ children: [/* @__PURE__ */ jsx(CircularProgress, { size: "small" }), /* @__PURE__ */ jsx(Typography, {
6291
+ variant: "body2",
6292
+ color: "secondary",
6293
+ children: t("auth_verifying_email")
6294
+ })]
6295
+ });
6296
+ return /* @__PURE__ */ jsxs("div", {
6297
+ className: "flex flex-col w-full gap-4 mt-2",
6298
+ children: [status === "verified" ? /* @__PURE__ */ jsxs("div", {
6299
+ className: "flex flex-col items-center text-center rounded-xl p-6 bg-surface-raised",
6300
+ children: [
6301
+ /* @__PURE__ */ jsx(CheckCircle2Icon, {
6302
+ size: iconSize.large,
6303
+ className: "mb-3 text-primary"
6304
+ }),
6305
+ /* @__PURE__ */ jsx(Typography, {
6306
+ variant: "subtitle1",
6307
+ className: "mb-2",
6308
+ children: t("auth_email_verified_title")
6309
+ }),
6310
+ /* @__PURE__ */ jsx(Typography, {
6311
+ variant: "body2",
6312
+ color: "secondary",
6313
+ children: t("auth_email_verified_body")
6314
+ })
6315
+ ]
6316
+ }) : /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Typography, {
6317
+ variant: "h6",
6318
+ className: "mb-0.5",
6319
+ children: t("auth_email_verification_failed_title")
6320
+ }), error && /* @__PURE__ */ jsx(ErrorView, { error })] }), /* @__PURE__ */ jsx(Button, {
6321
+ onClick: onDone,
6322
+ variant: "filled",
6323
+ color: "primary",
6324
+ size: "large",
6325
+ className: "w-full",
6326
+ children: t("auth_continue_to_sign_in")
6327
+ })]
6328
+ });
6329
+ }
5321
6330
  function ForgotPasswordForm({ onClose, authController }) {
5322
6331
  const [email, setEmail] = useState("");
5323
6332
  const [submitted, setSubmitted] = useState(false);
@@ -6050,6 +7059,30 @@ var en = {
6050
7059
  auth_have_account: "Have an account?",
6051
7060
  auth_sign_in: "Sign In",
6052
7061
  auth_sign_up: "Sign Up",
7062
+ auth_new_password_title: "Set a new password",
7063
+ auth_new_password_subtitle: "Choose the password you will sign in with from now on.",
7064
+ auth_password_requirements: "Password must be 8+ characters with uppercase, lowercase, and a number",
7065
+ auth_set_password: "Set password",
7066
+ auth_password_changed_title: "Password changed",
7067
+ auth_password_changed_body: "Sign in with your new password.",
7068
+ auth_continue_to_sign_in: "Continue to sign in",
7069
+ auth_link_invalid_or_expired: "This link is invalid or has expired. Request a new one from the sign-in screen.",
7070
+ auth_password_reset_unavailable: "Password reset is not available on this backend.",
7071
+ auth_verifying_email: "Confirming your email address…",
7072
+ auth_email_verified_title: "Email confirmed",
7073
+ auth_email_verified_body: "Your email address is confirmed. You can sign in now.",
7074
+ auth_email_verification_failed_title: "Email not confirmed",
7075
+ auth_email_verification_unavailable: "Email verification is not available on this backend.",
7076
+ auth_mfa_title: "Two-step verification",
7077
+ auth_mfa_subtitle: "Enter the code from your authenticator app, or one of your recovery codes.",
7078
+ auth_mfa_factor_label: "Verify with",
7079
+ auth_mfa_factor_totp: "Authenticator app",
7080
+ auth_mfa_code_label: "Verification code",
7081
+ auth_mfa_verify: "Verify",
7082
+ auth_mfa_code_invalid: "That code is not right. Try the current code from your authenticator app, or a recovery code.",
7083
+ auth_mfa_challenge_exhausted: "Too many wrong codes. Enter a new code from your authenticator app to try again.",
7084
+ auth_mfa_expired: "This sign-in has expired. Sign in again to continue.",
7085
+ auth_mfa_sign_in_again: "Sign in again",
6053
7086
  auto_setup_collections_button: "Automatically set up collections",
6054
7087
  auto_setup_collections_title: "Automatically set up collections?",
6055
7088
  auto_setup_collections_desc: "This will automatically create collection configs for collections that are <b>NOT</b> already mapped",
@@ -6212,10 +7245,12 @@ var en = {
6212
7245
  studio_sql_cannot_edit_missing_query: "Cannot edit rows: The underlying query is missing.",
6213
7246
  studio_sql_cannot_resolve_table: "Could not resolve table/primary key",
6214
7247
  studio_sql_missing_pk: "Row is missing primary key column(s): {{columns}}. Cannot safely update.",
7248
+ studio_sql_cannot_edit_other_connection: "These rows were read from another database or as another role. Run the query again to edit them.",
6215
7249
  studio_sql_update_failed: "Failed to update: {{message}}",
6216
7250
  studio_sql_execution_not_supported: "SQL execution is not supported by the current data source.",
6217
7251
  studio_sql_error_executing: "An error occurred while executing the query.",
6218
7252
  studio_sql_error_explaining: "An error occurred while explaining the query.",
7253
+ studio_sql_explain_single_statement: "Explain works on one statement at a time. Select the statement to explain and try again.",
6219
7254
  studio_sql_save_first_to_favorite: "Please save the snippet first before favoriting.",
6220
7255
  studio_sql_collections_label: "Collections:",
6221
7256
  studio_sql_admin_collections_tooltip: "Tables in this query that are mapped to collections",
@@ -6262,6 +7297,9 @@ var en = {
6262
7297
  studio_rls_enable_force_rls: "Enable Force RLS",
6263
7298
  studio_rls_disable_force_rls: "Disable Force RLS",
6264
7299
  studio_rls_edit: "Edit",
7300
+ studio_rls_edit_generated: "{{policy}} is generated by Rebase for every collection, so there is no rule of yours to edit. Add a policy of your own instead.",
7301
+ studio_rls_edit_not_declared: "{{policy}} exists only in the database, so there is no rule in your code to edit. Import it to the codebase first.",
7302
+ studio_rls_edit_several_operations: "{{policy}} is one of the policies a single rule compiles to, one per operation. Edit that rule in the collection's source file.",
6265
7303
  studio_rls_delete: "Delete",
6266
7304
  studio_rls_confirm_delete_title: "Delete Policy",
6267
7305
  studio_rls_confirm_delete_body: "Are you sure you want to delete this policy? This action cannot be undone.",
@@ -6442,6 +7480,8 @@ var en = {
6442
7480
  studio_cron_denied_title: "You cannot list this project's cron jobs",
6443
7481
  studio_cron_denied_hint: "Nothing is wrong with the project. The cron API is admin-only, and the signed-in account was refused.",
6444
7482
  studio_cron_read_failed: "Could not read this project's cron jobs",
7483
+ studio_cron_logs_read_failed: "Could not read this job's executions",
7484
+ studio_cron_logs_denied_title: "You cannot read this job's executions",
6445
7485
  studio_api_keys_denied_title: "You cannot list this project's API keys",
6446
7486
  studio_api_keys_denied_hint: "Nothing is wrong with the project. Managing API keys is an admin operation, and the signed-in account was refused.",
6447
7487
  studio_api_keys_read_failed: "Could not read this project's API keys",
@@ -6460,6 +7500,7 @@ var en = {
6460
7500
  studio_api_keys_status_active: "Active",
6461
7501
  studio_api_keys_status_expired: "Expired",
6462
7502
  studio_api_keys_status_revoked: "Revoked",
7503
+ studio_api_keys_inactive_heading: "Revoked / Expired",
6463
7504
  studio_api_keys_stat_status: "Status",
6464
7505
  studio_api_keys_stat_last_used: "Last used",
6465
7506
  studio_api_keys_stat_expires: "Expires",
@@ -7067,6 +8108,30 @@ var es = {
7067
8108
  auth_have_account: "¿Tienes una cuenta?",
7068
8109
  auth_sign_in: "Iniciar Sesión",
7069
8110
  auth_sign_up: "Regístrate",
8111
+ auth_new_password_title: "Establece una nueva contraseña",
8112
+ auth_new_password_subtitle: "Elige la contraseña con la que iniciarás sesión a partir de ahora.",
8113
+ auth_password_requirements: "La contraseña debe tener al menos 8 caracteres, con mayúsculas, minúsculas y un número",
8114
+ auth_set_password: "Establecer contraseña",
8115
+ auth_password_changed_title: "Contraseña cambiada",
8116
+ auth_password_changed_body: "Inicia sesión con tu nueva contraseña.",
8117
+ auth_continue_to_sign_in: "Continuar al inicio de sesión",
8118
+ auth_link_invalid_or_expired: "Este enlace no es válido o ha caducado. Solicita uno nuevo desde la pantalla de inicio de sesión.",
8119
+ auth_password_reset_unavailable: "El restablecimiento de contraseña no está disponible en este backend.",
8120
+ auth_verifying_email: "Confirmando tu dirección de correo…",
8121
+ auth_email_verified_title: "Correo confirmado",
8122
+ auth_email_verified_body: "Tu dirección de correo está confirmada. Ya puedes iniciar sesión.",
8123
+ auth_email_verification_failed_title: "Correo no confirmado",
8124
+ auth_email_verification_unavailable: "La verificación de correo no está disponible en este backend.",
8125
+ auth_mfa_title: "Verificación en dos pasos",
8126
+ auth_mfa_subtitle: "Introduce el código de tu aplicación de autenticación o uno de tus códigos de recuperación.",
8127
+ auth_mfa_factor_label: "Verificar con",
8128
+ auth_mfa_factor_totp: "Aplicación de autenticación",
8129
+ auth_mfa_code_label: "Código de verificación",
8130
+ auth_mfa_verify: "Verificar",
8131
+ auth_mfa_code_invalid: "Ese código no es correcto. Prueba el código actual de tu aplicación de autenticación o un código de recuperación.",
8132
+ auth_mfa_challenge_exhausted: "Demasiados códigos incorrectos. Introduce un código nuevo de tu aplicación de autenticación para volver a intentarlo.",
8133
+ auth_mfa_expired: "Este inicio de sesión ha caducado. Vuelve a iniciar sesión para continuar.",
8134
+ auth_mfa_sign_in_again: "Volver a iniciar sesión",
7070
8135
  auto_setup_collections_button: "Configurar colecciones automáticamente",
7071
8136
  auto_setup_collections_title: "¿Configurar colecciones automáticamente?",
7072
8137
  auto_setup_collections_desc: "Esto creará automáticamente la configuración de las colecciones que <b>NO</b> estén mapeadas",
@@ -7229,10 +8294,12 @@ var es = {
7229
8294
  studio_sql_cannot_edit_missing_query: "No se pueden editar las filas: falta la consulta subyacente.",
7230
8295
  studio_sql_cannot_resolve_table: "No se pudo determinar la tabla ni la clave primaria",
7231
8296
  studio_sql_missing_pk: "A la fila le faltan columnas de clave primaria: {{columns}}. No se puede actualizar con seguridad.",
8297
+ studio_sql_cannot_edit_other_connection: "Estas filas se leyeron de otra base de datos o con otro rol. Vuelve a ejecutar la consulta para editarlas.",
7232
8298
  studio_sql_update_failed: "Error al actualizar: {{message}}",
7233
8299
  studio_sql_execution_not_supported: "La fuente de datos actual no admite la ejecución de SQL.",
7234
8300
  studio_sql_error_executing: "Se produjo un error al ejecutar la consulta.",
7235
8301
  studio_sql_error_explaining: "Se produjo un error al explicar la consulta.",
8302
+ studio_sql_explain_single_statement: "Explain funciona con una sola sentencia a la vez. Selecciona la sentencia que quieres explicar y vuelve a intentarlo.",
7236
8303
  studio_sql_save_first_to_favorite: "Guarda el fragmento antes de marcarlo como favorito.",
7237
8304
  studio_sql_collections_label: "Collections:",
7238
8305
  studio_sql_admin_collections_tooltip: "Tables in this query that are mapped as admin collections",
@@ -7279,6 +8346,9 @@ var es = {
7279
8346
  studio_rls_enable_force_rls: "Activar RLS forzada",
7280
8347
  studio_rls_disable_force_rls: "Desactivar RLS forzada",
7281
8348
  studio_rls_edit: "Edit",
8349
+ studio_rls_edit_generated: "Rebase genera {{policy}} para todas las colecciones, así que no hay ninguna regla tuya que editar. Añade una política propia en su lugar.",
8350
+ studio_rls_edit_not_declared: "{{policy}} solo existe en la base de datos, así que no hay ninguna regla en tu código que editar. Impórtala primero al código.",
8351
+ studio_rls_edit_several_operations: "{{policy}} es una de las políticas en las que se compila una sola regla, una por operación. Edita esa regla en el archivo fuente de la colección.",
7282
8352
  studio_rls_delete: "Delete",
7283
8353
  studio_rls_confirm_delete_title: "Eliminar política",
7284
8354
  studio_rls_confirm_delete_body: "¿Seguro que quieres eliminar esta política? Esta acción no se puede deshacer.",
@@ -7459,6 +8529,8 @@ var es = {
7459
8529
  studio_cron_denied_title: "No puedes listar las tareas cron de este proyecto",
7460
8530
  studio_cron_denied_hint: "El proyecto está bien. La API de cron es solo para administradores, y la cuenta con la sesión iniciada fue rechazada.",
7461
8531
  studio_cron_read_failed: "No se han podido leer las tareas cron de este proyecto",
8532
+ studio_cron_logs_read_failed: "No se han podido leer las ejecuciones de esta tarea",
8533
+ studio_cron_logs_denied_title: "No puedes leer las ejecuciones de esta tarea",
7462
8534
  studio_api_keys_denied_title: "No puedes listar las claves de API de este proyecto",
7463
8535
  studio_api_keys_denied_hint: "El proyecto está bien. Gestionar claves de API es una operación de administrador, y la cuenta con la sesión iniciada fue rechazada.",
7464
8536
  studio_api_keys_read_failed: "No se han podido leer las claves de API de este proyecto",
@@ -7477,6 +8549,7 @@ var es = {
7477
8549
  studio_api_keys_status_active: "Activa",
7478
8550
  studio_api_keys_status_expired: "Caducada",
7479
8551
  studio_api_keys_status_revoked: "Revocada",
8552
+ studio_api_keys_inactive_heading: "Revocadas / Caducadas",
7480
8553
  studio_api_keys_stat_status: "Estado",
7481
8554
  studio_api_keys_stat_last_used: "Último uso",
7482
8555
  studio_api_keys_stat_expires: "Caduca",
@@ -8076,6 +9149,30 @@ var de = {
8076
9149
  auth_have_account: "Haben Sie bereits ein Konto?",
8077
9150
  auth_sign_in: "Anmelden",
8078
9151
  auth_sign_up: "Registrieren",
9152
+ auth_new_password_title: "Neues Passwort festlegen",
9153
+ auth_new_password_subtitle: "Wähle das Passwort, mit dem du dich ab jetzt anmeldest.",
9154
+ auth_password_requirements: "Das Passwort muss mindestens 8 Zeichen lang sein und Groß- und Kleinbuchstaben sowie eine Zahl enthalten",
9155
+ auth_set_password: "Passwort festlegen",
9156
+ auth_password_changed_title: "Passwort geändert",
9157
+ auth_password_changed_body: "Melde dich mit deinem neuen Passwort an.",
9158
+ auth_continue_to_sign_in: "Weiter zur Anmeldung",
9159
+ auth_link_invalid_or_expired: "Dieser Link ist ungültig oder abgelaufen. Fordere auf dem Anmeldebildschirm einen neuen an.",
9160
+ auth_password_reset_unavailable: "Das Zurücksetzen des Passworts ist in diesem Backend nicht verfügbar.",
9161
+ auth_verifying_email: "Deine E-Mail-Adresse wird bestätigt…",
9162
+ auth_email_verified_title: "E-Mail bestätigt",
9163
+ auth_email_verified_body: "Deine E-Mail-Adresse ist bestätigt. Du kannst dich jetzt anmelden.",
9164
+ auth_email_verification_failed_title: "E-Mail nicht bestätigt",
9165
+ auth_email_verification_unavailable: "Die E-Mail-Bestätigung ist in diesem Backend nicht verfügbar.",
9166
+ auth_mfa_title: "Bestätigung in zwei Schritten",
9167
+ auth_mfa_subtitle: "Gib den Code aus deiner Authenticator-App oder einen deiner Wiederherstellungscodes ein.",
9168
+ auth_mfa_factor_label: "Bestätigen mit",
9169
+ auth_mfa_factor_totp: "Authenticator-App",
9170
+ auth_mfa_code_label: "Bestätigungscode",
9171
+ auth_mfa_verify: "Bestätigen",
9172
+ auth_mfa_code_invalid: "Dieser Code stimmt nicht. Versuche den aktuellen Code aus deiner Authenticator-App oder einen Wiederherstellungscode.",
9173
+ auth_mfa_challenge_exhausted: "Zu viele falsche Codes. Gib einen neuen Code aus deiner Authenticator-App ein, um es erneut zu versuchen.",
9174
+ auth_mfa_expired: "Diese Anmeldung ist abgelaufen. Melde dich erneut an, um fortzufahren.",
9175
+ auth_mfa_sign_in_again: "Erneut anmelden",
8079
9176
  auto_setup_collections_button: "Sammlungen automatisch einrichten",
8080
9177
  auto_setup_collections_title: "Sammlungen automatisch einrichten?",
8081
9178
  auto_setup_collections_desc: "Dadurch werden automatisch Sammlungskonfigurationen für Sammlungen erstellt, die noch <b>NICHT</b> zugeordnet sind.",
@@ -8238,10 +9335,12 @@ var de = {
8238
9335
  studio_sql_cannot_edit_missing_query: "Zeilen können nicht bearbeitet werden: Die zugrunde liegende Abfrage fehlt.",
8239
9336
  studio_sql_cannot_resolve_table: "Tabelle bzw. Primärschlüssel konnte nicht ermittelt werden",
8240
9337
  studio_sql_missing_pk: "In der Zeile fehlen die Primärschlüsselspalten: {{columns}}. Eine sichere Aktualisierung ist nicht möglich.",
9338
+ studio_sql_cannot_edit_other_connection: "Diese Zeilen wurden aus einer anderen Datenbank oder mit einer anderen Rolle gelesen. Führen Sie die Abfrage erneut aus, um sie zu bearbeiten.",
8241
9339
  studio_sql_update_failed: "Aktualisierung fehlgeschlagen: {{message}}",
8242
9340
  studio_sql_execution_not_supported: "Die aktuelle Datenquelle unterstützt keine SQL-Ausführung.",
8243
9341
  studio_sql_error_executing: "Beim Ausführen der Abfrage ist ein Fehler aufgetreten.",
8244
9342
  studio_sql_error_explaining: "Beim Erklären der Abfrage ist ein Fehler aufgetreten.",
9343
+ studio_sql_explain_single_statement: "Explain erklärt jeweils nur eine Anweisung. Markieren Sie die Anweisung, die erklärt werden soll, und versuchen Sie es erneut.",
8245
9344
  studio_sql_save_first_to_favorite: "Bitte speichern Sie das Snippet, bevor Sie es zu den Favoriten hinzufügen.",
8246
9345
  studio_sql_collections_label: "Collections:",
8247
9346
  studio_sql_admin_collections_tooltip: "Tables in this query that are mapped as admin collections",
@@ -8288,6 +9387,9 @@ var de = {
8288
9387
  studio_rls_enable_force_rls: "Erzwungene RLS aktivieren",
8289
9388
  studio_rls_disable_force_rls: "Erzwungene RLS deaktivieren",
8290
9389
  studio_rls_edit: "Edit",
9390
+ studio_rls_edit_generated: "{{policy}} wird von Rebase für jede Collection erzeugt, es gibt also keine eigene Regel zum Bearbeiten. Legen Sie stattdessen eine eigene Policy an.",
9391
+ studio_rls_edit_not_declared: "{{policy}} existiert nur in der Datenbank, in Ihrem Code gibt es also keine Regel zum Bearbeiten. Importieren Sie sie zuerst in die Codebasis.",
9392
+ studio_rls_edit_several_operations: "{{policy}} ist eine der Policies, zu denen eine einzelne Regel kompiliert wird, eine pro Operation. Bearbeiten Sie diese Regel in der Quelldatei der Collection.",
8291
9393
  studio_rls_delete: "Delete",
8292
9394
  studio_rls_confirm_delete_title: "Richtlinie löschen",
8293
9395
  studio_rls_confirm_delete_body: "Möchten Sie diese Richtlinie wirklich löschen? Dieser Vorgang kann nicht rückgängig gemacht werden.",
@@ -8468,6 +9570,8 @@ var de = {
8468
9570
  studio_cron_denied_title: "Du darfst die Cron-Jobs dieses Projekts nicht auflisten",
8469
9571
  studio_cron_denied_hint: "Mit dem Projekt ist alles in Ordnung. Die Cron-API ist nur für Admins, und das angemeldete Konto wurde abgelehnt.",
8470
9572
  studio_cron_read_failed: "Die Cron-Jobs dieses Projekts konnten nicht gelesen werden",
9573
+ studio_cron_logs_read_failed: "Die Ausführungen dieses Jobs konnten nicht gelesen werden",
9574
+ studio_cron_logs_denied_title: "Du darfst die Ausführungen dieses Jobs nicht lesen",
8471
9575
  studio_api_keys_denied_title: "Du darfst die API-Schlüssel dieses Projekts nicht auflisten",
8472
9576
  studio_api_keys_denied_hint: "Mit dem Projekt ist alles in Ordnung. Das Verwalten von API-Schlüsseln ist eine Admin-Aktion, und das angemeldete Konto wurde abgelehnt.",
8473
9577
  studio_api_keys_read_failed: "Die API-Schlüssel dieses Projekts konnten nicht gelesen werden",
@@ -8486,6 +9590,7 @@ var de = {
8486
9590
  studio_api_keys_status_active: "Aktiv",
8487
9591
  studio_api_keys_status_expired: "Abgelaufen",
8488
9592
  studio_api_keys_status_revoked: "Widerrufen",
9593
+ studio_api_keys_inactive_heading: "Widerrufen / Abgelaufen",
8489
9594
  studio_api_keys_stat_status: "Status",
8490
9595
  studio_api_keys_stat_last_used: "Zuletzt verwendet",
8491
9596
  studio_api_keys_stat_expires: "Läuft ab",
@@ -9085,6 +10190,30 @@ var fr = {
9085
10190
  auth_have_account: "Vous avez déjà un compte ?",
9086
10191
  auth_sign_in: "Se connecter",
9087
10192
  auth_sign_up: "S'inscrire",
10193
+ auth_new_password_title: "Définir un nouveau mot de passe",
10194
+ auth_new_password_subtitle: "Choisissez le mot de passe avec lequel vous vous connecterez désormais.",
10195
+ auth_password_requirements: "Le mot de passe doit comporter au moins 8 caractères, avec des majuscules, des minuscules et un chiffre",
10196
+ auth_set_password: "Définir le mot de passe",
10197
+ auth_password_changed_title: "Mot de passe modifié",
10198
+ auth_password_changed_body: "Connectez-vous avec votre nouveau mot de passe.",
10199
+ auth_continue_to_sign_in: "Continuer vers la connexion",
10200
+ auth_link_invalid_or_expired: "Ce lien est invalide ou a expiré. Demandez-en un nouveau depuis l'écran de connexion.",
10201
+ auth_password_reset_unavailable: "La réinitialisation du mot de passe n'est pas disponible sur ce backend.",
10202
+ auth_verifying_email: "Confirmation de votre adresse e-mail…",
10203
+ auth_email_verified_title: "E-mail confirmé",
10204
+ auth_email_verified_body: "Votre adresse e-mail est confirmée. Vous pouvez maintenant vous connecter.",
10205
+ auth_email_verification_failed_title: "E-mail non confirmé",
10206
+ auth_email_verification_unavailable: "La vérification de l'e-mail n'est pas disponible sur ce backend.",
10207
+ auth_mfa_title: "Validation en deux étapes",
10208
+ auth_mfa_subtitle: "Saisissez le code de votre application d'authentification, ou l'un de vos codes de récupération.",
10209
+ auth_mfa_factor_label: "Valider avec",
10210
+ auth_mfa_factor_totp: "Application d'authentification",
10211
+ auth_mfa_code_label: "Code de vérification",
10212
+ auth_mfa_verify: "Valider",
10213
+ auth_mfa_code_invalid: "Ce code n'est pas le bon. Essayez le code actuel de votre application d'authentification, ou un code de récupération.",
10214
+ auth_mfa_challenge_exhausted: "Trop de codes erronés. Saisissez un nouveau code de votre application d'authentification pour réessayer.",
10215
+ auth_mfa_expired: "Cette connexion a expiré. Reconnectez-vous pour continuer.",
10216
+ auth_mfa_sign_in_again: "Se reconnecter",
9088
10217
  auto_setup_collections_button: "Configurer les collections automatiquement",
9089
10218
  auto_setup_collections_title: "Configurer les collections automatiquement ?",
9090
10219
  auto_setup_collections_desc: "Cela créera automatiquement des configurations de collection pour les collections qui ne sont <b>PAS</b> déjà mappées",
@@ -9247,10 +10376,12 @@ var fr = {
9247
10376
  studio_sql_cannot_edit_missing_query: "Impossible de modifier les lignes : la requête sous-jacente est absente.",
9248
10377
  studio_sql_cannot_resolve_table: "Impossible de déterminer la table ou la clé primaire",
9249
10378
  studio_sql_missing_pk: "La ligne n'a pas les colonnes de clé primaire : {{columns}}. Mise à jour impossible en toute sécurité.",
10379
+ studio_sql_cannot_edit_other_connection: "Ces lignes ont été lues dans une autre base de données ou avec un autre rôle. Exécutez à nouveau la requête pour les modifier.",
9250
10380
  studio_sql_update_failed: "Échec de la mise à jour : {{message}}",
9251
10381
  studio_sql_execution_not_supported: "La source de données actuelle ne prend pas en charge l'exécution de SQL.",
9252
10382
  studio_sql_error_executing: "Une erreur s'est produite lors de l'exécution de la requête.",
9253
10383
  studio_sql_error_explaining: "Une erreur s'est produite lors de l'analyse de la requête.",
10384
+ studio_sql_explain_single_statement: "Explain ne traite qu'une instruction à la fois. Sélectionnez l'instruction à expliquer et réessayez.",
9254
10385
  studio_sql_save_first_to_favorite: "Enregistrez l'extrait avant de l'ajouter aux favoris.",
9255
10386
  studio_sql_collections_label: "Collections:",
9256
10387
  studio_sql_admin_collections_tooltip: "Tables in this query that are mapped as admin collections",
@@ -9297,6 +10428,9 @@ var fr = {
9297
10428
  studio_rls_enable_force_rls: "Activer la RLS forcée",
9298
10429
  studio_rls_disable_force_rls: "Désactiver la RLS forcée",
9299
10430
  studio_rls_edit: "Edit",
10431
+ studio_rls_edit_generated: "{{policy}} est générée par Rebase pour chaque collection : aucune règle à vous n'est à modifier. Ajoutez plutôt votre propre politique.",
10432
+ studio_rls_edit_not_declared: "{{policy}} n'existe que dans la base de données : aucune règle de votre code n'est à modifier. Importez-la d'abord dans le code.",
10433
+ studio_rls_edit_several_operations: "{{policy}} fait partie des politiques qu'une seule règle produit, une par opération. Modifiez cette règle dans le fichier source de la collection.",
9300
10434
  studio_rls_delete: "Delete",
9301
10435
  studio_rls_confirm_delete_title: "Supprimer la politique",
9302
10436
  studio_rls_confirm_delete_body: "Voulez-vous vraiment supprimer cette politique ? Cette action est irréversible.",
@@ -9477,6 +10611,8 @@ var fr = {
9477
10611
  studio_cron_denied_title: "Vous ne pouvez pas lister les tâches cron de ce projet",
9478
10612
  studio_cron_denied_hint: "Le projet n'a rien d'anormal. L'API cron est réservée aux administrateurs, et le compte connecté a été refusé.",
9479
10613
  studio_cron_read_failed: "Impossible de lire les tâches cron de ce projet",
10614
+ studio_cron_logs_read_failed: "Impossible de lire les exécutions de cette tâche",
10615
+ studio_cron_logs_denied_title: "Vous ne pouvez pas lire les exécutions de cette tâche",
9480
10616
  studio_api_keys_denied_title: "Vous ne pouvez pas lister les clés d'API de ce projet",
9481
10617
  studio_api_keys_denied_hint: "Le projet n'a rien d'anormal. Gérer les clés d'API est une opération d'administrateur, et le compte connecté a été refusé.",
9482
10618
  studio_api_keys_read_failed: "Impossible de lire les clés d'API de ce projet",
@@ -9495,6 +10631,7 @@ var fr = {
9495
10631
  studio_api_keys_status_active: "Active",
9496
10632
  studio_api_keys_status_expired: "Expirée",
9497
10633
  studio_api_keys_status_revoked: "Révoquée",
10634
+ studio_api_keys_inactive_heading: "Révoquées / Expirées",
9498
10635
  studio_api_keys_stat_status: "Statut",
9499
10636
  studio_api_keys_stat_last_used: "Dernière utilisation",
9500
10637
  studio_api_keys_stat_expires: "Expire",
@@ -10094,6 +11231,30 @@ var it = {
10094
11231
  auth_have_account: "Hai già un account?",
10095
11232
  auth_sign_in: "Accedi",
10096
11233
  auth_sign_up: "Registrati",
11234
+ auth_new_password_title: "Imposta una nuova password",
11235
+ auth_new_password_subtitle: "Scegli la password con cui accederai d'ora in poi.",
11236
+ auth_password_requirements: "La password deve avere almeno 8 caratteri, con maiuscole, minuscole e un numero",
11237
+ auth_set_password: "Imposta password",
11238
+ auth_password_changed_title: "Password modificata",
11239
+ auth_password_changed_body: "Accedi con la tua nuova password.",
11240
+ auth_continue_to_sign_in: "Continua all'accesso",
11241
+ auth_link_invalid_or_expired: "Questo link non è valido o è scaduto. Richiedine uno nuovo dalla schermata di accesso.",
11242
+ auth_password_reset_unavailable: "Il ripristino della password non è disponibile su questo backend.",
11243
+ auth_verifying_email: "Conferma del tuo indirizzo email in corso…",
11244
+ auth_email_verified_title: "Email confermata",
11245
+ auth_email_verified_body: "Il tuo indirizzo email è confermato. Ora puoi accedere.",
11246
+ auth_email_verification_failed_title: "Email non confermata",
11247
+ auth_email_verification_unavailable: "La verifica dell'email non è disponibile su questo backend.",
11248
+ auth_mfa_title: "Verifica in due passaggi",
11249
+ auth_mfa_subtitle: "Inserisci il codice della tua app di autenticazione o uno dei tuoi codici di recupero.",
11250
+ auth_mfa_factor_label: "Verifica con",
11251
+ auth_mfa_factor_totp: "App di autenticazione",
11252
+ auth_mfa_code_label: "Codice di verifica",
11253
+ auth_mfa_verify: "Verifica",
11254
+ auth_mfa_code_invalid: "Questo codice non è corretto. Prova il codice attuale della tua app di autenticazione o un codice di recupero.",
11255
+ auth_mfa_challenge_exhausted: "Troppi codici errati. Inserisci un nuovo codice dalla tua app di autenticazione per riprovare.",
11256
+ auth_mfa_expired: "Questo accesso è scaduto. Accedi di nuovo per continuare.",
11257
+ auth_mfa_sign_in_again: "Accedi di nuovo",
10097
11258
  auto_setup_collections_button: "Configura le collezioni automaticamente",
10098
11259
  auto_setup_collections_title: "Configurazione automatica delle collezioni",
10099
11260
  auto_setup_collections_desc: "Configura le collezioni automaticamente in base ai dati Firestore esistenti. Lascia che Rebase configuri il pannello di amministrazione perfetto per te.",
@@ -10256,10 +11417,12 @@ var it = {
10256
11417
  studio_sql_cannot_edit_missing_query: "Impossibile modificare le righe: manca la query sottostante.",
10257
11418
  studio_sql_cannot_resolve_table: "Impossibile determinare la tabella o la chiave primaria",
10258
11419
  studio_sql_missing_pk: "Alla riga mancano le colonne della chiave primaria: {{columns}}. Impossibile aggiornarla in sicurezza.",
11420
+ studio_sql_cannot_edit_other_connection: "Queste righe sono state lette da un altro database o con un altro ruolo. Esegui di nuovo la query per modificarle.",
10259
11421
  studio_sql_update_failed: "Aggiornamento non riuscito: {{message}}",
10260
11422
  studio_sql_execution_not_supported: "L'origine dati attuale non supporta l'esecuzione di SQL.",
10261
11423
  studio_sql_error_executing: "Si è verificato un errore durante l'esecuzione della query.",
10262
11424
  studio_sql_error_explaining: "Si è verificato un errore durante l'analisi della query.",
11425
+ studio_sql_explain_single_statement: "Explain funziona su una sola istruzione alla volta. Seleziona l'istruzione da spiegare e riprova.",
10263
11426
  studio_sql_save_first_to_favorite: "Salva il frammento prima di aggiungerlo ai preferiti.",
10264
11427
  studio_sql_collections_label: "Collections:",
10265
11428
  studio_sql_admin_collections_tooltip: "Tables in this query that are mapped as admin collections",
@@ -10306,6 +11469,9 @@ var it = {
10306
11469
  studio_rls_enable_force_rls: "Attiva RLS forzata",
10307
11470
  studio_rls_disable_force_rls: "Disattiva RLS forzata",
10308
11471
  studio_rls_edit: "Edit",
11472
+ studio_rls_edit_generated: "{{policy}} è generata da Rebase per ogni collection, quindi non c'è una tua regola da modificare. Aggiungi invece una policy tua.",
11473
+ studio_rls_edit_not_declared: "{{policy}} esiste solo nel database, quindi nel tuo codice non c'è una regola da modificare. Importala prima nel codice.",
11474
+ studio_rls_edit_several_operations: "{{policy}} è una delle policy in cui viene compilata una singola regola, una per operazione. Modifica quella regola nel file sorgente della collection.",
10309
11475
  studio_rls_delete: "Delete",
10310
11476
  studio_rls_confirm_delete_title: "Elimina criterio",
10311
11477
  studio_rls_confirm_delete_body: "Vuoi davvero eliminare questo criterio? L'operazione non può essere annullata.",
@@ -10486,6 +11652,8 @@ var it = {
10486
11652
  studio_cron_denied_title: "Non puoi elencare i processi cron di questo progetto",
10487
11653
  studio_cron_denied_hint: "Il progetto non ha nulla che non va. L'API cron è riservata agli amministratori, e l'account connesso è stato rifiutato.",
10488
11654
  studio_cron_read_failed: "Non è stato possibile leggere i processi cron di questo progetto",
11655
+ studio_cron_logs_read_failed: "Non è stato possibile leggere le esecuzioni di questo processo",
11656
+ studio_cron_logs_denied_title: "Non puoi leggere le esecuzioni di questo processo",
10489
11657
  studio_api_keys_denied_title: "Non puoi elencare le chiavi API di questo progetto",
10490
11658
  studio_api_keys_denied_hint: "Il progetto non ha nulla che non va. Gestire le chiavi API è un'operazione da amministratore, e l'account connesso è stato rifiutato.",
10491
11659
  studio_api_keys_read_failed: "Non è stato possibile leggere le chiavi API di questo progetto",
@@ -10504,6 +11672,7 @@ var it = {
10504
11672
  studio_api_keys_status_active: "Attiva",
10505
11673
  studio_api_keys_status_expired: "Scaduta",
10506
11674
  studio_api_keys_status_revoked: "Revocata",
11675
+ studio_api_keys_inactive_heading: "Revocate / Scadute",
10507
11676
  studio_api_keys_stat_status: "Stato",
10508
11677
  studio_api_keys_stat_last_used: "Ultimo utilizzo",
10509
11678
  studio_api_keys_stat_expires: "Scade",
@@ -11103,6 +12272,30 @@ var hi = {
11103
12272
  auth_have_account: "क्या आपके पास एक खाता है?",
11104
12273
  auth_sign_in: "साइन इन करें",
11105
12274
  auth_sign_up: "साइन अप करें",
12275
+ auth_new_password_title: "नया पासवर्ड सेट करें",
12276
+ auth_new_password_subtitle: "वह पासवर्ड चुनें जिससे आप अब से साइन इन करेंगे।",
12277
+ auth_password_requirements: "पासवर्ड कम से कम 8 अक्षरों का होना चाहिए, जिसमें बड़े और छोटे अक्षर और एक संख्या हो",
12278
+ auth_set_password: "पासवर्ड सेट करें",
12279
+ auth_password_changed_title: "पासवर्ड बदल गया",
12280
+ auth_password_changed_body: "अपने नए पासवर्ड से साइन इन करें।",
12281
+ auth_continue_to_sign_in: "साइन इन पर जारी रखें",
12282
+ auth_link_invalid_or_expired: "यह लिंक अमान्य है या इसकी समय-सीमा समाप्त हो गई है। साइन-इन स्क्रीन से नया लिंक मंगवाएँ।",
12283
+ auth_password_reset_unavailable: "इस बैकएंड पर पासवर्ड रीसेट उपलब्ध नहीं है।",
12284
+ auth_verifying_email: "आपके ईमेल पते की पुष्टि हो रही है…",
12285
+ auth_email_verified_title: "ईमेल की पुष्टि हो गई",
12286
+ auth_email_verified_body: "आपके ईमेल पते की पुष्टि हो गई है। अब आप साइन इन कर सकते हैं।",
12287
+ auth_email_verification_failed_title: "ईमेल की पुष्टि नहीं हुई",
12288
+ auth_email_verification_unavailable: "इस बैकएंड पर ईमेल सत्यापन उपलब्ध नहीं है।",
12289
+ auth_mfa_title: "दो-चरणीय सत्यापन",
12290
+ auth_mfa_subtitle: "अपने ऑथेंटिकेटर ऐप का कोड या अपना कोई रिकवरी कोड दर्ज करें।",
12291
+ auth_mfa_factor_label: "इससे सत्यापित करें",
12292
+ auth_mfa_factor_totp: "ऑथेंटिकेटर ऐप",
12293
+ auth_mfa_code_label: "सत्यापन कोड",
12294
+ auth_mfa_verify: "सत्यापित करें",
12295
+ auth_mfa_code_invalid: "यह कोड सही नहीं है। अपने ऑथेंटिकेटर ऐप का मौजूदा कोड या कोई रिकवरी कोड आज़माएँ।",
12296
+ auth_mfa_challenge_exhausted: "बहुत अधिक गलत कोड। फिर से प्रयास करने के लिए अपने ऑथेंटिकेटर ऐप से नया कोड दर्ज करें।",
12297
+ auth_mfa_expired: "यह साइन-इन समाप्त हो गया है। जारी रखने के लिए फिर से साइन इन करें।",
12298
+ auth_mfa_sign_in_again: "फिर से साइन इन करें",
11106
12299
  auto_setup_collections_button: "संग्रहों को स्वचालित रूप से सेट करें",
11107
12300
  auto_setup_collections_title: "संग्रहों की स्वचालित सेटअप",
11108
12301
  auto_setup_collections_desc: "अपने मौजूदा Firestore डेटा के आधार पर संग्रहों को स्वचालित रूप से सेट करें। Rebase को आपके लिए परफेक्ट एडमिन पैनल कॉन्फ़िगर करने दें।",
@@ -11265,10 +12458,12 @@ var hi = {
11265
12458
  studio_sql_cannot_edit_missing_query: "पंक्तियाँ संपादित नहीं की जा सकतीं: अंतर्निहित क्वेरी अनुपलब्ध है।",
11266
12459
  studio_sql_cannot_resolve_table: "तालिका/प्राथमिक कुंजी निर्धारित नहीं हो सकी",
11267
12460
  studio_sql_missing_pk: "पंक्ति में प्राथमिक कुंजी स्तंभ अनुपलब्ध हैं: {{columns}}। सुरक्षित रूप से अपडेट नहीं किया जा सकता।",
12461
+ studio_sql_cannot_edit_other_connection: "ये पंक्तियाँ किसी अन्य डेटाबेस से या किसी अन्य भूमिका के रूप में पढ़ी गई थीं। इन्हें संपादित करने के लिए क्वेरी फिर से चलाएँ।",
11268
12462
  studio_sql_update_failed: "अपडेट विफल: {{message}}",
11269
12463
  studio_sql_execution_not_supported: "वर्तमान डेटा स्रोत SQL निष्पादन का समर्थन नहीं करता।",
11270
12464
  studio_sql_error_executing: "क्वेरी चलाते समय एक त्रुटि हुई।",
11271
12465
  studio_sql_error_explaining: "क्वेरी का विश्लेषण करते समय एक त्रुटि हुई।",
12466
+ studio_sql_explain_single_statement: "Explain एक बार में केवल एक स्टेटमेंट पर काम करता है। जिस स्टेटमेंट को समझाना है उसे चुनें और फिर से प्रयास करें।",
11272
12467
  studio_sql_save_first_to_favorite: "पसंदीदा में जोड़ने से पहले स्निपेट सहेजें।",
11273
12468
  studio_sql_collections_label: "Collections:",
11274
12469
  studio_sql_admin_collections_tooltip: "Tables in this query that are mapped as admin collections",
@@ -11315,6 +12510,9 @@ var hi = {
11315
12510
  studio_rls_enable_force_rls: "बाध्य RLS सक्षम करें",
11316
12511
  studio_rls_disable_force_rls: "बाध्य RLS निष्क्रिय करें",
11317
12512
  studio_rls_edit: "Edit",
12513
+ studio_rls_edit_generated: "{{policy}} को Rebase हर कलेक्शन के लिए बनाता है, इसलिए संपादित करने के लिए आपका कोई नियम नहीं है। इसके बजाय अपनी पॉलिसी जोड़ें।",
12514
+ studio_rls_edit_not_declared: "{{policy}} केवल डेटाबेस में मौजूद है, इसलिए आपके कोड में संपादित करने के लिए कोई नियम नहीं है। पहले इसे कोडबेस में इम्पोर्ट करें।",
12515
+ studio_rls_edit_several_operations: "{{policy}} उन पॉलिसियों में से एक है जिनमें एक ही नियम कंपाइल होता है, हर ऑपरेशन के लिए एक। उस नियम को कलेक्शन की सोर्स फ़ाइल में संपादित करें।",
11318
12516
  studio_rls_delete: "Delete",
11319
12517
  studio_rls_confirm_delete_title: "नीति हटाएँ",
11320
12518
  studio_rls_confirm_delete_body: "क्या आप वाकई यह नीति हटाना चाहते हैं? यह क्रिया पूर्ववत नहीं की जा सकती।",
@@ -11495,6 +12693,8 @@ var hi = {
11495
12693
  studio_cron_denied_title: "आप इस प्रोजेक्ट के क्रॉन जॉब सूचीबद्ध नहीं कर सकते",
11496
12694
  studio_cron_denied_hint: "प्रोजेक्ट में कोई गड़बड़ी नहीं है। क्रॉन API केवल एडमिन के लिए है, और साइन-इन किए गए खाते को मना कर दिया गया।",
11497
12695
  studio_cron_read_failed: "इस प्रोजेक्ट के क्रॉन जॉब नहीं पढ़े जा सके",
12696
+ studio_cron_logs_read_failed: "इस जॉब के रन नहीं पढ़े जा सके",
12697
+ studio_cron_logs_denied_title: "आप इस जॉब के रन नहीं पढ़ सकते",
11498
12698
  studio_api_keys_denied_title: "आप इस प्रोजेक्ट की API कुंजियाँ सूचीबद्ध नहीं कर सकते",
11499
12699
  studio_api_keys_denied_hint: "प्रोजेक्ट में कोई गड़बड़ी नहीं है। API कुंजियाँ प्रबंधित करना एडमिन कार्य है, और साइन-इन किए गए खाते को मना कर दिया गया।",
11500
12700
  studio_api_keys_read_failed: "इस प्रोजेक्ट की API कुंजियाँ नहीं पढ़ी जा सकीं",
@@ -11513,6 +12713,7 @@ var hi = {
11513
12713
  studio_api_keys_status_active: "सक्रिय",
11514
12714
  studio_api_keys_status_expired: "समाप्त",
11515
12715
  studio_api_keys_status_revoked: "निरस्त",
12716
+ studio_api_keys_inactive_heading: "निरस्त / समाप्त",
11516
12717
  studio_api_keys_stat_status: "स्थिति",
11517
12718
  studio_api_keys_stat_last_used: "अंतिम उपयोग",
11518
12719
  studio_api_keys_stat_expires: "समाप्ति",
@@ -12117,6 +13318,30 @@ var pt = {
12117
13318
  auth_have_account: "Já tem uma conta?",
12118
13319
  auth_sign_in: "Iniciar Sessão",
12119
13320
  auth_sign_up: "Registar",
13321
+ auth_new_password_title: "Definir uma nova palavra-passe",
13322
+ auth_new_password_subtitle: "Escolha a palavra-passe com que vai iniciar sessão a partir de agora.",
13323
+ auth_password_requirements: "A palavra-passe deve ter pelo menos 8 caracteres, com maiúsculas, minúsculas e um número",
13324
+ auth_set_password: "Definir palavra-passe",
13325
+ auth_password_changed_title: "Palavra-passe alterada",
13326
+ auth_password_changed_body: "Inicie sessão com a sua nova palavra-passe.",
13327
+ auth_continue_to_sign_in: "Continuar para iniciar sessão",
13328
+ auth_link_invalid_or_expired: "Esta ligação é inválida ou expirou. Peça uma nova no ecrã de início de sessão.",
13329
+ auth_password_reset_unavailable: "A reposição da palavra-passe não está disponível neste backend.",
13330
+ auth_verifying_email: "A confirmar o seu endereço de email…",
13331
+ auth_email_verified_title: "Email confirmado",
13332
+ auth_email_verified_body: "O seu endereço de email está confirmado. Já pode iniciar sessão.",
13333
+ auth_email_verification_failed_title: "Email não confirmado",
13334
+ auth_email_verification_unavailable: "A verificação de email não está disponível neste backend.",
13335
+ auth_mfa_title: "Verificação em dois passos",
13336
+ auth_mfa_subtitle: "Introduza o código da sua aplicação de autenticação ou um dos seus códigos de recuperação.",
13337
+ auth_mfa_factor_label: "Verificar com",
13338
+ auth_mfa_factor_totp: "Aplicação de autenticação",
13339
+ auth_mfa_code_label: "Código de verificação",
13340
+ auth_mfa_verify: "Verificar",
13341
+ auth_mfa_code_invalid: "Esse código não está correto. Experimente o código atual da sua aplicação de autenticação ou um código de recuperação.",
13342
+ auth_mfa_challenge_exhausted: "Demasiados códigos errados. Introduza um novo código da sua aplicação de autenticação para tentar novamente.",
13343
+ auth_mfa_expired: "Este início de sessão expirou. Inicie sessão novamente para continuar.",
13344
+ auth_mfa_sign_in_again: "Iniciar sessão novamente",
12120
13345
  auto_setup_collections_button: "Configurar coleções automaticamente",
12121
13346
  auto_setup_collections_title: "Configurar coleções automaticamente?",
12122
13347
  auto_setup_collections_desc: "Isto criará automaticamente configurações de coleção para coleções que <b>NÃO</b> estão já mapeadas",
@@ -12279,10 +13504,12 @@ var pt = {
12279
13504
  studio_sql_cannot_edit_missing_query: "Não é possível editar as linhas: falta a consulta subjacente.",
12280
13505
  studio_sql_cannot_resolve_table: "Não foi possível determinar a tabela ou a chave primária",
12281
13506
  studio_sql_missing_pk: "Faltam colunas de chave primária na linha: {{columns}}. Não é possível atualizar em segurança.",
13507
+ studio_sql_cannot_edit_other_connection: "Estas linhas foram lidas de outra base de dados ou com outra função. Execute a consulta novamente para editá-las.",
12282
13508
  studio_sql_update_failed: "Falha ao atualizar: {{message}}",
12283
13509
  studio_sql_execution_not_supported: "A fonte de dados atual não suporta a execução de SQL.",
12284
13510
  studio_sql_error_executing: "Ocorreu um erro ao executar a consulta.",
12285
13511
  studio_sql_error_explaining: "Ocorreu um erro ao explicar a consulta.",
13512
+ studio_sql_explain_single_statement: "O Explain funciona com uma instrução de cada vez. Selecione a instrução a explicar e tente novamente.",
12286
13513
  studio_sql_save_first_to_favorite: "Guarde o excerto antes de o marcar como favorito.",
12287
13514
  studio_sql_collections_label: "Collections:",
12288
13515
  studio_sql_admin_collections_tooltip: "Tables in this query that are mapped as admin collections",
@@ -12329,6 +13556,9 @@ var pt = {
12329
13556
  studio_rls_enable_force_rls: "Ativar RLS forçada",
12330
13557
  studio_rls_disable_force_rls: "Desativar RLS forçada",
12331
13558
  studio_rls_edit: "Edit",
13559
+ studio_rls_edit_generated: "{{policy}} é gerada pelo Rebase para todas as coleções, por isso não há nenhuma regra sua para editar. Adicione antes uma política sua.",
13560
+ studio_rls_edit_not_declared: "{{policy}} existe apenas na base de dados, por isso não há nenhuma regra no seu código para editar. Importe-a primeiro para o código.",
13561
+ studio_rls_edit_several_operations: "{{policy}} é uma das políticas em que uma única regra é compilada, uma por operação. Edite essa regra no ficheiro de origem da coleção.",
12332
13562
  studio_rls_delete: "Delete",
12333
13563
  studio_rls_confirm_delete_title: "Eliminar política",
12334
13564
  studio_rls_confirm_delete_body: "Tem a certeza de que pretende eliminar esta política? Esta ação não pode ser anulada.",
@@ -12509,6 +13739,8 @@ var pt = {
12509
13739
  studio_cron_denied_title: "Não pode listar as tarefas cron deste projeto",
12510
13740
  studio_cron_denied_hint: "O projeto não tem nada de errado. A API de cron é apenas para administradores, e a conta com sessão iniciada foi recusada.",
12511
13741
  studio_cron_read_failed: "Não foi possível ler as tarefas cron deste projeto",
13742
+ studio_cron_logs_read_failed: "Não foi possível ler as execuções desta tarefa",
13743
+ studio_cron_logs_denied_title: "Não pode ler as execuções desta tarefa",
12512
13744
  studio_api_keys_denied_title: "Não pode listar as chaves de API deste projeto",
12513
13745
  studio_api_keys_denied_hint: "O projeto não tem nada de errado. Gerir chaves de API é uma operação de administrador, e a conta com sessão iniciada foi recusada.",
12514
13746
  studio_api_keys_read_failed: "Não foi possível ler as chaves de API deste projeto",
@@ -12527,6 +13759,7 @@ var pt = {
12527
13759
  studio_api_keys_status_active: "Ativa",
12528
13760
  studio_api_keys_status_expired: "Expirada",
12529
13761
  studio_api_keys_status_revoked: "Revogada",
13762
+ studio_api_keys_inactive_heading: "Revogadas / Expiradas",
12530
13763
  studio_api_keys_stat_status: "Estado",
12531
13764
  studio_api_keys_stat_last_used: "Última utilização",
12532
13765
  studio_api_keys_stat_expires: "Expira",
@@ -12573,6 +13806,7 @@ var REBASE_LOCALE_STORAGE_KEY = "rebase_locale";
12573
13806
  function RebaseI18nProvider({ locale = "en", translations, children }) {
12574
13807
  const i18nRef = useRef(null);
12575
13808
  const [ready, setReady] = React.useState(false);
13809
+ const propLanguageRef = useRef(null);
12576
13810
  if (!i18nRef.current) {
12577
13811
  const instance = i18next.createInstance();
12578
13812
  const resources = buildResources(translations);
@@ -12590,13 +13824,20 @@ function RebaseI18nProvider({ locale = "en", translations, children }) {
12590
13824
  setReady(true);
12591
13825
  });
12592
13826
  instance.on("languageChanged", (lng) => {
13827
+ if (propLanguageRef.current === lng) {
13828
+ propLanguageRef.current = null;
13829
+ return;
13830
+ }
12593
13831
  if (typeof window !== "undefined") writeStoredString(REBASE_LOCALE_STORAGE_KEY, lng);
12594
13832
  });
12595
13833
  i18nRef.current = instance;
12596
13834
  }
12597
13835
  useEffect(() => {
12598
13836
  if (i18nRef.current && i18nRef.current.language !== locale) {
12599
- if (!Boolean(readStoredString("rebase_locale"))) i18nRef.current.changeLanguage(locale);
13837
+ if (!Boolean(readStoredString("rebase_locale"))) {
13838
+ propLanguageRef.current = locale;
13839
+ i18nRef.current.changeLanguage(locale);
13840
+ }
12600
13841
  }
12601
13842
  }, [locale]);
12602
13843
  useEffect(() => {
@@ -12844,7 +14085,7 @@ function Rebase(props) {
12844
14085
  propertyConfigs,
12845
14086
  componentsProp
12846
14087
  ]);
12847
- const analyticsController = useMemo(() => ({ onAnalyticsEvent }), []);
14088
+ const analyticsController = useMemo(() => ({ onAnalyticsEvent }), [onAnalyticsEvent]);
12848
14089
  const fallbackEffectiveRoleController = useBuildEffectiveRoleController();
12849
14090
  const activeEffectiveRoleController = effectiveRoleController ?? fallbackEffectiveRoleController;
12850
14091
  const modeController = useBuildModeController();
@@ -12932,9 +14173,10 @@ function RebaseInternal({ loading, children }) {
12932
14173
  plugins,
12933
14174
  scope: "root",
12934
14175
  scopeProps: { context },
12935
- children: [authReady && /* @__PURE__ */ jsx(PluginLifecycleManager, {
14176
+ children: [/* @__PURE__ */ jsx(PluginLifecycleManager, {
12936
14177
  plugins,
12937
- context
14178
+ context,
14179
+ authReady
12938
14180
  }), childrenResult]
12939
14181
  });
12940
14182
  return childrenResult;
@@ -13007,8 +14249,10 @@ function useRebaseAuthController(props = {}) {
13007
14249
  return;
13008
14250
  }
13009
14251
  isMountedRef.current = true;
14252
+ let latestUpdate = 0;
13010
14253
  const updateState = async (session) => {
13011
14254
  if (!isMountedRef.current) return;
14255
+ const update = ++latestUpdate;
13012
14256
  let userToSet = session?.user ?? null;
13013
14257
  const defineRolesFor = defineRolesForRef.current;
13014
14258
  if (userToSet && defineRolesFor) try {
@@ -13020,12 +14264,27 @@ function useRebaseAuthController(props = {}) {
13020
14264
  } catch (e) {
13021
14265
  console.error("Error defining roles:", e);
13022
14266
  }
13023
- if (isMountedRef.current) setUser(userToSet);
14267
+ if (update !== latestUpdate) return;
14268
+ if (isMountedRef.current) {
14269
+ if (userToSet) bindSessionCachesToUser(userToSet.uid);
14270
+ setUser(userToSet);
14271
+ }
13024
14272
  };
14273
+ let latestConfigRequest = 0;
14274
+ function loadAuthConfig() {
14275
+ if (!auth) return;
14276
+ const request = ++latestConfigRequest;
14277
+ auth.getAuthConfig().then((config) => {
14278
+ if (isMountedRef.current && request === latestConfigRequest) setAuthConfig(config);
14279
+ }).catch((e) => {
14280
+ console.warn("[Rebase] Could not load the backend auth configuration; the login view will fall back to defaults.", e);
14281
+ });
14282
+ }
13025
14283
  const syncState = async (event, session) => {
14284
+ if (event === "SIGNED_IN" || event === "SIGNED_OUT") loadAuthConfig();
14285
+ if (event === "SIGNED_OUT") clearSessionCaches();
13026
14286
  await updateState(session);
13027
14287
  if (event === "SIGNED_OUT") {
13028
- clearFetchCache();
13029
14288
  if (isMountedRef.current) setLoginSkipped(false);
13030
14289
  onSignOutRef.current?.();
13031
14290
  }
@@ -13039,11 +14298,7 @@ function useRebaseAuthController(props = {}) {
13039
14298
  }).catch(() => {
13040
14299
  if (isMountedRef.current) setInitialLoading(false);
13041
14300
  });
13042
- auth.getAuthConfig().then((config) => {
13043
- if (isMountedRef.current) setAuthConfig(config);
13044
- }).catch((e) => {
13045
- console.warn("[Rebase] Could not load the backend auth configuration; the login view will fall back to defaults.", e);
13046
- });
14301
+ loadAuthConfig();
13047
14302
  const unsubscribe = auth.onAuthStateChange(syncState);
13048
14303
  return () => {
13049
14304
  isMountedRef.current = false;
@@ -13124,6 +14379,37 @@ function useRebaseAuthController(props = {}) {
13124
14379
  setAuthLoading(false);
13125
14380
  }
13126
14381
  }, [auth]);
14382
+ /**
14383
+ * Open the challenge that finishes a sign-in refused with `MFA_REQUIRED`.
14384
+ *
14385
+ * `mfaToken` is that refusal's `details.mfaToken`, sent on this request
14386
+ * only. Every sign-in method above lands such a refusal in
14387
+ * `authProviderError`, which is where the login view reads it from.
14388
+ */
14389
+ const startMfaChallenge = useCallback(async (mfaToken, factorId) => {
14390
+ if (!auth?.mfa) throw new Error("Rebase client with MFA is required");
14391
+ const { challengeId } = await auth.mfa.challenge(factorId, { mfaToken });
14392
+ return challengeId;
14393
+ }, [auth]);
14394
+ /**
14395
+ * Answer that challenge. On success the SDK adopts the session and emits
14396
+ * SIGNED_IN, which sets the user like any other sign-in.
14397
+ *
14398
+ * A refused code is rethrown and NOT recorded in `authProviderError`: the
14399
+ * code step reports it itself, and the refusal it would replace is the
14400
+ * `MFA_REQUIRED` that says a sign-in is still waiting for its second
14401
+ * factor.
14402
+ */
14403
+ const verifyMfaChallenge = useCallback(async (mfaToken, challengeId, code) => {
14404
+ if (!auth?.mfa) throw new Error("Rebase client with MFA is required");
14405
+ setAuthLoading(true);
14406
+ try {
14407
+ await auth.mfa.verifyChallenge(challengeId, code, { mfaToken });
14408
+ setAuthProviderError(null);
14409
+ } finally {
14410
+ setAuthLoading(false);
14411
+ }
14412
+ }, [auth]);
13127
14413
  const skipLogin = useCallback(() => {
13128
14414
  setLoginSkipped(true);
13129
14415
  setUser(null);
@@ -13156,6 +14442,10 @@ function useRebaseAuthController(props = {}) {
13156
14442
  setAuthLoading(false);
13157
14443
  }
13158
14444
  }, [auth]);
14445
+ const verifyEmail = useCallback(async (token) => {
14446
+ if (!auth?.verifyEmail) throw new Error("Rebase client with email verification is required");
14447
+ await auth.verifyEmail(token);
14448
+ }, [auth]);
13159
14449
  const changePassword = useCallback(async (oldPassword, newPassword) => {
13160
14450
  if (!auth) throw new Error("Rebase client with auth is required");
13161
14451
  setAuthLoading(true);
@@ -13219,6 +14509,7 @@ function useRebaseAuthController(props = {}) {
13219
14509
  skipLogin,
13220
14510
  forgotPassword,
13221
14511
  resetPassword,
14512
+ verifyEmail,
13222
14513
  changePassword,
13223
14514
  updateProfile,
13224
14515
  fetchSessions,
@@ -13226,6 +14517,8 @@ function useRebaseAuthController(props = {}) {
13226
14517
  revokeAllSessions,
13227
14518
  clearError,
13228
14519
  setAuthProviderError,
14520
+ startMfaChallenge: auth?.mfa ? startMfaChallenge : void 0,
14521
+ verifyMfaChallenge: auth?.mfa ? verifyMfaChallenge : void 0,
13229
14522
  extra,
13230
14523
  setExtra,
13231
14524
  capabilities: {
@@ -13358,264 +14651,111 @@ var iconKeysMap = iconKeys.reduce((acc, key) => {
13358
14651
  * Render an icon element from a string key or existing React element.
13359
14652
  *
13360
14653
  * Whether a key names an icon is decided here, against `iconKeys` — an array
13361
- * of strings. The component behind the key is fetched by `LucideIconByName` on
13362
- * first use: this used to index lucide's full `icons` map, which is the whole
13363
- * library and cannot be tree-shaken.
13364
- */
13365
- function getIcon(iconKey, className, color, size) {
13366
- if (React.isValidElement(iconKey)) return iconKey;
13367
- if (!iconKey) return void 0;
13368
- if (typeof iconKey === "string") {
13369
- const lowerKey = iconKey.toLowerCase();
13370
- const slugifiedKey = slugify(iconKey).replace(/-/g, "_");
13371
- const mappedKey = iconKeysMap[iconKey] || iconKeysMap[lowerKey] || iconKeysMap[slugifiedKey];
13372
- if (!mappedKey) return;
13373
- return /* @__PURE__ */ jsx(LucideIconByName, {
13374
- name: mappedKey,
13375
- size: typeof size === "number" ? size : iconSize[size ?? "medium"],
13376
- className: cls(color ? colorClassesMapping[color] : "", "select-none shrink-0", className)
13377
- });
13378
- }
13379
- console.warn("Invalid icon key provided:", iconKey);
13380
- }
13381
- var IconForView = React.memo(function IconForView({ collectionOrView, className, color, size = "medium" }) {
13382
- if (!collectionOrView) return /* @__PURE__ */ jsx(Fragment, {});
13383
- const icon = getIcon(collectionOrView.icon, className, color, size);
13384
- if (collectionOrView?.icon && icon) return icon;
13385
- let pathname = slugify(("singularName" in collectionOrView ? collectionOrView.singularName : void 0) ?? collectionOrView.name).replace(/-/g, "_");
13386
- let key;
13387
- if (pathname in iconKeysMap) key = iconKeysMap[pathname];
13388
- if (!key) {
13389
- pathname = slugify(collectionOrView.slug).replace(/-/g, "_");
13390
- if (pathname in iconKeysMap) key = iconKeysMap[pathname];
13391
- }
13392
- const iconsCount = coolIconKeys.length;
13393
- if (!key) key = coolIconKeys[hashString(collectionOrView.slug) % iconsCount];
13394
- const sizeInPx = typeof size === "number" ? size : iconSize[size];
13395
- return /* @__PURE__ */ jsx(LucideIconByName, {
13396
- name: key,
13397
- size: sizeInPx,
13398
- className: cls(color ? colorClassesMapping[color] : "", "select-none shrink-0", className)
13399
- });
13400
- }, (prevProps, nextProps) => {
13401
- return deepEqual(prevProps.collectionOrView?.icon, nextProps.collectionOrView?.icon) && prevProps.collectionOrView?.name === nextProps.collectionOrView?.name && prevProps.collectionOrView?.slug === nextProps.collectionOrView?.slug && prevProps.collectionOrView?.singularName === nextProps.collectionOrView?.singularName && deepEqual(prevProps.color, nextProps.color) && prevProps.className === nextProps.className && prevProps.size === nextProps.size;
13402
- });
13403
- //#endregion
13404
- //#region src/util/createFormexStub.tsx
13405
- function createFormexStub(values) {
13406
- const errorMessage = "You are in a read-only context. You cannot modify the formex controller.";
13407
- return {
13408
- debugId: "",
13409
- values,
13410
- initialValues: values,
13411
- touched: {},
13412
- dirty: false,
13413
- errors: {},
13414
- submitCount: 0,
13415
- isSubmitting: false,
13416
- isValidating: false,
13417
- version: 0,
13418
- canUndo: false,
13419
- canRedo: false,
13420
- setValues: () => {
13421
- throw new Error(errorMessage);
13422
- },
13423
- setTouched(touched) {
13424
- throw new Error(errorMessage);
13425
- },
13426
- setFieldValue: () => {
13427
- throw new Error(errorMessage);
13428
- },
13429
- setFieldTouched: () => {
13430
- throw new Error(errorMessage);
13431
- },
13432
- setDirty: () => {
13433
- throw new Error(errorMessage);
13434
- },
13435
- setSubmitCount: () => {
13436
- throw new Error(errorMessage);
13437
- },
13438
- setFieldError: () => {
13439
- throw new Error(errorMessage);
13440
- },
13441
- handleChange: () => {
13442
- throw new Error(errorMessage);
13443
- },
13444
- handleBlur: () => {
13445
- throw new Error(errorMessage);
13446
- },
13447
- handleSubmit: () => {
13448
- throw new Error(errorMessage);
13449
- },
13450
- validate: () => {
13451
- throw new Error(errorMessage);
13452
- },
13453
- resetForm: () => {
13454
- throw new Error(errorMessage);
13455
- },
13456
- setSubmitting: () => {
13457
- throw new Error(errorMessage);
13458
- },
13459
- undo: () => {
13460
- throw new Error(errorMessage);
13461
- },
13462
- redo: () => {
13463
- throw new Error(errorMessage);
13464
- }
13465
- };
13466
- }
13467
- //#endregion
13468
- //#region src/util/entity_cache.ts
13469
- var LOCAL_STORAGE_PREFIX = "entity_cache::";
13470
- /**
13471
- * The in-session handoff channel: an edit in flight, parked here by the layout
13472
- * that is being replaced and picked up by the one taking over — the split's
13473
- * "hide list", full screen's "show list", the side panel's "open full screen".
13474
- * A record loaded from it opens *dirty*, because that is what it is for.
13475
- *
13476
- * Deliberately **not** hydrated from `sessionStorage`, and it is the one thing
13477
- * here that isn't. The persisted half of this file is the local-changes backup:
13478
- * a draft that outlives a reload, offered back through the banner or applied on
13479
- * open, according to the collection's `localChangesBackup`. Loading those
13480
- * drafts into this map on startup made every one of them look like a handoff —
13481
- * the first visit to the record after a reload silently opened dirty carrying a
13482
- * draft the user had walked away from, and the banner that exists to ask about
13483
- * it never appeared.
13484
- */
13485
- var entityCache = /* @__PURE__ */ new Map();
13486
- var isSessionStorageAvailable = typeof sessionStorage !== "undefined";
13487
- function customReplacer(key) {
13488
- const value = this[key];
13489
- if (value instanceof Date) return {
13490
- __type: "Date",
13491
- value: value.toISOString()
13492
- };
13493
- if (value instanceof EntityReference) return {
13494
- __type: "EntityReference",
13495
- id: value.id,
13496
- path: value.path,
13497
- driver: value.driver,
13498
- databaseId: value.databaseId
13499
- };
13500
- if (value instanceof EntityRelation) return {
13501
- __type: "EntityRelation",
13502
- id: value.id,
13503
- path: value.path,
13504
- data: value.data
13505
- };
13506
- if (value instanceof GeoPoint) return {
13507
- __type: "GeoPoint",
13508
- latitude: value.latitude,
13509
- longitude: value.longitude
13510
- };
13511
- if (value instanceof Vector) return {
13512
- __type: "Vector",
13513
- value: value.value
13514
- };
13515
- return value;
13516
- }
13517
- function customReviver(_key, value) {
13518
- if (value && typeof value === "object" && "__type" in value) {
13519
- const record = value;
13520
- switch (record.__type) {
13521
- case "date":
13522
- case "Date": return new Date(record.value);
13523
- case "reference":
13524
- case "EntityReference": return new EntityReference({
13525
- id: record.id,
13526
- path: record.path,
13527
- driver: record.driver,
13528
- databaseId: record.databaseId
13529
- });
13530
- case "relation":
13531
- case "EntityRelation": return new EntityRelation(record.id, record.path, record.data);
13532
- case "GeoPoint": return new GeoPoint(record.latitude, record.longitude);
13533
- case "Vector": return new Vector(record.value);
13534
- default: return value;
13535
- }
13536
- }
13537
- return value;
13538
- }
13539
- /**
13540
- * Saves data to the local-changes backup in `sessionStorage`.
13541
- *
13542
- * The backup only — a draft that survives a reload is not an edit in flight.
13543
- * {@link saveEntityToMemoryCache} is the other channel; see {@link entityCache}
13544
- * for what separates them.
13545
- * @param path - The unique path/key for the data.
13546
- * @param data - The data to cache and persist.
13547
- */
13548
- function saveEntityToCache(path, data) {
13549
- if (isSessionStorageAvailable) try {
13550
- const key = LOCAL_STORAGE_PREFIX + path;
13551
- const entityString = JSON.stringify(data, customReplacer);
13552
- sessionStorage.setItem(key, entityString);
13553
- } catch (error) {
13554
- console.error(`Failed to save entity for path "${path}" to sessionStorage:`, error);
13555
- }
13556
- }
13557
- /**
13558
- * Consumes a handoff. The channel is one-shot: whoever picks an edit up owns it
13559
- * from then on, and an edit left behind here is one that reopens a record dirty
13560
- * long after the user stopped looking at it.
13561
- */
13562
- function removeEntityFromMemoryCache(path) {
13563
- entityCache.delete(path);
13564
- }
13565
- /**
13566
- * Parks an edit in flight for the layout about to take over. See
13567
- * {@link entityCache}: this is the handoff, not the backup.
13568
- */
13569
- function saveEntityToMemoryCache(path, data) {
13570
- entityCache.set(path, data);
13571
- }
13572
- function getEntityFromMemoryCache(path) {
13573
- return entityCache.get(path);
13574
- }
13575
- /**
13576
- * Retrieves a entity from the local-changes backup in `sessionStorage`.
13577
- *
13578
- * The backup only — the handoff channel is read through
13579
- * {@link getEntityFromMemoryCache}, and the two mean different things to the
13580
- * form that receives them.
13581
- * @param path - The unique path/key for the entity.
13582
- * @returns The cached entity or `undefined` if not found.
13583
- */
13584
- function getEntityFromCache(path) {
13585
- if (isSessionStorageAvailable) try {
13586
- const key = LOCAL_STORAGE_PREFIX + path;
13587
- const entityString = sessionStorage.getItem(key);
13588
- if (entityString) return JSON.parse(entityString, customReviver);
13589
- } catch (error) {
13590
- console.error(`Failed to load entity for path "${path}" from sessionStorage:`, error);
13591
- }
13592
- }
13593
- /**
13594
- * Removes a entity from both the in-memory cache and `sessionStorage`.
13595
- * @param path - The unique path/key for the entity to remove.
14654
+ * of strings. The component behind the key is fetched by `LucideIconByName` on
14655
+ * first use: this used to index lucide's full `icons` map, which is the whole
14656
+ * library and cannot be tree-shaken.
13596
14657
  */
13597
- function removeEntityFromCache(path) {
13598
- console.debug("Removing entity from cache", path);
13599
- entityCache.delete(path);
13600
- if (isSessionStorageAvailable) try {
13601
- const key = LOCAL_STORAGE_PREFIX + path;
13602
- sessionStorage.removeItem(key);
13603
- } catch (error) {
13604
- console.error(`Failed to remove entity for path "${path}" from sessionStorage:`, error);
14658
+ function getIcon(iconKey, className, color, size) {
14659
+ if (React.isValidElement(iconKey)) return iconKey;
14660
+ if (!iconKey) return void 0;
14661
+ if (typeof iconKey === "string") {
14662
+ const lowerKey = iconKey.toLowerCase();
14663
+ const slugifiedKey = slugify(iconKey).replace(/-/g, "_");
14664
+ const mappedKey = iconKeysMap[iconKey] || iconKeysMap[lowerKey] || iconKeysMap[slugifiedKey];
14665
+ if (!mappedKey) return;
14666
+ return /* @__PURE__ */ jsx(LucideIconByName, {
14667
+ name: mappedKey,
14668
+ size: typeof size === "number" ? size : iconSize[size ?? "medium"],
14669
+ className: cls(color ? colorClassesMapping[color] : "", "select-none shrink-0", className)
14670
+ });
13605
14671
  }
14672
+ console.warn("Invalid icon key provided:", iconKey);
13606
14673
  }
13607
- function flattenKeys(obj, prefix = "", result = []) {
13608
- if (isObject(obj) || Array.isArray(obj)) if (!isPlainObject(obj) && prefix) result.push(prefix);
13609
- else {
13610
- const record = obj;
13611
- for (const key in record) if (Object.prototype.hasOwnProperty.call(record, key)) {
13612
- const newKey = prefix ? Array.isArray(obj) ? `${prefix}[${key}]` : `${prefix}.${key}` : key;
13613
- const val = record[key];
13614
- if (isObject(val) || Array.isArray(val)) flattenKeys(val, newKey, result);
13615
- else result.push(newKey);
13616
- }
14674
+ var IconForView = React.memo(function IconForView({ collectionOrView, className, color, size = "medium" }) {
14675
+ if (!collectionOrView) return /* @__PURE__ */ jsx(Fragment, {});
14676
+ const icon = getIcon(collectionOrView.icon, className, color, size);
14677
+ if (collectionOrView?.icon && icon) return icon;
14678
+ let pathname = slugify(("singularName" in collectionOrView ? collectionOrView.singularName : void 0) ?? collectionOrView.name).replace(/-/g, "_");
14679
+ let key;
14680
+ if (pathname in iconKeysMap) key = iconKeysMap[pathname];
14681
+ if (!key) {
14682
+ pathname = slugify(collectionOrView.slug).replace(/-/g, "_");
14683
+ if (pathname in iconKeysMap) key = iconKeysMap[pathname];
13617
14684
  }
13618
- return result;
14685
+ const iconsCount = coolIconKeys.length;
14686
+ if (!key) key = coolIconKeys[hashString(collectionOrView.slug) % iconsCount];
14687
+ const sizeInPx = typeof size === "number" ? size : iconSize[size];
14688
+ return /* @__PURE__ */ jsx(LucideIconByName, {
14689
+ name: key,
14690
+ size: sizeInPx,
14691
+ className: cls(color ? colorClassesMapping[color] : "", "select-none shrink-0", className)
14692
+ });
14693
+ }, (prevProps, nextProps) => {
14694
+ return deepEqual(prevProps.collectionOrView?.icon, nextProps.collectionOrView?.icon) && prevProps.collectionOrView?.name === nextProps.collectionOrView?.name && prevProps.collectionOrView?.slug === nextProps.collectionOrView?.slug && prevProps.collectionOrView?.singularName === nextProps.collectionOrView?.singularName && deepEqual(prevProps.color, nextProps.color) && prevProps.className === nextProps.className && prevProps.size === nextProps.size;
14695
+ });
14696
+ //#endregion
14697
+ //#region src/util/createFormexStub.tsx
14698
+ function createFormexStub(values) {
14699
+ const errorMessage = "You are in a read-only context. You cannot modify the formex controller.";
14700
+ return {
14701
+ debugId: "",
14702
+ values,
14703
+ initialValues: values,
14704
+ touched: {},
14705
+ dirty: false,
14706
+ errors: {},
14707
+ submitCount: 0,
14708
+ isSubmitting: false,
14709
+ isValidating: false,
14710
+ version: 0,
14711
+ canUndo: false,
14712
+ canRedo: false,
14713
+ setValues: () => {
14714
+ throw new Error(errorMessage);
14715
+ },
14716
+ setTouched(touched) {
14717
+ throw new Error(errorMessage);
14718
+ },
14719
+ setFieldValue: () => {
14720
+ throw new Error(errorMessage);
14721
+ },
14722
+ setFieldTouched: () => {
14723
+ throw new Error(errorMessage);
14724
+ },
14725
+ setDirty: () => {
14726
+ throw new Error(errorMessage);
14727
+ },
14728
+ setSubmitCount: () => {
14729
+ throw new Error(errorMessage);
14730
+ },
14731
+ setFieldError: () => {
14732
+ throw new Error(errorMessage);
14733
+ },
14734
+ handleChange: () => {
14735
+ throw new Error(errorMessage);
14736
+ },
14737
+ handleBlur: () => {
14738
+ throw new Error(errorMessage);
14739
+ },
14740
+ handleSubmit: () => {
14741
+ throw new Error(errorMessage);
14742
+ },
14743
+ validate: () => {
14744
+ throw new Error(errorMessage);
14745
+ },
14746
+ resetForm: () => {
14747
+ throw new Error(errorMessage);
14748
+ },
14749
+ setSubmitting: () => {
14750
+ throw new Error(errorMessage);
14751
+ },
14752
+ undo: () => {
14753
+ throw new Error(errorMessage);
14754
+ },
14755
+ redo: () => {
14756
+ throw new Error(errorMessage);
14757
+ }
14758
+ };
13619
14759
  }
13620
14760
  //#endregion
13621
14761
  //#region src/util/useStorageUploadController.tsx
@@ -14300,142 +15440,6 @@ function getEntityImagePreviewPropertyKey(collection) {
14300
15440
  }
14301
15441
  }
14302
15442
  //#endregion
14303
- //#region src/collections/entity-display-cache.ts
14304
- /** The identity of one role of one record, as a cache key. */
14305
- function entityDisplayKey(path, entityId, role) {
14306
- return `${role} ${path} ${entityId ?? ""}`;
14307
- }
14308
- var EntityDisplayCache = class {
14309
- entries = /* @__PURE__ */ new Map();
14310
- listeners = /* @__PURE__ */ new Set();
14311
- /**
14312
- * The resolved value, or `undefined` when this pair has not been resolved
14313
- * yet. `null` is a resolved absence, and the two must stay distinct: a
14314
- * caller that reads "not yet" as "nothing" flickers its fallback in on every
14315
- * mount.
14316
- */
14317
- peek(key) {
14318
- const entry = this.entries.get(key);
14319
- if (!entry || "promise" in entry) return void 0;
14320
- return entry.value;
14321
- }
14322
- /** True while a resolution for this pair is in flight. */
14323
- isLoading(key) {
14324
- const entry = this.entries.get(key);
14325
- return Boolean(entry && "promise" in entry);
14326
- }
14327
- /**
14328
- * Resolve once per record and role. Concurrent callers share the first
14329
- * call's promise; later callers get the cached value with no promise at all.
14330
- *
14331
- * A resolver that throws is recorded as "nothing" rather than retried: the
14332
- * alternative is every render re-running a call that just failed. And it is
14333
- * reported here, which is a correction.
14334
- *
14335
- * It used to say "the caller that saw the rejection is the one that logs
14336
- * it", and `useEntityDisplay` duly attached a `.catch()` that warned. But
14337
- * both failure paths below swallow and return a *resolved* promise, so that
14338
- * catch could never run — the two halves each did the reasonable thing and
14339
- * between them the log was unreachable. A resolver that blew up produced a
14340
- * blank chip and total silence, which is the failure mode
14341
- * `EntityDisplayResolver`'s own contract ("treated as `undefined` and logged
14342
- * once") exists to rule out.
14343
- *
14344
- * Reporting belongs here for the reason the caller could not do it: this is
14345
- * the one place that runs exactly once per key, so "once" is a property of
14346
- * the code rather than a hope about how many components mount.
14347
- */
14348
- resolve(key, resolver) {
14349
- const entry = this.entries.get(key);
14350
- if (entry) return "promise" in entry ? entry.promise : Promise.resolve(entry.value);
14351
- let produced;
14352
- try {
14353
- produced = resolver();
14354
- } catch (error) {
14355
- this.fail(key, error);
14356
- return Promise.resolve(null);
14357
- }
14358
- if (!isPromise(produced)) {
14359
- const value = normalize(produced);
14360
- this.set(key, value);
14361
- return Promise.resolve(value);
14362
- }
14363
- const promise = produced.then((resolved) => {
14364
- const value = normalize(resolved);
14365
- this.set(key, value);
14366
- return value;
14367
- }).catch((error) => {
14368
- this.fail(key, error);
14369
- return null;
14370
- });
14371
- this.entries.set(key, { promise });
14372
- return promise;
14373
- }
14374
- /**
14375
- * Drop what is known about a record, so the next ask resolves again. Called
14376
- * after a write: the row that just saved may be called something else now.
14377
- */
14378
- invalidate(path, entityId) {
14379
- const suffix = ` ${path} ${entityId ?? ""}`;
14380
- let changed = false;
14381
- for (const key of [...this.entries.keys()]) if (entityId === void 0 ? key.includes(` ${path} `) || key.endsWith(` ${path} `) : key.endsWith(suffix)) {
14382
- this.entries.delete(key);
14383
- changed = true;
14384
- }
14385
- if (changed) this.emit();
14386
- }
14387
- /** Drop everything. The user signed out, or the app swapped datasource. */
14388
- clear() {
14389
- if (this.entries.size === 0) return;
14390
- this.entries.clear();
14391
- this.emit();
14392
- }
14393
- subscribe(listener) {
14394
- this.listeners.add(listener);
14395
- return () => {
14396
- this.listeners.delete(listener);
14397
- };
14398
- }
14399
- set(key, value) {
14400
- this.entries.set(key, { value });
14401
- this.emit();
14402
- }
14403
- /**
14404
- * Record a failed resolution as "nothing", and say so once.
14405
- *
14406
- * The key is the message: it is `<role> <path> <id>`, which is exactly what a
14407
- * reader needs to find the resolver that blew up. A warning with no key would
14408
- * tell them a display resolver failed somewhere in a list of fifty rows.
14409
- *
14410
- * `console.warn` rather than a thrown error, because this runs while a row is
14411
- * rendering: the contract is that a title which cannot be fetched must not
14412
- * take down the row that shows it.
14413
- */
14414
- fail(key, error) {
14415
- console.warn(`[rebase] Could not resolve display value for ${key}:`, error);
14416
- this.set(key, null);
14417
- }
14418
- emit() {
14419
- for (const listener of [...this.listeners]) listener();
14420
- }
14421
- };
14422
- /**
14423
- * Empty strings and empty arrays are absences, not values — a title of `" "`
14424
- * would otherwise beat the derived one and render as a blank heading.
14425
- */
14426
- function normalize(value) {
14427
- if (value === void 0 || value === null) return null;
14428
- if (typeof value === "string") {
14429
- const trimmed = value.trim();
14430
- return trimmed.length > 0 ? trimmed : null;
14431
- }
14432
- if (Array.isArray(value)) return value.length > 0 ? value : null;
14433
- return value;
14434
- }
14435
- function isPromise(value) {
14436
- return typeof value?.then === "function";
14437
- }
14438
- //#endregion
14439
15443
  //#region src/collections/filter-operator-resolution.ts
14440
15444
  /**
14441
15445
  * Default operators offered per property type, before engine capabilities and
@@ -15219,6 +16223,6 @@ function getTitlePropertyKeyForValues(collection, values, entityId) {
15219
16223
  return candidates[0];
15220
16224
  }
15221
16225
  //#endregion
15222
- export { AIIcon, AIModifiedIndicator, AdminModeControllerContext, AdminModeControllerProvider, AnalyticsContext, ApiConfigProvider, AuthControllerContext, CONTAINER_FULL_WIDTH, CollectionComponentOverrideProvider, CollectionResolverRegistrationContext, CollectionScopeContext, CollectionScopeProvider, ComponentOverrideContext, ConfirmationDialog, CustomizationControllerContext, DEFAULT_API_PATH, DEFAULT_PAGE_SIZE, DataDriverContext, DataSourcesContext, DialogsControllerContext, DialogsProvider, EffectiveRoleControllerContext, EffectiveRoleControllerProvider, EntityDisplayCache, ErrorTooltip, ErrorView, FORM_CONTAINER_WIDTH, GlobalComponentOverrideProvider, IconForView, LanguageToggle, LoginView, ModeControllerContext, ModeControllerProvider, NavigationBlockerProvider, NotFoundPage, PluginProviderStack, REBASE_LOCALE_STORAGE_KEY, Rebase, RebaseAuth, RebaseClientInstanceContext, RebaseDataContext, RebaseI18nProvider, RebaseLogo, RebaseRegistryProvider, RebaseRouter, RebaseRoutes, SIDE_PANEL_DEFAULT_WIDTH, STUDIO_NAVIGATION_GROUPS, SUMMARY_RANK, SchemaDriftBanner, SchemaDriftProvider, SnackbarProvider, StorageSourceContext, StorageSourcesContext, StudioBridgeContext, StudioBridgeProvider, StudioBridgeRegistryContext, StudioBridgeRegistryProvider, UnsavedChangesDialog, UserConfigurationPersistenceContext, UserDisplay, UserSelectPopover, UserSettingsView, addInitialSlash, apiBaseOf, applyPropertyConditions, buildCollapsedDefaults, buildEnumLabel, canSummariseProperty, clearAuthConfigCache, clearFetchCache, createAuthConfigCache, createFormexStub, deleteEntityWithCallbacks, deriveSpan, en, entityDisplayKey, es, fetchAuthConfig, fillRows, flattenKeys, getAdminEntityChildViews, getAdminSubcollections, getCollectionBySlugWithin, getCollectionPathsCombinations, getColorScheme, getColumnKeysForProperty, getDisplayPropertyKey, getDisplayResolver, getEntityFromCache, getEntityFromMemoryCache, getEntityImagePreviewPropertyKey, getEntityPreviewKeys, getEntityTitlePropertyKey, getFormFieldKeys, getIcon, getLastSegment, getLeadingRelationTitleKey, getLocalChangesBackup, getNavigationEntriesFromPath, getParentReferencesFromPath, getPropertyInPath, getRedundantChildViewColumnIds, getRelationIncludeParams, getRowHeight, getSubcollectionColumnId, getTitlePropertyCandidates, getTitlePropertyKey, getTitlePropertyKeyForValues, hasDeclaredDisplay, iconsSearch, isAuditTimestamp, isDisabled, isEnumValueDisabled, isFilterableRelation, isHidden, isIdPropertyEditable, isLongTextProperty, isReadOnly, isSchemaDriftError, isStorageProperty, looksLikeIdentifierValue, populateFetchCache, rankSummaryProperty, readStoredString, removeEntityFromCache, removeEntityFromMemoryCache, removeInitialAndTrailingSlashes, removeInitialSlash, removeStoredString, removeTrailingSlash, resolveCollectionPathIds, resolveComponentRef, resolveDefaultSelectedView, resolveFilterOperators, resolveFormLayout, saveEntityToCache, saveEntityToMemoryCache, saveEntityWithCallbacks, useAdminModeController, useAnalyticsController, useApiBase, useApiConfig, useAuthController, useAuthSubscription, useBackendStorageSource, useBridgeRegistration, useBrowserTitleAndIcon, useBuildAdminModeController, useBuildEffectiveRoleController, useBuildLocalConfigurationPersistence, useBuildModeController, useClipboard, useCollapsedGroups, useCollection, useCollectionScope, useColumnIds, useComponentOverride, useCustomizationController, useData, useDataSources, useDataTableController, useDebouncedData, useDialogsController, useEffectiveRoleController, useFetch, useLargeLayout, useModeController, useNavigationBlocker, useNavigationGroupLabel, usePermissions, useRebaseAuthController, useRebaseClient, useRebaseContext, useRebaseRegistry, useRebaseRegistryDispatch, useRelationSelector, useResolvedComponent, useRestoreScroll, useSchemaDriftContext, useScrollRestoration, useSlot, useSnackbarController, useStorageSource, useStorageSources, useStorageUploadController, useStudioBreadcrumbs, useStudioCapabilities, useStudioCollectionRegistry, useStudioNavigationState, useStudioSchemaEditing, useStudioSidePanelController, useStudioUrlController, useTranslation, useUnsavedChangesDialog, useUserConfigurationPersistence, writeStoredString };
16226
+ export { AIIcon, AIModifiedIndicator, AdminModeControllerContext, AdminModeControllerProvider, AnalyticsContext, ApiConfigProvider, AuthControllerContext, CONTAINER_FULL_WIDTH, CollectionComponentOverrideProvider, CollectionResolverRegistrationContext, CollectionScopeContext, CollectionScopeProvider, ComponentOverrideContext, ConfirmationDialog, CustomizationControllerContext, DEFAULT_API_PATH, DEFAULT_PAGE_SIZE, DataDriverContext, DataSourcesContext, DialogsControllerContext, DialogsProvider, EffectiveRoleControllerContext, EffectiveRoleControllerProvider, EntityDisplayCache, ErrorTooltip, ErrorView, FORM_CONTAINER_WIDTH, GlobalComponentOverrideProvider, IconForView, LanguageToggle, LoginView, ModeControllerContext, ModeControllerProvider, NavigationBlockerProvider, NotFoundPage, PluginProviderStack, REBASE_LOCALE_STORAGE_KEY, Rebase, RebaseAuth, RebaseClientInstanceContext, RebaseDataContext, RebaseI18nProvider, RebaseLogo, RebaseRegistryProvider, RebaseRouter, RebaseRoutes, SIDE_PANEL_DEFAULT_WIDTH, STUDIO_NAVIGATION_GROUPS, SUMMARY_RANK, SchemaDriftBanner, SchemaDriftProvider, SnackbarProvider, StorageSourceContext, StorageSourcesContext, StudioBridgeContext, StudioBridgeProvider, StudioBridgeRegistryContext, StudioBridgeRegistryProvider, UnsavedChangesDialog, UserConfigurationPersistenceContext, UserDisplay, UserSelectPopover, UserSettingsView, addInitialSlash, apiBaseOf, applyPropertyConditions, buildCollapsedDefaults, buildEnumLabel, canSummariseProperty, clearAuthConfigCache, clearCollectionScrollCache, clearEntityDrafts, clearEntityMemoryCache, clearFetchCache, createAuthConfigCache, createFormexStub, deleteEntityWithCallbacks, deriveSpan, en, entityDisplayKey, es, fetchAuthConfig, fillRows, flattenKeys, getAdminEntityChildViews, getAdminSubcollections, getCollectionBySlugWithin, getCollectionPathsCombinations, getColorScheme, getColumnKeysForProperty, getDisplayPropertyKey, getDisplayResolver, getEntityFromCache, getEntityFromMemoryCache, getEntityImagePreviewPropertyKey, getEntityPreviewKeys, getEntityTitlePropertyKey, getFormFieldKeys, getIcon, getLastSegment, getLeadingRelationTitleKey, getLocalChangesBackup, getNavigationEntriesFromPath, getParentReferencesFromPath, getPropertyInPath, getRedundantChildViewColumnIds, getRelationIncludeParams, getRowHeight, getSharedEntityDisplayCache, getSubcollectionColumnId, getTitlePropertyCandidates, getTitlePropertyKey, getTitlePropertyKeyForValues, hasDeclaredDisplay, iconsSearch, isAuditTimestamp, isDisabled, isEnumValueDisabled, isFilterableRelation, isHidden, isIdPropertyEditable, isLongTextProperty, isReadOnly, isSchemaDriftError, isStorageProperty, looksLikeIdentifierValue, populateFetchCache, rankSummaryProperty, readStoredString, removeEntityFromCache, removeEntityFromMemoryCache, removeInitialAndTrailingSlashes, removeInitialSlash, removeStoredString, removeTrailingSlash, resolveCollectionPathIds, resolveComponentRef, resolveDefaultSelectedView, resolveFilterOperators, resolveFormLayout, saveEntityToCache, saveEntityToMemoryCache, saveEntityWithCallbacks, useAdminModeController, useAnalyticsController, useApiBase, useApiConfig, useAuthController, useAuthSubscription, useBackendStorageSource, useBridgeRegistration, useBrowserTitleAndIcon, useBuildAdminModeController, useBuildEffectiveRoleController, useBuildLocalConfigurationPersistence, useBuildModeController, useClipboard, useCollapsedGroups, useCollection, useCollectionScope, useColumnIds, useComponentOverride, useCustomizationController, useData, useDataSources, useDataTableController, useDebouncedData, useDialogsController, useEffectiveRoleController, useFetch, useLargeLayout, useModeController, useNavigationBlocker, useNavigationGroupLabel, usePermissions, useRebaseAuthController, useRebaseClient, useRebaseContext, useRebaseRegistry, useRebaseRegistryDispatch, useRelationSelector, useResolvedComponent, useRestoreScroll, useSchemaDriftContext, useScrollRestoration, useSlot, useSnackbarController, useStorageSource, useStorageSources, useStorageUploadController, useStudioBreadcrumbs, useStudioCapabilities, useStudioCollectionRegistry, useStudioNavigationState, useStudioSchemaEditing, useStudioSidePanelController, useStudioUrlController, useTranslation, useUnsavedChangesDialog, useUserConfigurationPersistence, writeStoredString };
15223
16227
 
15224
16228
  //# sourceMappingURL=index.es.js.map