@rebasepro/app 0.13.0 → 0.13.1-canary.g06dbe5b

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/dist/collections/entity-display-cache.d.ts +75 -0
  2. package/dist/collections/entity-display.d.ts +35 -0
  3. package/dist/collections/entity_image_preview.d.ts +8 -0
  4. package/dist/collections/form-layout.d.ts +6 -0
  5. package/dist/collections/index.d.ts +4 -0
  6. package/dist/collections/property-path.d.ts +16 -0
  7. package/dist/collections/property_presentation.d.ts +18 -3
  8. package/dist/collections/summary-property.d.ts +65 -0
  9. package/dist/components/LoginView/oauth-redirect-flow.d.ts +86 -0
  10. package/dist/components/common/useColumnsIds.d.ts +22 -0
  11. package/dist/core/Rebase.d.ts +1 -1
  12. package/dist/core/RebaseProps.d.ts +10 -2
  13. package/dist/hooks/data/collectionQuery.d.ts +45 -0
  14. package/dist/hooks/data/useRelationSelector.d.ts +12 -1
  15. package/dist/index.es.js +866 -241
  16. package/dist/index.es.js.map +1 -1
  17. package/dist/util/entity_cache.d.ts +19 -3
  18. package/dist/util/previews.d.ts +19 -0
  19. package/package.json +7 -7
  20. package/src/auth/useRebaseAuthController.ts +15 -1
  21. package/src/collections/entity-display-cache.ts +195 -0
  22. package/src/collections/entity-display.ts +105 -0
  23. package/src/collections/entity_image_preview.ts +13 -0
  24. package/src/collections/form-layout.ts +8 -1
  25. package/src/collections/index.ts +4 -0
  26. package/src/collections/property-path.ts +30 -0
  27. package/src/collections/property_presentation.ts +39 -4
  28. package/src/collections/summary-property.ts +114 -0
  29. package/src/collections/title-property.ts +13 -16
  30. package/src/components/LoginView/LoginView.tsx +48 -26
  31. package/src/components/LoginView/oauth-redirect-flow.ts +184 -0
  32. package/src/components/common/useColumnsIds.tsx +67 -29
  33. package/src/components/common/useDataTableController.tsx +10 -14
  34. package/src/core/Rebase.tsx +20 -2
  35. package/src/core/RebaseProps.tsx +10 -2
  36. package/src/hooks/data/collectionQuery.ts +55 -0
  37. package/src/hooks/data/useCollection.tsx +29 -21
  38. package/src/hooks/data/useFetch.tsx +17 -2
  39. package/src/hooks/data/useRelationSelector.tsx +46 -8
  40. package/src/hooks/useAuthSubscription.ts +18 -2
  41. package/src/hooks/useBuildLocalConfigurationPersistence.tsx +20 -12
  42. package/src/locales/de.ts +2 -0
  43. package/src/locales/en.ts +3 -0
  44. package/src/locales/es.ts +2 -0
  45. package/src/locales/fr.ts +2 -0
  46. package/src/locales/hi.ts +3 -1
  47. package/src/locales/it.ts +2 -0
  48. package/src/locales/pt.ts +2 -0
  49. package/src/util/entity_cache.ts +34 -31
  50. package/src/util/previews.ts +49 -29
package/dist/index.es.js CHANGED
@@ -1,19 +1,19 @@
1
1
  import React, { createContext, lazy, useCallback, useContext, useEffect, useId, useLayoutEffect, useMemo, useRef, useState } from "react";
2
2
  import { Alert, AlertCircleIcon, AlertTriangleIcon, AppWindow, ArrowDownIcon, ArrowLeftIcon, ArrowRightLeftIcon, ArrowUpDownIcon, ArrowUpIcon, Avatar, BooleanSwitch, Button, CalendarIcon, Card, CenteredView, CheckCircle2Icon, CheckIcon, Checkbox, ChevronDownIcon, ChevronLeftIcon, ChevronRightIcon, ChevronsLeftIcon, ChevronsRightIcon, Chip, CircleDotIcon, CircleUserIcon, CircularProgress, ColumnsIcon, Container, Dialog, DialogActions, DialogContent, DialogTitle, DollarSignIcon, ErrorBoundary, FileIcon, FileTextIcon, FilterChip, FilterIcon, FolderIcon, FolderKanbanIcon, IconButton, KanbanIcon, KanbanView, LanguagesIcon, LayoutGridIcon, ListIcon, ListPlusIcon, ListTodoIcon, LoadingButton, LogOutIcon, MailIcon, Menu, MenuItem, MessageCircleIcon, MoonIcon, MultiSelect, MultiSelectItem, PanelLeftIcon, Paper, PenLineIcon, PencilIcon, PhoneIcon, PinIcon, PlusIcon, Popover, RefreshCwIcon, SearchBar, Select, SelectItem, Separator, SettingsIcon, Skeleton, StickyNoteIcon, SunIcon, SunMoonIcon, Tab, Table, TableBody, TableCell, TableHeader, TableRow, Tabs, TagIcon, TextField, ToggleButtonGroup, Tooltip, Trash2Icon, TrendingUpIcon, TypeIcon, Typography, UserIcon, UserPlus, UsersIcon, VideoIcon, Wand2Icon, WrenchIcon, XIcon, cls, colorClassesMapping, coolIconKeys, defaultBorderMixin, getColorSchemeForKey, getColorSchemeForSeed, iconKeys, iconSize, lucideIcons } from "@rebasepro/ui";
3
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 } from "@rebasepro/types";
4
+ import { UNRENDERED_SLOTS, resolveAdminCollection } from "@rebasepro/admin-types";
4
5
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
5
6
  import { SnackbarProvider as SnackbarProvider$1, useSnackbar } from "notistack";
6
- import { buildRebaseData, canCreateEntity, canDeleteEntity, canEditEntity, canReadCollection, evaluateCondition, getEntityChildViews, getLabelOrConfigFrom, getPrimaryKeys, getSubcollections, isPropertyBuilder, resolveStorageFilenameString, resolveStoragePathString, resolveStorageSource, stripCollectionPath, wrapAsEntityData, wrapAsSdkData } from "@rebasepro/common";
7
+ import { buildRebaseData, canCreateEntity, canDeleteEntity, canEditEntity, canReadCollection, evaluateCondition, getChildViewDeclaringProperties, getChildViewRelationPropertyKeys, getEntityChildViews, getLabelOrConfigFrom, getPrimaryKeys, getSubcollections, isPropertyBuilder, resolveStorageFilenameString, resolveStoragePathString, resolveStorageSource, stripCollectionPath, wrapAsEntityData, wrapAsSdkData } from "@rebasepro/common";
7
8
  import { getTitlePropertyKey as getTitlePropertyKey$1, removeInitialAndTrailingSlashes as removeInitialAndTrailingSlashes$1 } from "@rebasepro/app";
8
9
  import { Link, Routes, createBrowserRouter, useBlocker, useLocation } from "react-router";
9
- import { generateForeignKeyName, hashString, isObject, isPlainObject, mergeDeep, randomString, slugify } from "@rebasepro/utils";
10
+ import { generateForeignKeyName, hashString, isArrayValue, isObject, isPlainObject, isRecordValue, mergeDeep, randomString, readStoredJson, slugify, writeStoredJson } from "@rebasepro/utils";
10
11
  import { I18nextProvider, initReactI18next, useTranslation as useTranslation$1 } from "react-i18next";
11
12
  import { deepEqual } from "fast-equals";
12
13
  import i18next from "i18next";
13
14
  import { RouterProvider } from "react-router/dom";
14
15
  import Fuse from "fuse.js";
15
16
  import Compressor from "compressorjs";
16
- import { resolveAdminCollection } from "@rebasepro/admin-types";
17
17
  //#region src/core/PluginProviderStack.tsx
18
18
  /**
19
19
  * Wraps children with all provider components from plugins that match the given scope.
@@ -414,6 +414,116 @@ function SchemaDriftBanner({ className }) {
414
414
  });
415
415
  }
416
416
  //#endregion
417
+ //#region src/hooks/data/collectionQuery.ts
418
+ /**
419
+ * Build the parameters for one collection read.
420
+ *
421
+ * Derived options live here rather than at the call sites, so they cannot
422
+ * disagree: `searchExplain` rides along exactly when there is a search string
423
+ * to explain, which is also the only time it costs anything.
424
+ */
425
+ function toFindParams(input) {
426
+ const { searchString, ...rest } = input;
427
+ return {
428
+ ...rest,
429
+ ...searchString ? {
430
+ searchString,
431
+ searchExplain: true
432
+ } : {}
433
+ };
434
+ }
435
+ //#endregion
436
+ //#region src/collections/summary-property.ts
437
+ /**
438
+ * Whether a property can stand in for a record in *one line*.
439
+ *
440
+ * Preview surfaces — the reference card, a list row, a board card — have a
441
+ * single line per property and no way to grow. That is a constraint on the
442
+ * *value*, not on the property's importance: a Markdown biography is the most
443
+ * interesting column on an author, and it is still the wrong thing to paste
444
+ * into a 44px card. Ranking it here means the picker can skip it without
445
+ * anyone having to configure their way out of a broken card.
446
+ *
447
+ * Kept beside {@link getTitlePropertyCandidates} because the two answer
448
+ * neighbouring questions — "what is this record called" and "what else can be
449
+ * said about it in a line" — from the same property schema.
450
+ *
451
+ * @group Collections
452
+ */
453
+ var SUMMARY_RANK = {
454
+ /**
455
+ * No single-line form exists. A map renders as a key/value table and an
456
+ * array of maps as a stack of them; there is no first line to take.
457
+ */
458
+ UNUSABLE: 0,
459
+ /**
460
+ * Readable in one line, but only as an excerpt — the property holds a
461
+ * document, and the line is its opening. Worth showing when nothing better
462
+ * is available, never in preference to a value that fits whole.
463
+ */
464
+ EXCERPT: 1,
465
+ /** Short and self-contained: the line *is* the value. */
466
+ DIRECT: 2
467
+ };
468
+ /**
469
+ * File-storage backed content (single image, array of images, generic upload…).
470
+ * Preview surfaces give these their own image slot, so they are never also a
471
+ * text line.
472
+ *
473
+ * @group Collections
474
+ */
475
+ function isStorageProperty(property) {
476
+ if (!property) return false;
477
+ if (property.type === "string" && (property.storage || property.admin?.urlPreview === "image")) return true;
478
+ if (property.type === "array" && property.of && !Array.isArray(property.of)) {
479
+ const inner = property.of;
480
+ if (inner.type === "string" && (inner.storage || inner.admin?.urlPreview === "image")) return true;
481
+ }
482
+ return false;
483
+ }
484
+ /**
485
+ * True when the property holds free text long enough to be a document rather
486
+ * than a value — Markdown, or an explicitly multi-line string.
487
+ *
488
+ * @group Collections
489
+ */
490
+ function isLongTextProperty(property) {
491
+ if (!property || property.type !== "string") return false;
492
+ return Boolean(property.admin?.markdown || property.admin?.multiline);
493
+ }
494
+ /**
495
+ * How well a property reads as one line of a preview. See {@link SUMMARY_RANK}.
496
+ *
497
+ * Decided from the property *schema* only: no values are consulted, so the
498
+ * answer is stable for a collection and can be memoised per collection rather
499
+ * than per row.
500
+ *
501
+ * @group Collections
502
+ */
503
+ function rankSummaryProperty(property) {
504
+ if (!property) return SUMMARY_RANK.UNUSABLE;
505
+ if (isStorageProperty(property)) return SUMMARY_RANK.UNUSABLE;
506
+ if (property.type === "map") return SUMMARY_RANK.UNUSABLE;
507
+ if (property.type === "array") {
508
+ if (Array.isArray(property.of)) return SUMMARY_RANK.UNUSABLE;
509
+ if (!property.of) return property.oneOf ? SUMMARY_RANK.UNUSABLE : SUMMARY_RANK.DIRECT;
510
+ const innerRank = rankSummaryProperty(property.of);
511
+ if (innerRank === SUMMARY_RANK.UNUSABLE) return SUMMARY_RANK.UNUSABLE;
512
+ if (innerRank === SUMMARY_RANK.EXCERPT) return SUMMARY_RANK.UNUSABLE;
513
+ return SUMMARY_RANK.DIRECT;
514
+ }
515
+ if (isLongTextProperty(property)) return SUMMARY_RANK.EXCERPT;
516
+ return SUMMARY_RANK.DIRECT;
517
+ }
518
+ /**
519
+ * True when the property has any single-line form at all.
520
+ *
521
+ * @group Collections
522
+ */
523
+ function canSummariseProperty(property) {
524
+ return rankSummaryProperty(property) !== SUMMARY_RANK.UNUSABLE;
525
+ }
526
+ //#endregion
417
527
  //#region src/util/previews.ts
418
528
  function isReferenceProperty(property) {
419
529
  if (!property) return null;
@@ -434,35 +544,47 @@ function isHiddenProperty(property) {
434
544
  return Boolean(property.admin?.hideFromCollection);
435
545
  }
436
546
  /**
437
- * Returns true when the property holds file-storage content (single image,
438
- * array of images, generic upload, …). These properties are rendered by the
439
- * dedicated image-slot and should NOT appear as regular preview columns.
547
+ * The properties a preview surface should render for a record, best first.
548
+ *
549
+ * Three things decide the answer, in this order:
550
+ *
551
+ * 1. `previewProperties` — passed in, or declared on the collection. A stated
552
+ * list is returned verbatim, ranking and limit included: a developer who
553
+ * asks for the Markdown biography gets the Markdown biography.
554
+ * 2. Whether the value has a one-line form at all. A map renders as a
555
+ * key/value table and a Markdown field as a document; neither fits a card
556
+ * line, so they never take a slot from a value that does. See
557
+ * {@link rankSummaryProperty}.
558
+ * 3. `propertiesOrder`, which breaks ties.
559
+ *
560
+ * The middle step is the one that is easy to get wrong by leaving out.
561
+ * `propertiesOrder` states the *column* order of a collection table — it is
562
+ * not a statement that the first three columns summarise a record, and reading
563
+ * it as one is how a card ends up rendering somebody's entire biography.
440
564
  */
441
- function isStorageProperty$1(property) {
442
- if (!property) return false;
443
- if (property.type === "string" && property.storage) return true;
444
- if (property.type === "string" && property.admin?.urlPreview === "image") return true;
445
- if (property.type === "array" && property.of && !Array.isArray(property.of)) {
446
- const inner = property.of;
447
- if (inner.type === "string" && (inner.storage || inner.admin?.urlPreview === "image")) return true;
448
- }
449
- return false;
450
- }
451
565
  function getEntityPreviewKeys(authController, targetCollection, fields, previewProperties, limit = 3) {
452
566
  const allProperties = Object.keys(targetCollection.properties);
453
567
  let listProperties = previewProperties?.filter((p) => allProperties.includes(p));
454
568
  if (!listProperties && targetCollection.previewProperties) listProperties = targetCollection.previewProperties?.filter((p) => allProperties.includes(p));
455
569
  if (listProperties && listProperties.length > 0) return listProperties;
456
- else {
457
- listProperties = targetCollection.propertiesOrder || allProperties;
458
- return listProperties.filter((key) => {
459
- const prop = targetCollection.properties[key];
460
- return !(prop && typeof prop === "object" && "isId" in prop && Boolean(prop.isId)) && key !== "id";
461
- }).filter((key) => {
462
- const property = targetCollection.properties[key];
463
- return property && !isPropertyBuilder(property) && !isReferenceProperty(property) && !isRelationProperty(property) && !isHiddenProperty(property) && !isStorageProperty$1(property);
464
- }).slice(0, limit);
570
+ const hasExplicitOrder = !!targetCollection.propertiesOrder;
571
+ const order = targetCollection.propertiesOrder || allProperties;
572
+ const direct = [];
573
+ const excerpt = [];
574
+ for (const key of order) {
575
+ const property = targetCollection.properties[key];
576
+ if (!property || isPropertyBuilder(property)) continue;
577
+ if (key === "id" || "isId" in property && Boolean(property.isId)) continue;
578
+ if (isReferenceProperty(property)) continue;
579
+ if (!hasExplicitOrder && isRelationProperty(property)) continue;
580
+ if (isHiddenProperty(property)) continue;
581
+ if (isStorageProperty(property)) continue;
582
+ const rank = rankSummaryProperty(property);
583
+ if (rank === SUMMARY_RANK.UNUSABLE) continue;
584
+ (rank === SUMMARY_RANK.EXCERPT ? excerpt : direct).push(key);
585
+ if (direct.length >= limit) break;
465
586
  }
587
+ return [...direct, ...excerpt].slice(0, limit);
466
588
  }
467
589
  var INCLUDE_ALL_RELATIONS = ["*"];
468
590
  /**
@@ -531,7 +653,7 @@ function useCollection({ path, collection, filterValues, sortBy, itemCount, offs
531
653
  };
532
654
  const accessor = dataClient.collection(path);
533
655
  const includeParams = getRelationIncludeParams(collection);
534
- if (accessor.listen) return accessor.listen({
656
+ if (accessor.listen) return accessor.listen(toFindParams({
535
657
  where: whereParams,
536
658
  limit: itemCount,
537
659
  offset,
@@ -539,12 +661,13 @@ function useCollection({ path, collection, filterValues, sortBy, itemCount, offs
539
661
  orderBy: orderByParams,
540
662
  searchString,
541
663
  include: includeParams
542
- }, (res) => onEntitiesUpdate({
664
+ }), (res) => onEntitiesUpdate({
543
665
  data: res.data,
544
666
  meta: res.meta
545
667
  }), onError);
546
668
  else {
547
- accessor.find({
669
+ let cancelled = false;
670
+ accessor.find(toFindParams({
548
671
  where: whereParams,
549
672
  limit: itemCount,
550
673
  offset,
@@ -552,11 +675,19 @@ function useCollection({ path, collection, filterValues, sortBy, itemCount, offs
552
675
  orderBy: orderByParams,
553
676
  searchString,
554
677
  include: includeParams
555
- }).then((res) => onEntitiesUpdate({
556
- data: res.data,
557
- meta: res.meta
558
- })).catch(onError);
559
- return () => {};
678
+ })).then((res) => {
679
+ if (cancelled) return;
680
+ onEntitiesUpdate({
681
+ data: res.data,
682
+ meta: res.meta
683
+ });
684
+ }).catch((e) => {
685
+ if (cancelled) return;
686
+ onError(e);
687
+ });
688
+ return () => {
689
+ cancelled = true;
690
+ };
560
691
  }
561
692
  }, [
562
693
  path,
@@ -836,8 +967,17 @@ function useFetch({ path, entityId, collection, databaseId, useCache = false })
836
967
  const accessor = dataClient.collection(path);
837
968
  if (accessor.listenById) return accessor.listenById(entityId, (entity) => onEntityUpdate(entity), onError);
838
969
  else {
839
- accessor.findById(entityId).then((entity) => onEntityUpdate(entity)).catch(onError);
840
- return () => {};
970
+ let cancelled = false;
971
+ accessor.findById(entityId).then((entity) => {
972
+ if (cancelled) return;
973
+ onEntityUpdate(entity);
974
+ }).catch((e) => {
975
+ if (cancelled) return;
976
+ onError(e);
977
+ });
978
+ return () => {
979
+ cancelled = true;
980
+ };
841
981
  }
842
982
  } else {
843
983
  onEntityUpdate(void 0);
@@ -867,7 +1007,7 @@ var DEFAULT_PAGE_SIZE$1 = 10;
867
1007
  /**
868
1008
  * Hook to manage relation selection with data fetching from Rebase data source
869
1009
  */
870
- function useRelationSelector({ path, collection, fixedFilter, pageSize = DEFAULT_PAGE_SIZE$1, getLabelFromEntity, getDescriptionFromEntity, descriptionProperty }) {
1010
+ function useRelationSelector({ path, collection, fixedFilter, pageSize = DEFAULT_PAGE_SIZE$1, getLabelFromEntity, getDescriptionFromEntity, descriptionProperty, enabled = true }) {
871
1011
  const dataClient = useData();
872
1012
  const [items, setItems] = useState([]);
873
1013
  const [isLoading, setIsLoading] = useState(false);
@@ -878,6 +1018,7 @@ function useRelationSelector({ path, collection, fixedFilter, pageSize = DEFAULT
878
1018
  const [limit, setLimit] = useState(pageSize);
879
1019
  const unsubscribeRef = useRef(null);
880
1020
  const searchTimeoutRef = useRef(null);
1021
+ const hasLoadedRef = useRef(false);
881
1022
  const setLoading = useCallback((loading) => {
882
1023
  isLoadingRef.current = loading;
883
1024
  setIsLoading(loading);
@@ -926,12 +1067,14 @@ function useRelationSelector({ path, collection, fixedFilter, pageSize = DEFAULT
926
1067
  const whereParams = fixedFilter && Object.keys(fixedFilter).length > 0 ? fixedFilter : void 0;
927
1068
  const onEntitiesUpdate = (res) => {
928
1069
  const newItems = res.data.map((e) => entityToRelationItem(e));
1070
+ hasLoadedRef.current = true;
929
1071
  setItems(newItems);
930
1072
  setHasMore(res.meta.hasMore);
931
1073
  setLoading(false);
932
1074
  };
933
1075
  const onErrorUpdate = (fetchError) => {
934
1076
  console.error("useRelationSelector: Error fetching data:", fetchError);
1077
+ hasLoadedRef.current = true;
935
1078
  setError(fetchError);
936
1079
  setLoading(false);
937
1080
  };
@@ -948,6 +1091,7 @@ function useRelationSelector({ path, collection, fixedFilter, pageSize = DEFAULT
948
1091
  meta: res.meta
949
1092
  }), onErrorUpdate);
950
1093
  else {
1094
+ let cancelled = false;
951
1095
  accessor.find({
952
1096
  where: whereParams,
953
1097
  limit,
@@ -955,11 +1099,19 @@ function useRelationSelector({ path, collection, fixedFilter, pageSize = DEFAULT
955
1099
  orderBy: void 0,
956
1100
  searchString: currentSearch,
957
1101
  include: includeParams
958
- }).then((res) => onEntitiesUpdate({
959
- data: res.data,
960
- meta: res.meta
961
- })).catch(onErrorUpdate);
962
- unsubscribe = () => {};
1102
+ }).then((res) => {
1103
+ if (cancelled) return;
1104
+ onEntitiesUpdate({
1105
+ data: res.data,
1106
+ meta: res.meta
1107
+ });
1108
+ }).catch((e) => {
1109
+ if (cancelled) return;
1110
+ onErrorUpdate(e);
1111
+ });
1112
+ unsubscribe = () => {
1113
+ cancelled = true;
1114
+ };
963
1115
  }
964
1116
  unsubscribeRef.current = unsubscribe || null;
965
1117
  }, [
@@ -992,11 +1144,16 @@ function useRelationSelector({ path, collection, fixedFilter, pageSize = DEFAULT
992
1144
  setLoading
993
1145
  ]);
994
1146
  useEffect(() => {
1147
+ if (!enabled) return;
995
1148
  fetchData();
996
1149
  return () => {
997
1150
  cleanupSubscription();
998
1151
  };
999
- }, [fetchData]);
1152
+ }, [
1153
+ fetchData,
1154
+ enabled,
1155
+ cleanupSubscription
1156
+ ]);
1000
1157
  useEffect(() => {
1001
1158
  return () => {
1002
1159
  if (searchTimeoutRef.current) clearTimeout(searchTimeoutRef.current);
@@ -1004,7 +1161,7 @@ function useRelationSelector({ path, collection, fixedFilter, pageSize = DEFAULT
1004
1161
  }, []);
1005
1162
  return useMemo(() => ({
1006
1163
  items,
1007
- isLoading,
1164
+ isLoading: isLoading || enabled && !hasLoadedRef.current,
1008
1165
  error,
1009
1166
  search,
1010
1167
  loadMore,
@@ -1017,7 +1174,8 @@ function useRelationSelector({ path, collection, fixedFilter, pageSize = DEFAULT
1017
1174
  search,
1018
1175
  loadMore,
1019
1176
  hasMore,
1020
- entityToRelationItem
1177
+ entityToRelationItem,
1178
+ enabled
1021
1179
  ]);
1022
1180
  }
1023
1181
  //#endregion
@@ -1254,7 +1412,15 @@ function useAuthSubscription(authClient) {
1254
1412
  setInitialLoading(true);
1255
1413
  authClient.getUser().then((user) => {
1256
1414
  if (user) setUser(user);
1257
- }).catch((e) => {}).finally(() => {
1415
+ setAuthError(void 0);
1416
+ }).catch((e) => {
1417
+ const status = e?.status;
1418
+ if (status === 401 || status === 403) {
1419
+ setAuthError(void 0);
1420
+ return;
1421
+ }
1422
+ setAuthError(e instanceof Error ? e : new Error(String(e)));
1423
+ }).finally(() => {
1258
1424
  setInitialLoading(false);
1259
1425
  });
1260
1426
  }
@@ -1630,8 +1796,10 @@ function useSlot(slot, props) {
1630
1796
  function useBuildLocalConfigurationPersistence() {
1631
1797
  const configCache = useRef({});
1632
1798
  const getCollectionFromStorage = useCallback((storageKey) => {
1633
- const item = localStorage.getItem(storageKey);
1634
- return item ? JSON.parse(item) : {};
1799
+ return readStoredJson(storageKey, {
1800
+ fallback: {},
1801
+ accept: isRecordValue
1802
+ });
1635
1803
  }, []);
1636
1804
  const getCollectionConfig = useCallback((path) => {
1637
1805
  const storageKey = `collection_config::${stripCollectionPath(path)}`;
@@ -1640,29 +1808,33 @@ function useBuildLocalConfigurationPersistence() {
1640
1808
  }, [getCollectionFromStorage]);
1641
1809
  const onCollectionModified = useCallback((path, data) => {
1642
1810
  const storageKey = `collection_config::${stripCollectionPath(path)}`;
1643
- localStorage.setItem(storageKey, JSON.stringify(data));
1811
+ writeStoredJson(storageKey, data);
1644
1812
  const cachedConfig = configCache.current[storageKey];
1645
- const newConfig = mergeDeep(cachedConfig ?? getCollectionFromStorage(path), data);
1813
+ const newConfig = mergeDeep(cachedConfig ?? getCollectionFromStorage(storageKey), data);
1646
1814
  configCache.current[storageKey] = mergeDeep(configCache.current[storageKey], newConfig);
1647
1815
  }, [getCollectionFromStorage]);
1648
1816
  const [recentlyVisitedPaths, _setRecentlyVisitedPaths] = useState([]);
1649
1817
  const [favouritePaths, _setFavouritePaths] = useState([]);
1650
1818
  const [collapsedGroups, _setCollapsedGroups] = useState([]);
1651
1819
  useEffect(() => {
1652
- _setRecentlyVisitedPaths(localStorage.getItem("recently_visited_paths") ? JSON.parse(localStorage.getItem("recently_visited_paths")) : []);
1653
- _setFavouritePaths(localStorage.getItem("favourite_paths") ? JSON.parse(localStorage.getItem("favourite_paths")) : []);
1654
- _setCollapsedGroups(localStorage.getItem("collapsed_groups") ? JSON.parse(localStorage.getItem("collapsed_groups")) : []);
1820
+ const readPaths = (key) => readStoredJson(key, {
1821
+ fallback: [],
1822
+ accept: isArrayValue
1823
+ });
1824
+ _setRecentlyVisitedPaths(readPaths("recently_visited_paths"));
1825
+ _setFavouritePaths(readPaths("favourite_paths"));
1826
+ _setCollapsedGroups(readPaths("collapsed_groups"));
1655
1827
  }, []);
1656
1828
  const setRecentlyVisitedPaths = useCallback((paths) => {
1657
- localStorage.setItem("recently_visited_paths", JSON.stringify(paths));
1829
+ writeStoredJson("recently_visited_paths", paths);
1658
1830
  _setRecentlyVisitedPaths(paths);
1659
1831
  }, []);
1660
1832
  const setFavouritePaths = useCallback((paths) => {
1661
- localStorage.setItem("favourite_paths", JSON.stringify(paths));
1833
+ writeStoredJson("favourite_paths", paths);
1662
1834
  _setFavouritePaths(paths);
1663
1835
  }, []);
1664
1836
  const setCollapsedGroups = useCallback((paths) => {
1665
- localStorage.setItem("collapsed_groups", JSON.stringify(paths));
1837
+ writeStoredJson("collapsed_groups", paths);
1666
1838
  _setCollapsedGroups(paths);
1667
1839
  }, []);
1668
1840
  return useMemo(() => ({
@@ -2325,24 +2497,202 @@ function useDebouncedData(data, deps, timeoutMs = 5e3) {
2325
2497
  return immediateUpdate ? data : deferredData;
2326
2498
  }
2327
2499
  //#endregion
2328
- //#region src/components/common/useColumnsIds.tsx
2500
+ //#region src/collections/property-path.ts
2329
2501
  /**
2330
- * Get a property in a property tree from a dot-path like `address.street`.
2331
- * Inlined here to avoid importing property-aware utilities from the admin layer.
2502
+ * The property at a dotted path, walking `map` children — `address.street`.
2503
+ *
2504
+ * The value counterpart is `getValueInPath` in `@rebasepro/utils`; this is the
2505
+ * schema half, and the two have to be used together. Reading a dotted path off
2506
+ * an entity while looking its property up with a flat `properties[path]` gives
2507
+ * the value and `undefined` for how to render it, which is how a declared title
2508
+ * on a nested field silently fell back to a derived one.
2509
+ *
2510
+ * There were three copies of this: one private to `useColumnsIds`, one exported
2511
+ * from the admin layer, and the flat lookup in the title resolver that was not
2512
+ * this function at all. This is the one, in the lowest layer that needs it —
2513
+ * admin re-exports it under the name it already published.
2332
2514
  */
2333
2515
  function getPropertyInPath(properties, path) {
2334
- if (typeof properties === "object") {
2335
- if (path in properties) return properties[path];
2336
- if (path.includes(".")) {
2337
- const pathSegments = path.split(".");
2338
- const childProperty = properties[pathSegments[0]];
2339
- if (typeof childProperty === "object" && childProperty.type === "map" && childProperty.properties) return getPropertyInPath(childProperty.properties, pathSegments.slice(1).join("."));
2516
+ if (typeof properties !== "object" || !properties) return void 0;
2517
+ if (path in properties) return properties[path];
2518
+ if (path.includes(".")) {
2519
+ const pathSegments = path.split(".");
2520
+ const childProperty = properties[pathSegments[0]];
2521
+ if (typeof childProperty === "object" && childProperty?.type === "map" && childProperty.properties) return getPropertyInPath(childProperty.properties, pathSegments.slice(1).join("."));
2522
+ }
2523
+ }
2524
+ //#endregion
2525
+ //#region src/collections/property_presentation.ts
2526
+ /**
2527
+ * A condition stated as a literal rather than as a rule.
2528
+ *
2529
+ * `PropertyConditions` accepts either, and the two are answered in different
2530
+ * places: a rule needs a context and so can only be evaluated while rendering a
2531
+ * particular entity, but a literal is already the answer. Reading it here is
2532
+ * what makes `hidden: true` work without every gate below having to build a
2533
+ * condition context first — and without the caller reaching for
2534
+ * `{ "==": [1, 1] }` to say something it can say with a boolean.
2535
+ */
2536
+ function literalCondition(condition) {
2537
+ return condition === true;
2538
+ }
2539
+ function isReadOnly(property) {
2540
+ if (property.admin?.readOnly) return true;
2541
+ if (literalCondition(property.conditions?.readOnly)) return true;
2542
+ if (property.type === "date") {
2543
+ if (property.autoValue) return true;
2544
+ }
2545
+ if (property.type === "reference") return !property.path && !("Field" in (property.admin || {}) && property.admin?.Field);
2546
+ return false;
2547
+ }
2548
+ function isHidden(property) {
2549
+ if (literalCondition(property.conditions?.hidden)) return true;
2550
+ return typeof property.admin?.disabled === "object" && Boolean(property.admin?.disabled.hidden);
2551
+ }
2552
+ /**
2553
+ * Whether the field is disabled by its own declaration, ignoring form state.
2554
+ *
2555
+ * The `admin.disabled` block and `conditions.disabled: true` say the same thing
2556
+ * two ways, so every caller that gated on the first now asks here instead of
2557
+ * growing a second check of its own.
2558
+ */
2559
+ function isDisabled(property) {
2560
+ return Boolean(property.admin?.disabled) || literalCondition(property.conditions?.disabled);
2561
+ }
2562
+ function applyPropertyConditions(property, context) {
2563
+ const { conditions } = property;
2564
+ if (!conditions) return property;
2565
+ const result = { ...property };
2566
+ if (conditions.disabled) {
2567
+ if (evaluateCondition(conditions.disabled, context)) {
2568
+ result.admin = result.admin || {};
2569
+ result.admin.disabled = {
2570
+ clearOnDisabled: conditions.clearOnDisabled ?? false,
2571
+ disabledMessage: conditions.disabledMessage,
2572
+ hidden: false
2573
+ };
2574
+ }
2575
+ }
2576
+ if (conditions.hidden) {
2577
+ if (evaluateCondition(conditions.hidden, context)) {
2578
+ result.admin = result.admin || {};
2579
+ result.admin.disabled = {
2580
+ ...typeof result.admin?.disabled === "object" ? result.admin.disabled : {},
2581
+ hidden: true,
2582
+ clearOnDisabled: conditions.clearOnDisabled ?? false
2583
+ };
2584
+ }
2585
+ }
2586
+ if (conditions.readOnly) {
2587
+ if (evaluateCondition(conditions.readOnly, context)) {
2588
+ result.admin = result.admin || {};
2589
+ result.admin.readOnly = true;
2590
+ }
2591
+ }
2592
+ if (conditions.required !== void 0) {
2593
+ const isRequired = evaluateCondition(conditions.required, context);
2594
+ result.validation = {
2595
+ ...result.validation,
2596
+ required: isRequired,
2597
+ requiredMessage: conditions.requiredMessage
2598
+ };
2599
+ }
2600
+ if (context.isNew && conditions.defaultValue !== void 0) result.defaultValue = evaluateCondition(conditions.defaultValue, context);
2601
+ if ("enum" in result && result.enum && (conditions.enumConditions || conditions.allowedEnumValues || conditions.excludedEnumValues)) result.enum = applyEnumConditions(result.enum, conditions, context);
2602
+ if (result.type === "reference") {
2603
+ if (conditions.referencePath) result.path = evaluateCondition(conditions.referencePath, context);
2604
+ if (conditions.referenceFilter) {
2605
+ result.admin = result.admin || {};
2606
+ result.admin.fixedFilter = evaluateCondition(conditions.referenceFilter, context);
2340
2607
  }
2341
2608
  }
2609
+ if (result.type === "array") {
2610
+ if (conditions.canAddElements !== void 0) {
2611
+ result.admin = result.admin || {};
2612
+ result.admin.canAddElements = evaluateCondition(conditions.canAddElements, context);
2613
+ }
2614
+ if (conditions.sortable !== void 0) {
2615
+ result.admin = result.admin || {};
2616
+ result.admin.sortable = evaluateCondition(conditions.sortable, context);
2617
+ }
2618
+ }
2619
+ return result;
2342
2620
  }
2621
+ /**
2622
+ * Convert an object with numeric keys back to an array.
2623
+ * Firestore stores arrays as {"0": "a", "1": "b"} to avoid nested arrays.
2624
+ */
2625
+ function objectToArray(obj) {
2626
+ if (Array.isArray(obj)) return obj.map(String);
2627
+ if (obj && typeof obj === "object") {
2628
+ const keys = Object.keys(obj);
2629
+ if (keys.length > 0 && keys.every((k) => !isNaN(Number(k)))) return keys.sort((a, b) => Number(a) - Number(b)).map((k) => obj[k]).filter((v) => typeof v === "string" || typeof v === "number").map(String);
2630
+ }
2631
+ return [];
2632
+ }
2633
+ /**
2634
+ * Apply enum-specific conditions to filter and modify enum values.
2635
+ */
2636
+ function applyEnumConditions(enumValues, conditions, context) {
2637
+ let result = [...enumValues];
2638
+ if (conditions.allowedEnumValues) {
2639
+ const allowedArray = objectToArray(evaluateCondition(conditions.allowedEnumValues, context));
2640
+ if (allowedArray.length > 0) result = result.filter((ev) => allowedArray.includes(String(ev.id)));
2641
+ }
2642
+ if (conditions.excludedEnumValues) {
2643
+ const excludedArray = objectToArray(evaluateCondition(conditions.excludedEnumValues, context));
2644
+ if (excludedArray.length > 0) result = result.filter((ev) => !excludedArray.includes(String(ev.id)));
2645
+ }
2646
+ if (conditions.enumConditions) result = result.map((ev) => {
2647
+ const evConditions = conditions.enumConditions?.[ev.id];
2648
+ if (!evConditions) return ev;
2649
+ if (evConditions.hidden && evaluateCondition(evConditions.hidden, context)) return null;
2650
+ if (evConditions.disabled && evaluateCondition(evConditions.disabled, context)) return {
2651
+ ...ev,
2652
+ disabled: true
2653
+ };
2654
+ return ev;
2655
+ }).filter((ev) => ev !== null);
2656
+ return result;
2657
+ }
2658
+ //#endregion
2659
+ //#region src/components/common/useColumnsIds.tsx
2343
2660
  function getSubcollectionColumnId(collection) {
2344
2661
  return `subcollection:${collection.slug}`;
2345
2662
  }
2663
+ /**
2664
+ * The jump-to-tab columns a collection table does not need, because the relation
2665
+ * behind them already has a column of its own.
2666
+ *
2667
+ * Every child view gets a 200px button column that opens its tab. For a relation
2668
+ * declared in `relations` that button is the relation's only presence in the
2669
+ * table. For one declared as a property it is the second: the property's own
2670
+ * column is already there, hydrated by the list fetch's `include: ["*"]`, showing
2671
+ * the child rows themselves — and carrying the *same heading*, because the tab
2672
+ * takes its name from the declaring property. Two columns called "Applications",
2673
+ * one of them a button, and nothing in the header to tell them apart.
2674
+ *
2675
+ * The property column wins: it shows what the children are, and each chip in it
2676
+ * opens one. The tab stays reachable by opening the record, which is what the
2677
+ * rest of the table's rows do anyway.
2678
+ *
2679
+ * Unless the author hid that column — `hideFromCollection` on the property, or a
2680
+ * `propertiesOrder` that omits it, is a statement about the column and not about
2681
+ * the relation, so the button comes back rather than the relation dropping out
2682
+ * of the table altogether.
2683
+ */
2684
+ function getRedundantChildViewColumnIds(collection) {
2685
+ const redundant = /* @__PURE__ */ new Set();
2686
+ const properties = collection.properties ?? {};
2687
+ const order = collection.propertiesOrder;
2688
+ for (const [viewKey, propertyKey] of getChildViewDeclaringProperties(collection)) {
2689
+ const property = properties[propertyKey];
2690
+ if (!property || property.admin?.hideFromCollection) continue;
2691
+ if (order && !order.includes(propertyKey)) continue;
2692
+ redundant.add(`subcollection:${viewKey}`);
2693
+ }
2694
+ return redundant;
2695
+ }
2346
2696
  function useColumnIds(collection, includeSubcollections) {
2347
2697
  return useMemo(() => {
2348
2698
  if (collection.propertiesOrder) return hideAndExpandKeys(collection, collection.propertiesOrder);
@@ -2357,8 +2707,10 @@ function hideAndExpandKeys(collection, keys) {
2357
2707
  if (rootProperty && rootProperty.type === "map" && rootProperty.admin?.spreadChildren && rootProperty.properties) rootsWithExplicitChildren.add(rootKey);
2358
2708
  }
2359
2709
  const processedPropertyKeys = /* @__PURE__ */ new Set();
2710
+ const redundantChildViewColumns = getRedundantChildViewColumnIds(collection);
2360
2711
  const result = keys.flatMap((key) => {
2361
2712
  if (processedPropertyKeys.has(key)) return [null];
2713
+ if (redundantChildViewColumns.has(key)) return [null];
2362
2714
  const property = collection.properties[key];
2363
2715
  if (property) {
2364
2716
  processedPropertyKeys.add(key);
@@ -2372,7 +2724,7 @@ function hideAndExpandKeys(collection, keys) {
2372
2724
  }
2373
2725
  return [{
2374
2726
  key,
2375
- disabled: Boolean(property.admin?.disabled) || Boolean(property.admin?.readOnly)
2727
+ disabled: isDisabled(property) || Boolean(property.admin?.readOnly)
2376
2728
  }];
2377
2729
  }
2378
2730
  if (key.includes(".")) {
@@ -2387,7 +2739,7 @@ function hideAndExpandKeys(collection, keys) {
2387
2739
  if (nestedProperty.admin?.disabled && typeof nestedProperty.admin?.disabled === "object" && nestedProperty.admin?.disabled.hidden) return [null];
2388
2740
  return [{
2389
2741
  key,
2390
- disabled: Boolean(rootProperty.admin?.disabled) || Boolean(rootProperty.admin?.readOnly) || Boolean(nestedProperty.admin?.disabled) || Boolean(nestedProperty.admin?.readOnly)
2742
+ disabled: isDisabled(rootProperty) || Boolean(rootProperty.admin?.readOnly) || isDisabled(nestedProperty) || Boolean(nestedProperty.admin?.readOnly)
2391
2743
  }];
2392
2744
  }
2393
2745
  }
@@ -2420,7 +2772,7 @@ function hideAndExpandKeys(collection, keys) {
2420
2772
  } else {
2421
2773
  result.push({
2422
2774
  key: propKey,
2423
- disabled: Boolean(property.admin?.disabled) || Boolean(property.admin?.readOnly)
2775
+ disabled: isDisabled(property) || Boolean(property.admin?.readOnly)
2424
2776
  });
2425
2777
  processedPropertyKeys.add(propKey);
2426
2778
  }
@@ -2439,14 +2791,20 @@ function getDefaultColumnKeys(collection, includeSubCollections) {
2439
2791
  return hideAndExpandKeys(collection, columnIds);
2440
2792
  }
2441
2793
  function getColumnKeysForProperty(property, key, disabled) {
2442
- if (property.type === "map" && property.admin?.spreadChildren && property.properties) return Object.entries(property.properties).flatMap(([childKey, childProperty]) => getColumnKeysForProperty(childProperty, `${key}.${childKey}`, disabled || Boolean(property.admin?.disabled) || Boolean(property.admin?.readOnly)));
2794
+ if (!property) return [];
2795
+ if (property.type === "map" && property.admin?.spreadChildren && property.properties) return Object.entries(property.properties).flatMap(([childKey, childProperty]) => getColumnKeysForProperty(childProperty, `${key}.${childKey}`, disabled || isDisabled(property) || Boolean(property.admin?.readOnly)));
2443
2796
  return [{
2444
2797
  key,
2445
- disabled: disabled || Boolean(property.admin?.disabled) || Boolean(property.admin?.readOnly)
2798
+ disabled: disabled || isDisabled(property) || Boolean(property.admin?.readOnly)
2446
2799
  }];
2447
2800
  }
2448
2801
  function getFormFieldKeys(collection) {
2449
- const propertyKeys = collection.properties ? Object.keys(collection.properties) : [];
2802
+ const properties = collection.properties ?? {};
2803
+ const consumedByChildView = getChildViewRelationPropertyKeys(collection);
2804
+ const propertyKeys = Object.keys(properties).filter((key) => {
2805
+ if (!consumedByChildView.has(key)) return true;
2806
+ return properties[key].admin?.renderInForm === true;
2807
+ });
2450
2808
  const additionalFields = collection.additionalFields ?? [];
2451
2809
  const allKeys = [...propertyKeys, ...additionalFields.map((field) => field.key)];
2452
2810
  if (collection.propertiesOrder) return collection.propertiesOrder.filter((key) => allKeys.includes(key));
@@ -2610,21 +2968,21 @@ function useDataTableController({ path, collection, scrollRestoration, entitiesD
2610
2968
  const orderByParams = sortBy ? [String(sortBy[0]), sortBy[1]] : void 0;
2611
2969
  let unsubscribe;
2612
2970
  const includeParams = getRelationIncludeParams(collection);
2613
- if (accessor.listen) unsubscribe = accessor.listen({
2971
+ if (accessor.listen) unsubscribe = accessor.listen(toFindParams({
2614
2972
  where: whereParams,
2615
2973
  limit: itemCount,
2616
2974
  orderBy: orderByParams,
2617
2975
  searchString,
2618
2976
  include: includeParams
2619
- }, (res) => onEntitiesUpdate(res.data), onError);
2977
+ }), (res) => onEntitiesUpdate(res.data), onError);
2620
2978
  else {
2621
- accessor.find({
2979
+ accessor.find(toFindParams({
2622
2980
  where: whereParams,
2623
2981
  limit: itemCount,
2624
2982
  orderBy: orderByParams,
2625
2983
  searchString,
2626
2984
  include: includeParams
2627
- }).then((res) => onEntitiesUpdate(res.data)).catch(onError);
2985
+ })).then((res) => onEntitiesUpdate(res.data)).catch(onError);
2628
2986
  unsubscribe = () => void 0;
2629
2987
  }
2630
2988
  return unsubscribe;
@@ -9009,6 +9367,133 @@ function UserDisplay({ user }) {
9009
9367
  });
9010
9368
  }
9011
9369
  //#endregion
9370
+ //#region src/components/LoginView/oauth-redirect-flow.ts
9371
+ /**
9372
+ * The browser half of the OAuth authorization-code flow.
9373
+ *
9374
+ * The server only ever sees a `code` and a `redirectUri` taken from the
9375
+ * request body, so `state`, PKCE and the binding between "this browser started
9376
+ * a login" and "this code came back" are, by construction, the client's job.
9377
+ * In the admin login they had been nobody's job: the authorize URL carried no
9378
+ * `state`, and the return leg accepted any `?code=` as long as a localStorage
9379
+ * marker naming the *provider* was set — a marker written on button click and
9380
+ * removed only on a callback that carried a code, so abandoning the flow left
9381
+ * it set indefinitely.
9382
+ *
9383
+ * Everything here is provider-agnostic on purpose. A button that wants a new
9384
+ * provider calls {@link startOAuthRedirect} and gets state, expiry and PKCE
9385
+ * without deciding anything, which is what stops the next provider shipping
9386
+ * without them.
9387
+ */
9388
+ /** sessionStorage, not localStorage: a login attempt belongs to one tab. */
9389
+ var STORAGE_KEY = "rebase_oauth_redirect";
9390
+ /** How long a started authorization stays valid. */
9391
+ var PENDING_TTL_MS = 600 * 1e3;
9392
+ function defaultStorage() {
9393
+ try {
9394
+ return window.sessionStorage;
9395
+ } catch {
9396
+ return null;
9397
+ }
9398
+ }
9399
+ function base64Url(bytes) {
9400
+ let binary = "";
9401
+ for (const byte of bytes) binary += String.fromCharCode(byte);
9402
+ return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
9403
+ }
9404
+ function randomToken(byteLength = 32) {
9405
+ const bytes = new Uint8Array(byteLength);
9406
+ crypto.getRandomValues(bytes);
9407
+ return base64Url(bytes);
9408
+ }
9409
+ async function pkceChallenge(verifier) {
9410
+ const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(verifier));
9411
+ return base64Url(new Uint8Array(digest));
9412
+ }
9413
+ function readPendingOAuthRedirect(storage = defaultStorage()) {
9414
+ if (!storage) return null;
9415
+ const raw = storage.getItem(STORAGE_KEY);
9416
+ if (!raw) return null;
9417
+ try {
9418
+ const parsed = JSON.parse(raw);
9419
+ if (!parsed?.provider || !parsed?.state) return null;
9420
+ return parsed;
9421
+ } catch {
9422
+ return null;
9423
+ }
9424
+ }
9425
+ function clearPendingOAuthRedirect(storage = defaultStorage()) {
9426
+ storage?.removeItem(STORAGE_KEY);
9427
+ }
9428
+ /**
9429
+ * Build the authorize URL and the pending record that will validate its
9430
+ * callback. Split out from {@link startOAuthRedirect} so it can be tested
9431
+ * without navigating.
9432
+ */
9433
+ async function buildOAuthAuthorization(request, now = Date.now()) {
9434
+ const state = randomToken();
9435
+ const url = new URL(request.authorizeUrl);
9436
+ url.searchParams.set("response_type", "code");
9437
+ url.searchParams.set("client_id", request.clientId);
9438
+ url.searchParams.set("redirect_uri", request.redirectUri);
9439
+ url.searchParams.set("scope", request.scope);
9440
+ url.searchParams.set("state", state);
9441
+ for (const [key, value] of Object.entries(request.params ?? {})) url.searchParams.set(key, value);
9442
+ let codeVerifier;
9443
+ if (request.pkce) {
9444
+ codeVerifier = randomToken(32);
9445
+ url.searchParams.set("code_challenge", await pkceChallenge(codeVerifier));
9446
+ url.searchParams.set("code_challenge_method", "S256");
9447
+ }
9448
+ return {
9449
+ url: url.toString(),
9450
+ pending: {
9451
+ provider: request.provider,
9452
+ state,
9453
+ codeVerifier,
9454
+ redirectUri: request.redirectUri,
9455
+ startedAt: now
9456
+ }
9457
+ };
9458
+ }
9459
+ /** Remember the authorization and send the browser to the provider. */
9460
+ async function startOAuthRedirect(request, storage = defaultStorage()) {
9461
+ const { url, pending } = await buildOAuthAuthorization(request);
9462
+ storage?.setItem(STORAGE_KEY, JSON.stringify(pending));
9463
+ window.location.href = url;
9464
+ }
9465
+ /**
9466
+ * Interpret a return from the provider.
9467
+ *
9468
+ * Clears the pending record on **every** path that saw a callback — including
9469
+ * `?error=` and a state mismatch — so a declined or abandoned consent screen
9470
+ * cannot leave the browser primed to accept somebody else's code later.
9471
+ */
9472
+ function consumeOAuthCallback(search, storage = defaultStorage(), now = Date.now()) {
9473
+ const params = new URLSearchParams(search);
9474
+ const code = params.get("code");
9475
+ const state = params.get("state");
9476
+ const error = params.get("error");
9477
+ if (!code && !error && !state) return { status: "none" };
9478
+ const pending = readPendingOAuthRedirect(storage);
9479
+ clearPendingOAuthRedirect(storage);
9480
+ if (error) return {
9481
+ status: "error",
9482
+ error
9483
+ };
9484
+ if (!code) return { status: "mismatch" };
9485
+ if (!pending) return { status: "mismatch" };
9486
+ if (now - pending.startedAt > PENDING_TTL_MS) return { status: "mismatch" };
9487
+ if (!state || state !== pending.state) return { status: "mismatch" };
9488
+ return {
9489
+ status: "ok",
9490
+ provider: pending.provider,
9491
+ code,
9492
+ codeVerifier: pending.codeVerifier,
9493
+ redirectUri: pending.redirectUri
9494
+ };
9495
+ }
9496
+ //#endregion
9012
9497
  //#region src/components/LoginView/LoginView.tsx
9013
9498
  /**
9014
9499
  * The shared field background mixin (`dark:bg-black/30`) is invisible on the
@@ -9078,19 +9563,25 @@ function LoginView({ logo, authController, noUserComponent, disableSignupScreen
9078
9563
  return () => clearTimeout(timer);
9079
9564
  }, []);
9080
9565
  useEffect(() => {
9081
- const code = new URLSearchParams(window.location.search).get("code");
9082
- const provider = localStorage.getItem("rebase_oauth_provider");
9083
- if (code && provider) {
9084
- localStorage.removeItem("rebase_oauth_provider");
9085
- const cleanUrl = window.location.origin + window.location.pathname;
9086
- window.history.replaceState({}, document.title, cleanUrl);
9087
- if (authController.oauthLogin) authController.oauthLogin(provider, {
9088
- code,
9089
- redirectUri: cleanUrl
9090
- }).catch((err) => {
9091
- console.error(`${provider} login failed:`, err);
9092
- });
9566
+ const result = consumeOAuthCallback(window.location.search);
9567
+ if (result.status === "none") return;
9568
+ const cleanUrl = window.location.origin + window.location.pathname;
9569
+ window.history.replaceState({}, document.title, cleanUrl);
9570
+ if (result.status === "error") {
9571
+ console.error(`OAuth sign-in failed: ${result.error}`);
9572
+ return;
9573
+ }
9574
+ if (result.status === "mismatch") {
9575
+ console.error("Ignoring an OAuth code that does not match a sign-in this browser started.");
9576
+ return;
9093
9577
  }
9578
+ if (authController.oauthLogin) authController.oauthLogin(result.provider, {
9579
+ code: result.code,
9580
+ redirectUri: result.redirectUri,
9581
+ ...result.codeVerifier ? { codeVerifier: result.codeVerifier } : {}
9582
+ }).catch((err) => {
9583
+ console.error(`${result.provider} login failed:`, err);
9584
+ });
9094
9585
  }, [authController]);
9095
9586
  let logoComponent;
9096
9587
  if (logo) logoComponent = /* @__PURE__ */ jsx("img", {
@@ -9361,10 +9852,13 @@ var GitHubIcon = () => /* @__PURE__ */ jsx("svg", {
9361
9852
  });
9362
9853
  function GitHubLoginButton({ disabled, githubClientId }) {
9363
9854
  const handleClick = () => {
9364
- localStorage.setItem("rebase_oauth_provider", "github");
9365
- const redirectUri = encodeURIComponent(window.location.origin + window.location.pathname);
9366
- const scope = "read:user,user:email";
9367
- window.location.href = `https://github.com/login/oauth/authorize?client_id=${githubClientId}&redirect_uri=${redirectUri}&scope=${scope}`;
9855
+ startOAuthRedirect({
9856
+ provider: "github",
9857
+ authorizeUrl: "https://github.com/login/oauth/authorize",
9858
+ clientId: githubClientId,
9859
+ redirectUri: window.location.origin + window.location.pathname,
9860
+ scope: "read:user,user:email"
9861
+ });
9368
9862
  };
9369
9863
  return /* @__PURE__ */ jsx(LoginButton, {
9370
9864
  disabled,
@@ -9382,10 +9876,13 @@ var LinkedInIcon = () => /* @__PURE__ */ jsx("svg", {
9382
9876
  });
9383
9877
  function LinkedInLoginButton({ disabled, linkedinClientId }) {
9384
9878
  const handleClick = () => {
9385
- localStorage.setItem("rebase_oauth_provider", "linkedin");
9386
- const redirectUri = encodeURIComponent(window.location.origin + window.location.pathname);
9387
- const scope = "openid profile email";
9388
- window.location.href = `https://www.linkedin.com/oauth/v2/authorization?response_type=code&client_id=${linkedinClientId}&redirect_uri=${redirectUri}&scope=${scope}`;
9879
+ startOAuthRedirect({
9880
+ provider: "linkedin",
9881
+ authorizeUrl: "https://www.linkedin.com/oauth/v2/authorization",
9882
+ clientId: linkedinClientId,
9883
+ redirectUri: window.location.origin + window.location.pathname,
9884
+ scope: "openid profile email"
9885
+ });
9389
9886
  };
9390
9887
  return /* @__PURE__ */ jsx(LoginButton, {
9391
9888
  disabled,
@@ -9821,6 +10318,7 @@ var en = {
9821
10318
  column_cannot_be_edited: "This column can't be edited directly",
9822
10319
  close: "Close",
9823
10320
  hide_list: "Hide list",
10321
+ show_list: "Show list",
9824
10322
  unsaved_local_changes: "Unsaved Local changes",
9825
10323
  unsaved_local_changes_description: "This document was edited locally and has unsaved changes. These local changes will be lost if you don't apply them.",
9826
10324
  preview_changes: "Preview Changes",
@@ -10120,6 +10618,7 @@ var en = {
10120
10618
  flatten_arrays: "Flatten arrays",
10121
10619
  download: "Download",
10122
10620
  large_number_of_documents: "This collection has a large number of documents ({{count}}).",
10621
+ too_many_documents_to_export: "This collection has more documents ({{count}}) than the browser can export in a single file (limit {{limit}}). Filter it down before exporting.",
10123
10622
  include_undefined_values: "Include undefined values",
10124
10623
  submit: "Submit",
10125
10624
  no_filterable_properties: "No filterable properties available",
@@ -10632,6 +11131,7 @@ var en = {
10632
11131
  inspect_record: "Inspect record",
10633
11132
  inspect: "Inspect",
10634
11133
  close_inspector: "Close inspector",
11134
+ open_in_live_site: "Open in the live site",
10635
11135
  history_revert: "Revert to this version",
10636
11136
  history_revert_title: "Revert data to this version?",
10637
11137
  history_reverted: "Reverted to selected version",
@@ -10720,6 +11220,7 @@ var es = {
10720
11220
  column_cannot_be_edited: "Esta columna no se puede editar directamente",
10721
11221
  close: "Cerrar",
10722
11222
  hide_list: "Ocultar lista",
11223
+ show_list: "Mostrar lista",
10723
11224
  unsaved_local_changes: "Cambios locales sin guardar",
10724
11225
  unsaved_local_changes_description: "Este documento ha sido editado localmente y tiene cambios sin guardar. Estos cambios locales se perderán si no los aplicas.",
10725
11226
  preview_changes: "Previsualizar cambios",
@@ -11016,6 +11517,7 @@ var es = {
11016
11517
  flatten_arrays: "Aplanar arrays",
11017
11518
  download: "Descargar",
11018
11519
  large_number_of_documents: "Esta colección posee un gran número de documentos ({{count}}).",
11520
+ too_many_documents_to_export: "Esta colección tiene más documentos ({{count}}) de los que el navegador puede exportar en un solo archivo (límite {{limit}}). Fíltrala antes de exportar.",
11019
11521
  include_undefined_values: "Incluir valores omitidos (undefined)",
11020
11522
  submit: "Enviar",
11021
11523
  no_filterable_properties: "No hay propiedades filtrables disponibles",
@@ -11576,6 +12078,7 @@ var de = {
11576
12078
  column_cannot_be_edited: "Diese Spalte kann nicht direkt bearbeitet werden",
11577
12079
  close: "Schließen",
11578
12080
  hide_list: "Liste ausblenden",
12081
+ show_list: "Liste einblenden",
11579
12082
  unsaved_local_changes: "Ungespeicherte lokale Änderungen",
11580
12083
  unsaved_local_changes_description: "Dieses Dokument wurde lokal bearbeitet und weist ungespeicherte Änderungen auf. Diese lokalen Änderungen gehen verloren, wenn Sie sie nicht anwenden.",
11581
12084
  preview_changes: "Änderungen in der Vorschau anzeigen",
@@ -11872,6 +12375,7 @@ var de = {
11872
12375
  flatten_arrays: "Arrays glätten",
11873
12376
  download: "Herunterladen",
11874
12377
  large_number_of_documents: "Diese Sammlung hat eine große Anzahl von Dokumenten ({{count}}).",
12378
+ too_many_documents_to_export: "Diese Sammlung enthält mehr Dokumente ({{count}}), als der Browser in einer einzigen Datei exportieren kann (Grenze {{limit}}). Filtern Sie sie vor dem Export.",
11875
12379
  include_undefined_values: "Undefinierte Werte einschließen",
11876
12380
  submit: "Einreichen",
11877
12381
  no_filterable_properties: "Keine filterbaren Eigenschaften verfügbar",
@@ -12422,6 +12926,7 @@ var fr = {
12422
12926
  column_cannot_be_edited: "Cette colonne ne peut pas être modifiée directement",
12423
12927
  close: "Fermer",
12424
12928
  hide_list: "Masquer la liste",
12929
+ show_list: "Afficher la liste",
12425
12930
  unsaved_local_changes: "Modifications locales non enregistrées",
12426
12931
  unsaved_local_changes_description: "Ce document a été modifié localement et contient des modifications non enregistrées. Ces modifications locales seront perdues si vous ne les appliquez pas.",
12427
12932
  preview_changes: "Aperçu des modifications",
@@ -12718,6 +13223,7 @@ var fr = {
12718
13223
  flatten_arrays: "Aplatir les tableaux",
12719
13224
  download: "Télécharger",
12720
13225
  large_number_of_documents: "Cette collection contient un grand nombre de documents ({{count}}).",
13226
+ too_many_documents_to_export: "Cette collection contient plus de documents ({{count}}) que le navigateur ne peut en exporter dans un seul fichier (limite {{limit}}). Filtrez-la avant d'exporter.",
12721
13227
  include_undefined_values: "Inclure les valeurs non définies",
12722
13228
  submit: "Soumettre",
12723
13229
  no_filterable_properties: "Aucune propriété filtrable disponible",
@@ -13268,6 +13774,7 @@ var it = {
13268
13774
  column_cannot_be_edited: "Questa colonna non può essere modificata direttamente",
13269
13775
  close: "Chiudi",
13270
13776
  hide_list: "Nascondi elenco",
13777
+ show_list: "Mostra elenco",
13271
13778
  unsaved_local_changes: "Modifiche locali non salvate",
13272
13779
  unsaved_local_changes_description: "Questo documento è stato modificato localmente e ha modifiche non salvate. Queste modifiche locali andranno perse se non le applichi.",
13273
13780
  preview_changes: "Anteprima modifiche",
@@ -13564,6 +14071,7 @@ var it = {
13564
14071
  flatten_arrays: "Appiattisci gli array",
13565
14072
  download: "Scarica",
13566
14073
  large_number_of_documents: "Questa collezione contiene un numero elevato di documenti ({{count}})",
14074
+ too_many_documents_to_export: "Questa collezione contiene più documenti ({{count}}) di quanti il browser possa esportarne in un solo file (limite {{limit}}). Filtrala prima di esportare.",
13567
14075
  include_undefined_values: "Includi valori non definiti",
13568
14076
  submit: "Invia",
13569
14077
  no_filterable_properties: "Nessuna proprietà filtrabile disponibile",
@@ -14106,7 +14614,7 @@ var hi = {
14106
14614
  group_by: "इसके द्वारा समूहित करें",
14107
14615
  initialize_kanban_order: "कानबन क्रम प्रारंभ करें",
14108
14616
  copy_id: "ID कॉपी करें",
14109
- add_specific: "विशिष्ट {{name}} जोड़ें",
14617
+ add_specific: "{{name}} जोड़ें",
14110
14618
  select_specific: "विशिष्ट {{name}} चुनें",
14111
14619
  select_from: "{{name}} से चुनें",
14112
14620
  done: "हो गया",
@@ -14114,6 +14622,7 @@ var hi = {
14114
14622
  column_cannot_be_edited: "इस कॉलम को सीधे संपादित नहीं किया जा सकता",
14115
14623
  close: "बंद करें",
14116
14624
  hide_list: "सूची छिपाएँ",
14625
+ show_list: "सूची दिखाएँ",
14117
14626
  unsaved_local_changes: "सहेजे नहीं गए स्थानीय परिवर्तन",
14118
14627
  unsaved_local_changes_description: "इस दस्तावेज़ को स्थानीय स्तर पर संपादित किया गया था और इसमें सहेजे नहीं गए परिवर्तन हैं। यदि आप इन्हें लागू नहीं करते हैं तो ये स्थानीय परिवर्तन खो जाएंगे।",
14119
14628
  preview_changes: "परिवर्तनों का पूर्वावलोकन करें",
@@ -14410,6 +14919,7 @@ var hi = {
14410
14919
  flatten_arrays: "ऐरे को फ़्लैट करें",
14411
14920
  download: "डाउनलोड",
14412
14921
  large_number_of_documents: "इस संग्रह में बड़ी संख्या में दस्तावेज़ हैं ({{count}})।",
14922
+ too_many_documents_to_export: "इस संग्रह में इतने दस्तावेज़ ({{count}}) हैं कि ब्राउज़र उन्हें एक ही फ़ाइल में निर्यात नहीं कर सकता (सीमा {{limit}})। निर्यात से पहले इसे फ़िल्टर करें।",
14413
14923
  include_undefined_values: "अपरिभाषित (undefined) मान शामिल करें",
14414
14924
  submit: "सबमिट करें",
14415
14925
  no_filterable_properties: "कोई फ़िल्टर करने योग्य गुण उपलब्ध नहीं हैं",
@@ -14965,6 +15475,7 @@ var pt = {
14965
15475
  column_cannot_be_edited: "Esta coluna não pode ser editada diretamente",
14966
15476
  close: "Fechar",
14967
15477
  hide_list: "Ocultar lista",
15478
+ show_list: "Mostrar lista",
14968
15479
  unsaved_local_changes: "Alterações locais não guardadas",
14969
15480
  unsaved_local_changes_description: "Este documento foi editado localmente e tem alterações não guardadas. Estas alterações locais serão perdidas se não as aplicar.",
14970
15481
  preview_changes: "Pré-visualizar alterações",
@@ -15261,6 +15772,7 @@ var pt = {
15261
15772
  flatten_arrays: "Achatar listas",
15262
15773
  download: "Descarregar",
15263
15774
  large_number_of_documents: "Esta coleção tem um grande número de documentos ({{count}}).",
15775
+ too_many_documents_to_export: "Esta coleção tem mais documentos ({{count}}) do que o navegador consegue exportar num único ficheiro (limite {{limit}}). Filtre-a antes de exportar.",
15264
15776
  include_undefined_values: "Incluir valores indefinidos",
15265
15777
  submit: "Submeter",
15266
15778
  no_filterable_properties: "Não há propriedades filtráveis disponíveis",
@@ -15840,6 +16352,11 @@ function Rebase(props) {
15840
16352
  if (new Set(keys).size !== keys.length) console.error("Duplicate plugin keys detected:", keys.filter((k, i) => keys.indexOf(k) !== i));
15841
16353
  }
15842
16354
  const resolvedSlots = useMemo(() => [...directSlots, ...(plugins ?? []).flatMap((p) => p.slots ?? [])], [directSlots, plugins]);
16355
+ useEffect(() => {
16356
+ const dead = new Set(UNRENDERED_SLOTS);
16357
+ const registered = new Set(resolvedSlots.map((s) => s.slot).filter((slot) => dead.has(slot)));
16358
+ for (const slot of registered) console.warn(`[Rebase] A component is registered for the "${slot}" slot, which this build declares but renders nowhere — nothing will appear. This is a gap in Rebase, not in your plugin.`);
16359
+ }, [resolvedSlots]);
15843
16360
  const clientAuthController = useAuthSubscription(authControllerProp ? void 0 : client?.auth);
15844
16361
  const authController = authControllerProp ?? clientAuthController;
15845
16362
  const normalizedDataSources = useMemo(() => {
@@ -16186,6 +16703,7 @@ function useRebaseAuthController(props = {}) {
16186
16703
  const syncState = async (event, session) => {
16187
16704
  await updateState(session);
16188
16705
  if (event === "SIGNED_OUT") {
16706
+ clearFetchCache();
16189
16707
  if (isMountedRef.current) setLoginSkipped(false);
16190
16708
  onSignOutRef.current?.();
16191
16709
  }
@@ -16201,7 +16719,9 @@ function useRebaseAuthController(props = {}) {
16201
16719
  });
16202
16720
  auth.getAuthConfig().then((config) => {
16203
16721
  if (isMountedRef.current) setAuthConfig(config);
16204
- }).catch(() => {});
16722
+ }).catch((e) => {
16723
+ console.warn("[Rebase] Could not load the backend auth configuration; the login view will fall back to defaults.", e);
16724
+ });
16205
16725
  const unsubscribe = auth.onAuthStateChange(syncState);
16206
16726
  return () => {
16207
16727
  isMountedRef.current = false;
@@ -16636,6 +17156,21 @@ function createFormexStub(values) {
16636
17156
  //#endregion
16637
17157
  //#region src/util/entity_cache.ts
16638
17158
  var LOCAL_STORAGE_PREFIX = "entity_cache::";
17159
+ /**
17160
+ * The in-session handoff channel: an edit in flight, parked here by the layout
17161
+ * that is being replaced and picked up by the one taking over — the split's
17162
+ * "hide list", full screen's "show list", the side panel's "open full screen".
17163
+ * A record loaded from it opens *dirty*, because that is what it is for.
17164
+ *
17165
+ * Deliberately **not** hydrated from `sessionStorage`, and it is the one thing
17166
+ * here that isn't. The persisted half of this file is the local-changes backup:
17167
+ * a draft that outlives a reload, offered back through the banner or applied on
17168
+ * open, according to the collection's `localChangesBackup`. Loading those
17169
+ * drafts into this map on startup made every one of them look like a handoff —
17170
+ * the first visit to the record after a reload silently opened dirty carrying a
17171
+ * draft the user had walked away from, and the banner that exists to ask about
17172
+ * it never appeared.
17173
+ */
16639
17174
  var entityCache = /* @__PURE__ */ new Map();
16640
17175
  var isSessionStorageAvailable = typeof sessionStorage !== "undefined";
16641
17176
  function customReplacer(key) {
@@ -16690,25 +17225,12 @@ function customReviver(_key, value) {
16690
17225
  }
16691
17226
  return value;
16692
17227
  }
16693
- if (isSessionStorageAvailable) try {
16694
- for (let i = 0; i < sessionStorage.length; i++) {
16695
- const fullKey = sessionStorage.key(i);
16696
- if (fullKey && fullKey.startsWith(LOCAL_STORAGE_PREFIX)) {
16697
- const path = fullKey.substring(14);
16698
- const entityString = sessionStorage.getItem(fullKey);
16699
- if (entityString) try {
16700
- const entity = JSON.parse(entityString, customReviver);
16701
- entityCache.set(path, entity);
16702
- } catch (parseError) {
16703
- console.error(`Failed to parse entity for path "${path}" from sessionStorage:`, parseError);
16704
- }
16705
- }
16706
- }
16707
- } catch (error) {
16708
- console.error("Error accessing sessionStorage during initialization:", error);
16709
- }
16710
17228
  /**
16711
- * Saves data to the in-memory cache and persists it individually in `sessionStorage`.
17229
+ * Saves data to the local-changes backup in `sessionStorage`.
17230
+ *
17231
+ * The backup only — a draft that survives a reload is not an edit in flight.
17232
+ * {@link saveEntityToMemoryCache} is the other channel; see {@link entityCache}
17233
+ * for what separates them.
16712
17234
  * @param path - The unique path/key for the data.
16713
17235
  * @param data - The data to cache and persist.
16714
17236
  */
@@ -16721,9 +17243,18 @@ function saveEntityToCache(path, data) {
16721
17243
  console.error(`Failed to save entity for path "${path}" to sessionStorage:`, error);
16722
17244
  }
16723
17245
  }
17246
+ /**
17247
+ * Consumes a handoff. The channel is one-shot: whoever picks an edit up owns it
17248
+ * from then on, and an edit left behind here is one that reopens a record dirty
17249
+ * long after the user stopped looking at it.
17250
+ */
16724
17251
  function removeEntityFromMemoryCache(path) {
16725
17252
  entityCache.delete(path);
16726
17253
  }
17254
+ /**
17255
+ * Parks an edit in flight for the layout about to take over. See
17256
+ * {@link entityCache}: this is the handoff, not the backup.
17257
+ */
16727
17258
  function saveEntityToMemoryCache(path, data) {
16728
17259
  entityCache.set(path, data);
16729
17260
  }
@@ -16731,8 +17262,11 @@ function getEntityFromMemoryCache(path) {
16731
17262
  return entityCache.get(path);
16732
17263
  }
16733
17264
  /**
16734
- * Retrieves a entity from the in-memory cache or `sessionStorage`.
16735
- * If the entity is not in the cache but exists in `sessionStorage`, it loads it into the cache.
17265
+ * Retrieves a entity from the local-changes backup in `sessionStorage`.
17266
+ *
17267
+ * The backup only — the handoff channel is read through
17268
+ * {@link getEntityFromMemoryCache}, and the two mean different things to the
17269
+ * form that receives them.
16736
17270
  * @param path - The unique path/key for the entity.
16737
17271
  * @returns The cached entity or `undefined` if not found.
16738
17272
  */
@@ -17359,8 +17893,82 @@ function getAdminEntityChildViews(collection) {
17359
17893
  }));
17360
17894
  }
17361
17895
  //#endregion
17896
+ //#region src/collections/entity-display.ts
17897
+ /**
17898
+ * Collections that have already warned about a deprecated field, so a list of
17899
+ * fifty rows produces one line in the console rather than fifty.
17900
+ */
17901
+ var deprecationWarned = /* @__PURE__ */ new Set();
17902
+ function collectionId(collection) {
17903
+ return collection.slug ?? collection.name ?? "collection";
17904
+ }
17905
+ function warnOnce(id, message) {
17906
+ const key = `${id}:${message}`;
17907
+ if (deprecationWarned.has(key)) return;
17908
+ deprecationWarned.add(key);
17909
+ console.warn(message);
17910
+ }
17911
+ /**
17912
+ * What the collection declares for a role, before deciding which form it is.
17913
+ *
17914
+ * `display.title` wins over the deprecated `titleProperty`: a collection setting
17915
+ * both is mid-migration, and the new field is the one it means.
17916
+ */
17917
+ function getDeclaredSource(collection, role) {
17918
+ const declared = collection.display?.[role];
17919
+ if (declared !== void 0) return declared;
17920
+ if (role === "title" && collection.titleProperty) {
17921
+ const id = collectionId(collection);
17922
+ warnOnce(id, `[rebase] Collection "${id}" uses admin.titleProperty, which is deprecated. Move it to admin.display.title — the same string works there, and display.title also accepts a resolver for a title the record does not carry.`);
17923
+ return collection.titleProperty;
17924
+ }
17925
+ }
17926
+ /**
17927
+ * The property path a role is declared to read, when it is declared as a path.
17928
+ *
17929
+ * Returns `undefined` for a role filled by a resolver — a resolver has no key —
17930
+ * and for a role the collection says nothing about, which is then derived.
17931
+ *
17932
+ * @group Collections
17933
+ */
17934
+ function getDisplayPropertyKey(collection, role) {
17935
+ const declared = getDeclaredSource(collection, role);
17936
+ return typeof declared === "string" ? declared : void 0;
17937
+ }
17938
+ /**
17939
+ * The resolver a role is declared to use, when it is declared as one.
17940
+ *
17941
+ * @group Collections
17942
+ */
17943
+ function getDisplayResolver(collection, role) {
17944
+ const declared = getDeclaredSource(collection, role);
17945
+ return typeof declared === "function" ? declared : void 0;
17946
+ }
17947
+ /**
17948
+ * True when the collection states this role at all, in either form.
17949
+ *
17950
+ * The derivation is a guess about what a collection probably means; a statement
17951
+ * outranks it, and the heuristics that look for "the first enum" or "the leading
17952
+ * relation" have to stand down when one exists.
17953
+ *
17954
+ * @group Collections
17955
+ */
17956
+ function hasDeclaredDisplay(collection, role) {
17957
+ return getDeclaredSource(collection, role) !== void 0;
17958
+ }
17959
+ //#endregion
17362
17960
  //#region src/collections/entity_image_preview.ts
17961
+ /**
17962
+ * The property that fills a record's image slot.
17963
+ *
17964
+ * `admin.display.image` first, then six fallbacks in descending confidence —
17965
+ * the first image-typed storage property, an array of them, a URL rendered as an
17966
+ * image, and so on. The ladder stays for collections that say nothing; a
17967
+ * collection that names its picture is not guessed at.
17968
+ */
17363
17969
  function getEntityImagePreviewPropertyKey(collection) {
17970
+ const declared = getDisplayPropertyKey(collection, "image");
17971
+ if (declared) return declared;
17364
17972
  for (const key in collection.properties) {
17365
17973
  const property = collection.properties[key];
17366
17974
  if (property.type === "string" && property.storage?.acceptedFiles?.includes("image/*")) return key;
@@ -17387,6 +17995,142 @@ function getEntityImagePreviewPropertyKey(collection) {
17387
17995
  }
17388
17996
  }
17389
17997
  //#endregion
17998
+ //#region src/collections/entity-display-cache.ts
17999
+ /** The identity of one role of one record, as a cache key. */
18000
+ function entityDisplayKey(path, entityId, role) {
18001
+ return `${role} ${path} ${entityId ?? ""}`;
18002
+ }
18003
+ var EntityDisplayCache = class {
18004
+ entries = /* @__PURE__ */ new Map();
18005
+ listeners = /* @__PURE__ */ new Set();
18006
+ /**
18007
+ * The resolved value, or `undefined` when this pair has not been resolved
18008
+ * yet. `null` is a resolved absence, and the two must stay distinct: a
18009
+ * caller that reads "not yet" as "nothing" flickers its fallback in on every
18010
+ * mount.
18011
+ */
18012
+ peek(key) {
18013
+ const entry = this.entries.get(key);
18014
+ if (!entry || "promise" in entry) return void 0;
18015
+ return entry.value;
18016
+ }
18017
+ /** True while a resolution for this pair is in flight. */
18018
+ isLoading(key) {
18019
+ const entry = this.entries.get(key);
18020
+ return Boolean(entry && "promise" in entry);
18021
+ }
18022
+ /**
18023
+ * Resolve once per record and role. Concurrent callers share the first
18024
+ * call's promise; later callers get the cached value with no promise at all.
18025
+ *
18026
+ * A resolver that throws is recorded as "nothing" rather than retried: the
18027
+ * alternative is every render re-running a call that just failed. And it is
18028
+ * reported here, which is a correction.
18029
+ *
18030
+ * It used to say "the caller that saw the rejection is the one that logs
18031
+ * it", and `useEntityDisplay` duly attached a `.catch()` that warned. But
18032
+ * both failure paths below swallow and return a *resolved* promise, so that
18033
+ * catch could never run — the two halves each did the reasonable thing and
18034
+ * between them the log was unreachable. A resolver that blew up produced a
18035
+ * blank chip and total silence, which is the failure mode
18036
+ * `EntityDisplayResolver`'s own contract ("treated as `undefined` and logged
18037
+ * once") exists to rule out.
18038
+ *
18039
+ * Reporting belongs here for the reason the caller could not do it: this is
18040
+ * the one place that runs exactly once per key, so "once" is a property of
18041
+ * the code rather than a hope about how many components mount.
18042
+ */
18043
+ resolve(key, resolver) {
18044
+ const entry = this.entries.get(key);
18045
+ if (entry) return "promise" in entry ? entry.promise : Promise.resolve(entry.value);
18046
+ let produced;
18047
+ try {
18048
+ produced = resolver();
18049
+ } catch (error) {
18050
+ this.fail(key, error);
18051
+ return Promise.resolve(null);
18052
+ }
18053
+ if (!isPromise(produced)) {
18054
+ const value = normalize(produced);
18055
+ this.set(key, value);
18056
+ return Promise.resolve(value);
18057
+ }
18058
+ const promise = produced.then((resolved) => {
18059
+ const value = normalize(resolved);
18060
+ this.set(key, value);
18061
+ return value;
18062
+ }).catch((error) => {
18063
+ this.fail(key, error);
18064
+ return null;
18065
+ });
18066
+ this.entries.set(key, { promise });
18067
+ return promise;
18068
+ }
18069
+ /**
18070
+ * Drop what is known about a record, so the next ask resolves again. Called
18071
+ * after a write: the row that just saved may be called something else now.
18072
+ */
18073
+ invalidate(path, entityId) {
18074
+ const suffix = ` ${path} ${entityId ?? ""}`;
18075
+ let changed = false;
18076
+ for (const key of [...this.entries.keys()]) if (entityId === void 0 ? key.includes(` ${path} `) || key.endsWith(` ${path} `) : key.endsWith(suffix)) {
18077
+ this.entries.delete(key);
18078
+ changed = true;
18079
+ }
18080
+ if (changed) this.emit();
18081
+ }
18082
+ /** Drop everything. The user signed out, or the app swapped datasource. */
18083
+ clear() {
18084
+ if (this.entries.size === 0) return;
18085
+ this.entries.clear();
18086
+ this.emit();
18087
+ }
18088
+ subscribe(listener) {
18089
+ this.listeners.add(listener);
18090
+ return () => {
18091
+ this.listeners.delete(listener);
18092
+ };
18093
+ }
18094
+ set(key, value) {
18095
+ this.entries.set(key, { value });
18096
+ this.emit();
18097
+ }
18098
+ /**
18099
+ * Record a failed resolution as "nothing", and say so once.
18100
+ *
18101
+ * The key is the message: it is `<role> <path> <id>`, which is exactly what a
18102
+ * reader needs to find the resolver that blew up. A warning with no key would
18103
+ * tell them a display resolver failed somewhere in a list of fifty rows.
18104
+ *
18105
+ * `console.warn` rather than a thrown error, because this runs while a row is
18106
+ * rendering: the contract is that a title which cannot be fetched must not
18107
+ * take down the row that shows it.
18108
+ */
18109
+ fail(key, error) {
18110
+ console.warn(`[rebase] Could not resolve display value for ${key}:`, error);
18111
+ this.set(key, null);
18112
+ }
18113
+ emit() {
18114
+ for (const listener of [...this.listeners]) listener();
18115
+ }
18116
+ };
18117
+ /**
18118
+ * Empty strings and empty arrays are absences, not values — a title of `" "`
18119
+ * would otherwise beat the derived one and render as a blank heading.
18120
+ */
18121
+ function normalize(value) {
18122
+ if (value === void 0 || value === null) return null;
18123
+ if (typeof value === "string") {
18124
+ const trimmed = value.trim();
18125
+ return trimmed.length > 0 ? trimmed : null;
18126
+ }
18127
+ if (Array.isArray(value)) return value.length > 0 ? value : null;
18128
+ return value;
18129
+ }
18130
+ function isPromise(value) {
18131
+ return typeof value?.then === "function";
18132
+ }
18133
+ //#endregion
17390
18134
  //#region src/collections/filter-operator-resolution.ts
17391
18135
  /**
17392
18136
  * Default operators offered per property type, before engine capabilities and
@@ -17497,115 +18241,6 @@ function resolveFilterOperators({ property, isArray, engine }) {
17497
18241
  return typeDefaults.filter((op) => engineOps.has(op) && (narrowingSet === void 0 || narrowingSet.has(op)));
17498
18242
  }
17499
18243
  //#endregion
17500
- //#region src/collections/property_presentation.ts
17501
- function isReadOnly(property) {
17502
- if (property.admin?.readOnly) return true;
17503
- if (property.type === "date") {
17504
- if (property.autoValue) return true;
17505
- }
17506
- if (property.type === "reference") return !property.path && !("Field" in (property.admin || {}) && property.admin?.Field);
17507
- return false;
17508
- }
17509
- function isHidden(property) {
17510
- return typeof property.admin?.disabled === "object" && Boolean(property.admin?.disabled.hidden);
17511
- }
17512
- function applyPropertyConditions(property, context) {
17513
- const { conditions } = property;
17514
- if (!conditions) return property;
17515
- const result = { ...property };
17516
- if (conditions.disabled) {
17517
- if (evaluateCondition(conditions.disabled, context)) {
17518
- result.admin = result.admin || {};
17519
- result.admin.disabled = {
17520
- clearOnDisabled: conditions.clearOnDisabled ?? false,
17521
- disabledMessage: conditions.disabledMessage,
17522
- hidden: false
17523
- };
17524
- }
17525
- }
17526
- if (conditions.hidden) {
17527
- if (evaluateCondition(conditions.hidden, context)) {
17528
- result.admin = result.admin || {};
17529
- result.admin.disabled = {
17530
- ...typeof result.admin?.disabled === "object" ? result.admin.disabled : {},
17531
- hidden: true,
17532
- clearOnDisabled: conditions.clearOnDisabled ?? false
17533
- };
17534
- }
17535
- }
17536
- if (conditions.readOnly) {
17537
- if (evaluateCondition(conditions.readOnly, context)) {
17538
- result.admin = result.admin || {};
17539
- result.admin.readOnly = true;
17540
- }
17541
- }
17542
- if (conditions.required !== void 0) {
17543
- const isRequired = evaluateCondition(conditions.required, context);
17544
- result.validation = {
17545
- ...result.validation,
17546
- required: isRequired,
17547
- requiredMessage: conditions.requiredMessage
17548
- };
17549
- }
17550
- if (context.isNew && conditions.defaultValue !== void 0) result.defaultValue = evaluateCondition(conditions.defaultValue, context);
17551
- if ("enum" in result && result.enum && (conditions.enumConditions || conditions.allowedEnumValues || conditions.excludedEnumValues)) result.enum = applyEnumConditions(result.enum, conditions, context);
17552
- if (result.type === "reference") {
17553
- if (conditions.referencePath) result.path = evaluateCondition(conditions.referencePath, context);
17554
- if (conditions.referenceFilter) {
17555
- result.admin = result.admin || {};
17556
- result.admin.fixedFilter = evaluateCondition(conditions.referenceFilter, context);
17557
- }
17558
- }
17559
- if (result.type === "array") {
17560
- if (conditions.canAddElements !== void 0) {
17561
- result.admin = result.admin || {};
17562
- result.admin.canAddElements = evaluateCondition(conditions.canAddElements, context);
17563
- }
17564
- if (conditions.sortable !== void 0) {
17565
- result.admin = result.admin || {};
17566
- result.admin.sortable = evaluateCondition(conditions.sortable, context);
17567
- }
17568
- }
17569
- return result;
17570
- }
17571
- /**
17572
- * Convert an object with numeric keys back to an array.
17573
- * Firestore stores arrays as {"0": "a", "1": "b"} to avoid nested arrays.
17574
- */
17575
- function objectToArray(obj) {
17576
- if (Array.isArray(obj)) return obj.map(String);
17577
- if (obj && typeof obj === "object") {
17578
- const keys = Object.keys(obj);
17579
- if (keys.length > 0 && keys.every((k) => !isNaN(Number(k)))) return keys.sort((a, b) => Number(a) - Number(b)).map((k) => obj[k]).filter((v) => typeof v === "string" || typeof v === "number").map(String);
17580
- }
17581
- return [];
17582
- }
17583
- /**
17584
- * Apply enum-specific conditions to filter and modify enum values.
17585
- */
17586
- function applyEnumConditions(enumValues, conditions, context) {
17587
- let result = [...enumValues];
17588
- if (conditions.allowedEnumValues) {
17589
- const allowedArray = objectToArray(evaluateCondition(conditions.allowedEnumValues, context));
17590
- if (allowedArray.length > 0) result = result.filter((ev) => allowedArray.includes(String(ev.id)));
17591
- }
17592
- if (conditions.excludedEnumValues) {
17593
- const excludedArray = objectToArray(evaluateCondition(conditions.excludedEnumValues, context));
17594
- if (excludedArray.length > 0) result = result.filter((ev) => !excludedArray.includes(String(ev.id)));
17595
- }
17596
- if (conditions.enumConditions) result = result.map((ev) => {
17597
- const evConditions = conditions.enumConditions?.[ev.id];
17598
- if (!evConditions) return ev;
17599
- if (evConditions.hidden && evaluateCondition(evConditions.hidden, context)) return null;
17600
- if (evConditions.disabled && evaluateCondition(evConditions.disabled, context)) return {
17601
- ...ev,
17602
- disabled: true
17603
- };
17604
- return ev;
17605
- }).filter((ev) => ev !== null);
17606
- return result;
17607
- }
17608
- //#endregion
17609
18244
  //#region src/collections/form-layout.ts
17610
18245
  /**
17611
18246
  * Columns in the form grid.
@@ -17796,7 +18431,8 @@ function resolveFormLayout({ collection, fieldKeys, status }) {
17796
18431
  title: section.title,
17797
18432
  collapsible: section.collapsible ?? titled,
17798
18433
  collapsed: titled ? Boolean(section.collapsed) : false,
17799
- fields
18434
+ fields,
18435
+ readVariant: section.readVariant
17800
18436
  });
17801
18437
  }
17802
18438
  const leftovers = fieldKeys.map((key) => available.get(key)).filter((f) => Boolean(f));
@@ -18079,18 +18715,6 @@ function isHidden$1(property) {
18079
18715
  return Boolean(property.admin?.hideFromCollection);
18080
18716
  }
18081
18717
  /**
18082
- * File-storage backed content (single image, array of images, generic upload…).
18083
- * Rendered by the dedicated image slot, so never a title.
18084
- */
18085
- function isStorageProperty(property) {
18086
- if (property.type === "string" && (property.storage || property.admin?.urlPreview === "image")) return true;
18087
- if (property.type === "array" && property.of && !Array.isArray(property.of)) {
18088
- const inner = property.of;
18089
- if (inner.type === "string" && (inner.storage || inner.admin?.urlPreview === "image")) return true;
18090
- }
18091
- return false;
18092
- }
18093
- /**
18094
18718
  * Every column on this collection that stores a foreign key: the `localKey` of
18095
18719
  * each owning relation (declared inline on a property or in `relations[]`), the
18096
18720
  * source column of a junction, and the key a many-to-many joins on.
@@ -18170,7 +18794,8 @@ function scoreTitleCandidate(property, key, idKeys, foreignKeys) {
18170
18794
  */
18171
18795
  function getTitlePropertyCandidates(collection) {
18172
18796
  if (!collection.properties) return [];
18173
- if (collection.titleProperty && collection.properties[collection.titleProperty]) return [collection.titleProperty];
18797
+ const declared = getDisplayPropertyKey(collection, "title");
18798
+ if (declared && getPropertyInPath(collection.properties, declared)) return [declared];
18174
18799
  const idKeys = new Set(getPrimaryKeys(collection));
18175
18800
  const foreignKeys = getForeignKeyColumns(collection);
18176
18801
  const explicitOrder = collection.propertiesOrder;
@@ -18213,7 +18838,7 @@ function getTitlePropertyCandidates(collection) {
18213
18838
  */
18214
18839
  function getLeadingRelationTitleKey(collection) {
18215
18840
  if (!collection.properties) return void 0;
18216
- if (collection.titleProperty) return void 0;
18841
+ if (hasDeclaredDisplay(collection, "title")) return void 0;
18217
18842
  const idKeys = new Set(getPrimaryKeys(collection));
18218
18843
  const foreignKeys = getForeignKeyColumns(collection);
18219
18844
  const order = collection.propertiesOrder ?? Object.keys(collection.properties);
@@ -18274,6 +18899,6 @@ function getTitlePropertyKeyForValues(collection, values, entityId) {
18274
18899
  return candidates[0];
18275
18900
  }
18276
18901
  //#endregion
18277
- export { AIIcon, AIModifiedIndicator, AdminModeControllerContext, AdminModeControllerProvider, AnalyticsContext, ApiConfigProvider, AuthControllerContext, CONTAINER_FULL_WIDTH, CollectionComponentOverrideProvider, CollectionResolverRegistrationContext, CollectionScopeContext, CollectionScopeProvider, ComponentOverrideContext, ConfirmationDialog, CrmDashboardDemo, CustomizationControllerContext, DEFAULT_API_PATH, DEFAULT_PAGE_SIZE, DataDriverContext, DataSourcesContext, DialogsControllerContext, DialogsProvider, EffectiveRoleControllerContext, EffectiveRoleControllerProvider, 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, SchemaDriftBanner, SchemaDriftProvider, SnackbarProvider, StorageSourceContext, StorageSourcesContext, StudioBridgeContext, StudioBridgeProvider, StudioBridgeRegistryContext, StudioBridgeRegistryProvider, UIReferenceView, UIStyleGuide, UnsavedChangesDialog, UserConfigurationPersistenceContext, UserDisplay, UserSelectPopover, UserSettingsView, addInitialSlash, apiBaseOf, applyPropertyConditions, buildCollapsedDefaults, buildEnumLabel, clearAuthConfigCache, clearFetchCache, createAuthConfigCache, createFormexStub, deleteEntityWithCallbacks, deriveSpan, en, es, fetchAuthConfig, fillRows, flattenKeys, getAdminEntityChildViews, getAdminSubcollections, getCollectionBySlugWithin, getCollectionPathsCombinations, getColorScheme, getColumnKeysForProperty, getEntityFromCache, getEntityFromMemoryCache, getEntityImagePreviewPropertyKey, getEntityPreviewKeys, getEntityTitlePropertyKey, getFormFieldKeys, getIcon, getLastSegment, getLeadingRelationTitleKey, getLocalChangesBackup, getNavigationEntriesFromPath, getParentReferencesFromPath, getRelationIncludeParams, getRowHeight, getSubcollectionColumnId, getTitlePropertyCandidates, getTitlePropertyKey, getTitlePropertyKeyForValues, iconsSearch, isAuditTimestamp, isEnumValueDisabled, isFilterableRelation, isHidden, isIdPropertyEditable, isReadOnly, isSchemaDriftError, looksLikeIdentifierValue, populateFetchCache, removeEntityFromCache, removeEntityFromMemoryCache, removeInitialAndTrailingSlashes, removeInitialSlash, 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, usePermissions, useRebaseAuthController, useRebaseClient, useRebaseContext, useRebaseRegistry, useRebaseRegistryDispatch, useRelationSelector, useResolvedComponent, useRestoreScroll, useSchemaDriftContext, useScrollRestoration, useSlot, useSnackbarController, useStorageSource, useStorageSources, useStorageUploadController, useStudioBreadcrumbs, useStudioCapabilities, useStudioCollectionRegistry, useStudioNavigationState, useStudioSidePanelController, useStudioUrlController, useTranslation, useUnsavedChangesDialog, useUserConfigurationPersistence };
18902
+ export { AIIcon, AIModifiedIndicator, AdminModeControllerContext, AdminModeControllerProvider, AnalyticsContext, ApiConfigProvider, AuthControllerContext, CONTAINER_FULL_WIDTH, CollectionComponentOverrideProvider, CollectionResolverRegistrationContext, CollectionScopeContext, CollectionScopeProvider, ComponentOverrideContext, ConfirmationDialog, CrmDashboardDemo, 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, UIReferenceView, UIStyleGuide, 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, removeEntityFromCache, removeEntityFromMemoryCache, removeInitialAndTrailingSlashes, removeInitialSlash, 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, usePermissions, useRebaseAuthController, useRebaseClient, useRebaseContext, useRebaseRegistry, useRebaseRegistryDispatch, useRelationSelector, useResolvedComponent, useRestoreScroll, useSchemaDriftContext, useScrollRestoration, useSlot, useSnackbarController, useStorageSource, useStorageSources, useStorageUploadController, useStudioBreadcrumbs, useStudioCapabilities, useStudioCollectionRegistry, useStudioNavigationState, useStudioSidePanelController, useStudioUrlController, useTranslation, useUnsavedChangesDialog, useUserConfigurationPersistence };
18278
18903
 
18279
18904
  //# sourceMappingURL=index.es.js.map