@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/auth/session_caches.d.ts +26 -0
- package/dist/auth/types.d.ts +30 -0
- package/dist/collections/entity-display-cache.d.ts +1 -0
- package/dist/components/LoginView/email-link.d.ts +28 -0
- package/dist/components/LoginView/mfa-required.d.ts +33 -0
- package/dist/components/common/useDataTableController.d.ts +1 -1
- package/dist/components/common/useScrollRestoration.d.ts +7 -0
- package/dist/core/PluginLifecycleManager.d.ts +12 -4
- package/dist/hooks/data/useStableFilterValues.d.ts +9 -0
- package/dist/index.es.js +1857 -853
- package/dist/index.es.js.map +1 -1
- package/dist/util/entity_cache.d.ts +11 -0
- package/dist/vitePlugin.d.ts +6 -0
- package/dist/vitePlugin.js +17 -3
- package/package.json +7 -7
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)`
|
|
44
|
-
* - Calls `lifecycle.
|
|
45
|
-
* -
|
|
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
|
|
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 (
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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/
|
|
1526
|
-
|
|
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
|
-
*
|
|
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
|
-
*
|
|
1668
|
-
*
|
|
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
|
-
|
|
1672
|
-
|
|
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
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
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
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
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/
|
|
1566
|
+
//#region src/util/entity_cache.ts
|
|
1567
|
+
var LOCAL_STORAGE_PREFIX = "entity_cache::";
|
|
1722
1568
|
/**
|
|
1723
|
-
*
|
|
1724
|
-
*
|
|
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
|
-
*
|
|
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
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
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
|
|
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
|
-
|
|
2325
|
-
|
|
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: `${
|
|
2820
|
+
url: scopedToken ? `${fileUrl}?token=${encodeURIComponent(scopedToken)}` : fileUrl,
|
|
2328
2821
|
metadata: result.data
|
|
2329
2822
|
};
|
|
2330
|
-
urlsCache.set(cacheKey,
|
|
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
|
-
|
|
2342
|
-
if (
|
|
2343
|
-
|
|
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 =
|
|
2843
|
+
const fileName = (bucket ? `${bucket}/${key}` : key).split("/").pop() || "file";
|
|
2350
2844
|
return new File([blob], fileName, { type: blob.type });
|
|
2351
|
-
}, [
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
3266
|
-
|
|
3267
|
-
|
|
3268
|
-
|
|
3269
|
-
|
|
3270
|
-
|
|
3271
|
-
|
|
3272
|
-
|
|
3273
|
-
|
|
3274
|
-
|
|
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
|
|
3336
|
-
|
|
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(
|
|
3344
|
-
else window.history.replaceState(
|
|
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"
|
|
3357
|
-
entries["__sort_order"
|
|
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
|
|
3362
|
-
|
|
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}
|
|
3380
|
-
entries[encodeURIComponent(`${key}
|
|
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
|
-
|
|
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(
|
|
3398
|
-
const field = key.
|
|
3399
|
-
const
|
|
3400
|
-
const
|
|
3401
|
-
|
|
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
|
-
|
|
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 [
|
|
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(
|
|
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
|
|
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: [
|
|
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: () =>
|
|
4953
|
-
onForgotPassword:
|
|
4954
|
-
|
|
4955
|
-
|
|
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
|
-
|
|
5454
|
+
!isBootstrapMode && pendingMfa && /* @__PURE__ */ jsx(MfaChallengeForm, {
|
|
5455
|
+
pending: pendingMfa,
|
|
4962
5456
|
authController,
|
|
4963
|
-
|
|
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
|
-
|
|
5024
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
5163
|
-
if (!err
|
|
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")))
|
|
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: [
|
|
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 (
|
|
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
|
-
|
|
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
|
|
13598
|
-
|
|
13599
|
-
|
|
13600
|
-
if (
|
|
13601
|
-
const
|
|
13602
|
-
|
|
13603
|
-
|
|
13604
|
-
|
|
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
|
|
13608
|
-
if (
|
|
13609
|
-
|
|
13610
|
-
|
|
13611
|
-
|
|
13612
|
-
|
|
13613
|
-
|
|
13614
|
-
|
|
13615
|
-
|
|
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
|
-
|
|
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
|