@rebasepro/app 0.13.0 → 0.13.1-canary.g3660bd5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.es.js CHANGED
@@ -1,6 +1,7 @@
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
7
  import { buildRebaseData, canCreateEntity, canDeleteEntity, canEditEntity, canReadCollection, evaluateCondition, getEntityChildViews, getLabelOrConfigFrom, getPrimaryKeys, getSubcollections, isPropertyBuilder, resolveStorageFilenameString, resolveStoragePathString, resolveStorageSource, stripCollectionPath, wrapAsEntityData, wrapAsSdkData } from "@rebasepro/common";
@@ -13,7 +14,6 @@ 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,97 @@ function SchemaDriftBanner({ className }) {
414
414
  });
415
415
  }
416
416
  //#endregion
417
+ //#region src/collections/summary-property.ts
418
+ /**
419
+ * Whether a property can stand in for a record in *one line*.
420
+ *
421
+ * Preview surfaces — the reference card, a list row, a board card — have a
422
+ * single line per property and no way to grow. That is a constraint on the
423
+ * *value*, not on the property's importance: a Markdown biography is the most
424
+ * interesting column on an author, and it is still the wrong thing to paste
425
+ * into a 44px card. Ranking it here means the picker can skip it without
426
+ * anyone having to configure their way out of a broken card.
427
+ *
428
+ * Kept beside {@link getTitlePropertyCandidates} because the two answer
429
+ * neighbouring questions — "what is this record called" and "what else can be
430
+ * said about it in a line" — from the same property schema.
431
+ *
432
+ * @group Collections
433
+ */
434
+ var SUMMARY_RANK = {
435
+ /**
436
+ * No single-line form exists. A map renders as a key/value table and an
437
+ * array of maps as a stack of them; there is no first line to take.
438
+ */
439
+ UNUSABLE: 0,
440
+ /**
441
+ * Readable in one line, but only as an excerpt — the property holds a
442
+ * document, and the line is its opening. Worth showing when nothing better
443
+ * is available, never in preference to a value that fits whole.
444
+ */
445
+ EXCERPT: 1,
446
+ /** Short and self-contained: the line *is* the value. */
447
+ DIRECT: 2
448
+ };
449
+ /**
450
+ * File-storage backed content (single image, array of images, generic upload…).
451
+ * Preview surfaces give these their own image slot, so they are never also a
452
+ * text line.
453
+ *
454
+ * @group Collections
455
+ */
456
+ function isStorageProperty(property) {
457
+ if (!property) return false;
458
+ if (property.type === "string" && (property.storage || property.admin?.urlPreview === "image")) return true;
459
+ if (property.type === "array" && property.of && !Array.isArray(property.of)) {
460
+ const inner = property.of;
461
+ if (inner.type === "string" && (inner.storage || inner.admin?.urlPreview === "image")) return true;
462
+ }
463
+ return false;
464
+ }
465
+ /**
466
+ * True when the property holds free text long enough to be a document rather
467
+ * than a value — Markdown, or an explicitly multi-line string.
468
+ *
469
+ * @group Collections
470
+ */
471
+ function isLongTextProperty(property) {
472
+ if (!property || property.type !== "string") return false;
473
+ return Boolean(property.admin?.markdown || property.admin?.multiline);
474
+ }
475
+ /**
476
+ * How well a property reads as one line of a preview. See {@link SUMMARY_RANK}.
477
+ *
478
+ * Decided from the property *schema* only: no values are consulted, so the
479
+ * answer is stable for a collection and can be memoised per collection rather
480
+ * than per row.
481
+ *
482
+ * @group Collections
483
+ */
484
+ function rankSummaryProperty(property) {
485
+ if (!property) return SUMMARY_RANK.UNUSABLE;
486
+ if (isStorageProperty(property)) return SUMMARY_RANK.UNUSABLE;
487
+ if (property.type === "map") return SUMMARY_RANK.UNUSABLE;
488
+ if (property.type === "array") {
489
+ if (Array.isArray(property.of)) return SUMMARY_RANK.UNUSABLE;
490
+ if (!property.of) return property.oneOf ? SUMMARY_RANK.UNUSABLE : SUMMARY_RANK.DIRECT;
491
+ const innerRank = rankSummaryProperty(property.of);
492
+ if (innerRank === SUMMARY_RANK.UNUSABLE) return SUMMARY_RANK.UNUSABLE;
493
+ if (innerRank === SUMMARY_RANK.EXCERPT) return SUMMARY_RANK.UNUSABLE;
494
+ return SUMMARY_RANK.DIRECT;
495
+ }
496
+ if (isLongTextProperty(property)) return SUMMARY_RANK.EXCERPT;
497
+ return SUMMARY_RANK.DIRECT;
498
+ }
499
+ /**
500
+ * True when the property has any single-line form at all.
501
+ *
502
+ * @group Collections
503
+ */
504
+ function canSummariseProperty(property) {
505
+ return rankSummaryProperty(property) !== SUMMARY_RANK.UNUSABLE;
506
+ }
507
+ //#endregion
417
508
  //#region src/util/previews.ts
418
509
  function isReferenceProperty(property) {
419
510
  if (!property) return null;
@@ -434,35 +525,47 @@ function isHiddenProperty(property) {
434
525
  return Boolean(property.admin?.hideFromCollection);
435
526
  }
436
527
  /**
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.
528
+ * The properties a preview surface should render for a record, best first.
529
+ *
530
+ * Three things decide the answer, in this order:
531
+ *
532
+ * 1. `previewProperties` — passed in, or declared on the collection. A stated
533
+ * list is returned verbatim, ranking and limit included: a developer who
534
+ * asks for the Markdown biography gets the Markdown biography.
535
+ * 2. Whether the value has a one-line form at all. A map renders as a
536
+ * key/value table and a Markdown field as a document; neither fits a card
537
+ * line, so they never take a slot from a value that does. See
538
+ * {@link rankSummaryProperty}.
539
+ * 3. `propertiesOrder`, which breaks ties.
540
+ *
541
+ * The middle step is the one that is easy to get wrong by leaving out.
542
+ * `propertiesOrder` states the *column* order of a collection table — it is
543
+ * not a statement that the first three columns summarise a record, and reading
544
+ * it as one is how a card ends up rendering somebody's entire biography.
440
545
  */
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
546
  function getEntityPreviewKeys(authController, targetCollection, fields, previewProperties, limit = 3) {
452
547
  const allProperties = Object.keys(targetCollection.properties);
453
548
  let listProperties = previewProperties?.filter((p) => allProperties.includes(p));
454
549
  if (!listProperties && targetCollection.previewProperties) listProperties = targetCollection.previewProperties?.filter((p) => allProperties.includes(p));
455
550
  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);
551
+ const hasExplicitOrder = !!targetCollection.propertiesOrder;
552
+ const order = targetCollection.propertiesOrder || allProperties;
553
+ const direct = [];
554
+ const excerpt = [];
555
+ for (const key of order) {
556
+ const property = targetCollection.properties[key];
557
+ if (!property || isPropertyBuilder(property)) continue;
558
+ if (key === "id" || "isId" in property && Boolean(property.isId)) continue;
559
+ if (isReferenceProperty(property)) continue;
560
+ if (!hasExplicitOrder && isRelationProperty(property)) continue;
561
+ if (isHiddenProperty(property)) continue;
562
+ if (isStorageProperty(property)) continue;
563
+ const rank = rankSummaryProperty(property);
564
+ if (rank === SUMMARY_RANK.UNUSABLE) continue;
565
+ (rank === SUMMARY_RANK.EXCERPT ? excerpt : direct).push(key);
566
+ if (direct.length >= limit) break;
465
567
  }
568
+ return [...direct, ...excerpt].slice(0, limit);
466
569
  }
467
570
  var INCLUDE_ALL_RELATIONS = ["*"];
468
571
  /**
@@ -1254,7 +1357,15 @@ function useAuthSubscription(authClient) {
1254
1357
  setInitialLoading(true);
1255
1358
  authClient.getUser().then((user) => {
1256
1359
  if (user) setUser(user);
1257
- }).catch((e) => {}).finally(() => {
1360
+ setAuthError(void 0);
1361
+ }).catch((e) => {
1362
+ const status = e?.status;
1363
+ if (status === 401 || status === 403) {
1364
+ setAuthError(void 0);
1365
+ return;
1366
+ }
1367
+ setAuthError(e instanceof Error ? e : new Error(String(e)));
1368
+ }).finally(() => {
1258
1369
  setInitialLoading(false);
1259
1370
  });
1260
1371
  }
@@ -2325,21 +2436,32 @@ function useDebouncedData(data, deps, timeoutMs = 5e3) {
2325
2436
  return immediateUpdate ? data : deferredData;
2326
2437
  }
2327
2438
  //#endregion
2328
- //#region src/components/common/useColumnsIds.tsx
2439
+ //#region src/collections/property-path.ts
2329
2440
  /**
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.
2441
+ * The property at a dotted path, walking `map` children — `address.street`.
2442
+ *
2443
+ * The value counterpart is `getValueInPath` in `@rebasepro/utils`; this is the
2444
+ * schema half, and the two have to be used together. Reading a dotted path off
2445
+ * an entity while looking its property up with a flat `properties[path]` gives
2446
+ * the value and `undefined` for how to render it, which is how a declared title
2447
+ * on a nested field silently fell back to a derived one.
2448
+ *
2449
+ * There were three copies of this: one private to `useColumnsIds`, one exported
2450
+ * from the admin layer, and the flat lookup in the title resolver that was not
2451
+ * this function at all. This is the one, in the lowest layer that needs it —
2452
+ * admin re-exports it under the name it already published.
2332
2453
  */
2333
2454
  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("."));
2340
- }
2455
+ if (typeof properties !== "object" || !properties) return void 0;
2456
+ if (path in properties) return properties[path];
2457
+ if (path.includes(".")) {
2458
+ const pathSegments = path.split(".");
2459
+ const childProperty = properties[pathSegments[0]];
2460
+ if (typeof childProperty === "object" && childProperty?.type === "map" && childProperty.properties) return getPropertyInPath(childProperty.properties, pathSegments.slice(1).join("."));
2341
2461
  }
2342
2462
  }
2463
+ //#endregion
2464
+ //#region src/components/common/useColumnsIds.tsx
2343
2465
  function getSubcollectionColumnId(collection) {
2344
2466
  return `subcollection:${collection.slug}`;
2345
2467
  }
@@ -9821,6 +9943,7 @@ var en = {
9821
9943
  column_cannot_be_edited: "This column can't be edited directly",
9822
9944
  close: "Close",
9823
9945
  hide_list: "Hide list",
9946
+ show_list: "Show list",
9824
9947
  unsaved_local_changes: "Unsaved Local changes",
9825
9948
  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
9949
  preview_changes: "Preview Changes",
@@ -10632,6 +10755,7 @@ var en = {
10632
10755
  inspect_record: "Inspect record",
10633
10756
  inspect: "Inspect",
10634
10757
  close_inspector: "Close inspector",
10758
+ open_in_live_site: "Open in the live site",
10635
10759
  history_revert: "Revert to this version",
10636
10760
  history_revert_title: "Revert data to this version?",
10637
10761
  history_reverted: "Reverted to selected version",
@@ -10720,6 +10844,7 @@ var es = {
10720
10844
  column_cannot_be_edited: "Esta columna no se puede editar directamente",
10721
10845
  close: "Cerrar",
10722
10846
  hide_list: "Ocultar lista",
10847
+ show_list: "Mostrar lista",
10723
10848
  unsaved_local_changes: "Cambios locales sin guardar",
10724
10849
  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
10850
  preview_changes: "Previsualizar cambios",
@@ -11576,6 +11701,7 @@ var de = {
11576
11701
  column_cannot_be_edited: "Diese Spalte kann nicht direkt bearbeitet werden",
11577
11702
  close: "Schließen",
11578
11703
  hide_list: "Liste ausblenden",
11704
+ show_list: "Liste einblenden",
11579
11705
  unsaved_local_changes: "Ungespeicherte lokale Änderungen",
11580
11706
  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
11707
  preview_changes: "Änderungen in der Vorschau anzeigen",
@@ -12422,6 +12548,7 @@ var fr = {
12422
12548
  column_cannot_be_edited: "Cette colonne ne peut pas être modifiée directement",
12423
12549
  close: "Fermer",
12424
12550
  hide_list: "Masquer la liste",
12551
+ show_list: "Afficher la liste",
12425
12552
  unsaved_local_changes: "Modifications locales non enregistrées",
12426
12553
  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
12554
  preview_changes: "Aperçu des modifications",
@@ -13268,6 +13395,7 @@ var it = {
13268
13395
  column_cannot_be_edited: "Questa colonna non può essere modificata direttamente",
13269
13396
  close: "Chiudi",
13270
13397
  hide_list: "Nascondi elenco",
13398
+ show_list: "Mostra elenco",
13271
13399
  unsaved_local_changes: "Modifiche locali non salvate",
13272
13400
  unsaved_local_changes_description: "Questo documento è stato modificato localmente e ha modifiche non salvate. Queste modifiche locali andranno perse se non le applichi.",
13273
13401
  preview_changes: "Anteprima modifiche",
@@ -14114,6 +14242,7 @@ var hi = {
14114
14242
  column_cannot_be_edited: "इस कॉलम को सीधे संपादित नहीं किया जा सकता",
14115
14243
  close: "बंद करें",
14116
14244
  hide_list: "सूची छिपाएँ",
14245
+ show_list: "सूची दिखाएँ",
14117
14246
  unsaved_local_changes: "सहेजे नहीं गए स्थानीय परिवर्तन",
14118
14247
  unsaved_local_changes_description: "इस दस्तावेज़ को स्थानीय स्तर पर संपादित किया गया था और इसमें सहेजे नहीं गए परिवर्तन हैं। यदि आप इन्हें लागू नहीं करते हैं तो ये स्थानीय परिवर्तन खो जाएंगे।",
14119
14248
  preview_changes: "परिवर्तनों का पूर्वावलोकन करें",
@@ -14965,6 +15094,7 @@ var pt = {
14965
15094
  column_cannot_be_edited: "Esta coluna não pode ser editada diretamente",
14966
15095
  close: "Fechar",
14967
15096
  hide_list: "Ocultar lista",
15097
+ show_list: "Mostrar lista",
14968
15098
  unsaved_local_changes: "Alterações locais não guardadas",
14969
15099
  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
15100
  preview_changes: "Pré-visualizar alterações",
@@ -15840,6 +15970,11 @@ function Rebase(props) {
15840
15970
  if (new Set(keys).size !== keys.length) console.error("Duplicate plugin keys detected:", keys.filter((k, i) => keys.indexOf(k) !== i));
15841
15971
  }
15842
15972
  const resolvedSlots = useMemo(() => [...directSlots, ...(plugins ?? []).flatMap((p) => p.slots ?? [])], [directSlots, plugins]);
15973
+ useEffect(() => {
15974
+ const dead = new Set(UNRENDERED_SLOTS);
15975
+ const registered = new Set(resolvedSlots.map((s) => s.slot).filter((slot) => dead.has(slot)));
15976
+ 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.`);
15977
+ }, [resolvedSlots]);
15843
15978
  const clientAuthController = useAuthSubscription(authControllerProp ? void 0 : client?.auth);
15844
15979
  const authController = authControllerProp ?? clientAuthController;
15845
15980
  const normalizedDataSources = useMemo(() => {
@@ -16201,7 +16336,9 @@ function useRebaseAuthController(props = {}) {
16201
16336
  });
16202
16337
  auth.getAuthConfig().then((config) => {
16203
16338
  if (isMountedRef.current) setAuthConfig(config);
16204
- }).catch(() => {});
16339
+ }).catch((e) => {
16340
+ console.warn("[Rebase] Could not load the backend auth configuration; the login view will fall back to defaults.", e);
16341
+ });
16205
16342
  const unsubscribe = auth.onAuthStateChange(syncState);
16206
16343
  return () => {
16207
16344
  isMountedRef.current = false;
@@ -17359,8 +17496,82 @@ function getAdminEntityChildViews(collection) {
17359
17496
  }));
17360
17497
  }
17361
17498
  //#endregion
17499
+ //#region src/collections/entity-display.ts
17500
+ /**
17501
+ * Collections that have already warned about a deprecated field, so a list of
17502
+ * fifty rows produces one line in the console rather than fifty.
17503
+ */
17504
+ var deprecationWarned = /* @__PURE__ */ new Set();
17505
+ function collectionId(collection) {
17506
+ return collection.slug ?? collection.name ?? "collection";
17507
+ }
17508
+ function warnOnce(id, message) {
17509
+ const key = `${id}:${message}`;
17510
+ if (deprecationWarned.has(key)) return;
17511
+ deprecationWarned.add(key);
17512
+ console.warn(message);
17513
+ }
17514
+ /**
17515
+ * What the collection declares for a role, before deciding which form it is.
17516
+ *
17517
+ * `display.title` wins over the deprecated `titleProperty`: a collection setting
17518
+ * both is mid-migration, and the new field is the one it means.
17519
+ */
17520
+ function getDeclaredSource(collection, role) {
17521
+ const declared = collection.display?.[role];
17522
+ if (declared !== void 0) return declared;
17523
+ if (role === "title" && collection.titleProperty) {
17524
+ const id = collectionId(collection);
17525
+ 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.`);
17526
+ return collection.titleProperty;
17527
+ }
17528
+ }
17529
+ /**
17530
+ * The property path a role is declared to read, when it is declared as a path.
17531
+ *
17532
+ * Returns `undefined` for a role filled by a resolver — a resolver has no key —
17533
+ * and for a role the collection says nothing about, which is then derived.
17534
+ *
17535
+ * @group Collections
17536
+ */
17537
+ function getDisplayPropertyKey(collection, role) {
17538
+ const declared = getDeclaredSource(collection, role);
17539
+ return typeof declared === "string" ? declared : void 0;
17540
+ }
17541
+ /**
17542
+ * The resolver a role is declared to use, when it is declared as one.
17543
+ *
17544
+ * @group Collections
17545
+ */
17546
+ function getDisplayResolver(collection, role) {
17547
+ const declared = getDeclaredSource(collection, role);
17548
+ return typeof declared === "function" ? declared : void 0;
17549
+ }
17550
+ /**
17551
+ * True when the collection states this role at all, in either form.
17552
+ *
17553
+ * The derivation is a guess about what a collection probably means; a statement
17554
+ * outranks it, and the heuristics that look for "the first enum" or "the leading
17555
+ * relation" have to stand down when one exists.
17556
+ *
17557
+ * @group Collections
17558
+ */
17559
+ function hasDeclaredDisplay(collection, role) {
17560
+ return getDeclaredSource(collection, role) !== void 0;
17561
+ }
17562
+ //#endregion
17362
17563
  //#region src/collections/entity_image_preview.ts
17564
+ /**
17565
+ * The property that fills a record's image slot.
17566
+ *
17567
+ * `admin.display.image` first, then six fallbacks in descending confidence —
17568
+ * the first image-typed storage property, an array of them, a URL rendered as an
17569
+ * image, and so on. The ladder stays for collections that say nothing; a
17570
+ * collection that names its picture is not guessed at.
17571
+ */
17363
17572
  function getEntityImagePreviewPropertyKey(collection) {
17573
+ const declared = getDisplayPropertyKey(collection, "image");
17574
+ if (declared) return declared;
17364
17575
  for (const key in collection.properties) {
17365
17576
  const property = collection.properties[key];
17366
17577
  if (property.type === "string" && property.storage?.acceptedFiles?.includes("image/*")) return key;
@@ -17387,6 +17598,142 @@ function getEntityImagePreviewPropertyKey(collection) {
17387
17598
  }
17388
17599
  }
17389
17600
  //#endregion
17601
+ //#region src/collections/entity-display-cache.ts
17602
+ /** The identity of one role of one record, as a cache key. */
17603
+ function entityDisplayKey(path, entityId, role) {
17604
+ return `${role} ${path} ${entityId ?? ""}`;
17605
+ }
17606
+ var EntityDisplayCache = class {
17607
+ entries = /* @__PURE__ */ new Map();
17608
+ listeners = /* @__PURE__ */ new Set();
17609
+ /**
17610
+ * The resolved value, or `undefined` when this pair has not been resolved
17611
+ * yet. `null` is a resolved absence, and the two must stay distinct: a
17612
+ * caller that reads "not yet" as "nothing" flickers its fallback in on every
17613
+ * mount.
17614
+ */
17615
+ peek(key) {
17616
+ const entry = this.entries.get(key);
17617
+ if (!entry || "promise" in entry) return void 0;
17618
+ return entry.value;
17619
+ }
17620
+ /** True while a resolution for this pair is in flight. */
17621
+ isLoading(key) {
17622
+ const entry = this.entries.get(key);
17623
+ return Boolean(entry && "promise" in entry);
17624
+ }
17625
+ /**
17626
+ * Resolve once per record and role. Concurrent callers share the first
17627
+ * call's promise; later callers get the cached value with no promise at all.
17628
+ *
17629
+ * A resolver that throws is recorded as "nothing" rather than retried: the
17630
+ * alternative is every render re-running a call that just failed. And it is
17631
+ * reported here, which is a correction.
17632
+ *
17633
+ * It used to say "the caller that saw the rejection is the one that logs
17634
+ * it", and `useEntityDisplay` duly attached a `.catch()` that warned. But
17635
+ * both failure paths below swallow and return a *resolved* promise, so that
17636
+ * catch could never run — the two halves each did the reasonable thing and
17637
+ * between them the log was unreachable. A resolver that blew up produced a
17638
+ * blank chip and total silence, which is the failure mode
17639
+ * `EntityDisplayResolver`'s own contract ("treated as `undefined` and logged
17640
+ * once") exists to rule out.
17641
+ *
17642
+ * Reporting belongs here for the reason the caller could not do it: this is
17643
+ * the one place that runs exactly once per key, so "once" is a property of
17644
+ * the code rather than a hope about how many components mount.
17645
+ */
17646
+ resolve(key, resolver) {
17647
+ const entry = this.entries.get(key);
17648
+ if (entry) return "promise" in entry ? entry.promise : Promise.resolve(entry.value);
17649
+ let produced;
17650
+ try {
17651
+ produced = resolver();
17652
+ } catch (error) {
17653
+ this.fail(key, error);
17654
+ return Promise.resolve(null);
17655
+ }
17656
+ if (!isPromise(produced)) {
17657
+ const value = normalize(produced);
17658
+ this.set(key, value);
17659
+ return Promise.resolve(value);
17660
+ }
17661
+ const promise = produced.then((resolved) => {
17662
+ const value = normalize(resolved);
17663
+ this.set(key, value);
17664
+ return value;
17665
+ }).catch((error) => {
17666
+ this.fail(key, error);
17667
+ return null;
17668
+ });
17669
+ this.entries.set(key, { promise });
17670
+ return promise;
17671
+ }
17672
+ /**
17673
+ * Drop what is known about a record, so the next ask resolves again. Called
17674
+ * after a write: the row that just saved may be called something else now.
17675
+ */
17676
+ invalidate(path, entityId) {
17677
+ const suffix = ` ${path} ${entityId ?? ""}`;
17678
+ let changed = false;
17679
+ for (const key of [...this.entries.keys()]) if (entityId === void 0 ? key.includes(` ${path} `) || key.endsWith(` ${path} `) : key.endsWith(suffix)) {
17680
+ this.entries.delete(key);
17681
+ changed = true;
17682
+ }
17683
+ if (changed) this.emit();
17684
+ }
17685
+ /** Drop everything. The user signed out, or the app swapped datasource. */
17686
+ clear() {
17687
+ if (this.entries.size === 0) return;
17688
+ this.entries.clear();
17689
+ this.emit();
17690
+ }
17691
+ subscribe(listener) {
17692
+ this.listeners.add(listener);
17693
+ return () => {
17694
+ this.listeners.delete(listener);
17695
+ };
17696
+ }
17697
+ set(key, value) {
17698
+ this.entries.set(key, { value });
17699
+ this.emit();
17700
+ }
17701
+ /**
17702
+ * Record a failed resolution as "nothing", and say so once.
17703
+ *
17704
+ * The key is the message: it is `<role> <path> <id>`, which is exactly what a
17705
+ * reader needs to find the resolver that blew up. A warning with no key would
17706
+ * tell them a display resolver failed somewhere in a list of fifty rows.
17707
+ *
17708
+ * `console.warn` rather than a thrown error, because this runs while a row is
17709
+ * rendering: the contract is that a title which cannot be fetched must not
17710
+ * take down the row that shows it.
17711
+ */
17712
+ fail(key, error) {
17713
+ console.warn(`[rebase] Could not resolve display value for ${key}:`, error);
17714
+ this.set(key, null);
17715
+ }
17716
+ emit() {
17717
+ for (const listener of [...this.listeners]) listener();
17718
+ }
17719
+ };
17720
+ /**
17721
+ * Empty strings and empty arrays are absences, not values — a title of `" "`
17722
+ * would otherwise beat the derived one and render as a blank heading.
17723
+ */
17724
+ function normalize(value) {
17725
+ if (value === void 0 || value === null) return null;
17726
+ if (typeof value === "string") {
17727
+ const trimmed = value.trim();
17728
+ return trimmed.length > 0 ? trimmed : null;
17729
+ }
17730
+ if (Array.isArray(value)) return value.length > 0 ? value : null;
17731
+ return value;
17732
+ }
17733
+ function isPromise(value) {
17734
+ return typeof value?.then === "function";
17735
+ }
17736
+ //#endregion
17390
17737
  //#region src/collections/filter-operator-resolution.ts
17391
17738
  /**
17392
17739
  * Default operators offered per property type, before engine capabilities and
@@ -17796,7 +18143,8 @@ function resolveFormLayout({ collection, fieldKeys, status }) {
17796
18143
  title: section.title,
17797
18144
  collapsible: section.collapsible ?? titled,
17798
18145
  collapsed: titled ? Boolean(section.collapsed) : false,
17799
- fields
18146
+ fields,
18147
+ readVariant: section.readVariant
17800
18148
  });
17801
18149
  }
17802
18150
  const leftovers = fieldKeys.map((key) => available.get(key)).filter((f) => Boolean(f));
@@ -18079,18 +18427,6 @@ function isHidden$1(property) {
18079
18427
  return Boolean(property.admin?.hideFromCollection);
18080
18428
  }
18081
18429
  /**
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
18430
  * Every column on this collection that stores a foreign key: the `localKey` of
18095
18431
  * each owning relation (declared inline on a property or in `relations[]`), the
18096
18432
  * source column of a junction, and the key a many-to-many joins on.
@@ -18170,7 +18506,8 @@ function scoreTitleCandidate(property, key, idKeys, foreignKeys) {
18170
18506
  */
18171
18507
  function getTitlePropertyCandidates(collection) {
18172
18508
  if (!collection.properties) return [];
18173
- if (collection.titleProperty && collection.properties[collection.titleProperty]) return [collection.titleProperty];
18509
+ const declared = getDisplayPropertyKey(collection, "title");
18510
+ if (declared && getPropertyInPath(collection.properties, declared)) return [declared];
18174
18511
  const idKeys = new Set(getPrimaryKeys(collection));
18175
18512
  const foreignKeys = getForeignKeyColumns(collection);
18176
18513
  const explicitOrder = collection.propertiesOrder;
@@ -18213,7 +18550,7 @@ function getTitlePropertyCandidates(collection) {
18213
18550
  */
18214
18551
  function getLeadingRelationTitleKey(collection) {
18215
18552
  if (!collection.properties) return void 0;
18216
- if (collection.titleProperty) return void 0;
18553
+ if (hasDeclaredDisplay(collection, "title")) return void 0;
18217
18554
  const idKeys = new Set(getPrimaryKeys(collection));
18218
18555
  const foreignKeys = getForeignKeyColumns(collection);
18219
18556
  const order = collection.propertiesOrder ?? Object.keys(collection.properties);
@@ -18274,6 +18611,6 @@ function getTitlePropertyKeyForValues(collection, values, entityId) {
18274
18611
  return candidates[0];
18275
18612
  }
18276
18613
  //#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 };
18614
+ 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, getRelationIncludeParams, getRowHeight, getSubcollectionColumnId, getTitlePropertyCandidates, getTitlePropertyKey, getTitlePropertyKeyForValues, hasDeclaredDisplay, iconsSearch, isAuditTimestamp, 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
18615
 
18279
18616
  //# sourceMappingURL=index.es.js.map