@rebasepro/admin 0.13.0 → 0.13.1-canary.gcd6689e

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 (60) hide show
  1. package/dist/{CollectionEditorDialog-CjXkWZ64.js → CollectionEditorDialog-CZZZpYOz.js} +3 -3
  2. package/dist/{CollectionEditorDialog-CjXkWZ64.js.map → CollectionEditorDialog-CZZZpYOz.js.map} +1 -1
  3. package/dist/{PropertyEditView-W8I3YV89.js → PropertyEditView-CeoHSmUg.js} +2 -2
  4. package/dist/{PropertyEditView-W8I3YV89.js.map → PropertyEditView-CeoHSmUg.js.map} +1 -1
  5. package/dist/{RouterCollectionsStudioView-BsLTNoqX.js → RouterCollectionsStudioView-CMKFWM2J.js} +4 -4
  6. package/dist/{RouterCollectionsStudioView-BsLTNoqX.js.map → RouterCollectionsStudioView-CMKFWM2J.js.map} +1 -1
  7. package/dist/collection_editor_ui.js +4 -4
  8. package/dist/components/AdditionalFieldValue.d.ts +25 -0
  9. package/dist/components/CollectionViewBinding/SlotValue.d.ts +29 -0
  10. package/dist/components/CollectionViewBinding/SplitListCloseButton.d.ts +5 -3
  11. package/dist/components/CollectionViewBinding/SplitListShowButton.d.ts +12 -0
  12. package/dist/components/CollectionViewBinding/usePreviewSlots.d.ts +29 -4
  13. package/dist/components/DetailViewBinding.d.ts +6 -0
  14. package/dist/components/EditViewBinding.d.ts +7 -1
  15. package/dist/components/EntityIdentityBar.d.ts +10 -1
  16. package/dist/components/EntityViewBinding.d.ts +41 -6
  17. package/dist/components/app/Scaffold.d.ts +4 -3
  18. package/dist/{export-J79rMoNj.js → export-l19WevKo.js} +2 -2
  19. package/dist/{export-J79rMoNj.js.map → export-l19WevKo.js.map} +1 -1
  20. package/dist/form/components/FieldBlock.d.ts +16 -0
  21. package/dist/{history-D7L_JlLO.js → history-JL1ZmSKD.js} +4 -4
  22. package/dist/{history-D7L_JlLO.js.map → history-JL1ZmSKD.js.map} +1 -1
  23. package/dist/hooks/useEntityDisplay.d.ts +102 -0
  24. package/dist/hooks/useEntityDisplayTitle.d.ts +18 -9
  25. package/dist/{import-BvYl--U4.js → import-DNrKrFQV.js} +3 -3
  26. package/dist/{import-BvYl--U4.js.map → import-DNrKrFQV.js.map} +1 -1
  27. package/dist/index.js +79 -15
  28. package/dist/index.js.map +1 -1
  29. package/dist/preview/components/BooleanPreview.d.ts +3 -1
  30. package/dist/routes/RebaseRoute.d.ts +1 -1
  31. package/dist/types/components/PropertyPreviewProps.d.ts +7 -0
  32. package/dist/{util-GiwPgxnL.js → util-pQ3YwBve.js} +1022 -641
  33. package/dist/util-pQ3YwBve.js.map +1 -0
  34. package/package.json +9 -9
  35. package/src/components/AdditionalFieldValue.tsx +68 -0
  36. package/src/components/CollectionTableBinding/CollectionTableBinding.tsx +5 -6
  37. package/src/components/CollectionViewBinding/BoardCardBinding.tsx +9 -27
  38. package/src/components/CollectionViewBinding/CollectionListViewBinding.tsx +11 -24
  39. package/src/components/CollectionViewBinding/EntityCardBinding.tsx +10 -33
  40. package/src/components/CollectionViewBinding/SlotValue.tsx +52 -0
  41. package/src/components/CollectionViewBinding/SplitListCloseButton.tsx +7 -5
  42. package/src/components/CollectionViewBinding/SplitListShowButton.tsx +28 -0
  43. package/src/components/CollectionViewBinding/usePreviewSlots.ts +115 -48
  44. package/src/components/DetailViewBinding.tsx +71 -54
  45. package/src/components/EditViewBinding.tsx +51 -33
  46. package/src/components/EntityIdentityBar.tsx +26 -4
  47. package/src/components/EntityPreviewBinding.tsx +24 -12
  48. package/src/components/EntityViewBinding.tsx +237 -39
  49. package/src/components/InlineEntityPreview.tsx +10 -10
  50. package/src/components/app/Scaffold.tsx +48 -7
  51. package/src/form/EntityForm.tsx +6 -24
  52. package/src/form/components/FieldBlock.tsx +24 -0
  53. package/src/hooks/navigation/useBuildCollectionRegistryController.tsx +29 -5
  54. package/src/hooks/useEntityDisplay.tsx +258 -0
  55. package/src/hooks/useEntityDisplayTitle.tsx +27 -38
  56. package/src/preview/PropertyPreview.tsx +1 -1
  57. package/src/preview/components/BooleanPreview.tsx +5 -2
  58. package/src/routes/RebaseRoute.tsx +62 -4
  59. package/src/types/components/PropertyPreviewProps.tsx +8 -0
  60. package/dist/util-GiwPgxnL.js.map +0 -1
@@ -1,11 +1,12 @@
1
1
  import * as React$1 from "react";
2
2
  import React, { Suspense, createContext, createElement, forwardRef, lazy, memo, useCallback, useContext, useEffect, useId, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
3
3
  import { deepEqual } from "fast-equals";
4
- import { Alert, AlertCircleIcon, AlertTriangleIcon, AlignLeftIcon, ArrowDownIcon, ArrowLeftIcon, ArrowRightToLineIcon, ArrowUpDownIcon, ArrowUpIcon, Badge, BooleanSwitchWithLabel, Button, CHIP_COLORS, CHIP_SEED_KEYS, CalendarIcon, Card, CardView, CenteredView, CheckCircleIcon, CheckIcon, Checkbox, ChevronDownIcon, ChevronRightIcon, ChevronUpIcon, ChevronsUpDownIcon, Chip, CircleIcon, CircularProgress, CircularProgressCenter, CodeIcon, Collapse, ColumnsIcon, CopyIcon, DateTimeField, Dialog, DialogActions, DialogContent, DialogTitle, ErrorBoundary, ExpandablePanel, ExternalLinkIcon, EyeIcon, EyeOffIcon, FileTextIcon, FileUpload, FilterChip, FilterIcon, FilterXIcon, FlagIcon, FolderUpIcon, FunctionSquareIcon, GlobeIcon, HandleIcon, HashIcon, HistoryIcon, IconButton, ImageOffIcon, KanbanIcon, KanbanView, KeyRoundIcon, Label, LayoutGridIcon, Link2Icon, LinkIcon, ListIcon, ListOrderedIcon, ListView, LoadingButton, MailIcon, Markdown, Maximize2Icon, Menu, MenuItem, MinusIcon, MoreVerticalIcon, MultiSelect, MultiSelectItem, PencilIcon, PlusIcon, Popover, PopoverPrimitive, Portal, QuoteIcon, RadioGroup, RadioGroupItem, RefreshCwIcon, RepeatIcon, ResizablePanels, Rows3Icon, SearchBar, SearchIcon, Select, SelectItem, Separator, SettingsIcon, Sheet, Skeleton, Tab, Table, TableBody, TableCell, TableHeader, TableIcon, TableRow, Tabs, TextField, TextIcon, TextareaAutosize, ToggleButtonGroup, Tooltip, Trash2Icon, Typography, Unlink2Icon, UploadIcon, UserIcon, VirtualTable, VirtualTableDateField, VirtualTableInput, VirtualTableInput as VirtualTableInput$1, VirtualTableNumberInput, VirtualTableSwitch, VoteIcon, XCircleIcon, XIcon, cls, coolIconKeys, createVirtualTableSelectionStore, debounce, defaultBorderMixin, fieldBackgroundDisabledMixin, fieldBackgroundHoverMixin, fieldBackgroundInvisibleMixin, fieldBackgroundMixin, focusedDisabled, getColorSchemeForSeed, iconSize, lucideIcons, paperMixin, useDebouncedCallback, useInjectStyles, useOutsideAlerter } from "@rebasepro/ui";
4
+ import { Alert, AlertCircleIcon, AlertTriangleIcon, AlignLeftIcon, ArrowDownIcon, ArrowLeftIcon, ArrowRightToLineIcon, ArrowUpDownIcon, ArrowUpIcon, Badge, BooleanSwitchWithLabel, Button, CHIP_COLORS, CHIP_SEED_KEYS, CalendarIcon, Card, CardView, CenteredView, CheckCircleIcon, CheckIcon, Checkbox, ChevronDownIcon, ChevronRightIcon, ChevronUpIcon, ChevronsLeftIcon, ChevronsRightIcon, ChevronsUpDownIcon, Chip, CircleIcon, CircularProgress, CircularProgressCenter, CodeIcon, Collapse, ColumnsIcon, CopyIcon, DateTimeField, Dialog, DialogActions, DialogContent, DialogTitle, ErrorBoundary, ExpandablePanel, ExternalLinkIcon, EyeIcon, EyeOffIcon, FileTextIcon, FileUpload, FilterChip, FilterIcon, FilterXIcon, FlagIcon, FolderUpIcon, FunctionSquareIcon, GlobeIcon, HandleIcon, HashIcon, HistoryIcon, IconButton, ImageOffIcon, KanbanIcon, KanbanView, KeyRoundIcon, Label, LayoutGridIcon, Link2Icon, LinkIcon, ListIcon, ListOrderedIcon, ListView, LoadingButton, MailIcon, Markdown, Maximize2Icon, Menu, MenuItem, MinusIcon, MoreVerticalIcon, MultiSelect, MultiSelectItem, PencilIcon, PlusIcon, Popover, PopoverPrimitive, Portal, QuoteIcon, RadioGroup, RadioGroupItem, RefreshCwIcon, RepeatIcon, ResizablePanels, Rows3Icon, SearchBar, SearchIcon, Select, SelectItem, Separator, SettingsIcon, Sheet, Skeleton, Tab, Table, TableBody, TableCell, TableHeader, TableIcon, TableRow, Tabs, TextField, TextIcon, TextareaAutosize, ToggleButtonGroup, Tooltip, Trash2Icon, Typography, Unlink2Icon, UploadIcon, UserIcon, VirtualTable, VirtualTableDateField, VirtualTableInput, VirtualTableInput as VirtualTableInput$1, VirtualTableNumberInput, VirtualTableSwitch, VoteIcon, XCircleIcon, XIcon, cls, coolIconKeys, createVirtualTableSelectionStore, debounce, defaultBorderMixin, fieldBackgroundDisabledMixin, fieldBackgroundHoverMixin, fieldBackgroundInvisibleMixin, fieldBackgroundMixin, focusedDisabled, getColorSchemeForSeed, iconSize, lucideIcons, paperMixin, useDebouncedCallback, useInjectStyles, useOutsideAlerter } from "@rebasepro/ui";
5
5
  import { CollectionRegistry, DEFAULT_ONE_OF_TYPE, DEFAULT_ONE_OF_VALUE, canReadCollection, enumToObjectEntries, getArrayResolvedProperties, getDefaultValueFor, getDefaultValueFortype, getDefaultValuesFor, getEntityChildViews, getLabelOrConfigFrom, getReferenceFrom, getRelationFrom, getSubcollections, getTableName, isPropertyBuilder, normalizeToEntityRelation, resolveArrayProperties, resolveEnumValues, resolveProperty, resolveRelationProperty, resolveStorageFilenameString, resolveStoragePathString, resolveStorageSource } from "@rebasepro/common";
6
- import { CONTAINER_FULL_WIDTH, CollectionScopeProvider, DEFAULT_PAGE_SIZE, ErrorTooltip, ErrorView, IconForView, PluginProviderStack, SIDE_PANEL_DEFAULT_WIDTH, StorageSourceContext, UnsavedChangesDialog, UserDisplay, addInitialSlash, apiBaseOf, buildEnumLabel, createFormexStub, deleteEntityWithCallbacks, flattenKeys, getAdminEntityChildViews, getCollectionBySlugWithin, getCollectionPathsCombinations, getColorScheme, getColumnKeysForProperty, getEntityFromCache, getEntityFromMemoryCache, getEntityImagePreviewPropertyKey, getFormFieldKeys, getIcon, getLastSegment as getLastSegment$1, getLeadingRelationTitleKey, getLocalChangesBackup, getNavigationEntriesFromPath, getParentReferencesFromPath, getRelationIncludeParams, getRowHeight, getTitlePropertyCandidates, getTitlePropertyKey, getTitlePropertyKeyForValues, iconsSearch, isFilterableRelation, isHidden, isReadOnly, looksLikeIdentifierValue, removeEntityFromCache, removeEntityFromMemoryCache, removeInitialAndTrailingSlashes, removeInitialAndTrailingSlashes as removeInitialAndTrailingSlashes$1, removeInitialSlash, removeTrailingSlash, removeTrailingSlash as removeTrailingSlash$1, resolveCollectionPathIds, resolveCollectionPathIds as resolveCollectionPathIds$1, resolveComponentRef, resolveDefaultSelectedView, resolveFilterOperators, resolveFormLayout, saveEntityToCache, saveEntityToMemoryCache, saveEntityWithCallbacks, useAnalyticsController, useApiBase, useApiConfig, useAuthController, useCollectionScope, useColumnIds, useComponentOverride, useCustomizationController, useData, useDataTableController, useFetch, useLargeLayout, useModeController, useNavigationBlocker, usePermissions, useRebaseClient, useRebaseContext, useRelationSelector, useResolvedComponent, useScrollRestoration, useSlot, useSnackbarController, useStorageSource, useStorageSources, useStorageUploadController, useTranslation, useUnsavedChangesDialog, useUserConfigurationPersistence } from "@rebasepro/app";
6
+ import { CONTAINER_FULL_WIDTH, CollectionScopeProvider, DEFAULT_PAGE_SIZE, EntityDisplayCache, ErrorTooltip, ErrorView, IconForView, PluginProviderStack, SIDE_PANEL_DEFAULT_WIDTH, StorageSourceContext, UnsavedChangesDialog, UserDisplay, addInitialSlash, apiBaseOf, buildEnumLabel, createFormexStub, deleteEntityWithCallbacks, entityDisplayKey, flattenKeys, getAdminEntityChildViews, getCollectionBySlugWithin, getCollectionPathsCombinations, getColorScheme, getColumnKeysForProperty, getDisplayPropertyKey, getDisplayResolver, getEntityFromCache, getEntityFromMemoryCache, getEntityImagePreviewPropertyKey, getFormFieldKeys, getIcon, getLastSegment as getLastSegment$1, getLeadingRelationTitleKey, getLocalChangesBackup, getNavigationEntriesFromPath, getParentReferencesFromPath, getPropertyInPath, getRelationIncludeParams, getRowHeight, getTitlePropertyCandidates, getTitlePropertyKey, getTitlePropertyKeyForValues, iconsSearch, isFilterableRelation, isHidden, isReadOnly, looksLikeIdentifierValue, removeEntityFromCache, removeEntityFromMemoryCache, removeInitialAndTrailingSlashes, removeInitialAndTrailingSlashes as removeInitialAndTrailingSlashes$1, removeInitialSlash, removeTrailingSlash, removeTrailingSlash as removeTrailingSlash$1, resolveCollectionPathIds, resolveCollectionPathIds as resolveCollectionPathIds$1, resolveComponentRef, resolveDefaultSelectedView, resolveFilterOperators, resolveFormLayout, saveEntityToCache, saveEntityToMemoryCache, saveEntityWithCallbacks, useAnalyticsController, useApiBase, useApiConfig, useAuthController, useCollectionScope, useColumnIds, useComponentOverride, useCustomizationController, useData, useDataTableController, useFetch, useLargeLayout, useModeController, useNavigationBlocker, usePermissions, useRebaseClient, useRebaseContext, useRelationSelector, useResolvedComponent, useScrollRestoration, useSlot, useSnackbarController, useStorageSource, useStorageSources, useStorageUploadController, useTranslation, useUnsavedChangesDialog, useUserConfigurationPersistence } from "@rebasepro/app";
7
7
  import { Field, Formex, getIn, setIn, useCreateFormex, useFormex } from "@rebasepro/forms";
8
8
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
9
+ import { resolveAdminCollection } from "@rebasepro/admin-types";
9
10
  import { defaultDateFormat, getHashValue, getValueInPath, hydrateRegExp, isObject, mergeDeep, randomString, toArray } from "@rebasepro/utils";
10
11
  import { format } from "date-fns";
11
12
  import * as locales from "date-fns/locale";
@@ -14,7 +15,7 @@ import { useDropzone } from "react-dropzone";
14
15
  import { EntityReference, EntityRelation, GeoPoint, Vector, getCollectionDataPath, getDataSourceCapabilities, isRelationalCollectionConfig } from "@rebasepro/types";
15
16
  import { z } from "zod";
16
17
  import useMeasure from "react-use-measure";
17
- import { useLocation, useNavigate, useSearchParams } from "react-router";
18
+ import { Link, useLocation, useNavigate, useSearchParams } from "react-router";
18
19
  import { DndContext, KeyboardSensor, PointerSensor, closestCenter, useSensor, useSensors } from "@dnd-kit/core";
19
20
  import { restrictToVerticalAxis } from "@dnd-kit/modifiers";
20
21
  import { SortableContext, horizontalListSortingStrategy, sortableKeyboardCoordinates, useSortable, verticalListSortingStrategy } from "@dnd-kit/sortable";
@@ -23,7 +24,6 @@ import ExcelJS from "exceljs";
23
24
  import { inferTypeFromValue } from "@rebasepro/inference";
24
25
  import { Highlight, themes } from "prism-react-renderer";
25
26
  import { generateKeyBetween, generateNKeysBetween } from "fractional-indexing";
26
- import { resolveAdminCollection } from "@rebasepro/admin-types";
27
27
  //#region src/util/property_utils.tsx
28
28
  /**
29
29
  * Resolve a size value (string token or number) to a numeric pixel value
@@ -95,13 +95,13 @@ function getIconForProperty(property, size = "small", fields = {}) {
95
95
  * @param properties
96
96
  * @param path
97
97
  */
98
- function getPropertyInPath(properties, path) {
98
+ function getPropertyInPath$1(properties, path) {
99
99
  if (typeof properties === "object") {
100
100
  if (path in properties) return properties[path];
101
101
  if (path.includes(".")) {
102
102
  const pathSegments = path.split(".");
103
103
  const childProperty = properties[pathSegments[0]];
104
- if (typeof childProperty === "object" && childProperty.type === "map" && childProperty.properties) return getPropertyInPath(childProperty.properties, pathSegments.slice(1).join("."));
104
+ if (typeof childProperty === "object" && childProperty.type === "map" && childProperty.properties) return getPropertyInPath$1(childProperty.properties, pathSegments.slice(1).join("."));
105
105
  }
106
106
  }
107
107
  }
@@ -199,6 +199,28 @@ function PropertyIdCopyTooltipContent({ propertyKey }) {
199
199
  //#endregion
200
200
  //#region src/form/components/FieldBlock.tsx
201
201
  /**
202
+ * Grid placement for a span.
203
+ *
204
+ * Written out as literal class strings rather than composed, because Tailwind
205
+ * scans source text: a template literal would produce classes that exist at
206
+ * runtime and were never generated into the stylesheet.
207
+ *
208
+ * Below the column breakpoint every field takes the single available column, so
209
+ * the side panel, the dialog and mobile all get a plain stack.
210
+ *
211
+ * Lives here rather than in the form, because the read-only rendering of a
212
+ * record places its fields on the same grid — a record that changes shape when
213
+ * you press Edit is the bug this whole layout exists to avoid.
214
+ */
215
+ function spanClass(span) {
216
+ switch (span) {
217
+ case 1: return "@2xl/col:col-span-1";
218
+ case 2: return "@2xl/col:col-span-2";
219
+ case 3: return "@2xl/col:col-span-3";
220
+ case 4: return "@2xl/col:col-span-4";
221
+ }
222
+ }
223
+ /**
202
224
  * Does this property's editor supply its own heading?
203
225
  *
204
226
  * Arrays, maps, `oneOf` blocks and key-value editors render as a collapsible
@@ -273,6 +295,61 @@ function FieldBlock({ propertyKey, property, showLabel, children }) {
273
295
  });
274
296
  }
275
297
  //#endregion
298
+ //#region src/components/AdditionalFieldValue.tsx
299
+ /**
300
+ * Renders an `additionalFields` entry that supplies a `value` rather than a
301
+ * `Builder`.
302
+ *
303
+ * `value` is typed `string | number | Promise<string | number>`, and the promise
304
+ * arm has been part of that type from the start — the CSV export awaits it
305
+ * correctly. Every *renderer* called `.toString()` on the result instead, so an
306
+ * async value drew the literal text `[object Promise]` in the cell: a shape the
307
+ * type promised, that worked in one of its two consumers, and failed silently in
308
+ * the other. That is what made the obvious port of a FireCMS column — one that
309
+ * fetches a title from a subcollection — look supported and not be.
310
+ *
311
+ * Three call sites had the same three lines copied between them (the table cell,
312
+ * the form and the read-only record), which is why the bug reached all three.
313
+ * Now there is one.
314
+ */
315
+ function AdditionalFieldValue({ field, entity, context }) {
316
+ const [resolved, setResolved] = useState(() => {
317
+ const value = field.value?.({
318
+ entity,
319
+ context
320
+ });
321
+ return isPromise(value) ? void 0 : value;
322
+ });
323
+ useEffect(() => {
324
+ const value = field.value?.({
325
+ entity,
326
+ context
327
+ });
328
+ if (!isPromise(value)) {
329
+ setResolved(value);
330
+ return;
331
+ }
332
+ let live = true;
333
+ value.then((result) => {
334
+ if (live) setResolved(result);
335
+ }, (error) => {
336
+ if (live) setResolved(void 0);
337
+ console.warn(`[rebase] Additional field "${field.key}" failed to resolve:`, error);
338
+ });
339
+ return () => {
340
+ live = false;
341
+ };
342
+ }, [
343
+ field,
344
+ entity,
345
+ context
346
+ ]);
347
+ return /* @__PURE__ */ jsx(Fragment, { children: resolved === void 0 || resolved === null ? null : String(resolved) });
348
+ }
349
+ function isPromise(value) {
350
+ return typeof value?.then === "function";
351
+ }
352
+ //#endregion
276
353
  //#region src/form/components/FormRail.tsx
277
354
  /**
278
355
  * The metadata column beside the form.
@@ -1096,6 +1173,219 @@ function resolveTitleToString(title) {
1096
1173
  }
1097
1174
  }
1098
1175
  //#endregion
1176
+ //#region src/hooks/useResolvedUsers.ts
1177
+ var caches = /* @__PURE__ */ new Map();
1178
+ function getCache(baseUrl) {
1179
+ let cache = caches.get(baseUrl);
1180
+ if (!cache) {
1181
+ cache = {
1182
+ users: /* @__PURE__ */ new Map(),
1183
+ pending: /* @__PURE__ */ new Map(),
1184
+ queue: /* @__PURE__ */ new Set()
1185
+ };
1186
+ caches.set(baseUrl, cache);
1187
+ }
1188
+ return cache;
1189
+ }
1190
+ async function fetchUsers(apiBase, ids, token) {
1191
+ const resolved = /* @__PURE__ */ new Map();
1192
+ for (const id of ids) resolved.set(id, null);
1193
+ const params = new URLSearchParams({ ids: ids.join(",") });
1194
+ const response = await fetch(`${apiBase}/admin/users?${params}`, { headers: token ? { Authorization: `Bearer ${token}` } : {} });
1195
+ if (!response.ok) return resolved;
1196
+ const data = await response.json();
1197
+ const rows = data.users ?? data.data ?? [];
1198
+ for (const row of rows) {
1199
+ const uid = row.uid ?? row.id;
1200
+ if (!uid) continue;
1201
+ resolved.set(uid, {
1202
+ uid,
1203
+ email: row.email ?? "",
1204
+ displayName: row.displayName ?? row.display_name ?? null,
1205
+ photoURL: row.photoURL ?? row.photo_url ?? null,
1206
+ providerId: "custom",
1207
+ isAnonymous: false,
1208
+ roles: row.roles ?? []
1209
+ });
1210
+ }
1211
+ return resolved;
1212
+ }
1213
+ function requestUser(baseUrl, uid, getAuthToken) {
1214
+ const cache = getCache(baseUrl);
1215
+ const existing = cache.pending.get(uid);
1216
+ if (existing) return existing;
1217
+ cache.queue.add(uid);
1218
+ const promise = new Promise((resolve, reject) => {
1219
+ if (cache.flush) clearTimeout(cache.flush);
1220
+ cache.flush = setTimeout(async () => {
1221
+ const ids = [...cache.queue];
1222
+ cache.queue.clear();
1223
+ cache.flush = void 0;
1224
+ try {
1225
+ const resolved = await fetchUsers(baseUrl, ids, await getAuthToken?.());
1226
+ for (const id of ids) {
1227
+ cache.users.set(id, resolved.get(id) ?? null);
1228
+ cache.pending.delete(id);
1229
+ }
1230
+ resolve();
1231
+ } catch (error) {
1232
+ for (const id of ids) cache.pending.delete(id);
1233
+ reject(error);
1234
+ }
1235
+ }, 0);
1236
+ });
1237
+ cache.pending.set(uid, promise);
1238
+ return promise;
1239
+ }
1240
+ /**
1241
+ * The auth user behind a `userSelect` value, or `null` while it is loading or
1242
+ * when it cannot be resolved.
1243
+ */
1244
+ function useResolvedUser(uid) {
1245
+ const client = useRebaseClient();
1246
+ const { getAuthToken } = useAuthController();
1247
+ const baseUrl = apiBaseOf(client);
1248
+ const [user, setUser] = useState(() => baseUrl && uid ? getCache(baseUrl).users.get(uid) ?? null : null);
1249
+ useEffect(() => {
1250
+ if (!baseUrl || !uid) {
1251
+ setUser(null);
1252
+ return;
1253
+ }
1254
+ const cache = getCache(baseUrl);
1255
+ if (cache.users.has(uid)) {
1256
+ setUser(cache.users.get(uid) ?? null);
1257
+ return;
1258
+ }
1259
+ let active = true;
1260
+ requestUser(baseUrl, uid, getAuthToken).then(() => {
1261
+ if (active) setUser(getCache(baseUrl).users.get(uid) ?? null);
1262
+ }).catch(() => {
1263
+ if (active) setUser(null);
1264
+ });
1265
+ return () => {
1266
+ active = false;
1267
+ };
1268
+ }, [
1269
+ baseUrl,
1270
+ uid,
1271
+ getAuthToken
1272
+ ]);
1273
+ return user;
1274
+ }
1275
+ /** Human-readable label for a resolved user. */
1276
+ function getUserLabel(user) {
1277
+ return user.displayName || user.email || user.uid;
1278
+ }
1279
+ //#endregion
1280
+ //#region src/hooks/useEntityDisplay.tsx
1281
+ /**
1282
+ * One shared store, so every surface agrees on a record and resolves it once.
1283
+ *
1284
+ * Module-level rather than a provider: two app instances on one page share a
1285
+ * cache keyed by collection path and record id, which *is* the same record.
1286
+ * Nothing here is user-specific — a resolver that reads per-user data must
1287
+ * invalidate on sign-out, which {@link useEntityDisplayCache} exposes.
1288
+ */
1289
+ var sharedCache;
1290
+ /**
1291
+ * The store behind {@link useEntityDisplay}, for invalidating after a write.
1292
+ *
1293
+ * Built on first use rather than at module scope. Importing a module should not
1294
+ * construct anything: it runs during SSR where there is nothing to cache, and it
1295
+ * made this module unimportable from any test that stubs `@rebasepro/app` —
1296
+ * which several do, and which is a legitimate thing for them to do.
1297
+ */
1298
+ function useEntityDisplayCache() {
1299
+ if (!sharedCache) sharedCache = new EntityDisplayCache();
1300
+ return sharedCache;
1301
+ }
1302
+ /**
1303
+ * What fills one display role of one record.
1304
+ *
1305
+ * This is the only implementation. Before it there were seven for the title
1306
+ * alone — the identity bar, the detail header, inline previews, reference cards,
1307
+ * list slots, relation chips and the headless view — each re-deriving it from
1308
+ * `collection.properties` and disagreeing about the answer.
1309
+ *
1310
+ * Resolution order, per role:
1311
+ *
1312
+ * 1. `admin.display[role]` as a **resolver** — awaited, cached per record and
1313
+ * role, with the derived value showing meanwhile so nothing flickers empty.
1314
+ * 2. `admin.display[role]` as a **property path** (or, for the title, the
1315
+ * deprecated `titleProperty`).
1316
+ * 3. The **derived** answer: for the title, the best-ranked property carrying a
1317
+ * readable value; for the rest, today's per-role heuristics.
1318
+ *
1319
+ * A user-picker title resolves to the person behind the id, as it always did —
1320
+ * that path is now here rather than in two of the seven.
1321
+ */
1322
+ function useEntityDisplay(role, { collection, entity, values: valuesProp, enabled = true }) {
1323
+ const context = useRebaseContext();
1324
+ const cache = useEntityDisplayCache();
1325
+ const values = valuesProp ?? entity?.values;
1326
+ const declaredKey = collection ? getDisplayPropertyKey(collection, role) : void 0;
1327
+ const propertyKey = collection && role === "title" && !declaredKey ? getEntityTitlePropertyKeyForEntity(collection, values, entity?.id) : declaredKey;
1328
+ const rawValue = values && propertyKey ? getValueInPath(values, propertyKey) : void 0;
1329
+ const titleUser = useResolvedUser(collection && role === "title" && isUserSelectProperty(collection, propertyKey) && typeof rawValue === "string" ? rawValue : void 0);
1330
+ const derived = useMemo(() => {
1331
+ if (titleUser) return getUserLabel(titleUser);
1332
+ if (rawValue === void 0 || rawValue === null) return void 0;
1333
+ if (role === "title" || role === "subtitle" || role === "status") return resolveTitleToString(rawValue) || void 0;
1334
+ return rawValue;
1335
+ }, [
1336
+ titleUser,
1337
+ rawValue,
1338
+ role
1339
+ ]);
1340
+ const resolver = collection ? getDisplayResolver(collection, role) : void 0;
1341
+ const canResolve = Boolean(enabled && resolver && entity);
1342
+ const key = entity ? entityDisplayKey(entity.path, entity.id, role) : void 0;
1343
+ const resolved = useSyncExternalStore((cb) => cache.subscribe(cb), () => canResolve && key ? cache.peek(key) : void 0, () => void 0);
1344
+ const loading = Boolean(canResolve && key && resolved === void 0);
1345
+ useEffect(() => {
1346
+ if (!canResolve || !key || !resolver || !entity) return;
1347
+ if (cache.peek(key) !== void 0) return;
1348
+ cache.resolve(key, () => resolver({
1349
+ entity,
1350
+ context
1351
+ })).catch((error) => {
1352
+ console.warn(`[rebase] Could not resolve ${role} for ${key}:`, error);
1353
+ });
1354
+ }, [
1355
+ cache,
1356
+ canResolve,
1357
+ key,
1358
+ resolver,
1359
+ entity,
1360
+ context,
1361
+ role
1362
+ ]);
1363
+ const value = (canResolve && resolved !== null ? resolved : void 0) ?? derived;
1364
+ return useMemo(() => ({
1365
+ value,
1366
+ loading,
1367
+ source: collection && propertyKey && value === derived && derived !== void 0 ? {
1368
+ key: propertyKey,
1369
+ property: getPropertyInPath(collection.properties, propertyKey),
1370
+ value: rawValue
1371
+ } : void 0
1372
+ }), [
1373
+ value,
1374
+ loading,
1375
+ propertyKey,
1376
+ derived,
1377
+ collection,
1378
+ rawValue
1379
+ ]);
1380
+ }
1381
+ /**
1382
+ * What the record is called — the one role asked for often enough on its own to
1383
+ * deserve a name.
1384
+ */
1385
+ function useEntityTitle(params) {
1386
+ return useEntityDisplay("title", params);
1387
+ }
1388
+ //#endregion
1099
1389
  //#region src/hooks/navigation/contexts/CollectionRegistryContext.tsx
1100
1390
  var CollectionRegistryContext = createContext({
1101
1391
  getCollection: () => void 0,
@@ -1172,6 +1462,10 @@ function EntityPreviewBindingData({ actions, collection: collectionProp, preview
1172
1462
  const customizationController = useCustomizationController();
1173
1463
  const collectionRegistryController = useCollectionRegistryController();
1174
1464
  const collection = collectionProp ?? collectionRegistryController.getCollection(entity.path);
1465
+ const title = useEntityTitle({
1466
+ collection,
1467
+ entity
1468
+ });
1175
1469
  const listProperties = useMemo(() => {
1176
1470
  if (!collection) return [];
1177
1471
  return previewKeys ?? getEntityPreviewKeys(authController, collection, customizationController.propertyConfigs, previewKeys, size === "medium" || size === "large" ? 3 : 2);
@@ -1183,7 +1477,7 @@ function EntityPreviewBindingData({ actions, collection: collectionProp, preview
1183
1477
  customizationController.propertyConfigs
1184
1478
  ]);
1185
1479
  if (!collection) return /* @__PURE__ */ jsx(Fragment, { children: /* @__PURE__ */ jsx(ErrorView, { error: `Collection not found: ${entity.path}` }) });
1186
- const titleProperty = includeTitle ? getEntityTitlePropertyKeyForEntity(collection, entity.values, entity.id) : void 0;
1480
+ const titleProperty = includeTitle ? title.source?.key : void 0;
1187
1481
  const imagePropertyKey = includeImage ? getEntityImagePreviewPropertyKey(collection) : void 0;
1188
1482
  const imageProperty = imagePropertyKey ? collection.properties[imagePropertyKey] : void 0;
1189
1483
  const ofProp = imageProperty && "of" in imageProperty ? imageProperty.of : void 0;
@@ -1222,20 +1516,20 @@ function EntityPreviewBindingData({ actions, collection: collectionProp, preview
1222
1516
  children: entity.id
1223
1517
  })
1224
1518
  }) : /* @__PURE__ */ jsx(Skeleton, {})),
1225
- titleProperty && /* @__PURE__ */ jsx("div", {
1519
+ includeTitle && (titleProperty || title.value) && /* @__PURE__ */ jsx("div", {
1226
1520
  className: "truncate my-0.5 text-sm font-medium text-text-primary dark:text-text-primary-dark",
1227
- children: entity ? /* @__PURE__ */ jsx(PropertyPreview, {
1228
- propertyKey: titleProperty,
1229
- value: getValueInPath(entity.values, titleProperty),
1230
- property: collection.properties[titleProperty],
1521
+ children: !entity && titleProperty ? /* @__PURE__ */ jsx(SkeletonPropertyComponent, {
1522
+ property: getPropertyInPath$1(collection.properties, titleProperty),
1231
1523
  size: "medium"
1232
- }) : /* @__PURE__ */ jsx(SkeletonPropertyComponent, {
1233
- property: collection.properties[titleProperty],
1524
+ }) : titleProperty ? /* @__PURE__ */ jsx(PropertyPreview, {
1525
+ propertyKey: titleProperty,
1526
+ value: title.source?.value,
1527
+ property: getPropertyInPath$1(collection.properties, titleProperty),
1234
1528
  size: "medium"
1235
- })
1529
+ }) : title.value
1236
1530
  }),
1237
1531
  restProperties && restProperties.map((key) => {
1238
- const childProperty = getPropertyInPath(collection.properties, key);
1532
+ const childProperty = getPropertyInPath$1(collection.properties, key);
1239
1533
  if (!childProperty) return null;
1240
1534
  const valueInPath = getValueInPath(entity.values, key);
1241
1535
  return /* @__PURE__ */ jsx("div", {
@@ -1355,9 +1649,12 @@ function InlineEntityPreview({ entity, collection: collectionProp, label, disabl
1355
1649
  const analyticsController = useAnalyticsController();
1356
1650
  const sidePanelController = useSidePanel();
1357
1651
  const collection = collectionProp ?? collectionRegistryController.getCollection(entity.path);
1358
- const titlePropertyKey = collection ? getEntityTitlePropertyKeyForEntity(collection, entity.values, entity.id) : void 0;
1359
- const titleValue = titlePropertyKey ? getValueInPath(entity.values, titlePropertyKey) : void 0;
1360
- const resolvedLabel = label ?? (titleValue !== void 0 && titleValue !== null && String(titleValue).trim().length > 0 ? String(titleValue) : String(entity.id));
1652
+ const { value: title } = useEntityTitle({
1653
+ collection,
1654
+ entity,
1655
+ enabled: Boolean(collection)
1656
+ });
1657
+ const resolvedLabel = label ?? title ?? String(entity.id);
1361
1658
  const interactive = !disabled && (onClick !== void 0 || includeEntityLink && collection !== void 0);
1362
1659
  const handleClick = (event) => {
1363
1660
  if (!interactive) return;
@@ -2106,147 +2403,43 @@ function DatePreview({ date, mode = "date_time", timezone }) {
2106
2403
  /**
2107
2404
  * @group Preview components
2108
2405
  */
2109
- function BooleanPreview({ value, size, property }) {
2406
+ function BooleanPreview({ value, size, property, hideLabel }) {
2110
2407
  return /* @__PURE__ */ jsxs("div", {
2111
2408
  className: "flex flex-row gap-2 items-center",
2112
2409
  children: [/* @__PURE__ */ jsx(Checkbox, {
2113
2410
  checked: value,
2114
2411
  padding: false,
2115
- size,
2116
- color: "secondary"
2117
- }), property.name && /* @__PURE__ */ jsx("span", {
2118
- className: cls("text-text-secondary dark:text-text-secondary-dark", size === "small" ? "text-sm" : ""),
2119
- children: property.name
2120
- })]
2121
- });
2122
- }
2123
- //#endregion
2124
- //#region src/preview/property_previews/NumberPropertyPreview.tsx
2125
- /**
2126
- * @group Preview components
2127
- */
2128
- function NumberPropertyPreview({ value, property, size }) {
2129
- const numValue = value;
2130
- if (property.enum) {
2131
- const enumKey = numValue;
2132
- const enumValues = enumToObjectEntries(property.enum);
2133
- if (!enumValues) return /* @__PURE__ */ jsx("span", {
2134
- className: size === "small" ? "text-sm" : "",
2135
- children: numValue
2136
- });
2137
- return /* @__PURE__ */ jsx(EnumValuesChip, {
2138
- enumKey,
2139
- enumValues,
2140
- size: size !== "medium" ? "small" : "medium"
2141
- });
2142
- } else return /* @__PURE__ */ jsx("span", {
2143
- className: size === "small" ? "text-sm" : "",
2144
- children: numValue
2145
- });
2146
- }
2147
- //#endregion
2148
- //#region src/hooks/useResolvedUsers.ts
2149
- var caches = /* @__PURE__ */ new Map();
2150
- function getCache(baseUrl) {
2151
- let cache = caches.get(baseUrl);
2152
- if (!cache) {
2153
- cache = {
2154
- users: /* @__PURE__ */ new Map(),
2155
- pending: /* @__PURE__ */ new Map(),
2156
- queue: /* @__PURE__ */ new Set()
2157
- };
2158
- caches.set(baseUrl, cache);
2159
- }
2160
- return cache;
2161
- }
2162
- async function fetchUsers(apiBase, ids, token) {
2163
- const resolved = /* @__PURE__ */ new Map();
2164
- for (const id of ids) resolved.set(id, null);
2165
- const params = new URLSearchParams({ ids: ids.join(",") });
2166
- const response = await fetch(`${apiBase}/admin/users?${params}`, { headers: token ? { Authorization: `Bearer ${token}` } : {} });
2167
- if (!response.ok) return resolved;
2168
- const data = await response.json();
2169
- const rows = data.users ?? data.data ?? [];
2170
- for (const row of rows) {
2171
- const uid = row.uid ?? row.id;
2172
- if (!uid) continue;
2173
- resolved.set(uid, {
2174
- uid,
2175
- email: row.email ?? "",
2176
- displayName: row.displayName ?? row.display_name ?? null,
2177
- photoURL: row.photoURL ?? row.photo_url ?? null,
2178
- providerId: "custom",
2179
- isAnonymous: false,
2180
- roles: row.roles ?? []
2181
- });
2182
- }
2183
- return resolved;
2184
- }
2185
- function requestUser(baseUrl, uid, getAuthToken) {
2186
- const cache = getCache(baseUrl);
2187
- const existing = cache.pending.get(uid);
2188
- if (existing) return existing;
2189
- cache.queue.add(uid);
2190
- const promise = new Promise((resolve, reject) => {
2191
- if (cache.flush) clearTimeout(cache.flush);
2192
- cache.flush = setTimeout(async () => {
2193
- const ids = [...cache.queue];
2194
- cache.queue.clear();
2195
- cache.flush = void 0;
2196
- try {
2197
- const resolved = await fetchUsers(baseUrl, ids, await getAuthToken?.());
2198
- for (const id of ids) {
2199
- cache.users.set(id, resolved.get(id) ?? null);
2200
- cache.pending.delete(id);
2201
- }
2202
- resolve();
2203
- } catch (error) {
2204
- for (const id of ids) cache.pending.delete(id);
2205
- reject(error);
2206
- }
2207
- }, 0);
2208
- });
2209
- cache.pending.set(uid, promise);
2210
- return promise;
2211
- }
2212
- /**
2213
- * The auth user behind a `userSelect` value, or `null` while it is loading or
2214
- * when it cannot be resolved.
2215
- */
2216
- function useResolvedUser(uid) {
2217
- const client = useRebaseClient();
2218
- const { getAuthToken } = useAuthController();
2219
- const baseUrl = apiBaseOf(client);
2220
- const [user, setUser] = useState(() => baseUrl && uid ? getCache(baseUrl).users.get(uid) ?? null : null);
2221
- useEffect(() => {
2222
- if (!baseUrl || !uid) {
2223
- setUser(null);
2224
- return;
2225
- }
2226
- const cache = getCache(baseUrl);
2227
- if (cache.users.has(uid)) {
2228
- setUser(cache.users.get(uid) ?? null);
2229
- return;
2230
- }
2231
- let active = true;
2232
- requestUser(baseUrl, uid, getAuthToken).then(() => {
2233
- if (active) setUser(getCache(baseUrl).users.get(uid) ?? null);
2234
- }).catch(() => {
2235
- if (active) setUser(null);
2236
- });
2237
- return () => {
2238
- active = false;
2239
- };
2240
- }, [
2241
- baseUrl,
2242
- uid,
2243
- getAuthToken
2244
- ]);
2245
- return user;
2412
+ size,
2413
+ color: "secondary"
2414
+ }), !hideLabel && property.name && /* @__PURE__ */ jsx("span", {
2415
+ className: cls("text-text-secondary dark:text-text-secondary-dark", size === "small" ? "text-sm" : ""),
2416
+ children: property.name
2417
+ })]
2418
+ });
2246
2419
  }
2247
- /** Human-readable label for a resolved user. */
2248
- function getUserLabel(user) {
2249
- return user.displayName || user.email || user.uid;
2420
+ //#endregion
2421
+ //#region src/preview/property_previews/NumberPropertyPreview.tsx
2422
+ /**
2423
+ * @group Preview components
2424
+ */
2425
+ function NumberPropertyPreview({ value, property, size }) {
2426
+ const numValue = value;
2427
+ if (property.enum) {
2428
+ const enumKey = numValue;
2429
+ const enumValues = enumToObjectEntries(property.enum);
2430
+ if (!enumValues) return /* @__PURE__ */ jsx("span", {
2431
+ className: size === "small" ? "text-sm" : "",
2432
+ children: numValue
2433
+ });
2434
+ return /* @__PURE__ */ jsx(EnumValuesChip, {
2435
+ enumKey,
2436
+ enumValues,
2437
+ size: size !== "medium" ? "small" : "medium"
2438
+ });
2439
+ } else return /* @__PURE__ */ jsx("span", {
2440
+ className: size === "small" ? "text-sm" : "",
2441
+ children: numValue
2442
+ });
2250
2443
  }
2251
2444
  //#endregion
2252
2445
  //#region src/preview/components/UserPreview.tsx
@@ -2455,7 +2648,8 @@ var PropertyPreview = React.memo(function PropertyPreview(props) {
2455
2648
  else if (property.type === "boolean") if (typeof value === "boolean") content = /* @__PURE__ */ jsx(BooleanPreview, {
2456
2649
  value,
2457
2650
  size,
2458
- property
2651
+ property,
2652
+ hideLabel: props.hideLabel
2459
2653
  });
2460
2654
  else content = buildWrongValueType(propertyKey, property.type, value);
2461
2655
  else if (property.type === "number") if (typeof value === "number") content = /* @__PURE__ */ jsx(NumberPropertyPreview, {
@@ -6369,13 +6563,6 @@ var CollectionTableBinding = function CollectionTableBinding({ className, style,
6369
6563
  }), {}) : entity;
6370
6564
  const Builder = additionalField.Builder;
6371
6565
  if (!Builder && !additionalField.value) throw new Error("When using additional fields you need to provide a Builder or a value");
6372
- const child = Builder ? /* @__PURE__ */ jsx(Builder, {
6373
- entity,
6374
- context
6375
- }) : /* @__PURE__ */ jsx(Fragment, { children: additionalField.value?.({
6376
- entity,
6377
- context
6378
- })?.toString() });
6379
6566
  return /* @__PURE__ */ jsx(EntityTableCell, {
6380
6567
  width,
6381
6568
  size,
@@ -6392,7 +6579,14 @@ var CollectionTableBinding = function CollectionTableBinding({ className, style,
6392
6579
  isDragging,
6393
6580
  isDraggable,
6394
6581
  frozen,
6395
- children: /* @__PURE__ */ jsx(ErrorBoundary, { children: child })
6582
+ children: /* @__PURE__ */ jsx(ErrorBoundary, { children: Builder ? /* @__PURE__ */ jsx(Builder, {
6583
+ entity,
6584
+ context
6585
+ }) : /* @__PURE__ */ jsx(AdditionalFieldValue, {
6586
+ field: additionalField,
6587
+ entity,
6588
+ context
6589
+ }) })
6396
6590
  }, `additional_table_cell_${entity.id}_${column.key}`);
6397
6591
  }, [size]);
6398
6592
  const engine = useCollectionScope()?.engine;
@@ -7471,7 +7665,7 @@ function convertDataToEntity(authController, navigation, data, idColumn, headers
7471
7665
  if (idColumn) delete flatObject[idColumn];
7472
7666
  const mappedKeysObject = Object.entries(flatObject).map(([key, value]) => {
7473
7667
  const mappedKey = getIn(headersMapping, key) ?? key;
7474
- const mappedProperty = getPropertyInPath(properties, mappedKey);
7668
+ const mappedProperty = getPropertyInPath$1(properties, mappedKey);
7475
7669
  if (!mappedProperty) return {};
7476
7670
  const processedValue = processValueMapping(authController, value, navigation, mappedProperty);
7477
7671
  return { [mappedKey]: processedValue };
@@ -7592,8 +7786,8 @@ function DataNewPropertiesMapping({ importConfig, destinationProperties, buildPr
7592
7786
  ] }), /* @__PURE__ */ jsx(TableBody, { children: destinationProperties && headingsOrder.map((importKey) => {
7593
7787
  const mappedKey = headersMapping[importKey];
7594
7788
  const propertyKey = headersMapping[importKey];
7595
- const property = mappedKey ? getPropertyInPath(destinationProperties, mappedKey) : null;
7596
- const originProperty = getPropertyInPath(originProperties, importKey);
7789
+ const property = mappedKey ? getPropertyInPath$1(destinationProperties, mappedKey) : null;
7790
+ const originProperty = getPropertyInPath$1(originProperties, importKey);
7597
7791
  const origintype = originProperty ? originProperty.type === "array" && typeof originProperty.of === "object" ? `${originProperty.type} - ${originProperty.of.type}` : originProperty.type : void 0;
7598
7792
  return /* @__PURE__ */ jsxs(TableRow, {
7599
7793
  style: { height: "90px" },
@@ -7646,7 +7840,7 @@ function DataNewPropertiesMapping({ importConfig, destinationProperties, buildPr
7646
7840
  children: "Default value"
7647
7841
  })
7648
7842
  ] }), /* @__PURE__ */ jsx(TableBody, { children: destinationProperties && getAllPropertyKeys(destinationProperties).map((key) => {
7649
- const property = getPropertyInPath(destinationProperties, key);
7843
+ const property = getPropertyInPath$1(destinationProperties, key);
7650
7844
  if (typeof property !== "object" || property === null) return null;
7651
7845
  if (![
7652
7846
  "number",
@@ -8072,8 +8266,8 @@ function EditorCollectionAction({ path, parentCollectionSlugs, parentEntityIds,
8072
8266
  }
8073
8267
  //#endregion
8074
8268
  //#region src/components/CollectionViewBinding/CollectionViewActions.tsx
8075
- var ImportCollectionAction = lazy(() => import("./import-BvYl--U4.js").then((n) => n.t).then((m) => ({ default: m.ImportCollectionAction })));
8076
- var ExportCollectionAction = lazy(() => import("./export-J79rMoNj.js").then((n) => n.t).then((m) => ({ default: m.ExportCollectionAction })));
8269
+ var ImportCollectionAction = lazy(() => import("./import-DNrKrFQV.js").then((n) => n.t).then((m) => ({ default: m.ImportCollectionAction })));
8270
+ var ExportCollectionAction = lazy(() => import("./export-l19WevKo.js").then((n) => n.t).then((m) => ({ default: m.ExportCollectionAction })));
8077
8271
  function CollectionViewActions({ collection, relativePath, parentCollectionSlugs, parentEntityIds, onNewClick, onAddExistingClick, onMultipleDeleteClick, selectionEnabled, path, selectionController, tableController, collectionEntitiesCount, compact, children, openNewDocument }) {
8078
8272
  const context = useAdminContext();
8079
8273
  const { canCreate, canDelete } = usePermissions();
@@ -8219,7 +8413,7 @@ function resolveCollectionSlotKeys(collection, authController, propertyConfigs)
8219
8413
  const titleKeyCandidates = leadingRelationKey ? [leadingRelationKey, ...rankedCandidates.filter((key) => key !== leadingRelationKey)] : rankedCandidates;
8220
8414
  const titleKey = titleKeyCandidates[0];
8221
8415
  const imageKey = getEntityImagePreviewPropertyKey(collection);
8222
- let statusKey;
8416
+ let statusKey = getDisplayPropertyKey(collection, "status");
8223
8417
  if (!statusKey && collection.previewProperties) for (const key of collection.previewProperties) {
8224
8418
  const p = collection.properties[key];
8225
8419
  if (p?.type === "string" && "enum" in p && p.enum && key !== titleKey) {
@@ -8243,17 +8437,20 @@ function resolveCollectionSlotKeys(collection, authController, propertyConfigs)
8243
8437
  break;
8244
8438
  }
8245
8439
  }
8246
- let dateKey;
8247
- for (const candidate of [
8440
+ let dateKey = getDisplayPropertyKey(collection, "date");
8441
+ const dateCandidates = [
8248
8442
  "updated_at",
8249
8443
  "updatedAt",
8250
8444
  "modified_at",
8251
8445
  "modifiedAt",
8252
8446
  "created_at",
8253
8447
  "createdAt"
8254
- ]) if (collection.properties[candidate]) {
8255
- dateKey = candidate;
8256
- break;
8448
+ ];
8449
+ if (!dateKey) {
8450
+ for (const candidate of dateCandidates) if (collection.properties[candidate]) {
8451
+ dateKey = candidate;
8452
+ break;
8453
+ }
8257
8454
  }
8258
8455
  if (!dateKey) {
8259
8456
  for (const [key, prop] of Object.entries(collection.properties)) if (prop.type === "date") {
@@ -8284,7 +8481,7 @@ function resolveCollectionSlotKeys(collection, authController, propertyConfigs)
8284
8481
  titleKey,
8285
8482
  titleKeyCandidates,
8286
8483
  imageKey,
8287
- subtitleKey: sortedPreviewKeys.length > 0 ? sortedPreviewKeys[0] : void 0,
8484
+ subtitleKey: getDisplayPropertyKey(collection, "subtitle") ?? (sortedPreviewKeys.length > 0 ? sortedPreviewKeys[0] : void 0),
8288
8485
  relationKeys,
8289
8486
  statusKey,
8290
8487
  dateKey
@@ -8480,29 +8677,11 @@ function resolveRelationDisplayName(relation, prop) {
8480
8677
  if (resolved && typeof resolved === "object") targetCollection = resolved;
8481
8678
  } catch {}
8482
8679
  const extractDisplayName = (values) => {
8483
- if (targetCollection) {
8484
- const targetTitleKey = targetCollection.titleProperty;
8485
- if (targetTitleKey && values[targetTitleKey] !== void 0) return String(values[targetTitleKey]);
8486
- const isHiddenProp = (p) => {
8487
- if (p.admin?.hideFromCollection) return true;
8488
- if (typeof p.admin?.disabled === "object" && p.admin.disabled.hidden) return true;
8489
- return false;
8490
- };
8491
- const isDisplayCandidate = (p) => {
8492
- return p.type === "string" && !p.admin?.multiline && !p.admin?.markdown && !p.storage && !("isId" in p && p.isId) && !isHiddenProp(p);
8493
- };
8494
- for (const pk of [
8495
- "name",
8496
- "title",
8497
- "label",
8498
- "displayName"
8499
- ]) {
8500
- const p = targetCollection.properties[pk];
8501
- if (p && isDisplayCandidate(p) && values[pk] !== void 0 && values[pk] !== null) return String(values[pk]);
8502
- }
8503
- for (const [k, p] of Object.entries(targetCollection.properties)) if (isDisplayCandidate(p)) {
8504
- if (values[k] !== void 0 && values[k] !== null) return String(values[k]);
8505
- }
8680
+ if (targetCollection) for (const key of getTitlePropertyCandidates(targetCollection)) {
8681
+ const value = getValueInPath(values, key);
8682
+ if (value === void 0 || value === null || value === "") continue;
8683
+ if (typeof value === "string" && looksLikeIdentifierValue(value)) continue;
8684
+ return String(value);
8506
8685
  }
8507
8686
  for (const [, v] of Object.entries(values)) if (typeof v === "string" && v.length > 0 && v.length < 200) return v;
8508
8687
  };
@@ -8523,6 +8702,93 @@ function resolveRelationDisplayName(relation, prop) {
8523
8702
  } catch {}
8524
8703
  return id !== void 0 ? String(id) : void 0;
8525
8704
  }
8705
+ /**
8706
+ * The slots for one record, with `admin.display` resolvers applied.
8707
+ *
8708
+ * {@link resolveEntitySlots} answers the question from values already in hand —
8709
+ * which is everything a slot filled by a property needs, and nothing a slot
8710
+ * filled by a *resolver* can use. A computed title may have to be fetched, so it
8711
+ * arrives after the first render; this hook is where the two meet.
8712
+ *
8713
+ * Until a resolver lands, the derived slot shows. That is deliberate: a list of
8714
+ * fifty rows must not blank its titles for a beat while fifty promises settle,
8715
+ * and the derived answer — the best-ranked property carrying a readable value —
8716
+ * is a reasonable thing to be looking at meanwhile. When the resolved value
8717
+ * arrives it replaces the slot outright, property and all, because there is no
8718
+ * property behind a computed value (see {@link PreviewSlot}).
8719
+ *
8720
+ * Every role is hooked unconditionally, so the hook order is fixed regardless of
8721
+ * what a given collection declares.
8722
+ */
8723
+ function useEntitySlots(entity, collection, slotKeys) {
8724
+ const derived = useMemo(() => resolveEntitySlots(entity, collection, slotKeys), [
8725
+ entity,
8726
+ collection,
8727
+ slotKeys
8728
+ ]);
8729
+ const params = {
8730
+ collection,
8731
+ entity
8732
+ };
8733
+ const title = useEntityDisplay("title", params);
8734
+ const subtitle = useEntityDisplay("subtitle", params);
8735
+ const image = useEntityDisplay("image", params);
8736
+ const status = useEntityDisplay("status", params);
8737
+ const date = useEntityDisplay("date", params);
8738
+ return useMemo(() => {
8739
+ const computed = (resolved) => resolved.value !== void 0 && resolved.source === void 0 ? { value: resolved.value } : void 0;
8740
+ const computedDate = computed(date);
8741
+ return {
8742
+ ...derived,
8743
+ title: computed(title) ?? derived.title,
8744
+ subtitle: computed(subtitle) ?? derived.subtitle,
8745
+ image: computed(image) ?? derived.image,
8746
+ status: computed(status) ?? derived.status,
8747
+ date: computedDate ? {
8748
+ ...computedDate,
8749
+ formatted: formatDateValue$1(computedDate.value)
8750
+ } : derived.date
8751
+ };
8752
+ }, [
8753
+ derived,
8754
+ title,
8755
+ subtitle,
8756
+ image,
8757
+ status,
8758
+ date
8759
+ ]);
8760
+ }
8761
+ //#endregion
8762
+ //#region src/components/CollectionViewBinding/SlotValue.tsx
8763
+ /**
8764
+ * One display slot, rendered.
8765
+ *
8766
+ * A slot's value arrives one of two ways and they cannot be rendered the same:
8767
+ *
8768
+ * - **From a property** — render it with that property's own preview, so an
8769
+ * enum status keeps its colour, a date keeps its format and a storage path
8770
+ * becomes a thumbnail. This is why a declared path beats a resolver whenever
8771
+ * the value is actually on the record.
8772
+ * - **From a resolver** — there is no property, because the value was computed
8773
+ * or fetched. All that can be said about it is that it is text.
8774
+ *
8775
+ * The branch lives here, once, rather than at each of the fourteen places the
8776
+ * list row, the card and the board card render a slot. That count is the reason:
8777
+ * the last time a rule like this was inlined per call site, it drifted into
8778
+ * seven versions.
8779
+ */
8780
+ function SlotValue({ slot, size, textOnly, fill }) {
8781
+ if (!slot || slot.value === void 0 || slot.value === null) return null;
8782
+ if (!slot.property || !slot.propertyKey) return /* @__PURE__ */ jsx(Fragment, { children: Array.isArray(slot.value) ? slot.value.map((item) => String(item)).join(", ") : String(slot.value) });
8783
+ return /* @__PURE__ */ jsx(PropertyPreview, {
8784
+ propertyKey: slot.propertyKey,
8785
+ value: slot.value,
8786
+ property: slot.property,
8787
+ size,
8788
+ textOnly,
8789
+ fill
8790
+ });
8791
+ }
8526
8792
  //#endregion
8527
8793
  //#region src/components/CollectionViewBinding/EntityCardBinding.tsx
8528
8794
  /**
@@ -8532,12 +8798,7 @@ function resolveRelationDisplayName(relation, prop) {
8532
8798
  function EntityCardBinding({ entity, collection, onClick, selected, highlighted, onSelectionChange, selectionEnabled, size = "m" }) {
8533
8799
  const authController = useAuthController();
8534
8800
  const analyticsController = useAnalyticsController();
8535
- const slotKeys = useCollectionSlotKeys(collection, authController, useCustomizationController().propertyConfigs);
8536
- const slots = useMemo(() => resolveEntitySlots(entity, collection, slotKeys), [
8537
- entity,
8538
- collection,
8539
- slotKeys
8540
- ]);
8801
+ const slots = useEntitySlots(entity, collection, useCollectionSlotKeys(collection, authController, useCustomizationController().propertyConfigs));
8541
8802
  const handleClick = (e) => {
8542
8803
  if (e && (e.metaKey || e.ctrlKey) && selectionEnabled) {
8543
8804
  e.preventDefault();
@@ -8566,11 +8827,9 @@ function EntityCardBinding({ entity, collection, onClick, selected, highlighted,
8566
8827
  children: [
8567
8828
  slots.image ? /* @__PURE__ */ jsx("div", {
8568
8829
  className: "w-full h-full",
8569
- children: /* @__PURE__ */ jsx(PropertyPreview, {
8570
- property: slots.image.property,
8571
- propertyKey: slots.image.propertyKey,
8830
+ children: /* @__PURE__ */ jsx(SlotValue, {
8831
+ slot: slots.image,
8572
8832
  size: "medium",
8573
- value: slots.image.value,
8574
8833
  fill: true
8575
8834
  })
8576
8835
  }) : /* @__PURE__ */ jsx("div", {
@@ -8602,10 +8861,8 @@ function EntityCardBinding({ entity, collection, onClick, selected, highlighted,
8602
8861
  }),
8603
8862
  /* @__PURE__ */ jsx("div", {
8604
8863
  className: "truncate my-1 text-sm font-medium min-h-[20px]",
8605
- children: slots.title ? /* @__PURE__ */ jsx(PropertyPreview, {
8606
- propertyKey: slots.title.propertyKey,
8607
- value: slots.title.value,
8608
- property: slots.title.property,
8864
+ children: slots.title ? /* @__PURE__ */ jsx(SlotValue, {
8865
+ slot: slots.title,
8609
8866
  size: "small"
8610
8867
  }) : /* @__PURE__ */ jsx(Typography, {
8611
8868
  variant: "body2",
@@ -8615,10 +8872,8 @@ function EntityCardBinding({ entity, collection, onClick, selected, highlighted,
8615
8872
  }),
8616
8873
  slots.subtitle && /* @__PURE__ */ jsx("div", {
8617
8874
  className: "line-clamp-3 [&_div]:line-clamp-3 text-xs text-surface-600 dark:text-surface-400 [&_p]:!my-1 [&_p:first-child]:!mt-0 [&_p:last-child]:!mb-0",
8618
- children: /* @__PURE__ */ jsx(PropertyPreview, {
8619
- propertyKey: slots.subtitle.propertyKey,
8620
- value: slots.subtitle.value,
8621
- property: slots.subtitle.property,
8875
+ children: /* @__PURE__ */ jsx(SlotValue, {
8876
+ slot: slots.subtitle,
8622
8877
  size: "small"
8623
8878
  })
8624
8879
  }),
@@ -8636,10 +8891,8 @@ function EntityCardBinding({ entity, collection, onClick, selected, highlighted,
8636
8891
  }),
8637
8892
  slots.status && /* @__PURE__ */ jsx("div", {
8638
8893
  className: "mt-1",
8639
- children: /* @__PURE__ */ jsx(PropertyPreview, {
8640
- propertyKey: slots.status.propertyKey,
8641
- value: slots.status.value,
8642
- property: slots.status.property,
8894
+ children: /* @__PURE__ */ jsx(SlotValue, {
8895
+ slot: slots.status,
8643
8896
  size: "small"
8644
8897
  })
8645
8898
  })
@@ -9077,7 +9330,7 @@ function CollectionListViewBinding({ collection, tableController, onEntityClick,
9077
9330
  * the column system for developer-defined table layouts.
9078
9331
  */
9079
9332
  var ListRow = React.memo(function ListRow({ entity, collection, onClick, selected, highlighted, onSelectionChange, selectionEnabled, columns, slotKeys, rowClasses, showImage, size, isLast, isActive = false, listViewActions = [], context, path, selectionController, openEntityMode }) {
9080
- const slots = resolveEntitySlots(entity, collection, slotKeys);
9333
+ const slots = useEntitySlots(entity, collection, slotKeys);
9081
9334
  const handleClick = useCallback((e) => {
9082
9335
  if ((e.metaKey || e.ctrlKey) && selectionEnabled) {
9083
9336
  e.preventDefault();
@@ -9118,10 +9371,8 @@ var ListRow = React.memo(function ListRow({ entity, collection, onClick, selecte
9118
9371
  className: "flex-shrink-0",
9119
9372
  children: slots.image ? /* @__PURE__ */ jsx("div", {
9120
9373
  className: cls("w-10 h-10 rounded-lg border relative overflow-hidden bg-surface-100 dark:bg-surface-900", defaultBorderMixin),
9121
- children: /* @__PURE__ */ jsx(PropertyPreview, {
9122
- propertyKey: slots.image.propertyKey,
9123
- value: slots.image.value,
9124
- property: slots.image.property,
9374
+ children: /* @__PURE__ */ jsx(SlotValue, {
9375
+ slot: slots.image,
9125
9376
  size: "small",
9126
9377
  fill: true
9127
9378
  })
@@ -9143,10 +9394,8 @@ var ListRow = React.memo(function ListRow({ entity, collection, onClick, selecte
9143
9394
  component: "div",
9144
9395
  variant: "body2",
9145
9396
  className: "font-semibold text-surface-900 dark:text-surface-50 truncate transition-colors group-hover:text-primary-600 dark:group-hover:text-primary-400",
9146
- children: /* @__PURE__ */ jsx(PropertyPreview, {
9147
- propertyKey: slots.title.propertyKey,
9148
- value: slots.title.value,
9149
- property: slots.title.property,
9397
+ children: /* @__PURE__ */ jsx(SlotValue, {
9398
+ slot: slots.title,
9150
9399
  size: "small",
9151
9400
  textOnly: true
9152
9401
  })
@@ -9163,10 +9412,8 @@ var ListRow = React.memo(function ListRow({ entity, collection, onClick, selecte
9163
9412
  variant: "caption",
9164
9413
  component: "div",
9165
9414
  className: "text-surface-500 dark:text-surface-400 truncate",
9166
- children: /* @__PURE__ */ jsx(PropertyPreview, {
9167
- propertyKey: slots.subtitle.propertyKey,
9168
- value: slots.subtitle.value,
9169
- property: slots.subtitle.property,
9415
+ children: /* @__PURE__ */ jsx(SlotValue, {
9416
+ slot: slots.subtitle,
9170
9417
  size: "small",
9171
9418
  textOnly: true
9172
9419
  })
@@ -9237,10 +9484,8 @@ var ListRow = React.memo(function ListRow({ entity, collection, onClick, selecte
9237
9484
  className: "flex items-center gap-4 flex-shrink-0 ml-auto",
9238
9485
  children: [slots.status && /* @__PURE__ */ jsx("div", {
9239
9486
  className: "flex-shrink-0 @max-[400px]:hidden",
9240
- children: /* @__PURE__ */ jsx(PropertyPreview, {
9241
- propertyKey: slots.status.propertyKey,
9242
- value: slots.status.value,
9243
- property: slots.status.property,
9487
+ children: /* @__PURE__ */ jsx(SlotValue, {
9488
+ slot: slots.status,
9244
9489
  size: "small"
9245
9490
  })
9246
9491
  }), slots.date && /* @__PURE__ */ jsx("div", {
@@ -9273,260 +9518,174 @@ var ListRow = React.memo(function ListRow({ entity, collection, onClick, selecte
9273
9518
  openEntityMode: openEntityMode ?? collection?.openEntityMode ?? "full_screen"
9274
9519
  });
9275
9520
  },
9276
- children: getIcon(action.icon, void 0, void 0, "smallest")
9277
- })
9278
- }, action.key ?? index))
9279
- })
9280
- ]
9281
- });
9282
- });
9283
- //#endregion
9284
- //#region src/components/PropertyCollectionView.tsx
9285
- /**
9286
- * Build a readable label for a path and resolve the property
9287
- * Supports map and array (including arrays of maps)
9288
- */
9289
- function buildPropertyLabelAndGetProperty(properties, key) {
9290
- if (!key) return {
9291
- label: "",
9292
- property: void 0
9293
- };
9294
- const segments = [];
9295
- const re = /([^[.\]]+)|\[(\d+)\]/g;
9296
- let m;
9297
- while ((m = re.exec(key)) !== null) if (m[1] !== void 0) segments.push(m[1]);
9298
- else if (m[2] !== void 0) segments.push(Number(m[2]));
9299
- let currentProps = properties;
9300
- let currentProp;
9301
- let lastLabel = "";
9302
- const getArrayOfProp = (p) => {
9303
- if (!p || p.type !== "array") return void 0;
9304
- return Array.isArray(p.of) ? p.of[0] : p.of;
9305
- };
9306
- for (const seg of segments) {
9307
- if (typeof seg === "number") {
9308
- lastLabel = `[${seg}]`;
9309
- if (currentProp?.type === "array") {
9310
- currentProp = getArrayOfProp(currentProp);
9311
- if (currentProp?.type === "map" && currentProp.properties) currentProps = currentProp.properties;
9312
- else currentProps = void 0;
9313
- } else {
9314
- currentProp = void 0;
9315
- currentProps = void 0;
9316
- }
9317
- continue;
9318
- }
9319
- if (currentProps && currentProps[seg]) {
9320
- const nextProp = currentProps[seg];
9321
- currentProp = nextProp;
9322
- lastLabel = nextProp.name || String(seg);
9323
- if (nextProp.type === "map" && nextProp.properties) currentProps = nextProp.properties;
9324
- else if (nextProp.type === "array") currentProps = void 0;
9325
- else currentProps = void 0;
9326
- } else {
9327
- currentProp = void 0;
9328
- currentProps = void 0;
9329
- lastLabel = String(seg);
9330
- }
9331
- }
9332
- return {
9333
- label: lastLabel,
9334
- property: currentProp
9335
- };
9336
- }
9337
- var pathEndsWithIndex = (p) => /\[\d+\]$/.test(p);
9338
- /**
9339
- * Improved simple layout for nested changes:
9340
- * - Map or Array-of-Map -> section header + indented rows
9341
- * - Leaf or Array-of-Primitives -> single row with label and value
9342
- */
9343
- var PropertyCollectionView = ({ data, properties, baseKey = "", suppressHeader = false, size = "small" }) => {
9344
- const isTopLevel = !!baseKey && !baseKey.includes(".") && !baseKey.includes("[");
9345
- if (Array.isArray(data)) {
9346
- const { label: arrayLabel, property } = baseKey ? buildPropertyLabelAndGetProperty(properties, baseKey) : {
9347
- label: "",
9348
- property: void 0
9349
- };
9350
- const ofProp = property?.type === "array" ? Array.isArray(property.of) ? property.of[0] : property.of : void 0;
9351
- const isArrayOfMaps = ofProp?.type === "map";
9352
- const isArrayOfPrimitives = property?.type === "array" && ofProp && ofProp.type !== "map";
9353
- const isRelationArray = property?.type === "relation";
9354
- if (baseKey && property && (isArrayOfPrimitives || isRelationArray)) return /* @__PURE__ */ jsxs("div", {
9355
- className: `grid grid-cols-12 gap-x-4 ${isTopLevel ? "py-4" : "py-2"} items-start ${isTopLevel ? `border-b ${defaultBorderMixin} last:border-b-0` : ""}`,
9356
- children: [/* @__PURE__ */ jsx("div", {
9357
- className: "col-span-4 pr-2",
9358
- children: /* @__PURE__ */ jsx(Typography, {
9359
- variant: "caption",
9360
- color: "secondary",
9361
- component: "span",
9362
- className: "break-words",
9363
- children: arrayLabel
9364
- })
9365
- }), /* @__PURE__ */ jsx("div", {
9366
- className: "col-span-8",
9367
- children: /* @__PURE__ */ jsx(PropertyPreview, {
9368
- propertyKey: baseKey,
9369
- value: data,
9370
- property,
9371
- size
9372
- })
9373
- })]
9374
- });
9375
- return /* @__PURE__ */ jsxs("div", {
9376
- className: `${isTopLevel ? "py-4" : "py-1"} ${isTopLevel ? `border-b ${defaultBorderMixin} last:border-b-0` : ""}`,
9377
- children: [baseKey && arrayLabel && !suppressHeader && /* @__PURE__ */ jsx(Typography, {
9378
- variant: "caption",
9379
- color: "secondary",
9380
- component: "span",
9381
- children: arrayLabel
9382
- }), /* @__PURE__ */ jsx("div", {
9383
- className: baseKey ? `pl-4 mt-1 border-l ${defaultBorderMixin}` : "",
9384
- children: data.map((item, index) => {
9385
- if (item === null || item === void 0) return null;
9386
- const currentKey = baseKey ? `${baseKey}[${index}]` : `[${index}]`;
9387
- return /* @__PURE__ */ jsxs("div", {
9388
- className: "py-1",
9389
- children: [/* @__PURE__ */ jsx(Typography, {
9390
- variant: "caption",
9391
- color: "secondary",
9392
- component: "span",
9393
- children: isArrayOfMaps && ofProp?.name ? `${ofProp.name} [${index}]` : `[${index}]`
9394
- }), /* @__PURE__ */ jsx("div", {
9395
- className: `pl-4 mt-1 border-l ${defaultBorderMixin}`,
9396
- children: /* @__PURE__ */ jsx(PropertyCollectionView, {
9397
- data: item,
9398
- properties,
9399
- baseKey: currentKey,
9400
- suppressHeader: true,
9401
- size
9402
- })
9403
- })]
9404
- }, currentKey);
9405
- })
9406
- })]
9407
- });
9408
- }
9409
- if (typeof data === "object" && data !== null) {
9410
- const { label, property } = baseKey ? buildPropertyLabelAndGetProperty(properties, baseKey) : {
9411
- label: "",
9412
- property: void 0
9413
- };
9414
- if (baseKey && (!property || property.type !== "map" || !property.properties)) {
9415
- if (!property) return null;
9416
- return /* @__PURE__ */ jsxs("div", {
9417
- className: `grid grid-cols-12 gap-x-4 ${isTopLevel ? "py-4" : "py-2"} items-start ${isTopLevel ? `border-b ${defaultBorderMixin} last:border-b-0` : ""}`,
9418
- children: [/* @__PURE__ */ jsx("div", {
9419
- className: "col-span-4 pr-2",
9420
- children: /* @__PURE__ */ jsx(Typography, {
9421
- variant: "caption",
9422
- color: "secondary",
9423
- component: "span",
9424
- className: "break-words",
9425
- children: label
9426
- })
9427
- }), /* @__PURE__ */ jsx("div", {
9428
- className: "col-span-8",
9429
- children: /* @__PURE__ */ jsx(PropertyPreview, {
9430
- propertyKey: baseKey,
9431
- value: data,
9432
- property,
9433
- size
9521
+ children: getIcon(action.icon, void 0, void 0, "smallest")
9434
9522
  })
9435
- })]
9436
- });
9437
- }
9438
- const showMapHeader = baseKey && !suppressHeader && property?.type === "map" && (property.name || !pathEndsWithIndex(baseKey));
9439
- const headerText = property?.name || label;
9440
- return /* @__PURE__ */ jsxs("div", {
9441
- className: `${isTopLevel ? "py-4" : "py-1"} ${isTopLevel ? `border-b ${defaultBorderMixin} last:border-b-0` : ""}`,
9442
- children: [showMapHeader && /* @__PURE__ */ jsx(Typography, {
9443
- variant: "caption",
9444
- color: "secondary",
9445
- component: "span",
9446
- children: headerText
9447
- }), /* @__PURE__ */ jsx("div", {
9448
- className: baseKey ? `pl-4 mt-1 border-l ${defaultBorderMixin}` : "",
9449
- children: Object.entries(data).map(([key, value]) => {
9450
- if (value === null || value === void 0) return null;
9451
- if (baseKey === "" && !properties[key]) return null;
9452
- const currentKey = baseKey ? `${baseKey}.${key}` : key;
9453
- return /* @__PURE__ */ jsx(PropertyCollectionView, {
9454
- data: value,
9455
- properties,
9456
- baseKey: currentKey,
9457
- size
9458
- }, currentKey);
9459
- })
9460
- })]
9461
- });
9462
- }
9463
- if (baseKey) {
9464
- const { label, property } = buildPropertyLabelAndGetProperty(properties, baseKey);
9465
- if (!property) return null;
9466
- return /* @__PURE__ */ jsxs("div", {
9467
- className: `grid grid-cols-12 gap-x-4 ${isTopLevel ? "py-4" : "py-2"} items-start ${isTopLevel ? `border-b ${defaultBorderMixin} last:border-b-0` : ""}`,
9468
- children: [/* @__PURE__ */ jsx("div", {
9469
- className: "col-span-4 pr-2",
9470
- children: /* @__PURE__ */ jsx(Typography, {
9471
- variant: "caption",
9472
- color: "secondary",
9473
- component: "span",
9474
- className: "break-words",
9475
- children: label
9476
- })
9477
- }), /* @__PURE__ */ jsx("div", {
9478
- className: "col-span-8",
9479
- children: /* @__PURE__ */ jsx(PropertyPreview, {
9480
- propertyKey: baseKey,
9481
- value: data,
9482
- property,
9483
- size
9484
- })
9485
- })]
9486
- });
9487
- }
9488
- return null;
9489
- };
9523
+ }, action.key ?? index))
9524
+ })
9525
+ ]
9526
+ });
9527
+ });
9490
9528
  //#endregion
9491
9529
  //#region src/components/EntityViewBinding.tsx
9530
+ /** Nothing can be in error in a view that cannot be submitted. */
9531
+ var NO_ERRORS = /* @__PURE__ */ new Set();
9532
+ /**
9533
+ * The fields the read-only view lays out, in render order.
9534
+ *
9535
+ * The same set the form uses, minus the `additionalFields` when there is no
9536
+ * form context to hand them: those entries are arbitrary components, not
9537
+ * values, and one rendered against no context is nothing at all. They are
9538
+ * dropped *before* the layout resolves rather than skipped while rendering,
9539
+ * because the resolver has by then given each of them a full row — skipping
9540
+ * later leaves that row as a hole in the grid.
9541
+ */
9542
+ function readOnlyFieldKeys(collection, hasFormContext) {
9543
+ const keys = getFormFieldKeys(collection);
9544
+ if (hasFormContext) return keys;
9545
+ const additional = new Set((collection.additionalFields ?? []).map((field) => field.key));
9546
+ return keys.filter((key) => !additional.has(key));
9547
+ }
9492
9548
  /**
9493
9549
  * The read-only rendering of a record's values.
9494
9550
  *
9495
- * This used to prepend a synthetic `Id` row of its own. Between that row, the
9496
- * collection's own id property, and the `path/id` chip above it, the same UUID
9497
- * appeared three times and in the ~340px split pane each copy wrapped over
9498
- * four lines, so most of the first screen was one id repeated. The id is now a
9499
- * copyable chip in the identity bar, once.
9551
+ * This is the same layout as {@link EntityForm}: the sections, the grid spans,
9552
+ * the labels and the rail all come from the one resolver, so the record does
9553
+ * not rearrange itself when you press Edit. Only the controls differ each
9554
+ * field renders its {@link PropertyPreview} instead of an input.
9555
+ *
9556
+ * It used to be a flat two-column table of every value in the record: no
9557
+ * grouping, no widths, a label column sized for short names and a value column
9558
+ * that wrapped emails over two lines while the pane beside it stayed empty. A
9559
+ * boolean and a markdown body were given the same room.
9560
+ *
9561
+ * The synthetic `Id` row it used to prepend is gone with the same reasoning the
9562
+ * form applies: between that row, the collection's own id property and the
9563
+ * `path/id` chip above them, one UUID appeared three times. The id is a
9564
+ * copyable chip in the identity bar, and the rail's record block, once each.
9500
9565
  */
9501
- function EntityViewBinding({ entity, collection, path, className }) {
9502
- const customizationController = useCustomizationController();
9503
- const properties = React.useMemo(() => {
9504
- const entries = Object.entries(collection.properties ?? {});
9505
- const idKeys = entries.filter(([, p]) => p && "isId" in p && p.isId);
9506
- if (idKeys.length !== 1) return collection.properties;
9507
- return Object.fromEntries(entries.filter(([key]) => key !== idKeys[0][0]));
9508
- }, [collection.properties]);
9509
- const externalLink = customizationController?.entityLinkBuilder?.({ entity });
9510
- return /* @__PURE__ */ jsxs("div", {
9511
- className: "w-full " + (className ?? ""),
9512
- children: [externalLink && /* @__PURE__ */ jsx("div", {
9513
- className: "flex justify-end mb-2",
9514
- children: /* @__PURE__ */ jsx(Tooltip, {
9515
- title: "Open in the live site",
9516
- children: /* @__PURE__ */ jsx("a", {
9517
- href: externalLink,
9518
- rel: "noopener noreferrer",
9519
- target: "_blank",
9520
- children: /* @__PURE__ */ jsx(IconButton, {
9521
- size: "small",
9522
- children: /* @__PURE__ */ jsx(ExternalLinkIcon, {})
9566
+ function EntityViewBinding({ entity, collection, className, asPage = false, openEntityMode = "full_screen", formContext }) {
9567
+ const externalLink = useCustomizationController()?.entityLinkBuilder?.({ entity });
9568
+ const hasFormContext = Boolean(formContext);
9569
+ const fieldKeys = useMemo(() => readOnlyFieldKeys(collection, hasFormContext), [collection, hasFormContext]);
9570
+ const layout = useMemo(() => resolveFormLayout({
9571
+ collection,
9572
+ fieldKeys,
9573
+ status: "existing"
9574
+ }), [collection, fieldKeys]);
9575
+ const containerRef = useRef(null);
9576
+ const railVisible = useRailVisible(containerRef);
9577
+ const showRail = asPage && layout.hasRail && railVisible;
9578
+ const [collapsedSections, setCollapsedSections] = useState(() => new Set(layout.sections.filter((section) => section.collapsed).map((section) => section.key)));
9579
+ const toggleSection = useCallback((key) => {
9580
+ setCollapsedSections((prev) => {
9581
+ const next = new Set(prev);
9582
+ if (next.has(key)) next.delete(key);
9583
+ else next.add(key);
9584
+ return next;
9585
+ });
9586
+ }, []);
9587
+ const renderField = useCallback((field) => {
9588
+ if (field.additional) {
9589
+ const additionalField = collection.additionalFields?.find((f) => f.key === field.key);
9590
+ if (!additionalField || !formContext) return null;
9591
+ const AdditionalFieldBuilder = additionalField.Builder;
9592
+ const additionalFieldContext = formContext;
9593
+ const child = AdditionalFieldBuilder ? /* @__PURE__ */ jsx(AdditionalFieldBuilder, {
9594
+ entity,
9595
+ context: additionalFieldContext
9596
+ }) : /* @__PURE__ */ jsx("div", {
9597
+ className: "w-full",
9598
+ children: /* @__PURE__ */ jsx(AdditionalFieldValue, {
9599
+ field: additionalField,
9600
+ entity,
9601
+ context: additionalFieldContext
9602
+ })
9603
+ });
9604
+ return /* @__PURE__ */ jsxs("div", {
9605
+ className: spanClass(field.span),
9606
+ children: [/* @__PURE__ */ jsx(LabelWithIconAndTooltip, {
9607
+ propertyKey: field.key,
9608
+ icon: /* @__PURE__ */ jsx(AlignLeftIcon, { size: iconSize.small }),
9609
+ title: additionalField.name,
9610
+ className: "text-text-secondary dark:text-text-secondary-dark"
9611
+ }), /* @__PURE__ */ jsx("div", {
9612
+ className: cls(paperMixin, "w-full min-h-14 p-4 md:p-6 overflow-x-auto no-scrollbar"),
9613
+ children: /* @__PURE__ */ jsx(ErrorBoundary, { children: child })
9614
+ })]
9615
+ }, `additional_${field.key}`);
9616
+ }
9617
+ const property = collection.properties?.[field.key];
9618
+ if (!property) return null;
9619
+ return /* @__PURE__ */ jsx("div", {
9620
+ className: spanClass(field.span),
9621
+ children: /* @__PURE__ */ jsx(FieldBlock, {
9622
+ propertyKey: field.key,
9623
+ property,
9624
+ showLabel: true,
9625
+ children: /* @__PURE__ */ jsx("div", {
9626
+ className: "min-h-8 flex flex-col justify-center min-w-0 text-text-primary dark:text-text-primary-dark",
9627
+ children: /* @__PURE__ */ jsx(PropertyPreview, {
9628
+ propertyKey: field.key,
9629
+ value: getValueInPath(entity.values, field.key),
9630
+ property,
9631
+ hideLabel: true,
9632
+ size: "medium"
9523
9633
  })
9524
9634
  })
9525
9635
  })
9526
- }), /* @__PURE__ */ jsx(PropertyCollectionView, {
9527
- data: entity.values,
9528
- properties,
9529
- size: "medium"
9636
+ }, `field_${field.key}`);
9637
+ }, [
9638
+ collection,
9639
+ entity,
9640
+ formContext
9641
+ ]);
9642
+ const sections = showRail || !layout.sidebar.length ? layout.sections : [...layout.sections, {
9643
+ key: "__rail",
9644
+ title: "Settings",
9645
+ collapsible: false,
9646
+ collapsed: false,
9647
+ fields: layout.sidebar
9648
+ }];
9649
+ const content = /* @__PURE__ */ jsxs(Fragment, { children: [externalLink && /* @__PURE__ */ jsx("div", {
9650
+ className: "flex justify-end mb-2",
9651
+ children: /* @__PURE__ */ jsx(Tooltip, {
9652
+ title: "Open in the live site",
9653
+ children: /* @__PURE__ */ jsx("a", {
9654
+ href: externalLink,
9655
+ rel: "noopener noreferrer",
9656
+ target: "_blank",
9657
+ children: /* @__PURE__ */ jsx(IconButton, {
9658
+ size: "small",
9659
+ children: /* @__PURE__ */ jsx(ExternalLinkIcon, {})
9660
+ })
9661
+ })
9662
+ })
9663
+ }), /* @__PURE__ */ jsx(FormSections, {
9664
+ sections,
9665
+ collapsed: collapsedSections,
9666
+ onToggle: toggleSection,
9667
+ errorKeys: NO_ERRORS,
9668
+ renderField
9669
+ })] });
9670
+ if (!asPage) return /* @__PURE__ */ jsx("div", {
9671
+ ref: containerRef,
9672
+ className: cls("@container/col w-full min-w-0", className),
9673
+ children: content
9674
+ });
9675
+ return /* @__PURE__ */ jsxs("div", {
9676
+ ref: containerRef,
9677
+ className: cls("flex-1 flex flex-row w-full min-h-0", className),
9678
+ children: [/* @__PURE__ */ jsx("div", {
9679
+ className: "flex-1 min-w-0 overflow-y-auto flex justify-center items-start",
9680
+ children: /* @__PURE__ */ jsx("div", {
9681
+ className: cls("@container/col w-full max-w-3xl 2xl:max-w-4xl flex flex-col", openEntityMode === "dialog" ? "pt-5 pb-8 px-6 sm:px-8" : "pt-6 pb-12 px-5 sm:px-8"),
9682
+ children: content
9683
+ })
9684
+ }), showRail && /* @__PURE__ */ jsx(FormRail, {
9685
+ fields: layout.sidebar,
9686
+ showRecordMeta: layout.showRecordMeta,
9687
+ entity,
9688
+ renderField
9530
9689
  })]
9531
9690
  });
9532
9691
  }
@@ -9542,7 +9701,7 @@ function EntityViewBinding({ entity, collection, path, className }) {
9542
9701
  * editing. They live here instead, and the left half of a bar that was
9543
9702
  * previously empty now does the work.
9544
9703
  */
9545
- function EntityIdentityBar({ collection, title, entityId, status, dirty, saving, onSave, onDiscard, saveDisabled, hasErrors, onSaveAndClose, onBack, onInspect, onViewHistory, recordActions, pluginActions, trailing, leading }) {
9704
+ function EntityIdentityBar({ collection, collectionUrl, title, entityId, status, dirty, saving, onSave, onDiscard, saveDisabled, hasErrors, onSaveAndClose, onBack, onInspect, onViewHistory, recordActions, pluginActions, trailing, leading }) {
9546
9705
  const { t } = useTranslation();
9547
9706
  const saveLabel = status === "existing" ? t("save") : status === "copy" ? t("create_copy") : t("create");
9548
9707
  const closeLabel = status === "existing" ? t("save_and_close") : status === "copy" ? t("create_copy_and_close") : t("create_and_close");
@@ -9560,7 +9719,15 @@ function EntityIdentityBar({ collection, title, entityId, status, dirty, saving,
9560
9719
  children: /* @__PURE__ */ jsx(ArrowLeftIcon, { size: iconSize.smallest })
9561
9720
  })
9562
9721
  }),
9563
- /* @__PURE__ */ jsxs(Typography, {
9722
+ collectionUrl ? /* @__PURE__ */ jsx(Link, {
9723
+ to: collectionUrl,
9724
+ className: "visited:text-inherit dark:visited:text-inherit hidden sm:block",
9725
+ children: /* @__PURE__ */ jsxs(Typography, {
9726
+ variant: "caption",
9727
+ className: cls("text-text-disabled dark:text-text-disabled-dark whitespace-nowrap", "hover:text-text-primary dark:hover:text-text-primary-dark transition-colors"),
9728
+ children: [collection.name, "\xA0/"]
9729
+ })
9730
+ }) : /* @__PURE__ */ jsxs(Typography, {
9564
9731
  variant: "caption",
9565
9732
  className: "text-text-disabled dark:text-text-disabled-dark whitespace-nowrap hidden sm:block",
9566
9733
  children: [collection.name, "\xA0/"]
@@ -9696,9 +9863,11 @@ function SplitViewProvider({ value, children }) {
9696
9863
  * Closes the list of a split view, leaving the open record on its own.
9697
9864
  *
9698
9865
  * It leads the record's own app bar, where the breadcrumb starts — this is the
9699
- * entity pane's old "Open full screen" button, moved to that edge and wearing an
9700
- * X: closing the list and opening the record full screen are the same thing, so
9701
- * there is one control and one route (`#full`) rather than two.
9866
+ * entity pane's old "Open full screen" button, moved to that edge and wearing a
9867
+ * double chevron pointing at the list it folds away: closing the list and
9868
+ * opening the record full screen are the same thing, so there is one control
9869
+ * and one route (`#full`) rather than two. Full screen carries the mirrored
9870
+ * chevron to bring the list back.
9702
9871
  */
9703
9872
  function SplitListCloseButton() {
9704
9873
  const { t } = useTranslation();
@@ -9710,7 +9879,29 @@ function SplitListCloseButton() {
9710
9879
  size: "small",
9711
9880
  onClick: splitView.openFullScreen,
9712
9881
  "aria-label": t("hide_list"),
9713
- children: /* @__PURE__ */ jsx(XIcon, { size: iconSize.smallest })
9882
+ children: /* @__PURE__ */ jsx(ChevronsLeftIcon, { size: iconSize.smallest })
9883
+ })
9884
+ });
9885
+ }
9886
+ //#endregion
9887
+ //#region src/components/CollectionViewBinding/SplitListShowButton.tsx
9888
+ /**
9889
+ * Brings the list back beside a record that was opened full screen.
9890
+ *
9891
+ * The mirror of {@link SplitListCloseButton}: the same edge of the same bar,
9892
+ * the same double chevron pointing the other way, and the same single route —
9893
+ * showing the list is dropping the `#full` hash. It replaces the back arrow
9894
+ * where it applies, because the list it unfolds already holds the record.
9895
+ */
9896
+ function SplitListShowButton({ onClick }) {
9897
+ const { t } = useTranslation();
9898
+ return /* @__PURE__ */ jsx(Tooltip, {
9899
+ title: t("show_list"),
9900
+ children: /* @__PURE__ */ jsx(IconButton, {
9901
+ size: "small",
9902
+ onClick,
9903
+ "aria-label": t("show_list"),
9904
+ children: /* @__PURE__ */ jsx(ChevronsRightIcon, { size: iconSize.smallest })
9714
9905
  })
9715
9906
  });
9716
9907
  }
@@ -9744,7 +9935,7 @@ function JsonPreviewBinding({ values }) {
9744
9935
  }
9745
9936
  //#endregion
9746
9937
  //#region src/components/EntityInspector.tsx
9747
- var EntityHistoryView = lazy(() => import("./history-D7L_JlLO.js").then((m) => ({ default: m.EntityHistoryView })));
9938
+ var EntityHistoryView = lazy(() => import("./history-JL1ZmSKD.js").then((m) => ({ default: m.EntityHistoryView })));
9748
9939
  /**
9749
9940
  * Raw values and revision history, as an inspector rather than as tabs.
9750
9941
  *
@@ -9820,6 +10011,36 @@ function EntityInspector({ tab, onTabChange, onClose, collection, entity, formCo
9820
10011
  });
9821
10012
  }
9822
10013
  //#endregion
10014
+ //#region src/hooks/useEntityDisplayTitle.tsx
10015
+ /**
10016
+ * The heading for a record: the identity bar and the breadcrumb.
10017
+ *
10018
+ * Everything about *what a record is called* lives in {@link useEntityTitle}.
10019
+ * What is left here is the heading's own two rules, which are presentation, not
10020
+ * identity:
10021
+ *
10022
+ * The guard on `status` is the first. The title resolver returns the first
10023
+ * candidate carrying a value, and on a brand new entity the only values are the
10024
+ * property defaults — so creating a blog post opened a page headed **draft**,
10025
+ * the default of its `status` enum. A record that does not exist yet has no
10026
+ * name; it has an intent.
10027
+ *
10028
+ * The fallback is the second. A page needs a heading even when the record has
10029
+ * nothing readable in it, and the honest one is what kind of thing it is.
10030
+ */
10031
+ function useEntityDisplayTitle({ collection, entity, values, status }) {
10032
+ const isNew = status === "new";
10033
+ const singular = collection.singularName ?? collection.name;
10034
+ const { value: title } = useEntityTitle({
10035
+ collection,
10036
+ entity,
10037
+ values,
10038
+ enabled: !isNew
10039
+ });
10040
+ if (isNew) return `New ${singular.toLowerCase()}`;
10041
+ return title ?? singular;
10042
+ }
10043
+ //#endregion
9823
10044
  //#region src/util/view_constants.ts
9824
10045
  /**
9825
10046
  * Shared tab value constants used by EditViewBinding and DetailViewBinding.
@@ -9850,7 +10071,7 @@ function DetailViewBinding({ entityId, ...props }) {
9850
10071
  children: content
9851
10072
  });
9852
10073
  }
9853
- function DetailViewBindingInner({ path, entityId, selectedTab: selectedTabProp, collection, parentCollectionSlugs, parentEntityIds, onTabChange, onEditClick, entity, dataLoading, layout = "full_screen", barActions, barActionsStart }) {
10074
+ function DetailViewBindingInner({ path, entityId, selectedTab: selectedTabProp, collection, parentCollectionSlugs, parentEntityIds, onTabChange, onEditClick, entity, dataLoading, layout = "full_screen", barActions, barActionsStart, onShowList }) {
9854
10075
  const ResolvedCollectionView = useComponentOverride("Collection.View", CollectionViewBinding);
9855
10076
  const { t } = useTranslation();
9856
10077
  const context = useRebaseContext();
@@ -9966,10 +10187,11 @@ function DetailViewBindingInner({ path, entityId, selectedTab: selectedTabProp,
9966
10187
  ]));
9967
10188
  const formViewConfig = collection.formView;
9968
10189
  const FormViewBuilder = formViewConfig?.Builder ? resolveComponentRef(formViewConfig.Builder) : null;
9969
- const titlePropertyKey = getEntityTitlePropertyKeyForEntity(collection, usedEntity?.values, usedEntity?.id);
9970
- const rawTitle = usedEntity?.values && titlePropertyKey ? getValueInPath(usedEntity.values, titlePropertyKey) : void 0;
9971
- const titleUser = useResolvedUser(isUserSelectProperty(collection, titlePropertyKey) && typeof rawTitle === "string" ? rawTitle : void 0);
9972
- const title = titleUser ? getUserLabel(titleUser) : rawTitle !== void 0 && rawTitle !== null ? resolveTitleToString(rawTitle) : collection.singularName ?? collection.name;
10190
+ const title = useEntityDisplayTitle({
10191
+ collection,
10192
+ entity: usedEntity,
10193
+ status: "existing"
10194
+ });
9973
10195
  const nonActionCustomViews = useMemo(() => resolvedEntityViews.filter((e) => !e.includeActions), [resolvedEntityViews]);
9974
10196
  const customViewsView = customViews && nonActionCustomViews.map((customView) => {
9975
10197
  if (!customView) return null;
@@ -10035,21 +10257,30 @@ function DetailViewBindingInner({ path, entityId, selectedTab: selectedTabProp,
10035
10257
  collection
10036
10258
  ]);
10037
10259
  const propertyDetailView = () => {
10038
- if (FormViewBuilder && usedEntity) return /* @__PURE__ */ jsx(ErrorBoundary, { children: /* @__PURE__ */ jsx(Suspense, {
10039
- fallback: /* @__PURE__ */ jsx(CircularProgressCenter, {}),
10040
- children: /* @__PURE__ */ jsx(FormViewBuilder, {
10041
- collection,
10042
- parentCollectionSlugs,
10043
- parentEntityIds,
10044
- entity: usedEntity,
10045
- modifiedValues: usedEntity?.values,
10046
- formContext: readOnlyFormContext
10260
+ if (FormViewBuilder && usedEntity) return /* @__PURE__ */ jsx("div", {
10261
+ className: "flex-1 min-w-0 overflow-y-auto flex justify-center items-start",
10262
+ children: /* @__PURE__ */ jsx("div", {
10263
+ className: cls("w-full max-w-3xl 2xl:max-w-4xl flex flex-col", layout === "dialog" ? "pt-5 pb-12 px-6 sm:px-8" : "pt-6 pb-16 px-5 sm:px-8"),
10264
+ children: /* @__PURE__ */ jsx(ErrorBoundary, { children: /* @__PURE__ */ jsx(Suspense, {
10265
+ fallback: /* @__PURE__ */ jsx(CircularProgressCenter, {}),
10266
+ children: /* @__PURE__ */ jsx(FormViewBuilder, {
10267
+ collection,
10268
+ parentCollectionSlugs,
10269
+ parentEntityIds,
10270
+ entity: usedEntity,
10271
+ modifiedValues: usedEntity?.values,
10272
+ formContext: readOnlyFormContext
10273
+ })
10274
+ }) })
10047
10275
  })
10048
- }) });
10276
+ });
10049
10277
  return /* @__PURE__ */ jsx(Fragment, { children: usedEntity && /* @__PURE__ */ jsx(EntityViewBinding, {
10050
10278
  entity: usedEntity,
10051
10279
  collection,
10052
- path
10280
+ path,
10281
+ asPage: true,
10282
+ openEntityMode: layout,
10283
+ formContext: readOnlyFormContext
10053
10284
  }) });
10054
10285
  };
10055
10286
  const subcollectionTabs = subcollections && subcollections.map((subcollection) => {
@@ -10085,15 +10316,8 @@ function DetailViewBindingInner({ path, entityId, selectedTab: selectedTabProp,
10085
10316
  })
10086
10317
  }, `entity_detail_collection_tab_${view.name}`);
10087
10318
  });
10088
- const backToCollectionButton = layout === "full_screen" ? /* @__PURE__ */ jsx(Tooltip, {
10089
- title: t("back"),
10090
- children: /* @__PURE__ */ jsx(IconButton, {
10091
- size: "small",
10092
- "aria-label": t("back"),
10093
- onClick: () => navigate(urlController.buildUrlCollectionPath(path)),
10094
- children: /* @__PURE__ */ jsx(ArrowLeftIcon, { size: iconSize.smallest })
10095
- })
10096
- }) : null;
10319
+ const collectionUrl = withViewMode(urlController.buildUrlCollectionPath(path));
10320
+ const backToCollection = layout === "full_screen" && !onShowList ? () => navigate(collectionUrl) : void 0;
10097
10321
  const fullScreenButton = !barActions && (layout === "side_panel" || layout === "dialog") && entityId ? /* @__PURE__ */ jsx(Tooltip, {
10098
10322
  title: "Open full screen",
10099
10323
  children: /* @__PURE__ */ jsx(IconButton, {
@@ -10114,29 +10338,31 @@ function DetailViewBindingInner({ path, entityId, selectedTab: selectedTabProp,
10114
10338
  children: t("edit_entity")
10115
10339
  }) : null;
10116
10340
  const mainView = /* @__PURE__ */ jsx("div", {
10117
- className: cls("flex-1 flex flex-row w-full overflow-y-auto justify-center", !mainViewVisible ? "hidden" : ""),
10118
- children: /* @__PURE__ */ jsx("div", {
10119
- className: cls("w-full max-w-3xl 2xl:max-w-4xl flex flex-col", layout === "dialog" ? "pt-5 pb-12 px-6 sm:px-8" : "pt-6 pb-16 px-5 sm:px-8"),
10120
- children: propertyDetailView()
10121
- })
10341
+ className: cls("flex-1 flex flex-row w-full min-h-0", !mainViewVisible ? "hidden" : ""),
10342
+ children: propertyDetailView()
10122
10343
  });
10123
10344
  let result = /* @__PURE__ */ jsxs("div", {
10124
10345
  className: "relative flex flex-col h-full w-full bg-white dark:bg-surface-800",
10125
10346
  children: [
10126
10347
  /* @__PURE__ */ jsx(EntityIdentityBar, {
10127
10348
  collection,
10349
+ collectionUrl: layout === "full_screen" || layout === "split" ? collectionUrl : void 0,
10128
10350
  title,
10129
10351
  entityId,
10130
10352
  status: "existing",
10131
10353
  dirty: false,
10132
10354
  saving: false,
10355
+ onBack: backToCollection,
10133
10356
  onInspect: includeJsonView ? () => setInspectorTab("json") : void 0,
10134
10357
  onViewHistory: includeHistoryView ? () => setInspectorTab("history") : void 0,
10135
- leading: /* @__PURE__ */ jsxs(Fragment, { children: [barActionsStart, layout === "split" && /* @__PURE__ */ jsx(SplitListCloseButton, {})] }),
10358
+ leading: /* @__PURE__ */ jsxs(Fragment, { children: [
10359
+ barActionsStart,
10360
+ layout === "split" && /* @__PURE__ */ jsx(SplitListCloseButton, {}),
10361
+ layout === "full_screen" && onShowList && /* @__PURE__ */ jsx(SplitListShowButton, { onClick: onShowList })
10362
+ ] }),
10136
10363
  trailing: /* @__PURE__ */ jsxs(Fragment, { children: [
10137
10364
  editButton,
10138
10365
  pluginActionsTop,
10139
- backToCollectionButton,
10140
10366
  fullScreenButton,
10141
10367
  barActions?.({
10142
10368
  path,
@@ -10552,12 +10778,7 @@ function SplitListView({ collection, tableController, onEntityClick: externalOnE
10552
10778
  */
10553
10779
  function BoardCardBindingInner({ item, isDragging, isGroupedOver, style, collection, onClick, selected, onSelectionChange, selectionEnabled = false }) {
10554
10780
  const entity = item.data;
10555
- const slotKeys = useCollectionSlotKeys(collection, useAuthController(), useCustomizationController().propertyConfigs);
10556
- const slots = useMemo(() => resolveEntitySlots(entity, collection, slotKeys), [
10557
- entity,
10558
- collection,
10559
- slotKeys
10560
- ]);
10781
+ const slots = useEntitySlots(entity, collection, useCollectionSlotKeys(collection, useAuthController(), useCustomizationController().propertyConfigs));
10561
10782
  const handleClick = useCallback((e) => {
10562
10783
  if ((e.metaKey || e.ctrlKey) && selectionEnabled) {
10563
10784
  e.preventDefault();
@@ -10614,11 +10835,9 @@ function BoardCardBindingInner({ item, isDragging, isGroupedOver, style, collect
10614
10835
  onClick: handleThumbnailClick,
10615
10836
  children: [slots.image ? /* @__PURE__ */ jsx("div", {
10616
10837
  className: cls("w-10 h-10 rounded-md overflow-hidden transition-opacity duration-200", selectionEnabled && "group-hover/card:opacity-30", selected && "opacity-0"),
10617
- children: /* @__PURE__ */ jsx(PropertyPreview, {
10618
- property: slots.image.property,
10619
- propertyKey: slots.image.propertyKey,
10838
+ children: /* @__PURE__ */ jsx(SlotValue, {
10839
+ slot: slots.image,
10620
10840
  size: "small",
10621
- value: slots.image.value,
10622
10841
  fill: true
10623
10842
  })
10624
10843
  }) : /* @__PURE__ */ jsx("div", {
@@ -10645,10 +10864,8 @@ function BoardCardBindingInner({ item, isDragging, isGroupedOver, style, collect
10645
10864
  children: [
10646
10865
  /* @__PURE__ */ jsx("div", {
10647
10866
  className: "line-clamp-2 text-sm font-medium",
10648
- children: slots.title ? /* @__PURE__ */ jsx(PropertyPreview, {
10649
- propertyKey: slots.title.propertyKey,
10650
- value: slots.title.value,
10651
- property: slots.title.property,
10867
+ children: slots.title ? /* @__PURE__ */ jsx(SlotValue, {
10868
+ slot: slots.title,
10652
10869
  size: "small"
10653
10870
  }) : /* @__PURE__ */ jsx("span", {
10654
10871
  className: "text-surface-500",
@@ -10660,10 +10877,8 @@ function BoardCardBindingInner({ item, isDragging, isGroupedOver, style, collect
10660
10877
  children: typeof slots.subtitle.value === "string" ? /* @__PURE__ */ jsx(Markdown, {
10661
10878
  source: slots.subtitle.value,
10662
10879
  size: "small"
10663
- }) : /* @__PURE__ */ jsx(PropertyPreview, {
10664
- propertyKey: slots.subtitle.propertyKey,
10665
- value: slots.subtitle.value,
10666
- property: slots.subtitle.property,
10880
+ }) : /* @__PURE__ */ jsx(SlotValue, {
10881
+ slot: slots.subtitle,
10667
10882
  size: "small"
10668
10883
  })
10669
10884
  }) : /* @__PURE__ */ jsx("div", {
@@ -11324,14 +11539,14 @@ function CollectionBoardViewBinding({ collection, tableController, fullPath, par
11324
11539
  const rawOrderProperty = collection.orderProperty;
11325
11540
  const orderProperty = useMemo(() => {
11326
11541
  if (!rawOrderProperty) return void 0;
11327
- if (!getPropertyInPath(collection.properties, rawOrderProperty)) {
11542
+ if (!getPropertyInPath$1(collection.properties, rawOrderProperty)) {
11328
11543
  console.warn(`orderProperty "${rawOrderProperty}" is defined but does not exist in the collection properties. Treating as unconfigured.`);
11329
11544
  return;
11330
11545
  }
11331
11546
  return rawOrderProperty;
11332
11547
  }, [rawOrderProperty, collection.properties]);
11333
11548
  const { enumColumns, columnLabels, columnColors } = useMemo(() => {
11334
- const property = getPropertyInPath(collection.properties, columnProperty);
11549
+ const property = getPropertyInPath$1(collection.properties, columnProperty);
11335
11550
  if (!property || !("type" in property) || property.type !== "string") return {
11336
11551
  enumColumns: [],
11337
11552
  columnLabels: {}
@@ -12722,7 +12937,7 @@ function PopupFormFieldInternal({ tableKey, entityId, customFieldValidator, prop
12722
12937
  openEntityMode: "side_panel",
12723
12938
  disabled: false
12724
12939
  };
12725
- const property = propertyKey && getPropertyInPath(collection?.properties ?? {}, propertyKey);
12940
+ const property = propertyKey && getPropertyInPath$1(collection?.properties ?? {}, propertyKey);
12726
12941
  const fieldProps = propertyKey && property ? {
12727
12942
  propertyKey,
12728
12943
  disabled: isSubmitting || isReadOnly(property) || !!property.admin?.disabled,
@@ -13716,7 +13931,7 @@ var CollectionViewBindingInner = React.memo(function CollectionViewBindingInner(
13716
13931
  }, [userConfigPersistence]);
13717
13932
  const onColumnResize = useCallback(({ width, key }) => {
13718
13933
  const collection = collectionRef.current;
13719
- if (!getPropertyInPath(collection.properties, key)) return;
13934
+ if (!getPropertyInPath$1(collection.properties, key)) return;
13720
13935
  const localCollection = buildPropertyWidthOverwrite(key, width);
13721
13936
  onCollectionModifiedForUser(path, localCollection);
13722
13937
  }, [onCollectionModifiedForUser, path]);
@@ -13762,7 +13977,7 @@ var CollectionViewBindingInner = React.memo(function CollectionViewBindingInner(
13762
13977
  const resolvedCollection = collection;
13763
13978
  useMemo(() => {
13764
13979
  if (!collection.kanban?.columnProperty) return false;
13765
- const property = getPropertyInPath(resolvedCollection.properties, collection.kanban.columnProperty);
13980
+ const property = getPropertyInPath$1(resolvedCollection.properties, collection.kanban.columnProperty);
13766
13981
  if (!property || property.type !== "string") return false;
13767
13982
  return Boolean("enum" in property && property.enum);
13768
13983
  }, [collection.kanban?.columnProperty, resolvedCollection.properties]);
@@ -13827,8 +14042,8 @@ var CollectionViewBindingInner = React.memo(function CollectionViewBindingInner(
13827
14042
  analyticsController
13828
14043
  ]);
13829
14044
  const getPropertyFor = useCallback(({ propertyKey, entity }) => {
13830
- let property = getPropertyInPath(collection.properties, propertyKey);
13831
- if (!property) property = getPropertyInPath(resolvedCollection.properties, propertyKey);
14045
+ let property = getPropertyInPath$1(collection.properties, propertyKey);
14046
+ if (!property) property = getPropertyInPath$1(resolvedCollection.properties, propertyKey);
13832
14047
  return property ?? null;
13833
14048
  }, [collection.properties, resolvedCollection.properties]);
13834
14049
  const displayedColumnIds = useColumnIds(useMemo(() => {
@@ -13967,7 +14182,7 @@ var CollectionViewBindingInner = React.memo(function CollectionViewBindingInner(
13967
14182
  } : void 0;
13968
14183
  const onColumnsOrderChange = useCallback((newColumns) => {
13969
14184
  const seenKeys = /* @__PURE__ */ new Set();
13970
- const newPropertiesOrder = newColumns.filter((col) => !col.frozen && getPropertyInPath(collection.properties, col.key)).map((col) => col.key).filter((key) => {
14185
+ const newPropertiesOrder = newColumns.filter((col) => !col.frozen && getPropertyInPath$1(collection.properties, col.key)).map((col) => col.key).filter((key) => {
13971
14186
  if (seenKeys.has(key)) return false;
13972
14187
  seenKeys.add(key);
13973
14188
  return true;
@@ -14641,31 +14856,6 @@ function useRecordActions({ collection, path, entity }) {
14641
14856
  ]);
14642
14857
  }
14643
14858
  //#endregion
14644
- //#region src/hooks/useEntityDisplayTitle.tsx
14645
- /**
14646
- * The human name for a record, for the identity bar and the breadcrumb.
14647
- *
14648
- * The guard on `status` is the point. The title resolver walks candidate
14649
- * properties and returns the first non-empty one, and on a brand new entity the
14650
- * only populated values are the property defaults — so creating a blog post
14651
- * opened a page headed **draft**, the default of its `status` enum. A record
14652
- * that does not exist yet has no name; it has an intent.
14653
- */
14654
- function useEntityDisplayTitle({ collection, values, entityId, status }) {
14655
- const isNew = status === "new";
14656
- const titlePropertyKey = getEntityTitlePropertyKeyForEntity(collection, values, entityId);
14657
- const rawTitle = !isNew && values && titlePropertyKey ? getValueInPath(values, titlePropertyKey) : void 0;
14658
- const titleUser = useResolvedUser(isUserSelectProperty(collection, titlePropertyKey) && typeof rawTitle === "string" ? rawTitle : void 0);
14659
- const singular = collection.singularName ?? collection.name;
14660
- if (isNew) return `New ${singular.toLowerCase()}`;
14661
- if (titleUser) return getUserLabel(titleUser);
14662
- if (rawTitle !== void 0 && rawTitle !== null) {
14663
- const resolved = resolveTitleToString(rawTitle);
14664
- if (resolved) return resolved;
14665
- }
14666
- return singular;
14667
- }
14668
- //#endregion
14669
14859
  //#region src/components/EditViewBinding.tsx
14670
14860
  /**
14671
14861
  * This is the default view that is used as the content of a side panel when
@@ -14715,7 +14905,7 @@ function EditViewBinding({ entityId, ...props }) {
14715
14905
  children: content
14716
14906
  });
14717
14907
  }
14718
- function EditViewBindingInner({ path, entityId, selectedTab: selectedTabProp, collection, parentCollectionSlugs, parentEntityIds, onValuesModified, onSaved, onTabChange, navigateBack, entity, initialDirtyValues, dataLoading, layout = "side_panel", barActions, barActionsStart, status, setStatus, formProps, canEdit }) {
14908
+ function EditViewBindingInner({ path, entityId, selectedTab: selectedTabProp, collection, parentCollectionSlugs, parentEntityIds, onValuesModified, onSaved, onTabChange, navigateBack, entity, initialDirtyValues, dataLoading, layout = "side_panel", barActions, barActionsStart, onShowList, status, setStatus, formProps, canEdit }) {
14719
14909
  const ResolvedFormActions = useComponentOverride("EditView.FormActions", EditFormActions);
14720
14910
  const ResolvedEntityForm = useComponentOverride("Entity.Form", EntityFormBinding);
14721
14911
  const ResolvedCollectionView = useComponentOverride("Collection.View", CollectionViewBinding);
@@ -14860,8 +15050,8 @@ function EditViewBindingInner({ path, entityId, selectedTab: selectedTabProp, co
14860
15050
  const globalLoading = dataLoading && !usedEntity || canEdit === void 0 && (status === "existing" || status === "copy");
14861
15051
  const displayTitle = useEntityDisplayTitle({
14862
15052
  collection,
15053
+ entity: usedEntity,
14863
15054
  values: formContext?.values ?? usedEntity?.values,
14864
- entityId,
14865
15055
  status
14866
15056
  });
14867
15057
  const formex = formContext?.formex;
@@ -14927,48 +15117,43 @@ function EditViewBindingInner({ path, entityId, selectedTab: selectedTabProp, co
14927
15117
  if (status === "existing") onTabChange?.({
14928
15118
  path,
14929
15119
  entityId,
14930
- selectedTab: value === "__main_##Q$SC^#S6" ? void 0 : value,
14931
- collection
14932
- });
14933
- }, [
14934
- status,
14935
- onTabChange,
14936
- path,
14937
- entityId,
14938
- collection
14939
- ]);
14940
- const formViewConfig = collection.formView;
14941
- const FormViewBuilder = formViewConfig?.Builder ? resolveComponentRef(formViewConfig.Builder) : null;
14942
- formViewConfig?.includeActions;
14943
- const entityReadOnlyView = !canEdit && entity ? /* @__PURE__ */ jsx("div", {
14944
- className: cls("flex-1 flex flex-row w-full overflow-y-auto justify-center", canEdit || !mainViewVisible || selectedSecondaryForm ? "hidden" : ""),
14945
- children: /* @__PURE__ */ jsxs("div", {
14946
- className: cls("relative flex flex-col max-w-4xl lg:max-w-3xl xl:max-w-4xl 2xl:max-w-6xl w-full h-fit"),
14947
- children: [
14948
- /* @__PURE__ */ jsx(Typography, {
14949
- className: "mt-16 mb-8 mx-8",
14950
- variant: "h4",
14951
- children: collection.singularName ?? collection.name
14952
- }),
14953
- FormViewBuilder && readOnlyFormContext ? /* @__PURE__ */ jsx(ErrorBoundary, { children: /* @__PURE__ */ jsx(Suspense, {
14954
- fallback: /* @__PURE__ */ jsx(CircularProgressCenter, {}),
14955
- children: /* @__PURE__ */ jsx(FormViewBuilder, {
14956
- collection,
14957
- parentCollectionSlugs,
14958
- parentEntityIds,
14959
- entity: usedEntity,
14960
- modifiedValues: usedEntity?.values,
14961
- formContext: readOnlyFormContext
14962
- })
14963
- }) }) : /* @__PURE__ */ jsx(EntityViewBinding, {
14964
- className: "px-8 h-full overflow-auto",
14965
- entity,
14966
- path,
14967
- collection
14968
- }),
14969
- /* @__PURE__ */ jsx("div", { className: "h-16" })
14970
- ]
15120
+ selectedTab: value === "__main_##Q$SC^#S6" ? void 0 : value,
15121
+ collection
15122
+ });
15123
+ }, [
15124
+ status,
15125
+ onTabChange,
15126
+ path,
15127
+ entityId,
15128
+ collection
15129
+ ]);
15130
+ const formViewConfig = collection.formView;
15131
+ const FormViewBuilder = formViewConfig?.Builder ? resolveComponentRef(formViewConfig.Builder) : null;
15132
+ formViewConfig?.includeActions;
15133
+ const entityReadOnlyView = !canEdit && entity ? FormViewBuilder && readOnlyFormContext ? /* @__PURE__ */ jsx("div", {
15134
+ className: cls("flex-1 flex flex-row w-full overflow-y-auto justify-center", canEdit || !mainViewVisible || selectedSecondaryForm ? "hidden" : ""),
15135
+ children: /* @__PURE__ */ jsx("div", {
15136
+ className: "w-full max-w-3xl 2xl:max-w-4xl flex flex-col pt-6 pb-16 px-5 sm:px-8",
15137
+ children: /* @__PURE__ */ jsx(ErrorBoundary, { children: /* @__PURE__ */ jsx(Suspense, {
15138
+ fallback: /* @__PURE__ */ jsx(CircularProgressCenter, {}),
15139
+ children: /* @__PURE__ */ jsx(FormViewBuilder, {
15140
+ collection,
15141
+ parentCollectionSlugs,
15142
+ parentEntityIds,
15143
+ entity: usedEntity,
15144
+ modifiedValues: usedEntity?.values,
15145
+ formContext: readOnlyFormContext
15146
+ })
15147
+ }) })
14971
15148
  })
15149
+ }) : /* @__PURE__ */ jsx(EntityViewBinding, {
15150
+ className: cls(canEdit || !mainViewVisible || selectedSecondaryForm ? "hidden" : ""),
15151
+ entity,
15152
+ path,
15153
+ collection,
15154
+ asPage: true,
15155
+ openEntityMode: layout,
15156
+ formContext: readOnlyFormContext
14972
15157
  }) : null;
14973
15158
  const entityView = FormViewBuilder ? /* @__PURE__ */ jsx("div", {
14974
15159
  className: cls("relative flex-1 w-full h-full overflow-auto", (!mainViewVisible || !canEdit) && !selectedSecondaryForm ? "hidden" : ""),
@@ -15071,6 +15256,7 @@ function EditViewBindingInner({ path, entityId, selectedTab: selectedTabProp, co
15071
15256
  children: [
15072
15257
  /* @__PURE__ */ jsx(EntityIdentityBar, {
15073
15258
  collection,
15259
+ collectionUrl: layout === "full_screen" || layout === "split" ? withViewMode(urlController.buildUrlCollectionPath(path)) : void 0,
15074
15260
  title: displayTitle,
15075
15261
  entityId: status === "existing" ? entityId : void 0,
15076
15262
  status,
@@ -15078,7 +15264,7 @@ function EditViewBindingInner({ path, entityId, selectedTab: selectedTabProp, co
15078
15264
  saving: Boolean(formContext?.isSaving),
15079
15265
  hasErrors: hasFormErrors,
15080
15266
  saveDisabled: !canEdit || saveDisabled,
15081
- onBack: layout === "full_screen" ? () => navigate(withViewMode(urlController.buildUrlCollectionPath(path))) : void 0,
15267
+ onBack: layout === "full_screen" && !onShowList ? () => navigate(withViewMode(urlController.buildUrlCollectionPath(path))) : void 0,
15082
15268
  onSave: canEdit && formContext ? () => {
15083
15269
  sideDialogContext.setPendingClose?.(false);
15084
15270
  formContext.submit();
@@ -15092,7 +15278,11 @@ function EditViewBindingInner({ path, entityId, selectedTab: selectedTabProp, co
15092
15278
  onViewHistory: includeHistoryView ? () => setInspectorTab("history") : void 0,
15093
15279
  recordActions: recordActionItems,
15094
15280
  pluginActions: formPluginActions,
15095
- leading: /* @__PURE__ */ jsxs(Fragment, { children: [barActionsStart, layout === "split" && /* @__PURE__ */ jsx(SplitListCloseButton, {})] }),
15281
+ leading: /* @__PURE__ */ jsxs(Fragment, { children: [
15282
+ barActionsStart,
15283
+ layout === "split" && /* @__PURE__ */ jsx(SplitListCloseButton, {}),
15284
+ layout === "full_screen" && onShowList && /* @__PURE__ */ jsx(SplitListShowButton, { onClick: onShowList })
15285
+ ] }),
15096
15286
  trailing: /* @__PURE__ */ jsxs(Fragment, { children: [
15097
15287
  pluginActionsTop,
15098
15288
  fullScreenButton,
@@ -15877,10 +16067,11 @@ function useBuildCollectionRegistryController(props) {
15877
16067
  if (pathSegments.length > 0 && pathSegments.length % 2 === 0) collectionPath = pathSegments.slice(0, -1).join("/");
15878
16068
  if (!collectionPath) return void 0;
15879
16069
  let collection;
15880
- try {
16070
+ collection = registry.get(collectionPath);
16071
+ if (!collection) try {
15881
16072
  collection = registry.resolvePathToCollections(collectionPath).finalCollection;
15882
16073
  } catch (e) {
15883
- console.debug(`Could not resolve path to collection: ${collectionPath}`, e);
16074
+ console.warn(`[rebase] Could not resolve a collection for "${collectionPath}"`, e);
15884
16075
  return;
15885
16076
  }
15886
16077
  if (!collection) return;
@@ -20310,6 +20501,213 @@ function buildBottomActions({ savingError, entity, path, formActions, collection
20310
20501
  });
20311
20502
  }
20312
20503
  //#endregion
20504
+ //#region src/components/PropertyCollectionView.tsx
20505
+ /**
20506
+ * Build a readable label for a path and resolve the property
20507
+ * Supports map and array (including arrays of maps)
20508
+ */
20509
+ function buildPropertyLabelAndGetProperty(properties, key) {
20510
+ if (!key) return {
20511
+ label: "",
20512
+ property: void 0
20513
+ };
20514
+ const segments = [];
20515
+ const re = /([^[.\]]+)|\[(\d+)\]/g;
20516
+ let m;
20517
+ while ((m = re.exec(key)) !== null) if (m[1] !== void 0) segments.push(m[1]);
20518
+ else if (m[2] !== void 0) segments.push(Number(m[2]));
20519
+ let currentProps = properties;
20520
+ let currentProp;
20521
+ let lastLabel = "";
20522
+ const getArrayOfProp = (p) => {
20523
+ if (!p || p.type !== "array") return void 0;
20524
+ return Array.isArray(p.of) ? p.of[0] : p.of;
20525
+ };
20526
+ for (const seg of segments) {
20527
+ if (typeof seg === "number") {
20528
+ lastLabel = `[${seg}]`;
20529
+ if (currentProp?.type === "array") {
20530
+ currentProp = getArrayOfProp(currentProp);
20531
+ if (currentProp?.type === "map" && currentProp.properties) currentProps = currentProp.properties;
20532
+ else currentProps = void 0;
20533
+ } else {
20534
+ currentProp = void 0;
20535
+ currentProps = void 0;
20536
+ }
20537
+ continue;
20538
+ }
20539
+ if (currentProps && currentProps[seg]) {
20540
+ const nextProp = currentProps[seg];
20541
+ currentProp = nextProp;
20542
+ lastLabel = nextProp.name || String(seg);
20543
+ if (nextProp.type === "map" && nextProp.properties) currentProps = nextProp.properties;
20544
+ else if (nextProp.type === "array") currentProps = void 0;
20545
+ else currentProps = void 0;
20546
+ } else {
20547
+ currentProp = void 0;
20548
+ currentProps = void 0;
20549
+ lastLabel = String(seg);
20550
+ }
20551
+ }
20552
+ return {
20553
+ label: lastLabel,
20554
+ property: currentProp
20555
+ };
20556
+ }
20557
+ var pathEndsWithIndex = (p) => /\[\d+\]$/.test(p);
20558
+ /**
20559
+ * Improved simple layout for nested changes:
20560
+ * - Map or Array-of-Map -> section header + indented rows
20561
+ * - Leaf or Array-of-Primitives -> single row with label and value
20562
+ */
20563
+ var PropertyCollectionView = ({ data, properties, baseKey = "", suppressHeader = false, size = "small" }) => {
20564
+ const isTopLevel = !!baseKey && !baseKey.includes(".") && !baseKey.includes("[");
20565
+ if (Array.isArray(data)) {
20566
+ const { label: arrayLabel, property } = baseKey ? buildPropertyLabelAndGetProperty(properties, baseKey) : {
20567
+ label: "",
20568
+ property: void 0
20569
+ };
20570
+ const ofProp = property?.type === "array" ? Array.isArray(property.of) ? property.of[0] : property.of : void 0;
20571
+ const isArrayOfMaps = ofProp?.type === "map";
20572
+ const isArrayOfPrimitives = property?.type === "array" && ofProp && ofProp.type !== "map";
20573
+ const isRelationArray = property?.type === "relation";
20574
+ if (baseKey && property && (isArrayOfPrimitives || isRelationArray)) return /* @__PURE__ */ jsxs("div", {
20575
+ className: `grid grid-cols-12 gap-x-4 ${isTopLevel ? "py-4" : "py-2"} items-start ${isTopLevel ? `border-b ${defaultBorderMixin} last:border-b-0` : ""}`,
20576
+ children: [/* @__PURE__ */ jsx("div", {
20577
+ className: "col-span-4 pr-2",
20578
+ children: /* @__PURE__ */ jsx(Typography, {
20579
+ variant: "caption",
20580
+ color: "secondary",
20581
+ component: "span",
20582
+ className: "break-words",
20583
+ children: arrayLabel
20584
+ })
20585
+ }), /* @__PURE__ */ jsx("div", {
20586
+ className: "col-span-8",
20587
+ children: /* @__PURE__ */ jsx(PropertyPreview, {
20588
+ propertyKey: baseKey,
20589
+ value: data,
20590
+ property,
20591
+ size
20592
+ })
20593
+ })]
20594
+ });
20595
+ return /* @__PURE__ */ jsxs("div", {
20596
+ className: `${isTopLevel ? "py-4" : "py-1"} ${isTopLevel ? `border-b ${defaultBorderMixin} last:border-b-0` : ""}`,
20597
+ children: [baseKey && arrayLabel && !suppressHeader && /* @__PURE__ */ jsx(Typography, {
20598
+ variant: "caption",
20599
+ color: "secondary",
20600
+ component: "span",
20601
+ children: arrayLabel
20602
+ }), /* @__PURE__ */ jsx("div", {
20603
+ className: baseKey ? `pl-4 mt-1 border-l ${defaultBorderMixin}` : "",
20604
+ children: data.map((item, index) => {
20605
+ if (item === null || item === void 0) return null;
20606
+ const currentKey = baseKey ? `${baseKey}[${index}]` : `[${index}]`;
20607
+ return /* @__PURE__ */ jsxs("div", {
20608
+ className: "py-1",
20609
+ children: [/* @__PURE__ */ jsx(Typography, {
20610
+ variant: "caption",
20611
+ color: "secondary",
20612
+ component: "span",
20613
+ children: isArrayOfMaps && ofProp?.name ? `${ofProp.name} [${index}]` : `[${index}]`
20614
+ }), /* @__PURE__ */ jsx("div", {
20615
+ className: `pl-4 mt-1 border-l ${defaultBorderMixin}`,
20616
+ children: /* @__PURE__ */ jsx(PropertyCollectionView, {
20617
+ data: item,
20618
+ properties,
20619
+ baseKey: currentKey,
20620
+ suppressHeader: true,
20621
+ size
20622
+ })
20623
+ })]
20624
+ }, currentKey);
20625
+ })
20626
+ })]
20627
+ });
20628
+ }
20629
+ if (typeof data === "object" && data !== null) {
20630
+ const { label, property } = baseKey ? buildPropertyLabelAndGetProperty(properties, baseKey) : {
20631
+ label: "",
20632
+ property: void 0
20633
+ };
20634
+ if (baseKey && (!property || property.type !== "map" || !property.properties)) {
20635
+ if (!property) return null;
20636
+ return /* @__PURE__ */ jsxs("div", {
20637
+ className: `grid grid-cols-12 gap-x-4 ${isTopLevel ? "py-4" : "py-2"} items-start ${isTopLevel ? `border-b ${defaultBorderMixin} last:border-b-0` : ""}`,
20638
+ children: [/* @__PURE__ */ jsx("div", {
20639
+ className: "col-span-4 pr-2",
20640
+ children: /* @__PURE__ */ jsx(Typography, {
20641
+ variant: "caption",
20642
+ color: "secondary",
20643
+ component: "span",
20644
+ className: "break-words",
20645
+ children: label
20646
+ })
20647
+ }), /* @__PURE__ */ jsx("div", {
20648
+ className: "col-span-8",
20649
+ children: /* @__PURE__ */ jsx(PropertyPreview, {
20650
+ propertyKey: baseKey,
20651
+ value: data,
20652
+ property,
20653
+ size
20654
+ })
20655
+ })]
20656
+ });
20657
+ }
20658
+ const showMapHeader = baseKey && !suppressHeader && property?.type === "map" && (property.name || !pathEndsWithIndex(baseKey));
20659
+ const headerText = property?.name || label;
20660
+ return /* @__PURE__ */ jsxs("div", {
20661
+ className: `${isTopLevel ? "py-4" : "py-1"} ${isTopLevel ? `border-b ${defaultBorderMixin} last:border-b-0` : ""}`,
20662
+ children: [showMapHeader && /* @__PURE__ */ jsx(Typography, {
20663
+ variant: "caption",
20664
+ color: "secondary",
20665
+ component: "span",
20666
+ children: headerText
20667
+ }), /* @__PURE__ */ jsx("div", {
20668
+ className: baseKey ? `pl-4 mt-1 border-l ${defaultBorderMixin}` : "",
20669
+ children: Object.entries(data).map(([key, value]) => {
20670
+ if (value === null || value === void 0) return null;
20671
+ if (baseKey === "" && !properties[key]) return null;
20672
+ const currentKey = baseKey ? `${baseKey}.${key}` : key;
20673
+ return /* @__PURE__ */ jsx(PropertyCollectionView, {
20674
+ data: value,
20675
+ properties,
20676
+ baseKey: currentKey,
20677
+ size
20678
+ }, currentKey);
20679
+ })
20680
+ })]
20681
+ });
20682
+ }
20683
+ if (baseKey) {
20684
+ const { label, property } = buildPropertyLabelAndGetProperty(properties, baseKey);
20685
+ if (!property) return null;
20686
+ return /* @__PURE__ */ jsxs("div", {
20687
+ className: `grid grid-cols-12 gap-x-4 ${isTopLevel ? "py-4" : "py-2"} items-start ${isTopLevel ? `border-b ${defaultBorderMixin} last:border-b-0` : ""}`,
20688
+ children: [/* @__PURE__ */ jsx("div", {
20689
+ className: "col-span-4 pr-2",
20690
+ children: /* @__PURE__ */ jsx(Typography, {
20691
+ variant: "caption",
20692
+ color: "secondary",
20693
+ component: "span",
20694
+ className: "break-words",
20695
+ children: label
20696
+ })
20697
+ }), /* @__PURE__ */ jsx("div", {
20698
+ className: "col-span-8",
20699
+ children: /* @__PURE__ */ jsx(PropertyPreview, {
20700
+ propertyKey: baseKey,
20701
+ value: data,
20702
+ property,
20703
+ size
20704
+ })
20705
+ })]
20706
+ });
20707
+ }
20708
+ return null;
20709
+ };
20710
+ //#endregion
20313
20711
  //#region src/form/components/LocalChangesMenu.tsx
20314
20712
  function LocalChangesMenu({ cachedData, formex, onClearLocalChanges, cacheKey, properties }) {
20315
20713
  const snackbarController = useSnackbarController();
@@ -20775,10 +21173,11 @@ function EntityForm({ path, entityId: entityIdProp, collection, onValuesModified
20775
21173
  context: additionalFieldContext
20776
21174
  }) : /* @__PURE__ */ jsx("div", {
20777
21175
  className: "w-full",
20778
- children: additionalField.value?.({
21176
+ children: /* @__PURE__ */ jsx(AdditionalFieldValue, {
21177
+ field: additionalField,
20779
21178
  entity,
20780
21179
  context: additionalFieldContext
20781
- })?.toString()
21180
+ })
20782
21181
  });
20783
21182
  return /* @__PURE__ */ jsxs("div", {
20784
21183
  className: spanClass(field.span),
@@ -20956,24 +21355,6 @@ function EntityForm({ path, entityId: entityIdProp, collection, onValuesModified
20956
21355
  })
20957
21356
  });
20958
21357
  }
20959
- /**
20960
- * Grid placement for a span.
20961
- *
20962
- * Written out as literal class strings rather than composed, because Tailwind
20963
- * scans source text: a template literal would produce classes that exist at
20964
- * runtime and were never generated into the stylesheet.
20965
- *
20966
- * Below the column breakpoint every field takes the single available column, so
20967
- * the side panel, the dialog and mobile all get a plain stack.
20968
- */
20969
- function spanClass(span) {
20970
- switch (span) {
20971
- case 1: return "@2xl/col:col-span-1";
20972
- case 2: return "@2xl/col:col-span-2";
20973
- case 3: return "@2xl/col:col-span-3";
20974
- case 4: return "@2xl/col:col-span-4";
20975
- }
20976
- }
20977
21358
  function useOnAutoSave(autoSave, formex, lastSavedValues, save) {
20978
21359
  useEffect(() => {
20979
21360
  if (!autoSave) return;
@@ -22087,6 +22468,6 @@ function getFullIdPath(propertyKey, propertyNamespace) {
22087
22468
  return idToPropertiesPath(propertyNamespace ? `${propertyNamespace}.${propertyKey}` : propertyKey);
22088
22469
  }
22089
22470
  //#endregion
22090
- export { useBuildCollectionRegistryController as $, sanitizeUrl as $n, CollectionTableBinding as $t, getFieldId as A, ArrayOfReferencesPreview as An, convertFileToJson as At, MapFieldBinding as B, getEntityTitlePropertyKeyForEntity as Bn, mergeEntityActions as Bt, removeEmptyContainers as C, MapPropertyPreview as Cn, convertDataToEntity as Ct, getDefaultFieldConfig as D, ArrayEnumPreview as Dn, useImportConfig as Dt, DEFAULT_FIELD_CONFIGS as E, ArrayPropertyEnumPreview as En, getInferenceType as Et, SelectFieldBinding as F, useSidePanel as Fn, FieldCaption as Ft, useSelectionDialog as G, StorageThumbnailInternal as Gn, removeInitialAndTrailingSlashes$1 as Gt, DateTimeFieldBinding as H, StringPropertyPreview as Hn, getCollectionBySlugWithin as Ht, RepeatFieldBinding as I, CollectionRegistryContext as In, useBreadcrumbsController as It, useBuildUrlController as J, renderSkeletonIcon as Jn, resolveCollectionPathIds$1 as Jt, SelectionTableBinding as K, SkeletonPropertyComponent as Kn, removeInitialSlash as Kt, ReferenceFieldBinding as L, useCollectionRegistryController as Ln, BreadcrumbsProvider as Lt, TextFieldBinding as M, ReferencePreview as Mn, ArrayContainer as Mt, SwitchFieldBinding as N, EntityPreviewBinding as Nn, PropertyConfigBadge as Nt, getDefaultFieldId as O, ArrayOfStorageComponentsPreview as On, ImportSaveInProgress as Ot, StorageUploadFieldBinding as P, SidePanelControllerContext as Pn, SearchIconsView as Pt, useResolvedCollections as Q, ImagePreview as Qn, VirtualTableInput$1 as Qt, MultiSelectFieldBinding as R, getEntityPreviewKeys as Rn, resolveEntityAction as Rt, getInitialEntityValues as S, KeyValuePreview as Sn, DataNewPropertiesMapping as St, PropertyFieldBinding as T, ArrayOfStringsPreview as Tn, processValueMapping as Tt, BlockFieldBinding as U, EnumValuesChip as Un, getCollectionPathsCombinations as Ut, KeyValueFieldBinding as V, ArrayPropertyPreview as Vn, addInitialSlash as Vt, ArrayOfReferencesFieldBinding as W, StorageThumbnail as Wn, getLastSegment$1 as Wt, useTopLevelNavigation as X, renderSkeletonText as Xn, resolveViewMode as Xt, useBuildNavigationStateController as Y, renderSkeletonImageThumbnail as Yn, resolveOpenEntityMode as Yt, useResolvedViews as Z, UrlComponentPreview as Zn, useSelectionController as Zt, useCollectionsConfigController as _, getUserLabel as _n, CollectionViewActions as _t, namespaceToPropertiesPath as a, NavigationStateContext as an, FieldBlock as ar, useBuildSidePanel as at, extractTouchedValues as b, BooleanPreview as bn, useCollectionEditorDialogsState as bt, buildCollectionGenerationCallback as c, SideDialogsControllerContext as cn, getBracketNotation as cr, copyEntityAction as ct, fromSerializableCollectionConfigs as d, useClearRestoreValue as dn, getIconForWidget as dr, resetPasswordAction as dt, SelectableTable as en, EmptyValue as er, useHistory as et, fromSerializableProperties as f, ReadOnlyFieldBinding as fn, getPropertiesWithPropertiesOrder as fr, CreationResultDialog as ft, toSerializableProperty as g, UserPreview as gn, isRelationProperty as gr, EntityCardBinding as gt, toSerializableProperties as h, PropertyPreview as hn, isReferenceProperty as hr, CollectionCardViewBinding as ht, namespaceToPropertiesOrderPath as i, useUrlController as in, FormRail as ir, getEntityViewWidth as it, VectorFieldBinding as j, InlineEntityListPreview as jn, unflattenObject as jt, getFieldConfig as k, RelationPreview as kn, ImportFileUpload as kt, validateCollectionJson as l, SelectableTableContext as ln, getDefaultPropertiesOrder as lr, deleteEntityAction as lt, toSerializableCollectionConfig as m, ArrayOfMapsPreview as mn, getResolvedPropertyInPath as mr, EntityViewBinding as mt, getFullIdPath as n, useAdminContext as nn, LabelWithIcon as nr, useResolvedNavigationFrom as nt, CollectionGenerationApiError as o, useNavigationStateController as on, isSelfLabellingProperty as or, EditViewBinding as ot, fromSerializableProperty as p, FieldHelperText as pn, getPropertyInPath as pr, DetailViewBinding as pt, SideDialogs as q, renderSkeletonCaptionText as qn, removeTrailingSlash$1 as qt, idToPropertiesPath as r, UrlContext as rn, FormSections as rr, buildSidePanelsFromUrl as rt, DEFAULT_COLLECTION_GENERATION_ENDPOINT as s, useSideDialogsController as sn, PropertyIdCopyTooltip as sr, CollectionViewBinding as st, getFullId as t, CollectionRowActions as tn, LabelWithIconAndTooltip as tr, resolveNavigationFrom as tt, fromSerializableCollectionConfig as u, ArrayCustomShapedFieldBinding as un, getIconForProperty as ur, editEntityAction as ut, EntityFormBinding as v, useResolvedUser as vn, useCollectionEditorController as vt, zodToFormErrors as w, ArrayOneOfPreview as wn, flattenEntry as wt, getChanges as x, DatePreview as xn, ImportNewPropertyFieldPreview as xt, EntityForm as y, NumberPropertyPreview as yn, ConfigControllerProvider as yt, MarkdownEditorFieldBinding as z, getEntityTitlePropertyKey as zn, resolveEntityView as zt };
22471
+ export { useBuildCollectionRegistryController as $, sanitizeUrl as $n, CollectionTableBinding as $t, getFieldId as A, ReferencePreview as An, convertFileToJson as At, MapFieldBinding as B, getEntityTitlePropertyKeyForEntity as Bn, mergeEntityActions as Bt, removeEmptyContainers as C, ArrayOfStringsPreview as Cn, convertDataToEntity as Ct, getDefaultFieldConfig as D, RelationPreview as Dn, useImportConfig as Dt, DEFAULT_FIELD_CONFIGS as E, ArrayOfStorageComponentsPreview as En, getInferenceType as Et, SelectFieldBinding as F, useCollectionRegistryController as Fn, FieldCaption as Ft, useSelectionDialog as G, StorageThumbnailInternal as Gn, removeInitialAndTrailingSlashes$1 as Gt, DateTimeFieldBinding as H, StringPropertyPreview as Hn, getCollectionBySlugWithin as Ht, RepeatFieldBinding as I, getUserLabel as In, useBreadcrumbsController as It, useBuildUrlController as J, renderSkeletonIcon as Jn, resolveCollectionPathIds$1 as Jt, SelectionTableBinding as K, SkeletonPropertyComponent as Kn, removeInitialSlash as Kt, ReferenceFieldBinding as L, useResolvedUser as Ln, BreadcrumbsProvider as Lt, TextFieldBinding as M, SidePanelControllerContext as Mn, ArrayContainer as Mt, SwitchFieldBinding as N, useSidePanel as Nn, PropertyConfigBadge as Nt, getDefaultFieldId as O, ArrayOfReferencesPreview as On, ImportSaveInProgress as Ot, StorageUploadFieldBinding as P, CollectionRegistryContext as Pn, SearchIconsView as Pt, useResolvedCollections as Q, ImagePreview as Qn, VirtualTableInput$1 as Qt, MultiSelectFieldBinding as R, getEntityPreviewKeys as Rn, resolveEntityAction as Rt, getInitialEntityValues as S, ArrayOneOfPreview as Sn, DataNewPropertiesMapping as St, PropertyFieldBinding as T, ArrayEnumPreview as Tn, processValueMapping as Tt, BlockFieldBinding as U, EnumValuesChip as Un, getCollectionPathsCombinations as Ut, KeyValueFieldBinding as V, ArrayPropertyPreview as Vn, addInitialSlash as Vt, ArrayOfReferencesFieldBinding as W, StorageThumbnail as Wn, getLastSegment$1 as Wt, useTopLevelNavigation as X, renderSkeletonText as Xn, resolveViewMode as Xt, useBuildNavigationStateController as Y, renderSkeletonImageThumbnail as Yn, resolveOpenEntityMode as Yt, useResolvedViews as Z, UrlComponentPreview as Zn, useSelectionController as Zt, useCollectionsConfigController as _, NumberPropertyPreview as _n, isRelationProperty as _r, CollectionViewActions as _t, namespaceToPropertiesPath as a, NavigationStateContext as an, FieldBlock as ar, useBuildSidePanel as at, extractTouchedValues as b, KeyValuePreview as bn, useCollectionEditorDialogsState as bt, buildCollectionGenerationCallback as c, SideDialogsControllerContext as cn, PropertyIdCopyTooltip as cr, copyEntityAction as ct, fromSerializableCollectionConfigs as d, useClearRestoreValue as dn, getIconForProperty as dr, resetPasswordAction as dt, SelectableTable as en, EmptyValue as er, useHistory as et, fromSerializableProperties as f, ReadOnlyFieldBinding as fn, getIconForWidget as fr, CreationResultDialog as ft, toSerializableProperty as g, UserPreview as gn, isReferenceProperty as gr, EntityCardBinding as gt, toSerializableProperties as h, PropertyPreview as hn, getResolvedPropertyInPath as hr, CollectionCardViewBinding as ht, namespaceToPropertiesOrderPath as i, useUrlController as in, FormRail as ir, getEntityViewWidth as it, VectorFieldBinding as j, EntityPreviewBinding as jn, unflattenObject as jt, getFieldConfig as k, InlineEntityListPreview as kn, ImportFileUpload as kt, validateCollectionJson as l, SelectableTableContext as ln, getBracketNotation as lr, deleteEntityAction as lt, toSerializableCollectionConfig as m, ArrayOfMapsPreview as mn, getPropertyInPath$1 as mr, EntityViewBinding as mt, getFullIdPath as n, useAdminContext as nn, LabelWithIcon as nr, useResolvedNavigationFrom as nt, CollectionGenerationApiError as o, useNavigationStateController as on, isSelfLabellingProperty as or, EditViewBinding as ot, fromSerializableProperty as p, FieldHelperText as pn, getPropertiesWithPropertiesOrder as pr, DetailViewBinding as pt, SideDialogs as q, renderSkeletonCaptionText as qn, removeTrailingSlash$1 as qt, idToPropertiesPath as r, UrlContext as rn, FormSections as rr, buildSidePanelsFromUrl as rt, DEFAULT_COLLECTION_GENERATION_ENDPOINT as s, useSideDialogsController as sn, spanClass as sr, CollectionViewBinding as st, getFullId as t, CollectionRowActions as tn, LabelWithIconAndTooltip as tr, resolveNavigationFrom as tt, fromSerializableCollectionConfig as u, ArrayCustomShapedFieldBinding as un, getDefaultPropertiesOrder as ur, editEntityAction as ut, EntityFormBinding as v, BooleanPreview as vn, useCollectionEditorController as vt, zodToFormErrors as w, ArrayPropertyEnumPreview as wn, flattenEntry as wt, getChanges as x, MapPropertyPreview as xn, ImportNewPropertyFieldPreview as xt, EntityForm as y, DatePreview as yn, ConfigControllerProvider as yt, MarkdownEditorFieldBinding as z, getEntityTitlePropertyKey as zn, resolveEntityView as zt };
22091
22472
 
22092
- //# sourceMappingURL=util-GiwPgxnL.js.map
22473
+ //# sourceMappingURL=util-pQ3YwBve.js.map