@rebasepro/admin 0.12.1-canary.gf5f1d39 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (178) hide show
  1. package/README.md +1 -1
  2. package/dist/{CollectionEditorDialog-Dpc75wIZ.js → CollectionEditorDialog-CjXkWZ64.js} +75 -62
  3. package/dist/{CollectionEditorDialog-Dpc75wIZ.js.map → CollectionEditorDialog-CjXkWZ64.js.map} +1 -1
  4. package/dist/{PropertyEditView-DH3XZC2x.js → PropertyEditView-W8I3YV89.js} +36 -16
  5. package/dist/{PropertyEditView-DH3XZC2x.js.map → PropertyEditView-W8I3YV89.js.map} +1 -1
  6. package/dist/{RouterCollectionsStudioView-BZLq4MGp.js → RouterCollectionsStudioView-BsLTNoqX.js} +5 -5
  7. package/dist/{RouterCollectionsStudioView-BZLq4MGp.js.map → RouterCollectionsStudioView-BsLTNoqX.js.map} +1 -1
  8. package/dist/collection_editor/serializable_types.d.ts +4 -1
  9. package/dist/collection_editor/ui/collection_editor/CollectionRelationsTab.d.ts +1 -0
  10. package/dist/collection_editor/useLocalCollectionsConfigController.d.ts +8 -0
  11. package/dist/collection_editor_ui.js +5 -5
  12. package/dist/collection_editor_ui.js.map +1 -1
  13. package/dist/components/CollectionTableBinding/column_utils.d.ts +17 -0
  14. package/dist/components/CollectionViewBinding/CollectionViewStartActions.d.ts +7 -2
  15. package/dist/components/CollectionViewBinding/SortButton.d.ts +24 -0
  16. package/dist/components/CollectionViewBinding/SplitListCloseButton.d.ts +10 -0
  17. package/dist/components/CollectionViewBinding/SplitViewContext.d.ts +23 -0
  18. package/dist/components/CollectionViewBinding/hooks/useKanbanDragAndDrop.d.ts +26 -0
  19. package/dist/components/DetailViewBinding.d.ts +5 -0
  20. package/dist/components/EditFormActions.d.ts +12 -1
  21. package/dist/components/EditViewBinding.d.ts +6 -1
  22. package/dist/components/EntityIdentityBar.d.ts +53 -0
  23. package/dist/components/EntityInspector.d.ts +35 -0
  24. package/dist/components/EntityPreviewNesting.d.ts +25 -0
  25. package/dist/components/EntityViewBinding.d.ts +9 -0
  26. package/dist/components/InlineEntityPreview.d.ts +47 -0
  27. package/dist/components/history/EntityHistoryView.d.ts +4 -1
  28. package/dist/components/index.d.ts +2 -0
  29. package/dist/editor.js +18 -10
  30. package/dist/editor.js.map +1 -1
  31. package/dist/{export-BzIpUdSK.js → export-J79rMoNj.js} +2 -2
  32. package/dist/{export-BzIpUdSK.js.map → export-J79rMoNj.js.map} +1 -1
  33. package/dist/form/EntityForm.d.ts +1 -1
  34. package/dist/form/EntityFormActions.d.ts +11 -1
  35. package/dist/form/PropertyFieldBinding.d.ts +1 -1
  36. package/dist/form/components/FieldBlock.d.ts +30 -0
  37. package/dist/form/components/FormRail.d.ts +18 -0
  38. package/dist/form/components/FormSections.d.ts +16 -0
  39. package/dist/form/components/index.d.ts +3 -2
  40. package/dist/form/components/useRailVisible.d.ts +24 -0
  41. package/dist/form/field_bindings/BinaryFieldBinding.d.ts +1 -1
  42. package/dist/form/field_bindings/DateTimeFieldBinding.d.ts +1 -1
  43. package/dist/form/field_bindings/GeopointFieldBinding.d.ts +1 -1
  44. package/dist/form/field_bindings/MarkdownEditorFieldBinding.d.ts +1 -1
  45. package/dist/form/field_bindings/MultiSelectFieldBinding.d.ts +1 -1
  46. package/dist/form/field_bindings/ReadOnlyFieldBinding.d.ts +1 -1
  47. package/dist/form/field_bindings/SelectFieldBinding.d.ts +1 -1
  48. package/dist/form/field_bindings/StorageUploadFieldBinding.d.ts +1 -1
  49. package/dist/form/field_bindings/SwitchFieldBinding.d.ts +1 -1
  50. package/dist/form/field_bindings/TextFieldBinding.d.ts +1 -1
  51. package/dist/form/field_bindings/UserSelectFieldBinding.d.ts +1 -1
  52. package/dist/form/field_bindings/VectorFieldBinding.d.ts +1 -1
  53. package/dist/{history-g9688IaK.js → history-D7L_JlLO.js} +35 -33
  54. package/dist/history-D7L_JlLO.js.map +1 -0
  55. package/dist/hooks/navigation/utils.d.ts +16 -0
  56. package/dist/hooks/useBuildSidePanel.d.ts +14 -0
  57. package/dist/hooks/useEntityDisplayTitle.d.ts +18 -0
  58. package/dist/hooks/useHistory.d.ts +7 -0
  59. package/dist/hooks/useRecordActions.d.ts +17 -0
  60. package/dist/{import-BjUG-cIY.js → import-BvYl--U4.js} +2 -2
  61. package/dist/{import-BjUG-cIY.js.map → import-BvYl--U4.js.map} +1 -1
  62. package/dist/index.js +106 -35
  63. package/dist/index.js.map +1 -1
  64. package/dist/markdown-CklalUUk.js.map +1 -1
  65. package/dist/preview/components/InlineEntityListPreview.d.ts +15 -0
  66. package/dist/preview/index.d.ts +1 -0
  67. package/dist/preview/property_previews/ArrayOfReferencesPreview.d.ts +1 -1
  68. package/dist/preview/property_previews/ArrayOfRelationsPreview.d.ts +1 -1
  69. package/dist/types/components/EntityFormActionsProps.d.ts +0 -1
  70. package/dist/types/components/EntityFormProps.d.ts +0 -5
  71. package/dist/types/fields.d.ts +24 -0
  72. package/dist/{util-B5fJm1FA.js → util-GiwPgxnL.js} +2106 -1117
  73. package/dist/util-GiwPgxnL.js.map +1 -0
  74. package/package.json +32 -34
  75. package/src/collection_editor/ConfigControllerProvider.tsx +1 -1
  76. package/src/collection_editor/serializable_types.ts +4 -3
  77. package/src/collection_editor/serializable_utils.ts +51 -17
  78. package/src/collection_editor/ui/collection_editor/CollectionEditorDialog.tsx +65 -22
  79. package/src/collection_editor/ui/collection_editor/CollectionRLSTab.tsx +38 -30
  80. package/src/collection_editor/ui/collection_editor/CollectionRelationsTab.tsx +3 -1
  81. package/src/collection_editor/ui/collection_editor/RouterCollectionStudioView.tsx +1 -1
  82. package/src/collection_editor/ui/collection_editor/RouterCollectionsStudioView.tsx +1 -1
  83. package/src/collection_editor/ui/collection_editor/properties/RelationPropertyField.tsx +16 -0
  84. package/src/collection_editor/useLocalCollectionsConfigController.tsx +127 -15
  85. package/src/collection_editor/validateCollectionJson.ts +3 -3
  86. package/src/components/AdminModeSyncer.tsx +1 -1
  87. package/src/components/CollectionTableBinding/PropertyTableCell.tsx +10 -2
  88. package/src/components/CollectionTableBinding/column_utils.tsx +59 -2
  89. package/src/components/CollectionTableBinding/table_bindings.tsx +2 -1
  90. package/src/components/CollectionViewBinding/CollectionBoardViewBinding.tsx +23 -16
  91. package/src/components/CollectionViewBinding/CollectionViewActions.tsx +7 -1
  92. package/src/components/CollectionViewBinding/CollectionViewBinding.tsx +9 -2
  93. package/src/components/CollectionViewBinding/CollectionViewStartActions.tsx +47 -12
  94. package/src/components/CollectionViewBinding/SortButton.tsx +189 -0
  95. package/src/components/CollectionViewBinding/SplitListCloseButton.tsx +33 -0
  96. package/src/components/CollectionViewBinding/SplitListView.tsx +18 -3
  97. package/src/components/CollectionViewBinding/SplitViewContext.tsx +34 -0
  98. package/src/components/CollectionViewBinding/hooks/useKanbanDragAndDrop.ts +48 -9
  99. package/src/components/CollectionViewBinding/useBoardDataController.tsx +80 -25
  100. package/src/components/CollectionViewBinding/usePreviewSlots.ts +18 -6
  101. package/src/components/DefaultAppBar.tsx +1 -1
  102. package/src/components/DefaultDrawer.tsx +1 -1
  103. package/src/components/DetailViewBinding.tsx +112 -148
  104. package/src/components/DrawerNavigationItem.tsx +1 -1
  105. package/src/components/EditFormActions.tsx +54 -129
  106. package/src/components/EditViewBinding.tsx +202 -99
  107. package/src/components/EntityIdentityBar.tsx +288 -0
  108. package/src/components/EntityInspector.tsx +137 -0
  109. package/src/components/EntityPreviewBinding.tsx +6 -2
  110. package/src/components/EntityPreviewNesting.tsx +50 -0
  111. package/src/components/EntityViewBinding.tsx +35 -32
  112. package/src/components/HomePage/FavouritesView.tsx +1 -1
  113. package/src/components/HomePage/NavigationCardBinding.tsx +1 -1
  114. package/src/components/HomePage/SmallNavigationCard.tsx +1 -1
  115. package/src/components/InlineEntityPreview.tsx +149 -0
  116. package/src/components/RebaseLayout.tsx +1 -1
  117. package/src/components/RebaseNavigation.tsx +45 -5
  118. package/src/components/RebaseRouteDefs.tsx +1 -1
  119. package/src/components/SelectableTable/SelectableTable.tsx +2 -2
  120. package/src/components/SidePanelBinding.tsx +19 -13
  121. package/src/components/UserSelector.tsx +12 -6
  122. package/src/components/common/default_entity_actions.tsx +4 -3
  123. package/src/components/history/EntityHistoryEntry.tsx +28 -7
  124. package/src/components/history/EntityHistoryView.tsx +29 -24
  125. package/src/components/history/LastEditedByIndicator.tsx +4 -3
  126. package/src/components/index.ts +2 -0
  127. package/src/data_import/utils/get_import_inference_type.ts +6 -0
  128. package/src/form/EntityForm.tsx +267 -179
  129. package/src/form/EntityFormActions.tsx +50 -99
  130. package/src/form/EntityFormBinding.tsx +4 -2
  131. package/src/form/PropertyFieldBinding.tsx +4 -0
  132. package/src/form/components/FieldBlock.tsx +119 -0
  133. package/src/form/components/FormRail.tsx +125 -0
  134. package/src/form/components/FormSections.tsx +118 -0
  135. package/src/form/components/index.tsx +3 -2
  136. package/src/form/components/useRailVisible.tsx +46 -0
  137. package/src/form/field_bindings/BinaryFieldBinding.tsx +3 -2
  138. package/src/form/field_bindings/DateTimeFieldBinding.tsx +3 -2
  139. package/src/form/field_bindings/GeopointFieldBinding.tsx +3 -2
  140. package/src/form/field_bindings/MapFieldBinding.tsx +1 -2
  141. package/src/form/field_bindings/MarkdownEditorFieldBinding.tsx +3 -2
  142. package/src/form/field_bindings/MultiSelectFieldBinding.tsx +2 -1
  143. package/src/form/field_bindings/ReadOnlyFieldBinding.tsx +2 -1
  144. package/src/form/field_bindings/ReferenceFieldBinding.tsx +2 -1
  145. package/src/form/field_bindings/RelationFieldBinding.tsx +6 -4
  146. package/src/form/field_bindings/RepeatFieldBinding.tsx +4 -1
  147. package/src/form/field_bindings/SelectFieldBinding.tsx +3 -2
  148. package/src/form/field_bindings/StorageUploadFieldBinding.tsx +31 -8
  149. package/src/form/field_bindings/SwitchFieldBinding.tsx +7 -3
  150. package/src/form/field_bindings/TextFieldBinding.tsx +18 -16
  151. package/src/form/field_bindings/UserSelectFieldBinding.tsx +3 -2
  152. package/src/form/field_bindings/VectorFieldBinding.tsx +2 -1
  153. package/src/hooks/navigation/useBuildUrlController.tsx +1 -1
  154. package/src/hooks/navigation/useTopLevelNavigation.ts +18 -7
  155. package/src/hooks/navigation/utils.ts +20 -0
  156. package/src/hooks/useBuildSideDialogsController.tsx +2 -2
  157. package/src/hooks/useBuildSidePanel.tsx +22 -61
  158. package/src/hooks/useEntityDisplayTitle.tsx +60 -0
  159. package/src/hooks/useHistory.ts +20 -10
  160. package/src/hooks/useRecordActions.tsx +69 -0
  161. package/src/hooks/useResolvedUsers.ts +10 -5
  162. package/src/preview/PropertyPreview.tsx +48 -22
  163. package/src/preview/components/InlineEntityListPreview.tsx +42 -0
  164. package/src/preview/components/ReferencePreview.tsx +34 -14
  165. package/src/preview/components/RelationPreview.tsx +34 -14
  166. package/src/preview/index.ts +1 -0
  167. package/src/preview/property_previews/ArrayOfReferencesPreview.tsx +24 -1
  168. package/src/preview/property_previews/ArrayOfRelationsPreview.tsx +25 -1
  169. package/src/routes/RebaseRoute.tsx +2 -2
  170. package/src/types/components/EntityFormActionsProps.tsx +0 -1
  171. package/src/types/components/EntityFormProps.tsx +0 -6
  172. package/src/types/fields.tsx +27 -0
  173. package/dist/form/components/FormEntry.d.ts +0 -6
  174. package/dist/form/components/FormLayout.d.ts +0 -5
  175. package/dist/history-g9688IaK.js.map +0 -1
  176. package/dist/util-B5fJm1FA.js.map +0 -1
  177. package/src/form/components/FormEntry.tsx +0 -22
  178. package/src/form/components/FormLayout.tsx +0 -16
@@ -1,9 +1,9 @@
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, ArrowLeftIcon, ArrowRightToLineIcon, Badge, BooleanSwitchWithLabel, Button, CHIP_COLORS, 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, LoaderIcon, 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, TableView, Tabs, TextField, TextIcon, TextareaAutosize, ToggleButtonGroup, Tooltip, Trash2Icon, Typography, Unlink2Icon, UploadIcon, UserIcon, 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, 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 { ADDITIONAL_TAB_WIDTH, CONTAINER_FULL_WIDTH, CollectionScopeProvider, DEFAULT_PAGE_SIZE, ErrorTooltip, ErrorView, FORM_CONTAINER_WIDTH, IconForView, PluginProviderStack, StorageSourceContext, UnsavedChangesDialog, UserDisplay, addInitialSlash, buildEnumLabel, createFormexStub, deleteEntityWithCallbacks, flattenKeys, getAdminEntityChildViews, getCollectionBySlugWithin, getCollectionPathsCombinations, getColorScheme, getColumnKeysForProperty, getEntityFromCache, getEntityFromMemoryCache, getEntityImagePreviewPropertyKey, getFormFieldKeys, getIcon, getLastSegment as getLastSegment$1, 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, saveEntityToCache, saveEntityToMemoryCache, saveEntityWithCallbacks, useAnalyticsController, 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, 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";
7
7
  import { Field, Formex, getIn, setIn, useCreateFormex, useFormex } from "@rebasepro/forms";
8
8
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
9
9
  import { defaultDateFormat, getHashValue, getValueInPath, hydrateRegExp, isObject, mergeDeep, randomString, toArray } from "@rebasepro/utils";
@@ -14,7 +14,7 @@ import { useDropzone } from "react-dropzone";
14
14
  import { EntityReference, EntityRelation, GeoPoint, Vector, getCollectionDataPath, getDataSourceCapabilities, isRelationalCollectionConfig } from "@rebasepro/types";
15
15
  import { z } from "zod";
16
16
  import useMeasure from "react-use-measure";
17
- import { useLocation, useNavigate, useSearchParams } from "react-router-dom";
17
+ import { useLocation, useNavigate, useSearchParams } from "react-router";
18
18
  import { DndContext, KeyboardSensor, PointerSensor, closestCenter, useSensor, useSensors } from "@dnd-kit/core";
19
19
  import { restrictToVerticalAxis } from "@dnd-kit/modifiers";
20
20
  import { SortableContext, horizontalListSortingStrategy, sortableKeyboardCoordinates, useSortable, verticalListSortingStrategy } from "@dnd-kit/sortable";
@@ -24,40 +24,139 @@ import { inferTypeFromValue } from "@rebasepro/inference";
24
24
  import { Highlight, themes } from "prism-react-renderer";
25
25
  import { generateKeyBetween, generateNKeysBetween } from "fractional-indexing";
26
26
  import { resolveAdminCollection } from "@rebasepro/admin-types";
27
- //#region src/form/components/FormEntry.tsx
28
- function FormEntry({ propertyKey, widthPercentage = 100, children }) {
29
- return /* @__PURE__ */ jsx("div", {
30
- id: `form_field_${propertyKey}`,
31
- className: "relative",
32
- style: { width: widthPercentage === 100 ? "100%" : `calc(${widthPercentage}% - 8px)` },
33
- children: /* @__PURE__ */ jsx(ErrorBoundary, { children })
34
- });
27
+ //#region src/util/property_utils.tsx
28
+ /**
29
+ * Resolve a size value (string token or number) to a numeric pixel value
30
+ * suitable for passing to Lucide icons.
31
+ */
32
+ function resolveSize(size) {
33
+ if (typeof size === "number") return size;
34
+ return iconSize[size];
35
35
  }
36
- //#endregion
37
- //#region src/form/components/FormLayout.tsx
38
- function FormLayout({ children, className }) {
39
- return /* @__PURE__ */ jsx("div", {
40
- className: cls("flex flex-wrap gap-x-4 w-full space-y-6", className),
41
- children
42
- });
36
+ function isReferenceProperty(property) {
37
+ if (!property) return null;
38
+ if (property.type === "reference") return true;
39
+ if (property.type === "array") if (Array.isArray(property.of)) return false;
40
+ else return property.of?.type === "reference";
41
+ return false;
42
+ }
43
+ function isRelationProperty(property) {
44
+ if (!property) return null;
45
+ if (property.type === "relation") return true;
46
+ if (property.type === "array") if (Array.isArray(property.of)) return false;
47
+ else return property.of?.type === "relation";
48
+ return false;
49
+ }
50
+ function getIconForWidget(widget, size) {
51
+ return /* @__PURE__ */ jsx(widget?.Icon ?? CircleIcon, { size: resolveSize(size) });
43
52
  }
44
- //#endregion
45
- //#region src/form/components/LabelWithIcon.tsx
46
53
  /**
47
- * Render the label of with an icon and the title of a property
48
- * @group Form custom fields
54
+ * Returns a default Lucide icon component based on property type.
55
+ * This provides a sensible fallback when no PropertyConfig is available.
49
56
  */
50
- var LabelWithIcon = forwardRef(({ icon, title, small, className, required }, ref) => {
51
- return /* @__PURE__ */ jsxs("div", {
52
- ref,
53
- className: cls("align-middle inline-flex items-center my-0.5", small ? "gap-1" : "gap-2", className),
54
- children: [icon, /* @__PURE__ */ jsx("span", {
55
- className: `text-start font-medium text-${small ? "base" : "sm"} origin-top-left transform ${small ? "translate-x-2 scale-75" : ""}`,
56
- children: (title ?? "") + (required ? " *" : "")
57
- })]
57
+ function getDefaultIconForProperty(property) {
58
+ switch (property.type) {
59
+ case "string":
60
+ if (property.storage) return UploadIcon;
61
+ if (property.admin?.urlPreview) return GlobeIcon;
62
+ if (property.email) return MailIcon;
63
+ if (property.admin?.multiline || property.admin?.markdown) return AlignLeftIcon;
64
+ return TextIcon;
65
+ case "number": return HashIcon;
66
+ case "boolean": return FlagIcon;
67
+ case "date": return CalendarIcon;
68
+ case "map": return VoteIcon;
69
+ case "array": {
70
+ const of = property.of;
71
+ if (property.oneOf) return Rows3Icon;
72
+ if (of && !Array.isArray(of)) {
73
+ if (of.type === "reference") return LinkIcon;
74
+ if (of.type === "string" && of.storage) return UploadIcon;
75
+ }
76
+ return RepeatIcon;
77
+ }
78
+ case "reference": return LinkIcon;
79
+ case "relation": return LinkIcon;
80
+ case "vector": return HashIcon;
81
+ default: return CircleIcon;
82
+ }
83
+ }
84
+ function getIconForProperty(property, size = "small", fields = {}) {
85
+ const px = resolveSize(size);
86
+ if (isPropertyBuilder(property)) return /* @__PURE__ */ jsx(FunctionSquareIcon, { size: px });
87
+ const configId = property.propertyConfig || void 0;
88
+ const widget = configId ? fields[configId] : void 0;
89
+ if (widget?.Icon) return getIconForWidget(widget, size);
90
+ return /* @__PURE__ */ jsx(getDefaultIconForProperty(property), { size: px });
91
+ }
92
+ /**
93
+ * Get a property in a property tree from a path like
94
+ * `address.street`
95
+ * @param properties
96
+ * @param path
97
+ */
98
+ function getPropertyInPath(properties, path) {
99
+ if (typeof properties === "object") {
100
+ if (path in properties) return properties[path];
101
+ if (path.includes(".")) {
102
+ const pathSegments = path.split(".");
103
+ const childProperty = properties[pathSegments[0]];
104
+ if (typeof childProperty === "object" && childProperty.type === "map" && childProperty.properties) return getPropertyInPath(childProperty.properties, pathSegments.slice(1).join("."));
105
+ }
106
+ }
107
+ }
108
+ function getResolvedPropertyInPath(properties, path) {
109
+ if (typeof properties === "object") {
110
+ if (path in properties) return properties[path];
111
+ if (path.includes(".")) {
112
+ const pathSegments = path.split(".");
113
+ const childProperty = properties[pathSegments[0]];
114
+ if (childProperty.type === "map" && childProperty.properties) return getResolvedPropertyInPath(childProperty.properties, pathSegments.slice(1).join("."));
115
+ }
116
+ }
117
+ }
118
+ function getBracketNotation(path) {
119
+ return path.replace(/\.([^.]*)/g, "[$1]");
120
+ }
121
+ /**
122
+ * Get properties sorted by their order, but include ALL properties.
123
+ * Nested keys (like "data.mode") in propertiesOrder are ignored - they're for column ordering.
124
+ * @param properties
125
+ * @param propertiesOrder
126
+ */
127
+ function getPropertiesWithPropertiesOrder(properties, propertiesOrder) {
128
+ if (!propertiesOrder) return properties;
129
+ const propertyKeys = Object.keys(properties);
130
+ const validOrderKeys = propertiesOrder.filter((key) => !key.includes(".") && properties[key]);
131
+ const result = {};
132
+ validOrderKeys.forEach((key) => {
133
+ const property = properties[key];
134
+ if (typeof property === "object" && property.type === "map" && property.properties) {
135
+ const mapProp = property;
136
+ result[key] = {
137
+ ...mapProp,
138
+ properties: getPropertiesWithPropertiesOrder(mapProp.properties, mapProp.propertiesOrder ?? [])
139
+ };
140
+ } else if (property) result[key] = property;
58
141
  });
59
- });
60
- LabelWithIcon.displayName = "LabelWithIcon";
142
+ propertyKeys.forEach((key) => {
143
+ if (!result[key]) {
144
+ const property = properties[key];
145
+ if (typeof property === "object" && property.type === "map" && property.properties) {
146
+ const mapProp = property;
147
+ result[key] = {
148
+ ...mapProp,
149
+ properties: getPropertiesWithPropertiesOrder(mapProp.properties, mapProp.propertiesOrder ?? [])
150
+ };
151
+ } else if (property) result[key] = property;
152
+ }
153
+ });
154
+ return result;
155
+ }
156
+ function getDefaultPropertiesOrder(collection) {
157
+ if (collection.propertiesOrder) return collection.propertiesOrder;
158
+ return [...Object.keys(collection.properties), ...(collection.additionalFields ?? []).map((field) => field.key)];
159
+ }
61
160
  //#endregion
62
161
  //#region src/components/PropertyIdCopyTooltip.tsx
63
162
  function PropertyIdCopyTooltip({ propertyKey, className, children }) {
@@ -98,6 +197,289 @@ function PropertyIdCopyTooltipContent({ propertyKey }) {
98
197
  });
99
198
  }
100
199
  //#endregion
200
+ //#region src/form/components/FieldBlock.tsx
201
+ /**
202
+ * Does this property's editor supply its own heading?
203
+ *
204
+ * Arrays, maps, `oneOf` blocks and key-value editors render as a collapsible
205
+ * panel whose header *is* the label — hiding it would leave a panel you cannot
206
+ * identify or, worse, cannot expand. Those keep their own; everything else gets
207
+ * its label from {@link FieldBlock}, which is what makes a text field and a
208
+ * select finally agree on where a label goes.
209
+ *
210
+ * Exported because the layout resolver's span rules and this rule have to stay
211
+ * in step: the self-labelling types are exactly the ones that take a full row.
212
+ */
213
+ function isSelfLabellingProperty(property) {
214
+ if (!property) return false;
215
+ switch (property.type) {
216
+ case "map": return !property.admin?.spreadChildren;
217
+ case "array": {
218
+ if (property.oneOf) return true;
219
+ if (Array.isArray(property.of) || !property.of) return true;
220
+ const of = property.of;
221
+ if (of.type === "string" && (of.enum || of.storage)) return false;
222
+ if (of.type === "number" && of.enum) return false;
223
+ return true;
224
+ }
225
+ default: return false;
226
+ }
227
+ }
228
+ /**
229
+ * One field's worth of the form grid: label, control, description.
230
+ *
231
+ * The label carries a small type icon so a number reads differently from a
232
+ * relation at a glance, and the description sits under the control where it
233
+ * annotates what you just filled in.
234
+ */
235
+ function FieldBlock({ propertyKey, property, showLabel, children }) {
236
+ const description = property?.description?.trim();
237
+ const required = Boolean(property?.validation?.required);
238
+ return /* @__PURE__ */ jsxs("div", {
239
+ id: `form_field_${propertyKey}`,
240
+ className: "relative flex flex-col min-w-0",
241
+ children: [
242
+ showLabel && /* @__PURE__ */ jsx(PropertyIdCopyTooltip, {
243
+ propertyKey,
244
+ children: /* @__PURE__ */ jsxs("div", {
245
+ className: cls("flex items-center gap-1.5 text-sm font-medium leading-tight mb-1.5", "text-text-secondary dark:text-text-secondary-dark"),
246
+ children: [
247
+ property && /* @__PURE__ */ jsx("span", {
248
+ className: "shrink-0 text-text-disabled dark:text-text-disabled-dark",
249
+ children: getIconForProperty(property, "smallest")
250
+ }),
251
+ /* @__PURE__ */ jsx("span", {
252
+ className: "truncate",
253
+ children: property?.name ?? propertyKey
254
+ }),
255
+ required && /* @__PURE__ */ jsx("span", {
256
+ className: "text-red-500 dark:text-red-500 -ml-1",
257
+ children: "*"
258
+ })
259
+ ]
260
+ })
261
+ }),
262
+ /* @__PURE__ */ jsx("div", {
263
+ className: "min-w-0",
264
+ children: /* @__PURE__ */ jsx(ErrorBoundary, { children })
265
+ }),
266
+ showLabel && description && /* @__PURE__ */ jsx(Typography, {
267
+ variant: "caption",
268
+ color: "disabled",
269
+ className: "mt-1.5 ml-0.5 leading-snug",
270
+ children: description
271
+ })
272
+ ]
273
+ });
274
+ }
275
+ //#endregion
276
+ //#region src/form/components/FormRail.tsx
277
+ /**
278
+ * The metadata column beside the form.
279
+ *
280
+ * This replaces a `w-80 2xl:w-96` rail that held a Save button and several
281
+ * hundred pixels of nothing. It renders only when it has something to say, and
282
+ * the actions it used to hold now live in the identity bar, where they stay
283
+ * visible while you scroll.
284
+ */
285
+ function FormRail({ fields, showRecordMeta, entity, renderField }) {
286
+ if (!fields.length && !showRecordMeta) return null;
287
+ return /* @__PURE__ */ jsxs("aside", {
288
+ className: cls("flex flex-col gap-6 shrink-0 w-72 border-l overflow-y-auto", "px-5 py-6 bg-surface-50 dark:bg-surface-900", defaultBorderMixin),
289
+ children: [fields.length > 0 && /* @__PURE__ */ jsx("div", {
290
+ className: "flex flex-col gap-5",
291
+ children: fields.map(renderField)
292
+ }), showRecordMeta && entity && /* @__PURE__ */ jsx(RecordMeta, { entity })]
293
+ });
294
+ }
295
+ function RecordMeta({ entity }) {
296
+ const createdAt = pickDate(entity.values, [
297
+ "created_at",
298
+ "createdAt",
299
+ "created_on"
300
+ ]);
301
+ const updatedAt = pickDate(entity.values, [
302
+ "updated_at",
303
+ "updatedAt",
304
+ "modified_at"
305
+ ]);
306
+ return /* @__PURE__ */ jsxs("div", {
307
+ className: "flex flex-col gap-2.5",
308
+ children: [
309
+ /* @__PURE__ */ jsx(Typography, {
310
+ variant: "caption",
311
+ className: "text-xs font-semibold uppercase tracking-wider text-text-disabled dark:text-text-disabled-dark",
312
+ children: "Record"
313
+ }),
314
+ /* @__PURE__ */ jsx(MetaRow, {
315
+ label: "ID",
316
+ value: String(entity.id),
317
+ copyable: true
318
+ }),
319
+ createdAt && /* @__PURE__ */ jsx(MetaRow, {
320
+ label: "Created",
321
+ value: createdAt
322
+ }),
323
+ updatedAt && /* @__PURE__ */ jsx(MetaRow, {
324
+ label: "Updated",
325
+ value: updatedAt
326
+ })
327
+ ]
328
+ });
329
+ }
330
+ function MetaRow({ label, value, copyable }) {
331
+ const [copied, setCopied] = React.useState(false);
332
+ const copy = () => {
333
+ navigator.clipboard?.writeText(value).then(() => {
334
+ setCopied(true);
335
+ setTimeout(() => setCopied(false), 1600);
336
+ }, () => void 0);
337
+ };
338
+ return /* @__PURE__ */ jsxs("div", {
339
+ className: "flex items-baseline justify-between gap-3 min-w-0",
340
+ children: [/* @__PURE__ */ jsx("span", {
341
+ className: "text-xs text-text-disabled dark:text-text-disabled-dark shrink-0",
342
+ children: label
343
+ }), /* @__PURE__ */ jsxs("span", {
344
+ className: "flex items-center gap-1 min-w-0",
345
+ children: [/* @__PURE__ */ jsx("span", {
346
+ className: "font-mono text-xs text-text-secondary dark:text-text-secondary-dark truncate",
347
+ title: value,
348
+ children: value
349
+ }), copyable && /* @__PURE__ */ jsx(Tooltip, {
350
+ title: copied ? "Copied" : "Copy",
351
+ children: /* @__PURE__ */ jsx(IconButton, {
352
+ size: "smallest",
353
+ onClick: copy,
354
+ children: copied ? /* @__PURE__ */ jsx(CheckIcon, { size: iconSize.smallest }) : /* @__PURE__ */ jsx(CopyIcon, { size: iconSize.smallest })
355
+ })
356
+ })]
357
+ })]
358
+ });
359
+ }
360
+ /**
361
+ * Audit timestamps are a convention, not a contract — a collection may name them
362
+ * anything or not have them at all — so this reads the values it recognises and
363
+ * stays quiet otherwise rather than rendering an empty row.
364
+ */
365
+ function pickDate(values, keys) {
366
+ if (!values) return void 0;
367
+ for (const key of keys) {
368
+ const value = values[key];
369
+ if (value instanceof Date) return value.toLocaleDateString();
370
+ if (typeof value === "string" && value) {
371
+ const parsed = new Date(value);
372
+ if (!Number.isNaN(parsed.getTime())) return parsed.toLocaleDateString();
373
+ }
374
+ }
375
+ }
376
+ //#endregion
377
+ //#region src/form/components/FormSections.tsx
378
+ function FormSections({ sections, collapsed, onToggle, errorKeys, renderField }) {
379
+ return /* @__PURE__ */ jsx("div", {
380
+ className: "flex flex-col gap-8",
381
+ children: sections.map((section) => {
382
+ const hasError = section.fields.some((f) => errorKeys.has(f.key));
383
+ const isCollapsed = section.collapsible && collapsed.has(section.key) && !hasError;
384
+ return /* @__PURE__ */ jsxs("section", {
385
+ className: "min-w-0",
386
+ children: [section.title && /* @__PURE__ */ jsx(Header, {
387
+ title: section.title,
388
+ collapsible: section.collapsible,
389
+ collapsed: isCollapsed,
390
+ controls: `form_section_${section.key}`,
391
+ onToggle: () => onToggle(section.key)
392
+ }), !isCollapsed && /* @__PURE__ */ jsx("div", {
393
+ id: `form_section_${section.key}`,
394
+ className: cls("grid gap-x-4 gap-y-5 min-w-0", "grid-cols-1 @2xl:grid-cols-4"),
395
+ children: section.fields.map(renderField)
396
+ })]
397
+ }, section.key);
398
+ })
399
+ });
400
+ }
401
+ function Header({ title, collapsible, collapsed, controls, onToggle }) {
402
+ const content = /* @__PURE__ */ jsxs(Fragment, { children: [
403
+ collapsible && /* @__PURE__ */ jsx(ChevronDownIcon, {
404
+ size: iconSize.smallest,
405
+ className: cls("text-text-disabled dark:text-text-disabled-dark transition-transform duration-150", collapsed && "-rotate-90")
406
+ }),
407
+ /* @__PURE__ */ jsx("span", {
408
+ className: "text-xs font-semibold uppercase tracking-wider text-text-disabled dark:text-text-disabled-dark whitespace-nowrap",
409
+ children: title
410
+ }),
411
+ /* @__PURE__ */ jsx("span", { className: cls("flex-1 border-t", defaultBorderMixin) })
412
+ ] });
413
+ const className = "flex items-center gap-2.5 w-full mb-3.5";
414
+ if (!collapsible) return /* @__PURE__ */ jsx("div", {
415
+ className,
416
+ children: content
417
+ });
418
+ return /* @__PURE__ */ jsx("button", {
419
+ type: "button",
420
+ onClick: onToggle,
421
+ "aria-expanded": !collapsed,
422
+ "aria-controls": collapsed ? void 0 : controls,
423
+ className: cls(className, "text-left hover:opacity-80 transition-opacity"),
424
+ children: content
425
+ });
426
+ }
427
+ //#endregion
428
+ //#region src/form/components/useRailVisible.tsx
429
+ /**
430
+ * Below this the form is a single column and the rail is not shown.
431
+ *
432
+ * Roughly the 288px rail plus a content column still wide enough to hold a
433
+ * four-up row. Above it the rail is worth having even in the split view; below
434
+ * it — a narrow split pane, the side panel, the dialog, a phone — its fields
435
+ * fold back into the column.
436
+ */
437
+ var RAIL_MIN_WIDTH = 1e3;
438
+ /**
439
+ * Is there room beside the form for the metadata rail?
440
+ *
441
+ * Measured on the form element rather than the viewport, because the same form
442
+ * renders in a side panel and a dialog inside a wide window — a media query
443
+ * would say "desktop" and hand a 600px panel a rail it cannot fit.
444
+ *
445
+ * This has to be a real measurement rather than a CSS `hidden` class: the
446
+ * layout resolver *moves* fields into the rail, so hiding it with CSS did not
447
+ * fold them back into the column, it made them unreachable. Status, category
448
+ * and any other rail field simply could not be edited in the side panel, the
449
+ * dialog, or on a phone.
450
+ */
451
+ function useRailVisible(ref) {
452
+ const [visible, setVisible] = useState(false);
453
+ useLayoutEffect(() => {
454
+ const element = ref.current;
455
+ if (!element) return;
456
+ const measure = () => setVisible(element.getBoundingClientRect().width >= RAIL_MIN_WIDTH);
457
+ measure();
458
+ if (typeof ResizeObserver === "undefined") return;
459
+ const observer = new ResizeObserver(measure);
460
+ observer.observe(element);
461
+ return () => observer.disconnect();
462
+ }, [ref]);
463
+ return visible;
464
+ }
465
+ //#endregion
466
+ //#region src/form/components/LabelWithIcon.tsx
467
+ /**
468
+ * Render the label of with an icon and the title of a property
469
+ * @group Form custom fields
470
+ */
471
+ var LabelWithIcon = forwardRef(({ icon, title, small, className, required }, ref) => {
472
+ return /* @__PURE__ */ jsxs("div", {
473
+ ref,
474
+ className: cls("align-middle inline-flex items-center my-0.5", small ? "gap-1" : "gap-2", className),
475
+ children: [icon, /* @__PURE__ */ jsx("span", {
476
+ className: `text-start font-medium text-${small ? "base" : "sm"} origin-top-left transform ${small ? "translate-x-2 scale-75" : ""}`,
477
+ children: (title ?? "") + (required ? " *" : "")
478
+ })]
479
+ });
480
+ });
481
+ LabelWithIcon.displayName = "LabelWithIcon";
482
+ //#endregion
101
483
  //#region src/form/components/LabelWithIconAndTooltip.tsx
102
484
  /**
103
485
  * Render the label of with an icon and the title of a property
@@ -580,170 +962,36 @@ function StringPropertyPreview({ propertyKey, value, property, size, textOnly })
580
962
  //#region src/preview/property_previews/ArrayPropertyPreview.tsx
581
963
  /**
582
964
  * @group Preview components
583
- */
584
- function ArrayPropertyPreview({ propertyKey, value, property, size }) {
585
- if (property.type !== "array") throw Error("Picked wrong preview component ArrayPreview");
586
- if (!property.of) throw Error(`You need to specify an 'of' prop (or specify a custom field) in your array property ${propertyKey}`);
587
- const authController = useAuthController();
588
- const resolvedProperties = resolveArrayProperties({
589
- propertyKey,
590
- property,
591
- propertyConfigs: useCustomizationController().propertyConfigs,
592
- authController
593
- });
594
- const values = value;
595
- if (!values) return null;
596
- const childSize = size === "medium" ? "medium" : "small";
597
- return /* @__PURE__ */ jsx("div", {
598
- className: "w-full flex flex-col gap-2",
599
- children: values && values.map((val, index) => {
600
- if (!resolvedProperties) throw Error("Property resolvedProperties is undefined");
601
- const of = resolvedProperties[index] ?? resolvedProperties[index] ?? (Array.isArray(property.of) ? property.of[index] : property.of);
602
- return of ? /* @__PURE__ */ jsx(React.Fragment, { children: /* @__PURE__ */ jsx("div", {
603
- className: cls(defaultBorderMixin, "m-1 border-b last:border-b-0"),
604
- children: /* @__PURE__ */ jsx(ErrorBoundary, { children: /* @__PURE__ */ jsx(PropertyPreview, {
605
- propertyKey,
606
- value: val,
607
- property: of,
608
- size: childSize
609
- }) })
610
- }) }, "preview_array_" + index) : null;
611
- })
612
- });
613
- }
614
- //#endregion
615
- //#region src/util/property_utils.tsx
616
- /**
617
- * Resolve a size value (string token or number) to a numeric pixel value
618
- * suitable for passing to Lucide icons.
619
- */
620
- function resolveSize(size) {
621
- if (typeof size === "number") return size;
622
- return iconSize[size];
623
- }
624
- function isReferenceProperty(property) {
625
- if (!property) return null;
626
- if (property.type === "reference") return true;
627
- if (property.type === "array") if (Array.isArray(property.of)) return false;
628
- else return property.of?.type === "reference";
629
- return false;
630
- }
631
- function isRelationProperty(property) {
632
- if (!property) return null;
633
- if (property.type === "relation") return true;
634
- if (property.type === "array") if (Array.isArray(property.of)) return false;
635
- else return property.of?.type === "relation";
636
- return false;
637
- }
638
- function getIconForWidget(widget, size) {
639
- return /* @__PURE__ */ jsx(widget?.Icon ?? CircleIcon, { size: resolveSize(size) });
640
- }
641
- /**
642
- * Returns a default Lucide icon component based on property type.
643
- * This provides a sensible fallback when no PropertyConfig is available.
644
- */
645
- function getDefaultIconForProperty(property) {
646
- switch (property.type) {
647
- case "string":
648
- if (property.storage) return UploadIcon;
649
- if (property.admin?.urlPreview) return GlobeIcon;
650
- if (property.email) return MailIcon;
651
- if (property.admin?.multiline || property.admin?.markdown) return AlignLeftIcon;
652
- return TextIcon;
653
- case "number": return HashIcon;
654
- case "boolean": return FlagIcon;
655
- case "date": return CalendarIcon;
656
- case "map": return VoteIcon;
657
- case "array": {
658
- const of = property.of;
659
- if (property.oneOf) return Rows3Icon;
660
- if (of && !Array.isArray(of)) {
661
- if (of.type === "reference") return LinkIcon;
662
- if (of.type === "string" && of.storage) return UploadIcon;
663
- }
664
- return RepeatIcon;
665
- }
666
- case "reference": return LinkIcon;
667
- case "relation": return LinkIcon;
668
- case "vector": return HashIcon;
669
- default: return CircleIcon;
670
- }
671
- }
672
- function getIconForProperty(property, size = "small", fields = {}) {
673
- const px = resolveSize(size);
674
- if (isPropertyBuilder(property)) return /* @__PURE__ */ jsx(FunctionSquareIcon, { size: px });
675
- const configId = property.propertyConfig || void 0;
676
- const widget = configId ? fields[configId] : void 0;
677
- if (widget?.Icon) return getIconForWidget(widget, size);
678
- return /* @__PURE__ */ jsx(getDefaultIconForProperty(property), { size: px });
679
- }
680
- /**
681
- * Get a property in a property tree from a path like
682
- * `address.street`
683
- * @param properties
684
- * @param path
685
- */
686
- function getPropertyInPath(properties, path) {
687
- if (typeof properties === "object") {
688
- if (path in properties) return properties[path];
689
- if (path.includes(".")) {
690
- const pathSegments = path.split(".");
691
- const childProperty = properties[pathSegments[0]];
692
- if (typeof childProperty === "object" && childProperty.type === "map" && childProperty.properties) return getPropertyInPath(childProperty.properties, pathSegments.slice(1).join("."));
693
- }
694
- }
695
- }
696
- function getResolvedPropertyInPath(properties, path) {
697
- if (typeof properties === "object") {
698
- if (path in properties) return properties[path];
699
- if (path.includes(".")) {
700
- const pathSegments = path.split(".");
701
- const childProperty = properties[pathSegments[0]];
702
- if (childProperty.type === "map" && childProperty.properties) return getResolvedPropertyInPath(childProperty.properties, pathSegments.slice(1).join("."));
703
- }
704
- }
705
- }
706
- function getBracketNotation(path) {
707
- return path.replace(/\.([^.]*)/g, "[$1]");
708
- }
709
- /**
710
- * Get properties sorted by their order, but include ALL properties.
711
- * Nested keys (like "data.mode") in propertiesOrder are ignored - they're for column ordering.
712
- * @param properties
713
- * @param propertiesOrder
714
- */
715
- function getPropertiesWithPropertiesOrder(properties, propertiesOrder) {
716
- if (!propertiesOrder) return properties;
717
- const propertyKeys = Object.keys(properties);
718
- const validOrderKeys = propertiesOrder.filter((key) => !key.includes(".") && properties[key]);
719
- const result = {};
720
- validOrderKeys.forEach((key) => {
721
- const property = properties[key];
722
- if (typeof property === "object" && property.type === "map" && property.properties) {
723
- const mapProp = property;
724
- result[key] = {
725
- ...mapProp,
726
- properties: getPropertiesWithPropertiesOrder(mapProp.properties, mapProp.propertiesOrder ?? [])
727
- };
728
- } else if (property) result[key] = property;
965
+ */
966
+ function ArrayPropertyPreview({ propertyKey, value, property, size }) {
967
+ if (property.type !== "array") throw Error("Picked wrong preview component ArrayPreview");
968
+ if (!property.of) throw Error(`You need to specify an 'of' prop (or specify a custom field) in your array property ${propertyKey}`);
969
+ const authController = useAuthController();
970
+ const resolvedProperties = resolveArrayProperties({
971
+ propertyKey,
972
+ property,
973
+ propertyConfigs: useCustomizationController().propertyConfigs,
974
+ authController
729
975
  });
730
- propertyKeys.forEach((key) => {
731
- if (!result[key]) {
732
- const property = properties[key];
733
- if (typeof property === "object" && property.type === "map" && property.properties) {
734
- const mapProp = property;
735
- result[key] = {
736
- ...mapProp,
737
- properties: getPropertiesWithPropertiesOrder(mapProp.properties, mapProp.propertiesOrder ?? [])
738
- };
739
- } else if (property) result[key] = property;
740
- }
976
+ const values = value;
977
+ if (!values) return null;
978
+ const childSize = size === "medium" ? "medium" : "small";
979
+ return /* @__PURE__ */ jsx("div", {
980
+ className: "w-full flex flex-col gap-2",
981
+ children: values && values.map((val, index) => {
982
+ if (!resolvedProperties) throw Error("Property resolvedProperties is undefined");
983
+ const of = resolvedProperties[index] ?? resolvedProperties[index] ?? (Array.isArray(property.of) ? property.of[index] : property.of);
984
+ return of ? /* @__PURE__ */ jsx(React.Fragment, { children: /* @__PURE__ */ jsx("div", {
985
+ className: cls(defaultBorderMixin, "m-1 border-b last:border-b-0"),
986
+ children: /* @__PURE__ */ jsx(ErrorBoundary, { children: /* @__PURE__ */ jsx(PropertyPreview, {
987
+ propertyKey,
988
+ value: val,
989
+ property: of,
990
+ size: childSize
991
+ }) })
992
+ }) }, "preview_array_" + index) : null;
993
+ })
741
994
  });
742
- return result;
743
- }
744
- function getDefaultPropertiesOrder(collection) {
745
- if (collection.propertiesOrder) return collection.propertiesOrder;
746
- return [...Object.keys(collection.properties), ...(collection.additionalFields ?? []).map((field) => field.key)];
747
995
  }
748
996
  //#endregion
749
997
  //#region src/util/previews.ts
@@ -875,6 +1123,43 @@ var SidePanelControllerContext = React.createContext({});
875
1123
  */
876
1124
  var useSidePanel = () => useContext(SidePanelControllerContext);
877
1125
  //#endregion
1126
+ //#region src/components/EntityPreviewNesting.tsx
1127
+ /**
1128
+ * How deep the current entity preview is nested inside another one.
1129
+ *
1130
+ * A reference or relation rendered on its own (a form field, a table cell) is
1131
+ * at depth 0 and gets the full card treatment: image slot, id, title, preview
1132
+ * properties and a link to the side panel. A reference rendered *inside*
1133
+ * another entity preview is at depth 1+, where repeating that card produces a
1134
+ * box-inside-a-box with its own borders, id line and action button. Nested
1135
+ * previews collapse to a compact chip instead.
1136
+ */
1137
+ var EntityPreviewDepthContext = React$1.createContext(0);
1138
+ /**
1139
+ * Current entity preview nesting depth. 0 means "not inside another preview".
1140
+ */
1141
+ function useEntityPreviewDepth() {
1142
+ return React$1.useContext(EntityPreviewDepthContext);
1143
+ }
1144
+ /**
1145
+ * True when the preview being rendered lives inside another entity preview,
1146
+ * and should therefore render its compact variant.
1147
+ */
1148
+ function useIsNestedEntityPreview() {
1149
+ return useEntityPreviewDepth() > 0;
1150
+ }
1151
+ /**
1152
+ * Marks its children as being rendered inside an entity preview, so any
1153
+ * reference or relation below collapses to a chip.
1154
+ */
1155
+ function NestedEntityPreviewBoundary({ children }) {
1156
+ const depth = useEntityPreviewDepth();
1157
+ return /* @__PURE__ */ jsx(EntityPreviewDepthContext.Provider, {
1158
+ value: depth + 1,
1159
+ children
1160
+ });
1161
+ }
1162
+ //#endregion
878
1163
  //#region src/components/EntityPreviewBinding.tsx
879
1164
  /**
880
1165
  * This component contains the main logic and content for displaying a entity preview,
@@ -906,7 +1191,7 @@ function EntityPreviewBindingData({ actions, collection: collectionProp, preview
906
1191
  const restProperties = listProperties.filter((p) => p !== titleProperty && p !== imagePropertyKey);
907
1192
  const imageValue = imagePropertyKey ? getValueInPath(entity.values, imagePropertyKey) : void 0;
908
1193
  const usedImageValue = imageProperty !== void 0 ? "of" in imageProperty ? (imageValue ?? []).length > 0 ? imageValue[0] : void 0 : imageValue : void 0;
909
- return /* @__PURE__ */ jsxs(Fragment, { children: [
1194
+ return /* @__PURE__ */ jsxs(NestedEntityPreviewBoundary, { children: [
910
1195
  /* @__PURE__ */ jsxs("div", {
911
1196
  className: cls("flex shrink-0", {
912
1197
  "w-6 h-6 mx-1 my-0.5": size === "small" || size === "smallest",
@@ -1050,15 +1335,118 @@ var EntityPreviewContainer = React$1.forwardRef(({ children, hover, onClick, siz
1050
1335
  });
1051
1336
  EntityPreviewContainer.displayName = "EntityPreviewContainer";
1052
1337
  //#endregion
1338
+ //#region src/components/InlineEntityPreview.tsx
1339
+ var inlineMixin = "inline-flex items-center gap-1 min-w-0 max-w-full align-middle";
1340
+ /**
1341
+ * An entity rendered as one line of text: a small link glyph that marks it as
1342
+ * pointing elsewhere, then the target's title in whatever typography surrounds
1343
+ * it.
1344
+ *
1345
+ * This is what a reference or relation renders as when it is *not* the subject
1346
+ * of its slot — nested inside another entity preview, or filling a title slot.
1347
+ * The card treatment (image, id, preview properties, side-panel button) only
1348
+ * makes sense at the top level; repeating it one level down produces a bordered
1349
+ * box inside a bordered box, each with its own action buttons.
1350
+ *
1351
+ * @group Preview components
1352
+ */
1353
+ function InlineEntityPreview({ entity, collection: collectionProp, label, disabled, onClick, includeEntityLink = true, iconSize = 12, className }) {
1354
+ const collectionRegistryController = useCollectionRegistryController();
1355
+ const analyticsController = useAnalyticsController();
1356
+ const sidePanelController = useSidePanel();
1357
+ 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));
1361
+ const interactive = !disabled && (onClick !== void 0 || includeEntityLink && collection !== void 0);
1362
+ const handleClick = (event) => {
1363
+ if (!interactive) return;
1364
+ event.preventDefault();
1365
+ event.stopPropagation();
1366
+ if (onClick) {
1367
+ onClick(event);
1368
+ return;
1369
+ }
1370
+ analyticsController.onAnalyticsEvent?.("entity_click_from_reference", {
1371
+ path: entity.path,
1372
+ entityId: entity.id
1373
+ });
1374
+ sidePanelController.open({
1375
+ entityId: entity.id,
1376
+ path: entity.path,
1377
+ collection,
1378
+ updateUrl: true
1379
+ });
1380
+ };
1381
+ const content = /* @__PURE__ */ jsxs("span", {
1382
+ tabIndex: interactive ? 0 : void 0,
1383
+ role: interactive ? "button" : void 0,
1384
+ onClick: interactive ? handleClick : void 0,
1385
+ onKeyDown: interactive ? (event) => {
1386
+ if (event.key === "Enter" || event.key === " ") handleClick(event);
1387
+ } : void 0,
1388
+ className: cls(inlineMixin, interactive ? "cursor-pointer hover:underline underline-offset-2" : "", className),
1389
+ children: [/* @__PURE__ */ jsx(LinkIcon, {
1390
+ size: iconSize,
1391
+ className: "shrink-0 opacity-40"
1392
+ }), /* @__PURE__ */ jsx("span", {
1393
+ className: "truncate",
1394
+ children: resolvedLabel
1395
+ })]
1396
+ });
1397
+ if (!interactive) return content;
1398
+ return /* @__PURE__ */ jsx(Tooltip, {
1399
+ title: `See details for ${resolvedLabel}`,
1400
+ asChild: true,
1401
+ children: content
1402
+ });
1403
+ }
1404
+ /**
1405
+ * Placeholder shown while the target of an inline reference is being fetched.
1406
+ */
1407
+ function InlineEntityPreviewSkeleton({ className }) {
1408
+ return /* @__PURE__ */ jsxs("span", {
1409
+ className: cls(inlineMixin, className),
1410
+ children: [/* @__PURE__ */ jsx(LinkIcon, {
1411
+ size: 12,
1412
+ className: "shrink-0 opacity-40"
1413
+ }), /* @__PURE__ */ jsx(Skeleton, { className: "w-20 h-3" })]
1414
+ });
1415
+ }
1416
+ /**
1417
+ * Shown when the target of an inline reference cannot be resolved — a deleted
1418
+ * row, or a collection that is not registered. Keeps the line readable instead
1419
+ * of dropping an error box into the middle of a card.
1420
+ */
1421
+ function InlineEntityPreviewMissing({ label, tooltip, className }) {
1422
+ const content = /* @__PURE__ */ jsxs("span", {
1423
+ className: cls(inlineMixin, "text-text-secondary dark:text-text-secondary-dark", className),
1424
+ children: [/* @__PURE__ */ jsx(LinkIcon, {
1425
+ size: 12,
1426
+ className: "shrink-0 opacity-40"
1427
+ }), /* @__PURE__ */ jsx("span", {
1428
+ className: "truncate italic",
1429
+ children: label
1430
+ })]
1431
+ });
1432
+ if (!tooltip) return content;
1433
+ return /* @__PURE__ */ jsx(Tooltip, {
1434
+ title: tooltip,
1435
+ asChild: true,
1436
+ children: content
1437
+ });
1438
+ }
1439
+ //#endregion
1053
1440
  //#region src/preview/components/ReferencePreview.tsx
1054
1441
  /**
1055
1442
  * @group Preview components
1056
1443
  */
1057
1444
  var ReferencePreview = function ReferencePreview(props) {
1058
1445
  const reference = props.reference;
1446
+ const nested = useIsNestedEntityPreview();
1059
1447
  if (!(typeof reference === "object" && "isEntityReference" in reference && reference.isEntityReference())) {
1060
1448
  console.warn("Reference preview received value of type", typeof reference);
1061
- if (props.textOnly) return /* @__PURE__ */ jsx("span", { children: String(reference) });
1449
+ if (props.textOnly || nested) return /* @__PURE__ */ jsx("span", { children: String(reference) });
1062
1450
  return /* @__PURE__ */ jsx(EntityPreviewContainer, {
1063
1451
  onClick: props.onClick,
1064
1452
  size: props.size ?? "medium",
@@ -1073,9 +1461,14 @@ var ReferencePreview = function ReferencePreview(props) {
1073
1461
  var DefaultMissingReference$2 = () => null;
1074
1462
  function ReferencePreviewInternalInner({ disabled, reference, previewProperties, size, hover, onClick, includeEntityLink = true, includeId = true, textOnly, collection }) {
1075
1463
  const ResolvedMissingReference = useComponentOverride("Entity.MissingReference", DefaultMissingReference$2);
1464
+ const nested = useIsNestedEntityPreview();
1076
1465
  if (!collection) if (ResolvedMissingReference !== DefaultMissingReference$2) return /* @__PURE__ */ jsx(ResolvedMissingReference, { path: reference.path });
1077
1466
  else {
1078
1467
  if (textOnly) return /* @__PURE__ */ jsx("span", { children: reference.path });
1468
+ if (nested) return /* @__PURE__ */ jsx(InlineEntityPreviewMissing, {
1469
+ label: String(reference.id),
1470
+ tooltip: `Collection not found: ${reference.path}`
1471
+ });
1079
1472
  return /* @__PURE__ */ jsx(EntityPreviewContainer, {
1080
1473
  onClick,
1081
1474
  size: size ?? "medium",
@@ -1113,6 +1506,7 @@ function ReferencePreviewInternal(props) {
1113
1506
  function ReferencePreviewExisting({ reference, collection, previewProperties, size, disabled, includeEntityLink, includeId, onClick, hover, textOnly }) {
1114
1507
  const ResolvedEntityPreview = useComponentOverride("EntityPreview", EntityPreviewBinding);
1115
1508
  useCustomizationController();
1509
+ const inline = useIsNestedEntityPreview() || Boolean(textOnly);
1116
1510
  const { entity, dataLoading, dataLoadingError } = useFetch({
1117
1511
  path: reference.path,
1118
1512
  entityId: reference.id,
@@ -1128,7 +1522,10 @@ function ReferencePreviewExisting({ reference, collection, previewProperties, si
1128
1522
  tooltip: reference.path
1129
1523
  });
1130
1524
  if (body) {
1131
- if (textOnly) return /* @__PURE__ */ jsx("span", { children: reference.id });
1525
+ if (inline) return /* @__PURE__ */ jsx(InlineEntityPreviewMissing, {
1526
+ label: String(reference.id),
1527
+ tooltip: reference.pathWithId
1528
+ });
1132
1529
  return /* @__PURE__ */ jsx(EntityPreviewContainer, {
1133
1530
  onClick: disabled ? void 0 : onClick,
1134
1531
  hover: disabled ? void 0 : hover,
@@ -1137,7 +1534,7 @@ function ReferencePreviewExisting({ reference, collection, previewProperties, si
1137
1534
  });
1138
1535
  }
1139
1536
  if (dataLoading && !usedEntity) {
1140
- if (textOnly) return /* @__PURE__ */ jsx(Skeleton, { className: "inline-block w-20 h-4" });
1537
+ if (inline) return /* @__PURE__ */ jsx(InlineEntityPreviewSkeleton, {});
1141
1538
  return /* @__PURE__ */ jsx(EntityPreviewContainer, {
1142
1539
  onClick: disabled ? void 0 : onClick,
1143
1540
  hover: disabled ? void 0 : hover,
@@ -1146,7 +1543,10 @@ function ReferencePreviewExisting({ reference, collection, previewProperties, si
1146
1543
  });
1147
1544
  }
1148
1545
  if (!usedEntity) {
1149
- if (textOnly) return /* @__PURE__ */ jsx("span", { children: reference.id });
1546
+ if (inline) return /* @__PURE__ */ jsx(InlineEntityPreviewMissing, {
1547
+ label: String(reference.id),
1548
+ tooltip: "Entity not found"
1549
+ });
1150
1550
  return /* @__PURE__ */ jsx(EntityPreviewContainer, {
1151
1551
  onClick: disabled ? void 0 : onClick,
1152
1552
  hover: disabled ? void 0 : hover,
@@ -1154,14 +1554,13 @@ function ReferencePreviewExisting({ reference, collection, previewProperties, si
1154
1554
  children: /* @__PURE__ */ jsx(ErrorView, { error: "Entity not found" })
1155
1555
  });
1156
1556
  }
1157
- if (textOnly) {
1158
- const titleProperty = getEntityTitlePropertyKeyForEntity(collection, usedEntity.values, usedEntity.id);
1159
- const titleValue = titleProperty ? getValueInPath(usedEntity.values, titleProperty) : void 0;
1160
- return /* @__PURE__ */ jsx("span", {
1161
- className: "truncate",
1162
- children: titleValue !== void 0 && titleValue !== null ? String(titleValue) : String(reference.id)
1163
- });
1164
- }
1557
+ if (inline) return /* @__PURE__ */ jsx(InlineEntityPreview, {
1558
+ entity: usedEntity,
1559
+ collection,
1560
+ disabled,
1561
+ onClick,
1562
+ includeEntityLink: !textOnly && includeEntityLink !== false
1563
+ });
1165
1564
  return /* @__PURE__ */ jsx(ResolvedEntityPreview, {
1166
1565
  size,
1167
1566
  previewKeys: previewProperties,
@@ -1176,14 +1575,53 @@ function ReferencePreviewExisting({ reference, collection, previewProperties, si
1176
1575
  }
1177
1576
  var referencesCache = /* @__PURE__ */ new Map();
1178
1577
  //#endregion
1578
+ //#region src/preview/components/InlineEntityListPreview.tsx
1579
+ /**
1580
+ * How many links a nested list shows before collapsing the rest into a count.
1581
+ */
1582
+ var MAX_INLINE_ITEMS = 3;
1583
+ /**
1584
+ * One wrapping line of inline entity links, with the overflow collapsed into a
1585
+ * `+N`. Used for arrays of references or relations that are nested inside
1586
+ * another preview, where stacking full-width cards turns a card into a wall.
1587
+ *
1588
+ * @group Preview components
1589
+ */
1590
+ function InlineEntityListPreview({ items, renderItem, max = MAX_INLINE_ITEMS, className }) {
1591
+ if (!items || items.length === 0) return null;
1592
+ const shown = items.slice(0, max);
1593
+ const overflow = items.length - shown.length;
1594
+ return /* @__PURE__ */ jsxs("span", {
1595
+ className: cls("inline-flex flex-wrap items-center gap-x-2 gap-y-0.5 min-w-0 max-w-full", className),
1596
+ children: [shown.map((item, index) => renderItem(item, index)), overflow > 0 && /* @__PURE__ */ jsxs("span", {
1597
+ className: "text-text-secondary dark:text-text-secondary-dark shrink-0",
1598
+ children: ["+", overflow]
1599
+ })]
1600
+ });
1601
+ }
1602
+ //#endregion
1179
1603
  //#region src/preview/property_previews/ArrayOfReferencesPreview.tsx
1180
1604
  /**
1181
1605
  * @group Preview components
1182
1606
  */
1183
- function ArrayOfReferencesPreview({ propertyKey, value, property, size }) {
1607
+ function ArrayOfReferencesPreview({ propertyKey, value, property, size, textOnly }) {
1608
+ const nested = useIsNestedEntityPreview();
1184
1609
  if (Array.isArray(property?.of)) throw Error("Using array properties instead of single one in `of` in ArrayProperty");
1185
1610
  if (property?.type !== "array" || !property.of || property.of.type !== "reference") throw Error("Picked wrong preview component ArrayOfReferencesPreview");
1186
1611
  const childSize = size === "medium" ? "medium" : "small";
1612
+ const ofProperty = property.of;
1613
+ if (nested || textOnly) return /* @__PURE__ */ jsx(InlineEntityListPreview, {
1614
+ items: value ?? [],
1615
+ renderItem: (reference, index) => /* @__PURE__ */ jsx(ReferencePreview, {
1616
+ disabled: !ofProperty.path,
1617
+ previewProperties: ofProperty.admin?.previewProperties,
1618
+ size: childSize,
1619
+ reference,
1620
+ textOnly,
1621
+ includeId: ofProperty.admin?.includeId,
1622
+ includeEntityLink: ofProperty.admin?.includeEntityLink
1623
+ }, `preview_array_ref_${propertyKey}_${index}`)
1624
+ });
1187
1625
  return /* @__PURE__ */ jsx("div", {
1188
1626
  className: "flex flex-col w-full",
1189
1627
  children: value ? value.map((reference, index) => {
@@ -1248,9 +1686,10 @@ function extractDisplayFromPlainObject(obj) {
1248
1686
  */
1249
1687
  var RelationPreview = function RelationPreview(props) {
1250
1688
  const relation = props.relation;
1689
+ const nested = useIsNestedEntityPreview();
1251
1690
  if (!(typeof relation === "object" && "isEntityRelation" in relation && relation.isEntityRelation())) {
1252
1691
  console.warn("Relation preview received value of type", typeof relation);
1253
- if (props.textOnly) return /* @__PURE__ */ jsx("span", {
1692
+ if (props.textOnly || nested) return /* @__PURE__ */ jsx("span", {
1254
1693
  className: "truncate",
1255
1694
  children: extractDisplayFromPlainObject(relation)
1256
1695
  });
@@ -1268,9 +1707,14 @@ var RelationPreview = function RelationPreview(props) {
1268
1707
  var DefaultMissingReference$1 = () => null;
1269
1708
  function RelationPreviewInternalInner({ disabled, relation, previewProperties, size, hover, onClick, includeEntityLink = true, includeId = true, textOnly, collection }) {
1270
1709
  const ResolvedMissingReference = useComponentOverride("Entity.MissingReference", DefaultMissingReference$1);
1710
+ const nested = useIsNestedEntityPreview();
1271
1711
  if (!collection) if (ResolvedMissingReference !== DefaultMissingReference$1) return /* @__PURE__ */ jsx(ResolvedMissingReference, { path: relation.path });
1272
1712
  else {
1273
1713
  if (textOnly) return /* @__PURE__ */ jsx("span", { children: relation.path });
1714
+ if (nested) return /* @__PURE__ */ jsx(InlineEntityPreviewMissing, {
1715
+ label: String(relation.id),
1716
+ tooltip: `Collection not found: ${relation.path}`
1717
+ });
1274
1718
  return /* @__PURE__ */ jsx(EntityPreviewContainer, {
1275
1719
  size,
1276
1720
  children: /* @__PURE__ */ jsx(ErrorView, { error: `Collection not found: ${relation.path}` })
@@ -1305,6 +1749,7 @@ function RelationPreviewExisting({ relation, collection, previewProperties, size
1305
1749
  const passedEntity = relation.data;
1306
1750
  const ResolvedEntityPreview = useComponentOverride("EntityPreview", EntityPreviewBinding);
1307
1751
  useCustomizationController();
1752
+ const inline = useIsNestedEntityPreview() || Boolean(textOnly);
1308
1753
  const { entity, dataLoading, dataLoadingError } = useFetch({
1309
1754
  path: relation.path,
1310
1755
  entityId: passedEntity ? void 0 : relation.id,
@@ -1320,7 +1765,10 @@ function RelationPreviewExisting({ relation, collection, previewProperties, size
1320
1765
  tooltip: relation.path
1321
1766
  });
1322
1767
  if (body) {
1323
- if (textOnly) return /* @__PURE__ */ jsx("span", { children: relation.id });
1768
+ if (inline) return /* @__PURE__ */ jsx(InlineEntityPreviewMissing, {
1769
+ label: String(relation.id),
1770
+ tooltip: relation.pathWithId
1771
+ });
1324
1772
  return /* @__PURE__ */ jsx(EntityPreviewContainer, {
1325
1773
  onClick: disabled ? void 0 : onClick,
1326
1774
  hover: disabled ? void 0 : hover,
@@ -1329,7 +1777,7 @@ function RelationPreviewExisting({ relation, collection, previewProperties, size
1329
1777
  });
1330
1778
  }
1331
1779
  if (dataLoading && !usedEntity) {
1332
- if (textOnly) return /* @__PURE__ */ jsx(Skeleton, { className: "inline-block w-20 h-4" });
1780
+ if (inline) return /* @__PURE__ */ jsx(InlineEntityPreviewSkeleton, {});
1333
1781
  return /* @__PURE__ */ jsx(EntityPreviewContainer, {
1334
1782
  onClick: disabled ? void 0 : onClick,
1335
1783
  hover: disabled ? void 0 : hover,
@@ -1338,7 +1786,10 @@ function RelationPreviewExisting({ relation, collection, previewProperties, size
1338
1786
  });
1339
1787
  }
1340
1788
  if (!usedEntity) {
1341
- if (textOnly) return /* @__PURE__ */ jsx("span", { children: relation.id });
1789
+ if (inline) return /* @__PURE__ */ jsx(InlineEntityPreviewMissing, {
1790
+ label: String(relation.id),
1791
+ tooltip: "Entity not found"
1792
+ });
1342
1793
  return /* @__PURE__ */ jsx(EntityPreviewContainer, {
1343
1794
  onClick: disabled ? void 0 : onClick,
1344
1795
  hover: disabled ? void 0 : hover,
@@ -1346,14 +1797,13 @@ function RelationPreviewExisting({ relation, collection, previewProperties, size
1346
1797
  children: /* @__PURE__ */ jsx(ErrorView, { error: "Entity not found" })
1347
1798
  });
1348
1799
  }
1349
- if (textOnly) {
1350
- const titleProperty = getEntityTitlePropertyKeyForEntity(collection, usedEntity.values, usedEntity.id);
1351
- const titleValue = titleProperty ? getValueInPath(usedEntity.values, titleProperty) : void 0;
1352
- return /* @__PURE__ */ jsx("span", {
1353
- className: "truncate",
1354
- children: titleValue !== void 0 && titleValue !== null ? String(titleValue) : String(relation.id)
1355
- });
1356
- }
1800
+ if (inline) return /* @__PURE__ */ jsx(InlineEntityPreview, {
1801
+ entity: usedEntity,
1802
+ collection,
1803
+ disabled,
1804
+ onClick,
1805
+ includeEntityLink: !textOnly && includeEntityLink !== false
1806
+ });
1357
1807
  return /* @__PURE__ */ jsx(ResolvedEntityPreview, {
1358
1808
  size,
1359
1809
  previewKeys: previewProperties,
@@ -1372,10 +1822,23 @@ var relationsCache = /* @__PURE__ */ new Map();
1372
1822
  /**
1373
1823
  * @group Preview components
1374
1824
  */
1375
- function ArrayOfRelationsPreview({ propertyKey, value, property, size }) {
1825
+ function ArrayOfRelationsPreview({ propertyKey, value, property, size, textOnly }) {
1826
+ const nested = useIsNestedEntityPreview();
1376
1827
  if (Array.isArray(property?.of)) throw Error("Using array properties instead of single one in `of` in ArrayProperty");
1377
1828
  if (property?.type !== "array" || !property.of || property.of.type !== "relation") throw Error("Picked wrong preview component ArrayOfRelationsPreview");
1378
1829
  const ofProperty = property.of;
1830
+ if (nested || textOnly) return /* @__PURE__ */ jsx(InlineEntityListPreview, {
1831
+ items: (value ?? []).map((relation) => normalizeToEntityRelation(relation)).filter((relation) => Boolean(relation)),
1832
+ renderItem: (relation, index) => /* @__PURE__ */ jsx(RelationPreview, {
1833
+ disabled: !ofProperty.relation,
1834
+ previewProperties: ofProperty.admin?.previewProperties,
1835
+ size: "small",
1836
+ relation,
1837
+ textOnly,
1838
+ includeId: ofProperty.admin?.includeId,
1839
+ includeEntityLink: ofProperty.admin?.includeEntityLink
1840
+ }, `preview_array_rel_${propertyKey}_${index}`)
1841
+ });
1379
1842
  return /* @__PURE__ */ jsx("div", {
1380
1843
  className: "flex flex-col w-full gap-0.5",
1381
1844
  children: value ? value.map((relation, index) => {
@@ -1696,11 +2159,11 @@ function getCache(baseUrl) {
1696
2159
  }
1697
2160
  return cache;
1698
2161
  }
1699
- async function fetchUsers(baseUrl, ids, token) {
2162
+ async function fetchUsers(apiBase, ids, token) {
1700
2163
  const resolved = /* @__PURE__ */ new Map();
1701
2164
  for (const id of ids) resolved.set(id, null);
1702
2165
  const params = new URLSearchParams({ ids: ids.join(",") });
1703
- const response = await fetch(`${baseUrl}/api/users?${params}`, { headers: token ? { Authorization: `Bearer ${token}` } : {} });
2166
+ const response = await fetch(`${apiBase}/admin/users?${params}`, { headers: token ? { Authorization: `Bearer ${token}` } : {} });
1704
2167
  if (!response.ok) return resolved;
1705
2168
  const data = await response.json();
1706
2169
  const rows = data.users ?? data.data ?? [];
@@ -1753,7 +2216,7 @@ function requestUser(baseUrl, uid, getAuthToken) {
1753
2216
  function useResolvedUser(uid) {
1754
2217
  const client = useRebaseClient();
1755
2218
  const { getAuthToken } = useAuthController();
1756
- const baseUrl = client?.baseUrl;
2219
+ const baseUrl = apiBaseOf(client);
1757
2220
  const [user, setUser] = useState(() => baseUrl && uid ? getCache(baseUrl).users.get(uid) ?? null : null);
1758
2221
  useEffect(() => {
1759
2222
  if (!baseUrl || !uid) {
@@ -1815,6 +2278,7 @@ function UserPreview({ value }) {
1815
2278
  var PropertyPreview = React.memo(function PropertyPreview(props) {
1816
2279
  const authController = useAuthController();
1817
2280
  const customizationController = useCustomizationController();
2281
+ const nested = useIsNestedEntityPreview();
1818
2282
  let content;
1819
2283
  const { property: inputProperty, propertyKey, value, size, height, width, interactive, fill } = props;
1820
2284
  const property = resolveProperty({
@@ -1935,12 +2399,14 @@ var PropertyPreview = React.memo(function PropertyPreview(props) {
1935
2399
  property
1936
2400
  });
1937
2401
  else content = buildWrongValueType(propertyKey, property.type, value);
1938
- else if (property.type === "date") if (value instanceof Date) content = /* @__PURE__ */ jsx(DatePreview, {
1939
- date: value,
1940
- mode: property.mode
1941
- });
1942
- else content = buildWrongValueType(propertyKey, property.type, value);
1943
- else if (property.type === "reference") if (typeof property.path === "string") if (typeof value === "object" && value !== null && "isEntityReference" in value && value.isEntityReference()) content = /* @__PURE__ */ jsx(ReferencePreview, {
2402
+ else if (property.type === "date") {
2403
+ const date = asDate(value);
2404
+ if (date) content = /* @__PURE__ */ jsx(DatePreview, {
2405
+ date,
2406
+ mode: property.mode
2407
+ });
2408
+ else content = buildWrongValueType(propertyKey, property.type, value);
2409
+ } else if (property.type === "reference") if (typeof property.path === "string") if (typeof value === "object" && value !== null && "isEntityReference" in value && value.isEntityReference()) content = /* @__PURE__ */ jsx(ReferencePreview, {
1944
2410
  disabled: !property.path,
1945
2411
  previewProperties: property.admin?.previewProperties,
1946
2412
  includeId: property.admin?.includeId,
@@ -1952,26 +2418,28 @@ var PropertyPreview = React.memo(function PropertyPreview(props) {
1952
2418
  else content = buildWrongValueType(propertyKey, property.type, value);
1953
2419
  else content = /* @__PURE__ */ jsx(EmptyValue, {});
1954
2420
  else if (property.type === "relation") if (!value) content = /* @__PURE__ */ jsx(EmptyValue, {});
1955
- else if (Array.isArray(value)) content = /* @__PURE__ */ jsx("div", {
1956
- className: "flex flex-col w-full gap-0.5",
1957
- children: value.map((item, index) => {
1958
- const entityRelation = normalizeToEntityRelation(item, "relation");
1959
- if (!entityRelation) return null;
1960
- return /* @__PURE__ */ jsx("div", {
2421
+ else if (Array.isArray(value)) {
2422
+ const relations = value.map((item) => normalizeToEntityRelation(item, "relation")).filter(Boolean);
2423
+ const renderRelation = (entityRelation, index) => /* @__PURE__ */ jsx(RelationPreview, {
2424
+ disabled: !property.relation,
2425
+ previewProperties: property.admin?.previewProperties,
2426
+ includeId: property.admin?.includeId,
2427
+ includeEntityLink: property.admin?.includeEntityLink,
2428
+ size: "small",
2429
+ relation: entityRelation,
2430
+ textOnly: props.textOnly
2431
+ }, `preview_rel_${propertyKey}_${index}`);
2432
+ content = nested || props.textOnly ? /* @__PURE__ */ jsx(InlineEntityListPreview, {
2433
+ items: relations,
2434
+ renderItem: renderRelation
2435
+ }) : /* @__PURE__ */ jsx("div", {
2436
+ className: "flex flex-col w-full gap-0.5",
2437
+ children: relations.map((entityRelation, index) => /* @__PURE__ */ jsx("div", {
1961
2438
  className: "w-full",
1962
- children: /* @__PURE__ */ jsx(RelationPreview, {
1963
- disabled: !property.relation,
1964
- previewProperties: property.admin?.previewProperties,
1965
- includeId: property.admin?.includeId,
1966
- includeEntityLink: property.admin?.includeEntityLink,
1967
- size: "small",
1968
- relation: entityRelation,
1969
- textOnly: props.textOnly
1970
- })
1971
- }, `preview_rel_${propertyKey}_${index}`);
1972
- })
1973
- });
1974
- else {
2439
+ children: renderRelation(entityRelation, index)
2440
+ }, `preview_rel_wrapper_${propertyKey}_${index}`))
2441
+ });
2442
+ } else {
1975
2443
  const relationValue = normalizeToEntityRelation(value, "relation");
1976
2444
  if (relationValue) content = /* @__PURE__ */ jsx(RelationPreview, {
1977
2445
  disabled: !property.relation,
@@ -1999,6 +2467,16 @@ var PropertyPreview = React.memo(function PropertyPreview(props) {
1999
2467
  else content = JSON.stringify(value);
2000
2468
  return content === void 0 || content === null || Array.isArray(content) && content.length === 0 ? /* @__PURE__ */ jsx(EmptyValue, {}) : content;
2001
2469
  }, deepEqual);
2470
+ /**
2471
+ * A `Date`, or a string/number that unambiguously names one. Returns null for
2472
+ * anything else, so an actual type mismatch still reports as one.
2473
+ */
2474
+ function asDate(value) {
2475
+ if (value instanceof Date) return isNaN(value.getTime()) ? null : value;
2476
+ if (typeof value !== "string" && typeof value !== "number") return null;
2477
+ const parsed = new Date(value);
2478
+ return isNaN(parsed.getTime()) ? null : parsed;
2479
+ }
2002
2480
  function buildWrongValueType(name, type, value) {
2003
2481
  console.warn(`Unexpected value for property ${name}, of type ${type}`, value);
2004
2482
  return /* @__PURE__ */ jsx(ErrorView, {
@@ -2104,10 +2582,10 @@ function FieldHelperText({ error, showError, property, includeDescription = true
2104
2582
  * and tables to the specified properties.
2105
2583
  * @group Form fields
2106
2584
  */
2107
- function ReadOnlyFieldBinding({ propertyKey, value, error, showError, minimalistView, property, includeDescription, context }) {
2585
+ function ReadOnlyFieldBinding({ propertyKey, value, error, showError, minimalistView, property, includeDescription, hideLabel, context }) {
2108
2586
  const skipCardWrapper = property.type === "relation" || property.type === "reference";
2109
2587
  return /* @__PURE__ */ jsxs(Fragment, { children: [
2110
- !minimalistView && /* @__PURE__ */ jsx(LabelWithIconAndTooltip, {
2588
+ !minimalistView && !hideLabel && /* @__PURE__ */ jsx(LabelWithIconAndTooltip, {
2111
2589
  propertyKey,
2112
2590
  icon: getIconForProperty(property, "small"),
2113
2591
  required: property.validation?.required,
@@ -2265,8 +2743,10 @@ function VirtualTableSelect(props) {
2265
2743
  if (ref.current && focused) ref.current?.focus({ preventScroll: true });
2266
2744
  }, [focused, ref]);
2267
2745
  const onChange = useCallback((updatedValue) => {
2268
- if (valueType === "number") if (multiple) updateValue(updatedValue.map((v) => parseFloat(v)));
2269
- else updateValue(parseFloat(updatedValue));
2746
+ if (valueType === "number") if (multiple) {
2747
+ const newValue = updatedValue.map((v) => parseFloat(v));
2748
+ updateValue(newValue);
2749
+ } else updateValue(parseFloat(updatedValue));
2270
2750
  else if (valueType === "string") if (!updatedValue) updateValue(null);
2271
2751
  else updateValue(updatedValue);
2272
2752
  else throw Error("Missing mapping in TableSelect");
@@ -2344,7 +2824,8 @@ function useUserSelector({ pageSize = DEFAULT_PAGE_SIZE$2 } = {}) {
2344
2824
  const offsetRef = useRef(0);
2345
2825
  const userCacheRef = useRef(/* @__PURE__ */ new Map());
2346
2826
  const fetchUsers = useCallback(async (searchStr, offset, append) => {
2347
- if (!client?.baseUrl) return;
2827
+ const apiBase = apiBaseOf(client);
2828
+ if (!apiBase) return;
2348
2829
  setIsLoading(true);
2349
2830
  try {
2350
2831
  const token = await getAuthToken?.();
@@ -2353,12 +2834,12 @@ function useUserSelector({ pageSize = DEFAULT_PAGE_SIZE$2 } = {}) {
2353
2834
  offset: String(offset)
2354
2835
  });
2355
2836
  if (searchStr) params.set("search", searchStr);
2356
- const response = await fetch(`${client.baseUrl}/api/users?${params}`, { headers: token ? { Authorization: `Bearer ${token}` } : {} });
2837
+ const response = await fetch(`${apiBase}/admin/users?${params}`, { headers: token ? { Authorization: `Bearer ${token}` } : {} });
2357
2838
  if (!response.ok) throw new Error("Failed to fetch users");
2358
2839
  const data = await response.json();
2359
2840
  const newItems = (data.data ?? data.users ?? []).map((r) => {
2360
2841
  const user = {
2361
- uid: r.id ?? "",
2842
+ uid: r.uid ?? r.id ?? "",
2362
2843
  email: r.email ?? "",
2363
2844
  displayName: r.displayName ?? r.display_name ?? null,
2364
2845
  photoURL: r.photoURL ?? r.photo_url ?? null,
@@ -2383,6 +2864,7 @@ function useUserSelector({ pageSize = DEFAULT_PAGE_SIZE$2 } = {}) {
2383
2864
  }
2384
2865
  }, [
2385
2866
  client?.baseUrl,
2867
+ client?.apiPath,
2386
2868
  getAuthToken,
2387
2869
  pageSize
2388
2870
  ]);
@@ -3811,12 +4293,13 @@ function getTableBindingForProperty(property, selected) {
3811
4293
  updateValue
3812
4294
  }) };
3813
4295
  else if (property.type === "date") return {
3814
- Component: ({ propertyKey, error, validationError, disabled, selected, property, internalValue, updateValue }) => {
4296
+ Component: ({ propertyKey, error, validationError, disabled, selected, size, property, internalValue, updateValue }) => {
3815
4297
  const { locale } = useCustomizationController();
3816
4298
  return /* @__PURE__ */ jsx(VirtualTableDateField, {
3817
4299
  name: propertyKey,
3818
4300
  error: validationError ?? error,
3819
4301
  disabled,
4302
+ small: getPreviewSizeFrom(size) !== "medium",
3820
4303
  mode: property.mode,
3821
4304
  focused: selected,
3822
4305
  internalValue,
@@ -4427,6 +4910,7 @@ var PropertyTableCell = React.memo(function PropertyTableCell({ propertyKey, cus
4427
4910
  propertyKey
4428
4911
  });
4429
4912
  };
4913
+ const tableBinding = useMemo(() => getTableBindingForProperty(property, selected), [property, selected]);
4430
4914
  let innerComponent;
4431
4915
  let allowScroll = false;
4432
4916
  let showExpandIcon = false;
@@ -4460,7 +4944,6 @@ var PropertyTableCell = React.memo(function PropertyTableCell({ propertyKey, cus
4460
4944
  })
4461
4945
  }, `${propertyKey}_${entity.path}_${entity.id}`);
4462
4946
  if (!customField && (!customPreview || selected)) {
4463
- const tableBinding = getTableBindingForProperty(property, selected);
4464
4947
  if (tableBinding) {
4465
4948
  const Component = tableBinding.Component;
4466
4949
  innerComponent = /* @__PURE__ */ jsx(Component, {
@@ -4828,7 +5311,7 @@ function propertiesToColumns({ properties, sortable, fixedFilter, engine, Additi
4828
5311
  align: getTableCellAlignment(property),
4829
5312
  icon: getIconForProperty(property, "small"),
4830
5313
  title: property.name ?? key,
4831
- sortable,
5314
+ sortable: sortable && sortableProperty(property),
4832
5315
  filter: !disabledFilter && filterable,
4833
5316
  width: getTablePropertyColumnWidth(property),
4834
5317
  resizable: true,
@@ -4844,6 +5327,49 @@ function propertiesToColumns({ properties, sortable, fixedFilter, engine, Additi
4844
5327
  };
4845
5328
  });
4846
5329
  }
5330
+ /**
5331
+ * The properties a sort can be applied to, as keys and titles.
5332
+ *
5333
+ * Only the table renders headers, so the list, card and split views had no way
5334
+ * to order anything — the controller has carried `sortBy` all along, with no
5335
+ * control bound to it outside the table. This is the same walk
5336
+ * `propertiesToColumns` makes, under the same keys and the same
5337
+ * `sortableProperty` authority, so a sort picked from the toolbar means exactly
5338
+ * what clicking the header would have meant, and the two cannot drift into
5339
+ * offering different columns.
5340
+ */
5341
+ function getSortablePropertyOptions(properties) {
5342
+ return Object.entries(properties).flatMap(([key, property]) => getColumnKeysForProperty(property, key)).flatMap(({ key }) => {
5343
+ const property = getResolvedPropertyInPath(properties, key);
5344
+ if (!property || !sortableProperty(property)) return [];
5345
+ return [{
5346
+ key,
5347
+ title: property.name ?? key,
5348
+ property
5349
+ }];
5350
+ });
5351
+ }
5352
+ /**
5353
+ * Whether this column can be ordered by.
5354
+ *
5355
+ * `sortable` arrived as one flag for the whole table while `filter` was
5356
+ * computed per property, so every column advertised a sort — including the ones
5357
+ * no `ORDER BY` can be written for. A to-many relation is a *set* per row
5358
+ * (`posts.tags`), and there is no single value to order on; the driver used to
5359
+ * answer by silently dropping the sort and returning rows in whatever order
5360
+ * Postgres pleased, which is how a header that did nothing went unnoticed.
5361
+ *
5362
+ * Now that the driver refuses instead, offering the control would turn a
5363
+ * useless header into a failed request. Same authority as the filter side: ask
5364
+ * the property, not the table.
5365
+ */
5366
+ function sortableProperty(property) {
5367
+ if (property.type !== "relation") return true;
5368
+ const relationProperty = property;
5369
+ const kind = relationProperty.relation?.kind ?? relationProperty.resolvedRelation?.kind;
5370
+ if (!kind) return true;
5371
+ return kind === "belongsTo";
5372
+ }
4847
5373
  function filterableProperty(property, partOfArray = false, engine) {
4848
5374
  if (!isFilterableRelation(property, engine)) return false;
4849
5375
  if (partOfArray) return [
@@ -4974,7 +5500,8 @@ function StringNumberFilterField({ name, value, setValue, type, isArray, enumVal
4974
5500
  disabled: isNullOperation,
4975
5501
  placeholder: isNullOperation ? "null" : void 0,
4976
5502
  onChange: (evt) => {
4977
- updateFilter(operation, type === "number" ? parseFloat(evt.target.value) : evt.target.value);
5503
+ const val = type === "number" ? parseFloat(evt.target.value) : evt.target.value;
5504
+ updateFilter(operation, val);
4978
5505
  },
4979
5506
  endAdornment: internalValue !== void 0 && internalValue != null && /* @__PURE__ */ jsx(IconButton, {
4980
5507
  onClick: (e) => updateFilter(operation, void 0),
@@ -5710,7 +6237,7 @@ var SelectableTable = function SelectableTable({ onValueChange, cellRenderer, on
5710
6237
  children: /* @__PURE__ */ jsx("div", {
5711
6238
  className: "h-full w-full flex flex-col bg-white dark:bg-surface-900",
5712
6239
  ref,
5713
- children: /* @__PURE__ */ jsx(TableView, {
6240
+ children: /* @__PURE__ */ jsx(VirtualTable, {
5714
6241
  data,
5715
6242
  columns,
5716
6243
  cellRenderer: (props) => cellRenderer(props),
@@ -5842,6 +6369,13 @@ var CollectionTableBinding = function CollectionTableBinding({ className, style,
5842
6369
  }), {}) : entity;
5843
6370
  const Builder = additionalField.Builder;
5844
6371
  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() });
5845
6379
  return /* @__PURE__ */ jsx(EntityTableCell, {
5846
6380
  width,
5847
6381
  size,
@@ -5858,13 +6392,7 @@ var CollectionTableBinding = function CollectionTableBinding({ className, style,
5858
6392
  isDragging,
5859
6393
  isDraggable,
5860
6394
  frozen,
5861
- children: /* @__PURE__ */ jsx(ErrorBoundary, { children: Builder ? /* @__PURE__ */ jsx(Builder, {
5862
- entity,
5863
- context
5864
- }) : /* @__PURE__ */ jsx(Fragment, { children: additionalField.value?.({
5865
- entity,
5866
- context
5867
- })?.toString() }) })
6395
+ children: /* @__PURE__ */ jsx(ErrorBoundary, { children: child })
5868
6396
  }, `additional_table_cell_${entity.id}_${column.key}`);
5869
6397
  }, [size]);
5870
6398
  const engine = useCollectionScope()?.engine;
@@ -6035,35 +6563,6 @@ function useSelectionController(onSelectionChange) {
6035
6563
  ]);
6036
6564
  }
6037
6565
  //#endregion
6038
- //#region src/util/resolutions.ts
6039
- /**
6040
- * Built-in entity views that are resolved by token name.
6041
- * These are always available without needing to be registered
6042
- * in the customization controller's entityViews array.
6043
- */
6044
- var BUILTIN_ENTITY_VIEWS = { "__rebase_history": {
6045
- key: "__rebase_history",
6046
- name: "History",
6047
- tabComponent: React.createElement(HistoryIcon, { size: 20 }),
6048
- position: "end"
6049
- } };
6050
- function resolveEntityView(entityView, contextEntityViews) {
6051
- if (typeof entityView === "string") return BUILTIN_ENTITY_VIEWS[entityView] ?? contextEntityViews?.find((entry) => entry.key === entityView);
6052
- else return entityView;
6053
- }
6054
- function resolveEntityAction(entityAction, contextEntityActions) {
6055
- if (typeof entityAction === "string") return contextEntityActions?.find((entry) => entry.key === entityAction);
6056
- else return entityAction;
6057
- }
6058
- function resolvedSelectedEntityView(customViews, customizationController, selectedTab, _canEdit) {
6059
- const resolvedEntityViews = customViews ? customViews.map((e) => resolveEntityView(e, customizationController.entityViews)).filter(Boolean).filter((e) => e.key !== "__rebase_history") : [];
6060
- return {
6061
- resolvedEntityViews,
6062
- selectedEntityView: resolvedEntityViews.find((e) => e.key === selectedTab),
6063
- selectedSecondaryForm: customViews && resolvedEntityViews.filter((e) => e.includeActions).find((e) => e.key === selectedTab)
6064
- };
6065
- }
6066
- //#endregion
6067
6566
  //#region src/components/CollectionViewBinding/hooks/useCollectionInlineEditor.ts
6068
6567
  function useCollectionInlineEditor({ path, collection, dataClient, context }) {
6069
6568
  const uniqueFieldValidator = useCallback(async ({ name, value, property, entityId }) => {
@@ -6233,6 +6732,35 @@ function mergeEntityActions(currentActions, newActions) {
6233
6732
  });
6234
6733
  return updatedActions;
6235
6734
  }
6735
+ //#endregion
6736
+ //#region src/util/resolutions.ts
6737
+ /**
6738
+ * Built-in entity views that are resolved by token name.
6739
+ * These are always available without needing to be registered
6740
+ * in the customization controller's entityViews array.
6741
+ */
6742
+ var BUILTIN_ENTITY_VIEWS = { "__rebase_history": {
6743
+ key: "__rebase_history",
6744
+ name: "History",
6745
+ tabComponent: React.createElement(HistoryIcon, { size: 20 }),
6746
+ position: "end"
6747
+ } };
6748
+ function resolveEntityView(entityView, contextEntityViews) {
6749
+ if (typeof entityView === "string") return BUILTIN_ENTITY_VIEWS[entityView] ?? contextEntityViews?.find((entry) => entry.key === entityView);
6750
+ else return entityView;
6751
+ }
6752
+ function resolveEntityAction(entityAction, contextEntityActions) {
6753
+ if (typeof entityAction === "string") return contextEntityActions?.find((entry) => entry.key === entityAction);
6754
+ else return entityAction;
6755
+ }
6756
+ function resolvedSelectedEntityView(customViews, customizationController, selectedTab, _canEdit) {
6757
+ const resolvedEntityViews = customViews ? customViews.map((e) => resolveEntityView(e, customizationController.entityViews)).filter(Boolean).filter((e) => e.key !== "__rebase_history") : [];
6758
+ return {
6759
+ resolvedEntityViews,
6760
+ selectedEntityView: resolvedEntityViews.find((e) => e.key === selectedTab),
6761
+ selectedSecondaryForm: customViews && resolvedEntityViews.filter((e) => e.includeActions).find((e) => e.key === selectedTab)
6762
+ };
6763
+ }
6236
6764
  var BreadcrumbContext = React.createContext({
6237
6765
  breadcrumbs: [],
6238
6766
  set: () => {},
@@ -6315,7 +6843,10 @@ function SearchIconsView({ selectedIcon = "", onIconSelected }) {
6315
6843
  const [query, setQuery] = React.useState("");
6316
6844
  const updateSearchResults = React.useMemo(() => debounce((value) => {
6317
6845
  if (!value || value === "") setKeys(null);
6318
- else setKeys(iconsSearch.search(value).slice(0, 50).map((e) => e.item.key));
6846
+ else {
6847
+ const limited = iconsSearch.search(value).slice(0, 50);
6848
+ setKeys(limited.map((e) => e.item.key));
6849
+ }
6319
6850
  }, UPDATE_SEARCH_INDEX_WAIT_MS), []);
6320
6851
  React.useEffect(() => {
6321
6852
  updateSearchResults(query);
@@ -6516,15 +7047,18 @@ function ArrayContainer({ droppableId, addLabel, value, disabled = false, buildE
6516
7047
  }, []);
6517
7048
  const sensors = useSensors(useSensor(PointerSensor, { activationConstraint: { distance: 5 } }), useSensor(KeyboardSensor, {}));
6518
7049
  useEffect(() => {
6519
- if (hasValue && value && value.length !== internalIds.length) setInternalIds(value.map((v, index) => {
6520
- const hashValue = (getHashValue(v) ?? String(index)) + index;
6521
- if (hashValue in internalIdsRef.current) return internalIdsRef.current[hashValue];
6522
- else {
6523
- const newInternalId = getRandomId$1();
6524
- internalIdsRef.current[hashValue] = newInternalId;
6525
- return newInternalId;
6526
- }
6527
- }));
7050
+ if (hasValue && value && value.length !== internalIds.length) {
7051
+ const newInternalIds = value.map((v, index) => {
7052
+ const hashValue = (getHashValue(v) ?? String(index)) + index;
7053
+ if (hashValue in internalIdsRef.current) return internalIdsRef.current[hashValue];
7054
+ else {
7055
+ const newInternalId = getRandomId$1();
7056
+ internalIdsRef.current[hashValue] = newInternalId;
7057
+ return newInternalId;
7058
+ }
7059
+ });
7060
+ setInternalIds(newInternalIds);
7061
+ }
6528
7062
  }, [
6529
7063
  hasValue,
6530
7064
  internalIds.length,
@@ -6585,7 +7119,8 @@ function ArrayContainer({ droppableId, addLabel, value, disabled = false, buildE
6585
7119
  const oldIndex = internalIds.indexOf(active.id);
6586
7120
  const newIndex = internalIds.indexOf(over.id);
6587
7121
  if (oldIndex === -1 || newIndex === -1) return;
6588
- setInternalIds(arrayMove$1(internalIds, oldIndex, newIndex));
7122
+ const newIds = arrayMove$1(internalIds, oldIndex, newIndex);
7123
+ setInternalIds(newIds);
6589
7124
  onValueChange(arrayMove$1(value ?? [], oldIndex, newIndex));
6590
7125
  };
6591
7126
  return sortable ? /* @__PURE__ */ jsx(DndContext, {
@@ -6921,6 +7456,7 @@ var useImportConfig = () => {
6921
7456
  //#endregion
6922
7457
  //#region src/data_import/utils/get_import_inference_type.ts
6923
7458
  function getInferenceType(value) {
7459
+ if (value === null || value === void 0) return "string";
6924
7460
  if (typeof value === "number") return "number";
6925
7461
  else if (typeof value === "string") return "string";
6926
7462
  else if (typeof value === "boolean") return "boolean";
@@ -7308,8 +7844,9 @@ var ConfigControllerProvider = React.memo(function ConfigControllerProvider({ ch
7308
7844
  editedCollectionId
7309
7845
  });
7310
7846
  const namespace = propertyKey && propertyKey.includes(".") ? propertyKey.substring(0, propertyKey.lastIndexOf(".")) : void 0;
7847
+ const propertyKeyWithoutNamespace = propertyKey && propertyKey.includes(".") ? propertyKey.substring(propertyKey.lastIndexOf(".") + 1) : propertyKey;
7311
7848
  setCurrentPropertyDialog({
7312
- propertyKey: propertyKey && propertyKey.includes(".") ? propertyKey.substring(propertyKey.lastIndexOf(".") + 1) : propertyKey,
7849
+ propertyKey: propertyKeyWithoutNamespace,
7313
7850
  property,
7314
7851
  namespace,
7315
7852
  currentPropertiesOrder,
@@ -7365,7 +7902,10 @@ var ConfigControllerProvider = React.memo(function ConfigControllerProvider({ ch
7365
7902
  onFetchTableMetadata,
7366
7903
  handleClose: (collection) => {
7367
7904
  if (currentDialog?.redirect) {
7368
- if (collection && currentDialog?.isNewCollection && !currentDialog.parentCollectionSlugs.length) navigate(urlController.buildUrlCollectionPath(collection.slug));
7905
+ if (collection && currentDialog?.isNewCollection && !currentDialog.parentCollectionSlugs.length) {
7906
+ const url = urlController.buildUrlCollectionPath(collection.slug);
7907
+ navigate(url);
7908
+ }
7369
7909
  }
7370
7910
  setCurrentDialog(void 0);
7371
7911
  }
@@ -7532,8 +8072,8 @@ function EditorCollectionAction({ path, parentCollectionSlugs, parentEntityIds,
7532
8072
  }
7533
8073
  //#endregion
7534
8074
  //#region src/components/CollectionViewBinding/CollectionViewActions.tsx
7535
- var ImportCollectionAction = lazy(() => import("./import-BjUG-cIY.js").then((n) => n.t).then((m) => ({ default: m.ImportCollectionAction })));
7536
- var ExportCollectionAction = lazy(() => import("./export-BzIpUdSK.js").then((n) => n.t).then((m) => ({ default: m.ExportCollectionAction })));
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 })));
7537
8077
  function CollectionViewActions({ collection, relativePath, parentCollectionSlugs, parentEntityIds, onNewClick, onAddExistingClick, onMultipleDeleteClick, selectionEnabled, path, selectionController, tableController, collectionEntitiesCount, compact, children, openNewDocument }) {
7538
8078
  const context = useAdminContext();
7539
8079
  const { canCreate, canDelete } = usePermissions();
@@ -7578,8 +8118,9 @@ function CollectionViewActions({ collection, relativePath, parentCollectionSlugs
7578
8118
  })
7579
8119
  }));
7580
8120
  const multipleDeleteEnabled = canDelete(collection, path, null);
8121
+ const hasSelection = Boolean(selectedEntities?.length);
7581
8122
  let multipleDeleteButton;
7582
- {
8123
+ if (hasSelection) {
7583
8124
  const button = largeLayout && !compact ? /* @__PURE__ */ jsxs(Button, {
7584
8125
  variant: "text",
7585
8126
  disabled: !selectedEntities?.length || !multipleDeleteEnabled,
@@ -7673,7 +8214,9 @@ function relationCardinality(relation) {
7673
8214
  * Pure function — call inside `useMemo`.
7674
8215
  */
7675
8216
  function resolveCollectionSlotKeys(collection, authController, propertyConfigs) {
7676
- const titleKeyCandidates = getTitlePropertyCandidates(collection);
8217
+ const rankedCandidates = getTitlePropertyCandidates(collection);
8218
+ const leadingRelationKey = getLeadingRelationTitleKey(collection);
8219
+ const titleKeyCandidates = leadingRelationKey ? [leadingRelationKey, ...rankedCandidates.filter((key) => key !== leadingRelationKey)] : rankedCandidates;
7677
8220
  const titleKey = titleKeyCandidates[0];
7678
8221
  const imageKey = getEntityImagePreviewPropertyKey(collection);
7679
8222
  let statusKey;
@@ -7721,9 +8264,9 @@ function resolveCollectionSlotKeys(collection, authController, propertyConfigs)
7721
8264
  const hasExplicitOrder = !!collection.propertiesOrder;
7722
8265
  const relationKeys = [];
7723
8266
  if (!hasExplicitOrder) {
7724
- for (const [key, prop] of Object.entries(collection.properties)) if (prop.type === "relation") relationKeys.push(key);
8267
+ for (const [key, prop] of Object.entries(collection.properties)) if (prop.type === "relation" && key !== titleKey) relationKeys.push(key);
7725
8268
  }
7726
- const excludeKeys = new Set([
8269
+ const excludeKeys = /* @__PURE__ */ new Set([
7727
8270
  titleKey,
7728
8271
  imageKey,
7729
8272
  statusKey,
@@ -7882,9 +8425,7 @@ function resolveEntitySlots(entity, collection, slotKeys) {
7882
8425
  }
7883
8426
  }
7884
8427
  if (items.length > 0) {
7885
- const relIndex = relationKeys.indexOf(relKey);
7886
- const chipColors = Object.values(CHIP_COLORS);
7887
- const colorScheme = chipColors[relIndex % chipColors.length];
8428
+ const colorScheme = CHIP_COLORS[CHIP_SEED_KEYS[relationKeys.indexOf(relKey) % CHIP_SEED_KEYS.length]];
7888
8429
  relations.push({
7889
8430
  property: prop,
7890
8431
  propertyKey: relKey,
@@ -8356,7 +8897,7 @@ function CollectionListViewBinding({ collection, tableController, onEntityClick,
8356
8897
  const showStatus = statusPropertyKey && size !== "xs" && size !== "s";
8357
8898
  const subtitleKey = previewKeys.length > 0 ? previewKeys[0] : void 0;
8358
8899
  const allKeys = resolvedCollection.propertiesOrder || Object.keys(resolvedCollection.properties);
8359
- const usedKeys = new Set([
8900
+ const usedKeys = /* @__PURE__ */ new Set([
8360
8901
  titlePropertyKey,
8361
8902
  imagePropertyKey,
8362
8903
  statusPropertyKey,
@@ -8948,44 +9489,228 @@ var PropertyCollectionView = ({ data, properties, baseKey = "", suppressHeader =
8948
9489
  };
8949
9490
  //#endregion
8950
9491
  //#region src/components/EntityViewBinding.tsx
9492
+ /**
9493
+ * The read-only rendering of a record's values.
9494
+ *
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.
9500
+ */
8951
9501
  function EntityViewBinding({ entity, collection, path, className }) {
8952
9502
  const customizationController = useCustomizationController();
8953
- const properties = collection.properties;
8954
- return /* @__PURE__ */ jsx("div", {
8955
- className: "w-full " + className,
8956
- children: /* @__PURE__ */ jsxs("div", {
8957
- className: "w-full mb-4 p-4",
8958
- children: [/* @__PURE__ */ jsxs("div", {
8959
- className: `grid grid-cols-12 gap-x-4 py-4 items-start border-b ${defaultBorderMixin}`,
8960
- children: [/* @__PURE__ */ jsx("div", {
8961
- className: "col-span-4 pr-2",
8962
- children: /* @__PURE__ */ jsx(Typography, {
8963
- variant: "caption",
8964
- color: "secondary",
8965
- component: "span",
8966
- className: "break-words",
8967
- children: "Id"
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, {})
8968
9523
  })
8969
- }), /* @__PURE__ */ jsx("div", {
8970
- className: "col-span-8",
8971
- children: /* @__PURE__ */ jsxs("div", {
8972
- className: "flex-grow text-surface-900 dark:text-white flex items-center",
8973
- children: [/* @__PURE__ */ jsx("span", {
8974
- className: "flex-grow mr-2",
8975
- children: entity.id
8976
- }), customizationController?.entityLinkBuilder && /* @__PURE__ */ jsx("a", {
8977
- href: customizationController.entityLinkBuilder({ entity }),
8978
- rel: "noopener noreferrer",
8979
- target: "_blank",
8980
- children: /* @__PURE__ */ jsx(IconButton, { children: /* @__PURE__ */ jsx(ExternalLinkIcon, {}) })
8981
- })]
9524
+ })
9525
+ })
9526
+ }), /* @__PURE__ */ jsx(PropertyCollectionView, {
9527
+ data: entity.values,
9528
+ properties,
9529
+ size: "medium"
9530
+ })]
9531
+ });
9532
+ }
9533
+ //#endregion
9534
+ //#region src/components/EntityIdentityBar.tsx
9535
+ /**
9536
+ * The persistent header of an entity view: who this record is, and what you can
9537
+ * do to it.
9538
+ *
9539
+ * The title and the id used to live inside the scrolling form, above 72px of
9540
+ * empty space — 219px of chrome that carried nothing and scrolled away the
9541
+ * moment you touched the wheel, leaving no indication of which record you were
9542
+ * editing. They live here instead, and the left half of a bar that was
9543
+ * previously empty now does the work.
9544
+ */
9545
+ function EntityIdentityBar({ collection, title, entityId, status, dirty, saving, onSave, onDiscard, saveDisabled, hasErrors, onSaveAndClose, onBack, onInspect, onViewHistory, recordActions, pluginActions, trailing, leading }) {
9546
+ const { t } = useTranslation();
9547
+ const saveLabel = status === "existing" ? t("save") : status === "copy" ? t("create_copy") : t("create");
9548
+ const closeLabel = status === "existing" ? t("save_and_close") : status === "copy" ? t("create_copy_and_close") : t("create_and_close");
9549
+ const hasMenu = Boolean(recordActions) || Boolean(onInspect) || Boolean(onViewHistory);
9550
+ return /* @__PURE__ */ jsxs("div", {
9551
+ className: cls("h-[52px] shrink-0 flex items-center gap-2 pl-1.5 pr-2 border-b", "bg-surface-50 dark:bg-surface-900", defaultBorderMixin),
9552
+ children: [
9553
+ leading,
9554
+ onBack && /* @__PURE__ */ jsx(Tooltip, {
9555
+ title: t("back"),
9556
+ children: /* @__PURE__ */ jsx(IconButton, {
9557
+ size: "small",
9558
+ onClick: onBack,
9559
+ "aria-label": t("back"),
9560
+ children: /* @__PURE__ */ jsx(ArrowLeftIcon, { size: iconSize.smallest })
9561
+ })
9562
+ }),
9563
+ /* @__PURE__ */ jsxs(Typography, {
9564
+ variant: "caption",
9565
+ className: "text-text-disabled dark:text-text-disabled-dark whitespace-nowrap hidden sm:block",
9566
+ children: [collection.name, "\xA0/"]
9567
+ }),
9568
+ /* @__PURE__ */ jsx("span", {
9569
+ className: "font-headers font-semibold text-[15px] tracking-tight truncate min-w-0",
9570
+ title,
9571
+ children: title
9572
+ }),
9573
+ entityId !== void 0 && /* @__PURE__ */ jsx(IdChip, { value: String(entityId) }),
9574
+ /* @__PURE__ */ jsx("div", { className: "flex-1" }),
9575
+ onSave && /* @__PURE__ */ jsx(SaveState, {
9576
+ dirty,
9577
+ saving,
9578
+ status,
9579
+ t
9580
+ }),
9581
+ pluginActions,
9582
+ onDiscard && dirty && !saving && /* @__PURE__ */ jsx(Button, {
9583
+ variant: "text",
9584
+ size: "small",
9585
+ onClick: onDiscard,
9586
+ children: status === "existing" ? t("discard") : t("clear")
9587
+ }),
9588
+ onSave && /* @__PURE__ */ jsx(Tooltip, {
9589
+ title: hasErrors ? t("fix_errors_before_saving") ?? "Fix highlighted errors before saving" : void 0,
9590
+ children: /* @__PURE__ */ jsx(LoadingButton, {
9591
+ variant: onSaveAndClose ? "text" : "filled",
9592
+ color: "primary",
9593
+ size: "small",
9594
+ loading: saving && !onSaveAndClose,
9595
+ disabled: saveDisabled,
9596
+ onClick: onSave,
9597
+ children: saveLabel
9598
+ })
9599
+ }),
9600
+ onSaveAndClose && /* @__PURE__ */ jsx(LoadingButton, {
9601
+ variant: "filled",
9602
+ color: "primary",
9603
+ size: "small",
9604
+ loading: saving,
9605
+ disabled: saveDisabled,
9606
+ onClick: onSaveAndClose,
9607
+ children: closeLabel
9608
+ }),
9609
+ hasMenu && /* @__PURE__ */ jsxs(Menu, {
9610
+ align: "end",
9611
+ trigger: /* @__PURE__ */ jsx(IconButton, {
9612
+ size: "small",
9613
+ "aria-label": t("more_actions") ?? "More actions",
9614
+ children: /* @__PURE__ */ jsx(MoreVerticalIcon, { size: iconSize.smallest })
9615
+ }),
9616
+ children: [
9617
+ recordActions,
9618
+ recordActions && (onInspect || onViewHistory) && /* @__PURE__ */ jsx(Separator, { orientation: "horizontal" }),
9619
+ onViewHistory && /* @__PURE__ */ jsxs(MenuItem, {
9620
+ onClick: onViewHistory,
9621
+ children: [/* @__PURE__ */ jsx(HistoryIcon, { size: iconSize.smallest }), t("record_history") ?? "History"]
9622
+ }),
9623
+ onInspect && /* @__PURE__ */ jsxs(MenuItem, {
9624
+ onClick: onInspect,
9625
+ children: [/* @__PURE__ */ jsx(CodeIcon, { size: iconSize.smallest }), t("inspect_record") ?? "Inspect record"]
8982
9626
  })
8983
- })]
8984
- }), /* @__PURE__ */ jsx(PropertyCollectionView, {
8985
- data: entity.values,
8986
- properties,
8987
- size: "medium"
8988
- })]
9627
+ ]
9628
+ }),
9629
+ trailing
9630
+ ]
9631
+ });
9632
+ }
9633
+ /**
9634
+ * The words the unlabelled floating circle never said.
9635
+ *
9636
+ * That indicator was a ✓ / pencil / spinner chip stuck to the top-right of the
9637
+ * scroll area in an `h-0 overflow-visible` container, so it drifted across
9638
+ * field values as you scrolled — and it duplicated what the Save button was
9639
+ * already able to tell you.
9640
+ */
9641
+ function SaveState({ dirty, saving, status, t }) {
9642
+ if (saving) return /* @__PURE__ */ jsx(StateText, { children: t("saving") });
9643
+ if (dirty) return /* @__PURE__ */ jsx(StateText, { children: t("unsaved_changes_title") });
9644
+ if (status === "existing") return /* @__PURE__ */ jsx(StateText, { children: t("entity_saved") ?? "Saved" });
9645
+ return null;
9646
+ }
9647
+ function StateText({ children }) {
9648
+ return /* @__PURE__ */ jsx("span", {
9649
+ className: "text-xs text-text-disabled dark:text-text-disabled-dark whitespace-nowrap hidden md:inline",
9650
+ children
9651
+ });
9652
+ }
9653
+ /**
9654
+ * The id, as a chip you can copy rather than a full-width alert holding a UUID.
9655
+ */
9656
+ function IdChip({ value }) {
9657
+ const { t } = useTranslation();
9658
+ const [copied, setCopied] = useState(false);
9659
+ const truncated = value.length > 14 ? `${value.slice(0, 8)}…${value.slice(-4)}` : value;
9660
+ const copy = () => {
9661
+ navigator.clipboard?.writeText(value).then(() => {
9662
+ setCopied(true);
9663
+ setTimeout(() => setCopied(false), 1600);
9664
+ }, () => void 0);
9665
+ };
9666
+ return /* @__PURE__ */ jsx(Tooltip, {
9667
+ title: copied ? t("copied") : value,
9668
+ children: /* @__PURE__ */ jsxs("button", {
9669
+ type: "button",
9670
+ onClick: copy,
9671
+ "aria-label": `${t("copy_id")} ${value}`,
9672
+ className: cls("hidden md:inline-flex items-center gap-1.5 shrink-0 px-2 py-0.5 rounded-md", "font-mono text-[11px] text-text-secondary dark:text-text-secondary-dark", "bg-surface-accent-200/50 dark:bg-white/[0.055]", "hover:bg-surface-accent-200/75 dark:hover:bg-white/[0.09] transition-colors"),
9673
+ children: [truncated, copied ? /* @__PURE__ */ jsx(CheckIcon, { size: iconSize.smallest }) : /* @__PURE__ */ jsx(CopyIcon, { size: iconSize.smallest })]
9674
+ })
9675
+ });
9676
+ }
9677
+ //#endregion
9678
+ //#region src/components/CollectionViewBinding/SplitViewContext.tsx
9679
+ var SplitViewContext = React.createContext(void 0);
9680
+ /**
9681
+ * The enclosing split view, or `undefined` when the component is not rendered
9682
+ * inside one (a side panel, a dialog, a full-screen route).
9683
+ */
9684
+ function useSplitView() {
9685
+ return useContext(SplitViewContext);
9686
+ }
9687
+ function SplitViewProvider({ value, children }) {
9688
+ return /* @__PURE__ */ jsx(SplitViewContext.Provider, {
9689
+ value,
9690
+ children
9691
+ });
9692
+ }
9693
+ //#endregion
9694
+ //#region src/components/CollectionViewBinding/SplitListCloseButton.tsx
9695
+ /**
9696
+ * Closes the list of a split view, leaving the open record on its own.
9697
+ *
9698
+ * 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.
9702
+ */
9703
+ function SplitListCloseButton() {
9704
+ const { t } = useTranslation();
9705
+ const splitView = useSplitView();
9706
+ if (!splitView?.detailOpen) return null;
9707
+ return /* @__PURE__ */ jsx(Tooltip, {
9708
+ title: t("hide_list"),
9709
+ children: /* @__PURE__ */ jsx(IconButton, {
9710
+ size: "small",
9711
+ onClick: splitView.openFullScreen,
9712
+ "aria-label": t("hide_list"),
9713
+ children: /* @__PURE__ */ jsx(XIcon, { size: iconSize.smallest })
8989
9714
  })
8990
9715
  });
8991
9716
  }
@@ -9018,15 +9743,90 @@ function JsonPreviewBinding({ values }) {
9018
9743
  });
9019
9744
  }
9020
9745
  //#endregion
9746
+ //#region src/components/EntityInspector.tsx
9747
+ var EntityHistoryView = lazy(() => import("./history-D7L_JlLO.js").then((m) => ({ default: m.EntityHistoryView })));
9748
+ /**
9749
+ * Raw values and revision history, as an inspector rather than as tabs.
9750
+ *
9751
+ * These were the first two entries in the entity tab strip — two icon-only tabs
9752
+ * with no label and no tooltip, sitting *before* the record you opened the page
9753
+ * to edit. They are developer tools, not places the record lives, so they belong
9754
+ * behind one affordance in the bar; a content editor should never meet `<>`.
9755
+ *
9756
+ * The panel sits below the identity bar and beside the record deliberately: you
9757
+ * keep seeing which record you are inspecting, and keep being able to edit it.
9758
+ */
9759
+ function EntityInspector({ tab, onTabChange, onClose, collection, entity, formContext, values, includeHistory, refreshToken }) {
9760
+ const { t } = useTranslation();
9761
+ const open = tab !== null;
9762
+ useEffect(() => {
9763
+ if (!open) return;
9764
+ const onKeyDown = (e) => {
9765
+ if (e.key === "Escape") {
9766
+ e.stopPropagation();
9767
+ onClose();
9768
+ }
9769
+ };
9770
+ window.addEventListener("keydown", onKeyDown);
9771
+ return () => window.removeEventListener("keydown", onKeyDown);
9772
+ }, [open, onClose]);
9773
+ if (!open) return null;
9774
+ return /* @__PURE__ */ jsxs("div", {
9775
+ className: cls("relative flex flex-col shrink-0 h-full w-full max-w-md border-l", "bg-white dark:bg-surface-800", defaultBorderMixin),
9776
+ role: "complementary",
9777
+ "aria-label": t("inspect_record") ?? "Inspect record",
9778
+ children: [/* @__PURE__ */ jsxs("div", {
9779
+ className: cls("h-11 shrink-0 flex items-center gap-2 pl-4 pr-1.5 border-b", defaultBorderMixin),
9780
+ children: [
9781
+ /* @__PURE__ */ jsx("span", {
9782
+ className: "text-sm font-semibold",
9783
+ children: t("inspect") ?? "Inspect"
9784
+ }),
9785
+ /* @__PURE__ */ jsx("div", { className: "flex-1" }),
9786
+ includeHistory ? /* @__PURE__ */ jsxs(Tabs, {
9787
+ value: tab ?? "json",
9788
+ className: "!w-fit",
9789
+ onValueChange: (v) => onTabChange(v),
9790
+ children: [/* @__PURE__ */ jsx(Tab, {
9791
+ value: "json",
9792
+ className: "text-xs",
9793
+ children: t("json")
9794
+ }), /* @__PURE__ */ jsx(Tab, {
9795
+ value: "history",
9796
+ className: "text-xs",
9797
+ children: t("record_history") ?? "History"
9798
+ })]
9799
+ }) : null,
9800
+ /* @__PURE__ */ jsx(IconButton, {
9801
+ size: "small",
9802
+ onClick: onClose,
9803
+ "aria-label": t("close_inspector") ?? "Close inspector",
9804
+ children: /* @__PURE__ */ jsx(XIcon, { size: iconSize.smallest })
9805
+ })
9806
+ ]
9807
+ }), /* @__PURE__ */ jsx("div", {
9808
+ className: "flex-1 min-h-0 overflow-auto",
9809
+ children: /* @__PURE__ */ jsxs(ErrorBoundary, { children: [tab === "json" && /* @__PURE__ */ jsx(JsonPreviewBinding, { values: values ?? entity?.values ?? {} }), tab === "history" && includeHistory && formContext && /* @__PURE__ */ jsx(Suspense, {
9810
+ fallback: /* @__PURE__ */ jsx(CircularProgressCenter, {}),
9811
+ children: /* @__PURE__ */ jsx(EntityHistoryView, {
9812
+ collection,
9813
+ entity,
9814
+ formContext,
9815
+ refreshToken,
9816
+ modifiedValues: values ?? entity?.values
9817
+ })
9818
+ })] })
9819
+ })]
9820
+ });
9821
+ }
9822
+ //#endregion
9021
9823
  //#region src/util/view_constants.ts
9022
9824
  /**
9023
9825
  * Shared tab value constants used by EditViewBinding and DetailViewBinding.
9024
9826
  */
9025
9827
  var MAIN_TAB_VALUE = "__main_##Q$SC^#S6";
9026
- var JSON_TAB_VALUE = "__json";
9027
9828
  //#endregion
9028
9829
  //#region src/components/DetailViewBinding.tsx
9029
- var EntityHistoryView$1 = lazy(() => import("./history-g9688IaK.js").then((m) => ({ default: m.EntityHistoryView })));
9030
9830
  function DetailViewBinding({ entityId, ...props }) {
9031
9831
  const { entity, dataLoading, dataLoadingError } = useFetch({
9032
9832
  path: props.path,
@@ -9050,7 +9850,7 @@ function DetailViewBinding({ entityId, ...props }) {
9050
9850
  children: content
9051
9851
  });
9052
9852
  }
9053
- function DetailViewBindingInner({ path, entityId, selectedTab: selectedTabProp, collection, parentCollectionSlugs, parentEntityIds, onTabChange, onEditClick, entity, dataLoading, layout = "full_screen", barActions }) {
9853
+ function DetailViewBindingInner({ path, entityId, selectedTab: selectedTabProp, collection, parentCollectionSlugs, parentEntityIds, onTabChange, onEditClick, entity, dataLoading, layout = "full_screen", barActions, barActionsStart }) {
9054
9854
  const ResolvedCollectionView = useComponentOverride("Collection.View", CollectionViewBinding);
9055
9855
  const { t } = useTranslation();
9056
9856
  const context = useRebaseContext();
@@ -9093,22 +9893,20 @@ function DetailViewBindingInner({ path, entityId, selectedTab: selectedTabProp,
9093
9893
  const customViewsCount = customViews?.length ?? 0;
9094
9894
  const includeJsonView = collection.includeJsonView === void 0 ? true : collection.includeJsonView;
9095
9895
  const includeHistoryView = Boolean(collection.history);
9096
- const hasAdditionalViews = customViewsCount > 0 || subcollectionsCount > 0 || includeJsonView || includeHistoryView;
9896
+ const hasAdditionalViews = customViewsCount > 0 || subcollectionsCount > 0;
9897
+ const [inspectorTab, setInspectorTab] = useState(null);
9898
+ const legacyInspectorTab = selectedTab === "__json" ? "json" : selectedTab === "__rebase_history" ? "history" : void 0;
9899
+ useEffect(() => {
9900
+ if (legacyInspectorTab) setInspectorTab(legacyInspectorTab);
9901
+ }, [legacyInspectorTab]);
9097
9902
  const { resolvedEntityViews } = resolvedSelectedEntityView(customViews, customizationController, void 0, canEdit);
9098
9903
  const activeTab = useMemo(() => {
9099
- return new Set([
9904
+ return /* @__PURE__ */ new Set([
9100
9905
  "__main_##Q$SC^#S6",
9101
- ...includeJsonView ? ["__json"] : [],
9102
- ...includeHistoryView ? ["__rebase_history"] : [],
9103
9906
  ...resolvedEntityViews.map((v) => v.key),
9104
9907
  ...subcollections.map((s) => s.slug)
9105
9908
  ]);
9106
- }, [
9107
- includeJsonView,
9108
- includeHistoryView,
9109
- resolvedEntityViews,
9110
- subcollections
9111
- ]).has(selectedTab) ? selectedTab : MAIN_TAB_VALUE;
9909
+ }, [resolvedEntityViews, subcollections]).has(selectedTab) ? selectedTab : MAIN_TAB_VALUE;
9112
9910
  const { selectedEntityView } = resolvedSelectedEntityView(customViews, customizationController, activeTab, canEdit);
9113
9911
  const mainViewVisible = activeTab === MAIN_TAB_VALUE;
9114
9912
  const mountedTabsRef = useRef(/* @__PURE__ */ new Set());
@@ -9198,25 +9996,6 @@ function DetailViewBindingInner({ path, entityId, selectedTab: selectedTabProp,
9198
9996
  }, `custom_view_${customView.key}`);
9199
9997
  }).filter(Boolean);
9200
9998
  const globalLoading = dataLoading && !usedEntity;
9201
- const jsonTabMounted = mountedTabsRef.current.has(JSON_TAB_VALUE);
9202
- const jsonView = activeTab === "__json" || jsonTabMounted ? /* @__PURE__ */ jsx("div", {
9203
- className: cls("relative flex-1 h-full overflow-auto w-full", { "hidden": activeTab !== JSON_TAB_VALUE }),
9204
- role: "tabpanel",
9205
- children: /* @__PURE__ */ jsx(ErrorBoundary, { children: /* @__PURE__ */ jsx(JsonPreviewBinding, { values: usedEntity?.values ?? {} }) })
9206
- }, "json_view") : null;
9207
- const historyView = includeHistoryView && activeTab === "__rebase_history" ? /* @__PURE__ */ jsx("div", {
9208
- className: "relative flex-1 h-full overflow-auto w-full",
9209
- role: "tabpanel",
9210
- children: /* @__PURE__ */ jsx(ErrorBoundary, { children: /* @__PURE__ */ jsx(Suspense, {
9211
- fallback: /* @__PURE__ */ jsx(CircularProgressCenter, {}),
9212
- children: /* @__PURE__ */ jsx(EntityHistoryView$1, {
9213
- collection,
9214
- entity: usedEntity,
9215
- formContext: readOnlyFormContext,
9216
- modifiedValues: usedEntity?.values
9217
- })
9218
- }) })
9219
- }, "history_view") : null;
9220
9999
  const subCollectionsViews = childViews && childViews.map(({ collection: subcollection }) => {
9221
10000
  const subcollectionId = subcollection.slug;
9222
10001
  const newFullPath = usedEntity ? `${path}/${usedEntity?.id}/${removeInitialAndTrailingSlashes(getCollectionDataPath(subcollection))}` : void 0;
@@ -9315,13 +10094,13 @@ function DetailViewBindingInner({ path, entityId, selectedTab: selectedTabProp,
9315
10094
  children: /* @__PURE__ */ jsx(ArrowLeftIcon, { size: iconSize.smallest })
9316
10095
  })
9317
10096
  }) : null;
9318
- const shouldShowTopBar = Boolean(barActions) || hasAdditionalViews || layout === "side_panel" || layout === "dialog" || Boolean(backToCollectionButton);
9319
- const fullScreenButton = !barActions && (layout === "side_panel" || layout === "split" || layout === "dialog") && entityId ? /* @__PURE__ */ jsx(Tooltip, {
10097
+ const fullScreenButton = !barActions && (layout === "side_panel" || layout === "dialog") && entityId ? /* @__PURE__ */ jsx(Tooltip, {
9320
10098
  title: "Open full screen",
9321
10099
  children: /* @__PURE__ */ jsx(IconButton, {
9322
10100
  size: "small",
9323
10101
  onClick: () => {
9324
- navigate(`${urlController.buildUrlCollectionPath(`${path}/${entityId}`)}#full`);
10102
+ const entityUrl = urlController.buildUrlCollectionPath(`${path}/${entityId}`);
10103
+ navigate(`${entityUrl}#full`);
9325
10104
  },
9326
10105
  children: /* @__PURE__ */ jsx(Maximize2Icon, { size: iconSize.smallest })
9327
10106
  })
@@ -9336,59 +10115,27 @@ function DetailViewBindingInner({ path, entityId, selectedTab: selectedTabProp,
9336
10115
  }) : null;
9337
10116
  const mainView = /* @__PURE__ */ jsx("div", {
9338
10117
  className: cls("flex-1 flex flex-row w-full overflow-y-auto justify-center", !mainViewVisible ? "hidden" : ""),
9339
- children: /* @__PURE__ */ jsxs("div", {
9340
- className: cls("relative flex flex-row max-w-4xl lg:max-w-3xl xl:max-w-4xl 2xl:max-w-6xl w-full h-fit"),
9341
- children: [/* @__PURE__ */ jsxs("div", {
9342
- className: cls("flex flex-col w-full", layout === "dialog" ? "pt-4 pb-12 px-6 sm:px-8" : "pt-12 pb-16 px-4 sm:px-8 md:px-10"),
9343
- children: [
9344
- /* @__PURE__ */ jsxs("div", {
9345
- className: "w-full flex flex-col items-start my-4 lg:my-6",
9346
- children: [/* @__PURE__ */ jsxs("div", {
9347
- className: "flex items-center justify-between w-full",
9348
- children: [/* @__PURE__ */ jsx(Typography, {
9349
- className: cls("my-4 grow line-clamp-1", collection.hideIdFromForm ? "mb-6" : ""),
9350
- variant: "h4",
9351
- children: title
9352
- }), editButton]
9353
- }), /* @__PURE__ */ jsx(Alert, {
9354
- color: "base",
9355
- outerClassName: "w-full",
9356
- size: "small",
9357
- children: /* @__PURE__ */ jsxs("code", {
9358
- className: "text-xs select-all text-text-secondary dark:text-text-secondary-dark",
9359
- children: [
9360
- usedEntity?.path ?? path,
9361
- "/",
9362
- entityId
9363
- ]
9364
- })
9365
- })]
9366
- }),
9367
- /* @__PURE__ */ jsx("div", {
9368
- className: "mt-12 flex flex-col gap-8",
9369
- children: propertyDetailView()
9370
- }),
9371
- /* @__PURE__ */ jsx("div", { className: "h-16" })
9372
- ]
9373
- }), canEdit && onEditClick && layout === "full_screen" && /* @__PURE__ */ jsx("div", {
9374
- className: cls("overflow-auto h-full hidden @6xl:flex flex-col gap-2 w-80 2xl:w-96 px-4 py-16 sticky top-0 border-l", defaultBorderMixin),
9375
- children: /* @__PURE__ */ jsx(Button, {
9376
- fullWidth: true,
9377
- variant: "filled",
9378
- color: "primary",
9379
- startIcon: /* @__PURE__ */ jsx(PencilIcon, { size: iconSize.small }),
9380
- onClick: onEditClick,
9381
- children: t("edit_entity")
9382
- })
9383
- })]
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()
9384
10121
  })
9385
10122
  });
9386
10123
  let result = /* @__PURE__ */ jsxs("div", {
9387
10124
  className: "relative flex flex-col h-full w-full bg-white dark:bg-surface-800",
9388
10125
  children: [
9389
- shouldShowTopBar && /* @__PURE__ */ jsxs("div", {
9390
- className: cls("h-[52px] items-center flex overflow-hidden w-full border-b pl-2 pr-2 flex bg-surface-50 dark:bg-surface-900", defaultBorderMixin),
9391
- children: [
10126
+ /* @__PURE__ */ jsx(EntityIdentityBar, {
10127
+ collection,
10128
+ title,
10129
+ entityId,
10130
+ status: "existing",
10131
+ dirty: false,
10132
+ saving: false,
10133
+ onInspect: includeJsonView ? () => setInspectorTab("json") : void 0,
10134
+ onViewHistory: includeHistoryView ? () => setInspectorTab("history") : void 0,
10135
+ leading: /* @__PURE__ */ jsxs(Fragment, { children: [barActionsStart, layout === "split" && /* @__PURE__ */ jsx(SplitListCloseButton, {})] }),
10136
+ trailing: /* @__PURE__ */ jsxs(Fragment, { children: [
10137
+ editButton,
10138
+ pluginActionsTop,
9392
10139
  backToCollectionButton,
9393
10140
  fullScreenButton,
9394
10141
  barActions?.({
@@ -9396,51 +10143,53 @@ function DetailViewBindingInner({ path, entityId, selectedTab: selectedTabProp,
9396
10143
  entityId,
9397
10144
  values: usedEntity?.values ?? {},
9398
10145
  status: "existing"
9399
- }),
9400
- pluginActionsTop,
9401
- hasAdditionalViews && /* @__PURE__ */ jsx("div", {
9402
- className: "flex-1 flex justify-end min-w-0 shrink-0",
9403
- children: /* @__PURE__ */ jsxs(Tabs, {
9404
- className: "!w-fit max-w-full",
9405
- value: activeTab,
9406
- onValueChange: (value) => {
9407
- onSideTabClick(value);
9408
- },
9409
- children: [
9410
- includeJsonView && /* @__PURE__ */ jsx(Tab, {
9411
- disabled: !hasAdditionalViews,
9412
- value: "__json",
9413
- className: "text-sm",
9414
- children: /* @__PURE__ */ jsx(CodeIcon, { size: iconSize.smallest })
9415
- }),
9416
- includeHistoryView && /* @__PURE__ */ jsx(Tab, {
9417
- disabled: !hasAdditionalViews,
9418
- value: "__rebase_history",
9419
- className: "text-sm",
9420
- children: /* @__PURE__ */ jsx(HistoryIcon, { size: iconSize.smallest })
9421
- }),
9422
- /* @__PURE__ */ jsx(Tab, {
9423
- disabled: !hasAdditionalViews,
9424
- value: "__main_##Q$SC^#S6",
9425
- className: "text-sm min-w-[90px]",
9426
- children: /* @__PURE__ */ jsxs("span", {
9427
- className: "flex items-center gap-1.5",
9428
- children: [getIcon(collection.icon, void 0, void 0, "smallest"), collection.singularName ?? collection.name]
9429
- })
9430
- }),
9431
- customViewTabsStart,
9432
- customViewTabsEnd,
9433
- subcollectionTabs
9434
- ]
9435
- })
9436
10146
  })
9437
- ]
10147
+ ] })
10148
+ }),
10149
+ hasAdditionalViews && /* @__PURE__ */ jsx("div", {
10150
+ className: cls("h-10 shrink-0 flex items-stretch border-b px-2 min-w-0", defaultBorderMixin),
10151
+ children: /* @__PURE__ */ jsxs(Tabs, {
10152
+ className: "!w-full",
10153
+ innerClassName: "h-full",
10154
+ variant: "boxy",
10155
+ value: activeTab,
10156
+ onValueChange: (value) => {
10157
+ onSideTabClick(value);
10158
+ },
10159
+ children: [
10160
+ /* @__PURE__ */ jsx(Tab, {
10161
+ value: "__main_##Q$SC^#S6",
10162
+ children: /* @__PURE__ */ jsxs("span", {
10163
+ className: "flex items-center gap-1.5",
10164
+ children: [getIcon(collection.icon, void 0, void 0, "smallest"), collection.singularName ?? collection.name]
10165
+ })
10166
+ }),
10167
+ customViewTabsStart,
10168
+ customViewTabsEnd,
10169
+ subcollectionTabs
10170
+ ]
10171
+ })
9438
10172
  }),
9439
- globalLoading ? /* @__PURE__ */ jsx(DetailViewSkeleton, { collection }) : mainView,
9440
- jsonView,
9441
- historyView,
9442
- customViewsView,
9443
- subCollectionsViews
10173
+ /* @__PURE__ */ jsxs("div", {
10174
+ className: "flex-1 min-h-0 flex",
10175
+ children: [/* @__PURE__ */ jsxs("div", {
10176
+ className: "flex-1 min-w-0 flex flex-col",
10177
+ children: [
10178
+ globalLoading ? /* @__PURE__ */ jsx(DetailViewSkeleton, { collection }) : mainView,
10179
+ customViewsView,
10180
+ subCollectionsViews
10181
+ ]
10182
+ }), /* @__PURE__ */ jsx(EntityInspector, {
10183
+ tab: inspectorTab,
10184
+ onTabChange: setInspectorTab,
10185
+ onClose: () => setInspectorTab(null),
10186
+ collection,
10187
+ entity: usedEntity,
10188
+ formContext: readOnlyFormContext,
10189
+ values: usedEntity?.values,
10190
+ includeHistory: includeHistoryView
10191
+ })]
10192
+ })
9444
10193
  ]
9445
10194
  });
9446
10195
  if (plugins && plugins.length > 0) result = /* @__PURE__ */ jsx(PluginProviderStack, {
@@ -9753,27 +10502,43 @@ function SplitListView({ collection, tableController, onEntityClick: externalOnE
9753
10502
  }
9754
10503
  }, String(renderedEntityId)) })
9755
10504
  }) : /* @__PURE__ */ jsx(Fragment, {});
9756
- return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(ResizablePanels, {
9757
- stacked: !largeLayout,
9758
- firstPanel: listPanel,
9759
- secondPanel: detailPanel,
9760
- showSecondPanel: isDetailVisible,
9761
- panelSizePercent: animationPhase === "entering" ? 100 : panelSize,
9762
- onPanelSizeChange: setPanelSize,
9763
- minPanelSizePx: 240
9764
- }), /* @__PURE__ */ jsx(UnsavedChangesDialog, {
9765
- open: blocker?.state === "blocked",
9766
- handleOk: () => {
9767
- dirty.current = false;
9768
- blocker?.proceed?.();
9769
- },
9770
- handleCancel: () => blocker?.reset?.(),
9771
- body: /* @__PURE__ */ jsxs(Fragment, { children: [
9772
- "You have unsaved changes in this ",
9773
- /* @__PURE__ */ jsx("b", { children: collection.singularName ?? collection.name }),
9774
- "."
9775
- ] })
9776
- })] });
10505
+ const openFullScreen = useCallback(() => {
10506
+ if (selectedEntityId === void 0) return;
10507
+ navigate(`${buildUrl(`${path}/${selectedEntityId}${isEditMode ? "/edit" : ""}`)}#full`);
10508
+ }, [
10509
+ navigate,
10510
+ buildUrl,
10511
+ path,
10512
+ selectedEntityId,
10513
+ isEditMode
10514
+ ]);
10515
+ return /* @__PURE__ */ jsxs(SplitViewProvider, {
10516
+ value: useMemo(() => ({
10517
+ detailOpen: selectedEntityId !== void 0,
10518
+ openFullScreen
10519
+ }), [selectedEntityId, openFullScreen]),
10520
+ children: [/* @__PURE__ */ jsx(ResizablePanels, {
10521
+ stacked: !largeLayout,
10522
+ firstPanel: listPanel,
10523
+ secondPanel: detailPanel,
10524
+ showSecondPanel: isDetailVisible,
10525
+ panelSizePercent: animationPhase === "entering" ? 100 : panelSize,
10526
+ onPanelSizeChange: setPanelSize,
10527
+ minPanelSizePx: 240
10528
+ }), /* @__PURE__ */ jsx(UnsavedChangesDialog, {
10529
+ open: blocker?.state === "blocked",
10530
+ handleOk: () => {
10531
+ dirty.current = false;
10532
+ blocker?.proceed?.();
10533
+ },
10534
+ handleCancel: () => blocker?.reset?.(),
10535
+ body: /* @__PURE__ */ jsxs(Fragment, { children: [
10536
+ "You have unsaved changes in this ",
10537
+ /* @__PURE__ */ jsx("b", { children: collection.singularName ?? collection.name }),
10538
+ "."
10539
+ ] })
10540
+ })]
10541
+ });
9777
10542
  }
9778
10543
  //#endregion
9779
10544
  //#region src/components/CollectionViewBinding/BoardCardBinding.tsx
@@ -9927,6 +10692,12 @@ var BoardCardBinding = memo(BoardCardBindingInner);
9927
10692
  //#region src/components/CollectionViewBinding/useBoardDataController.tsx
9928
10693
  var DEFAULT_PAGE_SIZE$1 = 20;
9929
10694
  /**
10695
+ * How long a column waits for its subscription's first payload before reading
10696
+ * once over HTTP instead. Long enough that a healthy socket always wins the
10697
+ * race, short enough that a dead one does not hold a blank board.
10698
+ */
10699
+ var LIVE_FIRST_PAINT_TIMEOUT_MS = 2500;
10700
+ /**
9930
10701
  * Shallow equality for entity value records.
9931
10702
  * Handles primitives and reference equality for nested values.
9932
10703
  * Avoids JSON.stringify allocation on every comparison.
@@ -10152,36 +10923,66 @@ function useBoardDataController({ fullPath, collection, columnProperty, columns,
10152
10923
  };
10153
10924
  });
10154
10925
  };
10926
+ const accessor = currentDataClient.collection(currentResolvedPath);
10927
+ const includeParams = getRelationIncludeParams(currentCollection);
10928
+ const fetchOnce = () => accessor.find({
10929
+ where: whereFilter,
10930
+ limit: itemCount,
10931
+ orderBy: orderByParam,
10932
+ include: includeParams
10933
+ });
10155
10934
  const onError = (error) => {
10156
10935
  if (isCleaningUpRef.current) return;
10157
10936
  console.error(`Error loading column ${column}:`, error);
10937
+ fetchOnce().then((res) => onUpdate(res.data)).catch(() => {
10938
+ if (isCleaningUpRef.current) return;
10939
+ setColumnData((prev) => ({
10940
+ ...prev,
10941
+ [column]: {
10942
+ ...prev[column],
10943
+ entities: prev[column]?.entities ?? [],
10944
+ loading: false,
10945
+ hasMore: false,
10946
+ error
10947
+ }
10948
+ }));
10949
+ });
10950
+ };
10951
+ if (accessor.listen) {
10952
+ let liveDataReceived = false;
10953
+ const unsubscribe = accessor.listen({
10954
+ where: whereFilter,
10955
+ limit: itemCount,
10956
+ orderBy: orderByParam,
10957
+ include: includeParams
10958
+ }, (res) => {
10959
+ liveDataReceived = true;
10960
+ onUpdate(res.data);
10961
+ }, onError);
10962
+ const firstPaintFallback = setTimeout(() => {
10963
+ if (liveDataReceived || isCleaningUpRef.current) return;
10964
+ fetchOnce().then((res) => {
10965
+ if (liveDataReceived || isCleaningUpRef.current) return;
10966
+ onUpdate(res.data);
10967
+ }).catch(() => void 0);
10968
+ }, LIVE_FIRST_PAINT_TIMEOUT_MS);
10969
+ unsubscribersRef.current[column] = () => {
10970
+ clearTimeout(firstPaintFallback);
10971
+ unsubscribe?.();
10972
+ };
10973
+ } else fetchOnce().then((res) => onUpdate(res.data)).catch((error) => {
10974
+ if (isCleaningUpRef.current) return;
10158
10975
  setColumnData((prev) => ({
10159
10976
  ...prev,
10160
10977
  [column]: {
10161
10978
  ...prev[column],
10162
- entities: [],
10979
+ entities: prev[column]?.entities ?? [],
10163
10980
  loading: false,
10164
10981
  hasMore: false,
10165
10982
  error
10166
10983
  }
10167
10984
  }));
10168
- };
10169
- const accessor = currentDataClient.collection(currentResolvedPath);
10170
- const includeParams = getRelationIncludeParams(currentCollection);
10171
- if (accessor.listen) {
10172
- const unsubscribe = accessor.listen({
10173
- where: whereFilter,
10174
- limit: itemCount,
10175
- orderBy: orderByParam,
10176
- include: includeParams
10177
- }, (res) => onUpdate(res.data), onError);
10178
- unsubscribersRef.current[column] = unsubscribe;
10179
- } else accessor.find({
10180
- where: whereFilter,
10181
- limit: itemCount,
10182
- orderBy: orderByParam,
10183
- include: includeParams
10184
- }).then((res) => onUpdate(res.data)).catch(onError);
10985
+ });
10185
10986
  }, []);
10186
10987
  useEffect(() => {
10187
10988
  isCleaningUpRef.current = false;
@@ -10203,7 +11004,8 @@ function useBoardDataController({ fullPath, collection, columnProperty, columns,
10203
11004
  const timeoutId = setTimeout(() => {
10204
11005
  if (isCleaningUpRef.current) return;
10205
11006
  currentColumns.forEach((column) => {
10206
- subscribeToColumn(column, currentColumnItemCounts[column] ?? pageSize);
11007
+ const itemCount = currentColumnItemCounts[column] ?? pageSize;
11008
+ subscribeToColumn(column, itemCount);
10207
11009
  if (isColumnExcludedByFilter(column, currentFilterValues, currentColumnProperty)) return;
10208
11010
  const accessor = currentDataClient.collection(currentResolvedPath);
10209
11011
  if (accessor.count) {
@@ -10395,6 +11197,40 @@ function useBoardDataController({ fullPath, collection, columnProperty, columns,
10395
11197
  }
10396
11198
  //#endregion
10397
11199
  //#region src/components/CollectionViewBinding/hooks/useKanbanDragAndDrop.ts
11200
+ /**
11201
+ * The digits a board order key is built from: base36, lower case only.
11202
+ *
11203
+ * The library's default alphabet is base62, and its keys only sort correctly
11204
+ * under byte ordering. The column is sorted by the *database*, and Postgres'
11205
+ * default collation is not byte ordering — under `en_US.UTF-8`, `"aa"` sorts
11206
+ * before `"aC"`, so as soon as a board had been dragged around enough to reach
11207
+ * the upper-case digits its order stopped matching the keys. One case has no
11208
+ * such ambiguity: digits before letters is true of every collation in use.
11209
+ *
11210
+ * A key written with the old alphabet no longer validates, which is what makes
11211
+ * the board offer to initialise the column — one click, and the order is
11212
+ * rewritten in a form the database can sort.
11213
+ */
11214
+ var ORDER_KEY_DIGITS = "0123456789abcdefghijklmnopqrstuvwxyz";
11215
+ /**
11216
+ * Whether a stored order value is a key `fractional-indexing` can interpolate
11217
+ * against.
11218
+ *
11219
+ * Anything else — a plain `"12"` from a seed or a migration, an empty string —
11220
+ * makes `generateKeyBetween` throw, and the fallback below then hands out the
11221
+ * same first key to every card, so each drag lands at the bottom of the column
11222
+ * on top of the last one that did. Asking the library itself is the only
11223
+ * definition of "valid" worth trusting.
11224
+ */
11225
+ function isValidOrderKey(value) {
11226
+ if (typeof value !== "string" || value.length === 0) return false;
11227
+ try {
11228
+ generateKeyBetween(value, null, ORDER_KEY_DIGITS);
11229
+ return true;
11230
+ } catch {
11231
+ return false;
11232
+ }
11233
+ }
10398
11234
  function useKanbanDragAndDrop({ collection, fullPath, columnProperty, orderProperty, dataClient, context, boardDataController, analyticsController }) {
10399
11235
  const handleItemsReorder = useCallback(async (items, moveInfo) => {
10400
11236
  const entity = items.find((item) => item.id === moveInfo?.itemId)?.data;
@@ -10411,17 +11247,21 @@ function useKanbanDragAndDrop({ collection, fullPath, columnProperty, orderPrope
10411
11247
  if (orderProperty) {
10412
11248
  const targetColumnItems = items;
10413
11249
  const movedIndex = targetColumnItems.findIndex((item) => item.id === moveInfo.itemId);
10414
- const prevKey = movedIndex > 0 ? targetColumnItems[movedIndex - 1].data.values?.[orderProperty] ?? null : null;
10415
- const nextKey = movedIndex < targetColumnItems.length - 1 ? targetColumnItems[movedIndex + 1].data.values?.[orderProperty] ?? null : null;
11250
+ const keyAt = (index) => {
11251
+ const value = targetColumnItems[index]?.data.values?.[orderProperty];
11252
+ return isValidOrderKey(value) ? value : null;
11253
+ };
11254
+ const prevKey = movedIndex > 0 ? keyAt(movedIndex - 1) : null;
11255
+ const nextKey = movedIndex < targetColumnItems.length - 1 ? keyAt(movedIndex + 1) : null;
10416
11256
  try {
10417
11257
  const a = prevKey;
10418
11258
  let b = nextKey;
10419
11259
  if (a !== null && b !== null && a >= b) b = null;
10420
- const newKey = generateKeyBetween(a, b);
11260
+ const newKey = generateKeyBetween(a, b, ORDER_KEY_DIGITS);
10421
11261
  updatedValues = setIn(updatedValues, orderProperty, newKey);
10422
11262
  } catch (e) {
10423
11263
  console.warn("fractional-indexing error, falling back:", e);
10424
- const newKey = generateKeyBetween(null, null);
11264
+ const newKey = generateKeyBetween(null, null, ORDER_KEY_DIGITS);
10425
11265
  updatedValues = setIn(updatedValues, orderProperty, newKey);
10426
11266
  }
10427
11267
  }
@@ -10628,10 +11468,7 @@ function CollectionBoardViewBinding({ collection, tableController, fullPath, par
10628
11468
  const itemsNeedBackfill = useMemo(() => {
10629
11469
  if (!orderProperty || dataLoading) return false;
10630
11470
  if (missingOrderCount > 0) return true;
10631
- return allEntities.some((entity) => {
10632
- const orderValue = entity.values?.[orderProperty];
10633
- return orderValue === void 0 || orderValue === null;
10634
- });
11471
+ return allEntities.some((entity) => !isValidOrderKey(entity.values?.[orderProperty]));
10635
11472
  }, [
10636
11473
  allEntities,
10637
11474
  orderProperty,
@@ -10662,7 +11499,8 @@ function CollectionBoardViewBinding({ collection, tableController, fullPath, par
10662
11499
  loading: colData?.loading ?? true,
10663
11500
  hasMore: colData?.hasMore ?? false,
10664
11501
  itemCount: colData?.entities?.length ?? 0,
10665
- totalCount: colData?.totalCount
11502
+ totalCount: colData?.totalCount,
11503
+ error: colData?.error
10666
11504
  };
10667
11505
  });
10668
11506
  return state;
@@ -10693,11 +11531,8 @@ function CollectionBoardViewBinding({ collection, tableController, fullPath, par
10693
11531
  analyticsController.onAnalyticsEvent?.("kanban_backfill_order", { path: fullPath });
10694
11532
  setBackfillLoading(true);
10695
11533
  try {
10696
- const entitiesToUpdate = (await dataClient.collection(fullPath).find({ limit: 1e4 })).data.filter((entity) => {
10697
- const orderValue = entity.values?.[orderProperty];
10698
- return orderValue === void 0 || orderValue === null;
10699
- });
10700
- const keys = generateNKeysBetween(null, null, entitiesToUpdate.length);
11534
+ const entitiesToUpdate = (await dataClient.collection(fullPath).find({ limit: 1e4 })).data.filter((entity) => !isValidOrderKey(entity.values?.[orderProperty]));
11535
+ const keys = generateNKeysBetween(null, null, entitiesToUpdate.length, ORDER_KEY_DIGITS);
10701
11536
  const updates = [];
10702
11537
  entitiesToUpdate.forEach((entity, index) => {
10703
11538
  const updatedValues = setIn({ ...entity.values }, orderProperty, keys[index]);
@@ -10865,7 +11700,7 @@ function CollectionBoardViewBinding({ collection, tableController, fullPath, par
10865
11700
  })]
10866
11701
  }),
10867
11702
  /* @__PURE__ */ jsx("div", {
10868
- className: "flex-1 overflow-auto no-scrollbar",
11703
+ className: "flex-1 min-h-0 overflow-hidden",
10869
11704
  children: /* @__PURE__ */ jsx(KanbanView, {
10870
11705
  data: boardItems,
10871
11706
  columns,
@@ -11472,9 +12307,9 @@ function ResetPasswordActionDialog({ user, open, onClose }) {
11472
12307
  setLoading(true);
11473
12308
  setError(null);
11474
12309
  try {
11475
- const baseUrl = client?.baseUrl || "";
12310
+ const apiBase = apiBaseOf(client) ?? "";
11476
12311
  const token = await getAuthToken?.();
11477
- const response = await fetch(`${baseUrl}/api/admin/users/${user.uid}/reset-password`, {
12312
+ const response = await fetch(`${apiBase}/admin/users/${user.uid}/reset-password`, {
11478
12313
  method: "POST",
11479
12314
  headers: {
11480
12315
  "Content-Type": "application/json",
@@ -11786,7 +12621,8 @@ function PopupFormFieldInternal({ tableKey, entityId, customFieldValidator, prop
11786
12621
  const updatePopupLocation = useCallback((newPositionCandidate) => {
11787
12622
  const draggableBoundingRect = draggableRef.current?.getBoundingClientRect();
11788
12623
  if (!cellRect || !draggableBoundingRect || draggableBoundingRect.width === 0 || draggableBoundingRect.height === 0) return;
11789
- const newNormalizedPosition = normalizePosition(newPositionCandidate ?? getInitialLocation(), draggableBoundingRect, windowSize);
12624
+ const basePosition = newPositionCandidate ?? getInitialLocation();
12625
+ const newNormalizedPosition = normalizePosition(basePosition, draggableBoundingRect, windowSize);
11790
12626
  if (!popupLocation || newNormalizedPosition.x !== popupLocation.x || newNormalizedPosition.y !== popupLocation.y) setPopupLocation(newNormalizedPosition);
11791
12627
  }, [
11792
12628
  cellRect,
@@ -12341,13 +13177,120 @@ function FilterPresetsButton({ filterPresets, tableController, compact }) {
12341
13177
  });
12342
13178
  }
12343
13179
  //#endregion
13180
+ //#region src/components/CollectionViewBinding/SortButton.tsx
13181
+ /**
13182
+ * Toolbar control that orders the collection by one property.
13183
+ *
13184
+ * The table orders by clicking a column header, which leaves every other view
13185
+ * mode — list, cards, and either of them inside a split view — with no way to
13186
+ * order at all, even though the controller has always carried `sortBy` and the
13187
+ * query has always honoured it. This binds a control to it.
13188
+ *
13189
+ * Clicking the active property flips its direction, so ascending → descending
13190
+ * is one click rather than a trip through a direction selector; clicking a
13191
+ * different one starts it ascending. The popover stays open through both.
13192
+ */
13193
+ function SortButton({ tableController, properties }) {
13194
+ const { t } = useTranslation();
13195
+ const [open, setOpen] = useState(false);
13196
+ const options = useMemo(() => getSortablePropertyOptions(properties), [properties]);
13197
+ const { sortBy, setSortBy, setItemCount, pageSize, filterValues, checkFilterCombination } = tableController;
13198
+ const activeKey = sortBy?.[0];
13199
+ const activeDirection = sortBy?.[1];
13200
+ const applySort = useCallback((next) => {
13201
+ setSortBy?.(next);
13202
+ if (pageSize !== void 0) setItemCount?.(pageSize);
13203
+ }, [
13204
+ setSortBy,
13205
+ setItemCount,
13206
+ pageSize
13207
+ ]);
13208
+ const onOptionClick = useCallback((key) => {
13209
+ if (key === activeKey) applySort([key, activeDirection === "asc" ? "desc" : "asc"]);
13210
+ else applySort([key, "asc"]);
13211
+ }, [
13212
+ activeKey,
13213
+ activeDirection,
13214
+ applySort
13215
+ ]);
13216
+ if (!setSortBy || options.length === 0) return null;
13217
+ const activeOption = options.find((option) => option.key === activeKey);
13218
+ const label = activeOption ? `${t("sort_by")}: ${activeOption.title}` : t("sort");
13219
+ return /* @__PURE__ */ jsx(Popover, {
13220
+ open,
13221
+ onOpenChange: setOpen,
13222
+ modal: false,
13223
+ side: "bottom",
13224
+ align: "start",
13225
+ trigger: /* @__PURE__ */ jsx(IconButton, {
13226
+ size: "small",
13227
+ "aria-label": label,
13228
+ title: label,
13229
+ className: cls(activeOption && "text-primary"),
13230
+ children: activeDirection === "desc" ? /* @__PURE__ */ jsx(ArrowDownIcon, { size: iconSize.smallest }) : activeDirection === "asc" ? /* @__PURE__ */ jsx(ArrowUpIcon, { size: iconSize.smallest }) : /* @__PURE__ */ jsx(ArrowUpDownIcon, { size: iconSize.smallest })
13231
+ }),
13232
+ children: /* @__PURE__ */ jsxs("div", {
13233
+ className: "py-2 min-w-[240px] flex flex-col",
13234
+ children: [
13235
+ /* @__PURE__ */ jsx(Typography, {
13236
+ variant: "caption",
13237
+ color: "secondary",
13238
+ className: "font-medium uppercase tracking-wider px-3 pb-1",
13239
+ children: t("sort_by")
13240
+ }),
13241
+ /* @__PURE__ */ jsx("div", {
13242
+ className: "max-h-[320px] overflow-y-auto flex flex-col",
13243
+ children: options.map(({ key, title, property }) => {
13244
+ const active = key === activeKey;
13245
+ const disabled = checkFilterCombination ? !checkFilterCombination(filterValues ?? {}, [key, active && activeDirection === "asc" ? "desc" : "asc"]) : false;
13246
+ return /* @__PURE__ */ jsxs("button", {
13247
+ type: "button",
13248
+ role: "menuitemradio",
13249
+ "aria-checked": active,
13250
+ disabled,
13251
+ onClick: () => onOptionClick(key),
13252
+ className: cls("flex items-center gap-2 px-3 py-1.5 text-sm text-left w-full", disabled ? "opacity-50 cursor-not-allowed" : "cursor-pointer hover:bg-surface-accent-100 dark:hover:bg-surface-accent-900", active && "text-primary"),
13253
+ children: [
13254
+ /* @__PURE__ */ jsx("span", {
13255
+ className: "flex-shrink-0 flex items-center",
13256
+ children: getIconForProperty(property, "smallest")
13257
+ }),
13258
+ /* @__PURE__ */ jsx("span", {
13259
+ className: "flex-grow truncate",
13260
+ children: title
13261
+ }),
13262
+ active && /* @__PURE__ */ jsx("span", {
13263
+ className: "flex-shrink-0 flex items-center",
13264
+ children: activeDirection === "desc" ? /* @__PURE__ */ jsx(ArrowDownIcon, { size: iconSize.smallest }) : /* @__PURE__ */ jsx(ArrowUpIcon, { size: iconSize.smallest })
13265
+ })
13266
+ ]
13267
+ }, key);
13268
+ })
13269
+ }),
13270
+ /* @__PURE__ */ jsx("div", {
13271
+ className: "px-3 pt-2",
13272
+ children: /* @__PURE__ */ jsx(Button, {
13273
+ variant: "text",
13274
+ size: "small",
13275
+ className: "w-full",
13276
+ disabled: !activeOption,
13277
+ onClick: () => applySort(void 0),
13278
+ children: t("clear_sort")
13279
+ })
13280
+ })
13281
+ ]
13282
+ })
13283
+ });
13284
+ }
13285
+ //#endregion
12344
13286
  //#region src/components/CollectionViewBinding/CollectionViewStartActions.tsx
12345
- function CollectionViewStartActions({ collection, relativePath, parentCollectionSlugs, parentEntityIds, path, selectionController, tableController, collectionEntitiesCount, resolvedProperties, compact, openNewDocument }) {
13287
+ function CollectionViewStartActions({ collection, relativePath, parentCollectionSlugs, parentEntityIds, path, selectionController, tableController, collectionEntitiesCount, resolvedProperties, viewMode, compact, openNewDocument }) {
12346
13288
  const context = useAdminContext();
12347
13289
  const largeLayout = useLargeLayout();
12348
13290
  const { t } = useTranslation();
12349
13291
  const navigate = useNavigate();
12350
13292
  const urlController = useUrlController();
13293
+ const splitView = useSplitView();
12351
13294
  const [filtersDialogOpen, setFiltersDialogOpen] = useState(false);
12352
13295
  const filterValues = tableController.filterValues;
12353
13296
  const fixedFilter = collection.fixedFilter;
@@ -12371,15 +13314,16 @@ function CollectionViewStartActions({ collection, relativePath, parentCollection
12371
13314
  urlController,
12372
13315
  path
12373
13316
  ]);
12374
- const backButton = compact && /* @__PURE__ */ jsx(Tooltip, {
12375
- title: t("back"),
13317
+ const backButton = compact && !splitView?.detailOpen && /* @__PURE__ */ jsx(Tooltip, {
13318
+ title: t("close"),
12376
13319
  children: /* @__PURE__ */ jsx(IconButton, {
12377
13320
  size: "small",
12378
13321
  onClick: handleBackClick,
13322
+ "aria-label": t("close"),
12379
13323
  className: "mr-1",
12380
- children: /* @__PURE__ */ jsx(ArrowLeftIcon, { size: iconSize.small })
13324
+ children: /* @__PURE__ */ jsx(XIcon, { size: iconSize.smallest })
12381
13325
  })
12382
- }, "back_tooltip");
13326
+ }, "close_tooltip");
12383
13327
  const filtersButton = resolvedProperties && tableController.setFilterValues && /* @__PURE__ */ jsx(Tooltip, {
12384
13328
  title: t("filters"),
12385
13329
  children: /* @__PURE__ */ jsx(Badge, {
@@ -12392,16 +13336,19 @@ function CollectionViewStartActions({ collection, relativePath, parentCollection
12392
13336
  startIcon: /* @__PURE__ */ jsx(FilterIcon, { size: iconSize.small }),
12393
13337
  className: cls(activeFilterCount > 0 && "text-primary"),
12394
13338
  children: activeFilterCount > 0 ? `(${activeFilterCount})` : t("filters")
12395
- }) : /* @__PURE__ */ jsx(Button, {
12396
- variant: "text",
13339
+ }) : /* @__PURE__ */ jsx(IconButton, {
12397
13340
  size: "small",
12398
13341
  onClick: () => setFiltersDialogOpen(true),
12399
13342
  className: cls(activeFilterCount > 0 && "text-primary"),
12400
- children: /* @__PURE__ */ jsx(FilterIcon, { size: iconSize.small })
13343
+ children: /* @__PURE__ */ jsx(FilterIcon, { size: iconSize.smallest })
12401
13344
  })
12402
13345
  })
12403
13346
  }, "filters_tooltip");
12404
- const filterPresetsButton = collection.filterPresets?.length ? /* @__PURE__ */ jsx(FilterPresetsButton, {
13347
+ const sortButton = resolvedProperties && (viewMode === "list" || viewMode === "cards") && /* @__PURE__ */ jsx(SortButton, {
13348
+ tableController,
13349
+ properties: resolvedProperties
13350
+ }, "sort_button");
13351
+ const filterPresetsButton = !compact && collection.filterPresets?.length ? /* @__PURE__ */ jsx(FilterPresetsButton, {
12405
13352
  filterPresets: collection.filterPresets,
12406
13353
  tableController,
12407
13354
  compact
@@ -12410,6 +13357,7 @@ function CollectionViewStartActions({ collection, relativePath, parentCollection
12410
13357
  [
12411
13358
  backButton,
12412
13359
  filtersButton,
13360
+ sortButton,
12413
13361
  /* @__PURE__ */ jsx(ClearFilterSortButton, {
12414
13362
  tableController,
12415
13363
  enabled: !collection.fixedFilter
@@ -12587,7 +13535,10 @@ var CollectionViewBindingInner = React.memo(function CollectionViewBindingInner(
12587
13535
  }, [defaultViewMode, setSearchParams]);
12588
13536
  useEffect(() => {
12589
13537
  if (urlView) setViewModeState(urlView);
12590
- else setViewModeState(getSavedView() ?? defaultViewMode);
13538
+ else {
13539
+ const savedView = getSavedView();
13540
+ setViewModeState(savedView ?? defaultViewMode);
13541
+ }
12591
13542
  }, [
12592
13543
  urlView,
12593
13544
  getSavedView,
@@ -12619,9 +13570,10 @@ var CollectionViewBindingInner = React.memo(function CollectionViewBindingInner(
12619
13570
  entityId: clickedEntity.id
12620
13571
  });
12621
13572
  if (collection) addRecentId(collection.slug, clickedEntity.id);
13573
+ const entityPath = path ?? clickedEntity.path;
12622
13574
  navigateToEntity({
12623
13575
  navigation: urlController,
12624
- path: path ?? clickedEntity.path,
13576
+ path: entityPath,
12625
13577
  sidePanelController,
12626
13578
  openEntityMode,
12627
13579
  collection,
@@ -12765,7 +13717,8 @@ var CollectionViewBindingInner = React.memo(function CollectionViewBindingInner(
12765
13717
  const onColumnResize = useCallback(({ width, key }) => {
12766
13718
  const collection = collectionRef.current;
12767
13719
  if (!getPropertyInPath(collection.properties, key)) return;
12768
- onCollectionModifiedForUser(path, buildPropertyWidthOverwrite(key, width));
13720
+ const localCollection = buildPropertyWidthOverwrite(key, width);
13721
+ onCollectionModifiedForUser(path, localCollection);
12769
13722
  }, [onCollectionModifiedForUser, path]);
12770
13723
  const onListSizeChanged = useCallback((size) => {
12771
13724
  setListSize(size);
@@ -12947,21 +13900,24 @@ var CollectionViewBindingInner = React.memo(function CollectionViewBindingInner(
12947
13900
  return (largeLayout ? 80 + actionsWidth : 70 + actionsWidth) + (collapsedActions.length > 0 ? largeLayout ? 40 : 30 : 0);
12948
13901
  }, [getActionsForEntity, largeLayout]);
12949
13902
  const tableRowActionsBuilder = useCallback(({ entity, size, width, frozen }) => {
13903
+ const isSelected = Boolean(usedSelectionController.selectedEntities.find((e) => e.id == entity.id && e.path == entity.path));
13904
+ const customEntityActions = (collection.entityActions ?? EMPTY_ARRAY).map((action) => resolveEntityAction(action, customizationController.entityActions)).filter(Boolean);
13905
+ const actions = getActionsForEntity({
13906
+ entity,
13907
+ customEntityActions
13908
+ });
12950
13909
  return /* @__PURE__ */ jsx(CollectionRowActions, {
12951
13910
  entity,
12952
13911
  width,
12953
13912
  frozen,
12954
- isSelected: Boolean(usedSelectionController.selectedEntities.find((e) => e.id == entity.id && e.path == entity.path)),
13913
+ isSelected,
12955
13914
  selectionEnabled: activeSelectionEnabled,
12956
13915
  size,
12957
13916
  highlightEntity: setHighlightedEntity,
12958
13917
  unhighlightEntity: unselectNavigatedEntity,
12959
13918
  collection,
12960
13919
  path,
12961
- actions: getActionsForEntity({
12962
- entity,
12963
- customEntityActions: (collection.entityActions ?? EMPTY_ARRAY).map((action) => resolveEntityAction(action, customizationController.entityActions)).filter(Boolean)
12964
- }),
13920
+ actions,
12965
13921
  hideId: collection?.hideIdFromCollection,
12966
13922
  onCollectionChange: updateLastDeleteTimestamp,
12967
13923
  selectionController: usedSelectionController,
@@ -12983,12 +13939,13 @@ var CollectionViewBindingInner = React.memo(function CollectionViewBindingInner(
12983
13939
  const { resolvedSlots } = customizationController;
12984
13940
  const headerActionContributions = useMemo(() => resolvedSlots.filter((s) => s.slot === "collection.header.action").sort((a, b) => (a.order ?? 50) - (b.order ?? 50)), [resolvedSlots]);
12985
13941
  const buildAdditionalHeaderWidget = useCallback(({ property, propertyKey, onHover }) => {
13942
+ const collection = collectionRef.current;
12986
13943
  const headerSlotProps = {
12987
13944
  property,
12988
13945
  propertyKey,
12989
13946
  onHover,
12990
13947
  path,
12991
- collection: collectionRef.current,
13948
+ collection,
12992
13949
  tableController,
12993
13950
  parentCollectionSlugs: parentCollectionSlugs ?? EMPTY_ARRAY,
12994
13951
  parentEntityIds: parentEntityIds ?? EMPTY_ARRAY
@@ -13084,6 +14041,7 @@ var CollectionViewBindingInner = React.memo(function CollectionViewBindingInner(
13084
14041
  selectionController: usedSelectionController,
13085
14042
  collectionEntitiesCount: docsCount ?? void 0,
13086
14043
  resolvedProperties: resolvedCollection.properties,
14044
+ viewMode,
13087
14045
  openNewDocument,
13088
14046
  compact: isCompact
13089
14047
  }),
@@ -13242,7 +14200,7 @@ var CollectionViewBindingInner = React.memo(function CollectionViewBindingInner(
13242
14200
  }, `split-list-view-${path}`) : /* @__PURE__ */ jsxs("div", {
13243
14201
  className: "flex flex-col w-full h-full",
13244
14202
  children: [toolbarNode, /* @__PURE__ */ jsx("div", {
13245
- className: cls("flex-1 flex flex-col", (viewMode === "list" || viewMode === "cards") && "overflow-y-auto"),
14203
+ className: cls("flex-1 min-h-0 flex flex-col", (viewMode === "list" || viewMode === "cards") && "overflow-y-auto"),
13246
14204
  children: collectionSurface
13247
14205
  })]
13248
14206
  }),
@@ -13455,7 +14413,18 @@ function EntityIdHeaderWidget({ collection, path, idPath, openEntityMode }) {
13455
14413
  }
13456
14414
  //#endregion
13457
14415
  //#region src/components/EditFormActions.tsx
13458
- function EditFormActions({ collection, path, entity, layout, savingError, formex, disabled, status, pluginActions, openEntityMode, showDefaultActions = true, navigateBack, formContext }) {
14416
+ /**
14417
+ * The entity form's footer.
14418
+ *
14419
+ * It used to render in two shapes: this footer, and a `w-80 2xl:w-96` sticky
14420
+ * side rail carrying the same Save and Discard above several hundred pixels of
14421
+ * nothing. The rail is gone, and Save/Discard have moved to the identity bar
14422
+ * where they stay visible while the form scrolls — so with
14423
+ * `showDefaultActions={false}` this renders only what the bar cannot host:
14424
+ * record actions (copy, delete, custom), plugin actions, the saving error, and
14425
+ * the side-panel-only "save and close".
14426
+ */
14427
+ function EditFormActions({ collection, path, entity, savingError, formex, disabled, status, pluginActions, openEntityMode, showDefaultActions = true, navigateBack, formContext }) {
13459
14428
  const { canCreate, canDelete } = usePermissions();
13460
14429
  const context = useAdminContext();
13461
14430
  const sidePanelController = context.sidePanelController;
@@ -13464,31 +14433,29 @@ function EditFormActions({ collection, path, entity, layout, savingError, formex
13464
14433
  const { t } = useTranslation();
13465
14434
  const childViewSource = useChildViewSource(path);
13466
14435
  const isLinkedChildView = childViewSource?.kind === "relation" && childViewSource.mode === "linked";
13467
- const entityActions = useMemo(() => {
13468
- const customEntityActions = (collection.entityActions ?? []).map((action) => resolveEntityAction(action, customizationController.entityActions)).filter(Boolean);
13469
- const createEnabled = canCreate(collection, path);
13470
- const deleteEnabled = entity ? canDelete(collection, path, entity) : false;
13471
- const disableActions = collection.disableDefaultActions ?? [];
13472
- const actions = [];
13473
- if (createEnabled && !disableActions.includes("copy")) actions.push(copyEntityAction);
13474
- if (deleteEnabled && !disableActions.includes("delete")) actions.push(isLinkedChildView ? unlinkEntityAction : deleteEntityAction);
13475
- if (customEntityActions) return mergeEntityActions(actions, customEntityActions);
13476
- return actions;
13477
- }, [
13478
- canCreate,
13479
- canDelete,
13480
- collection,
13481
- path,
13482
- customizationController.entityActions?.length,
13483
- entity,
13484
- collection.disableDefaultActions,
13485
- isLinkedChildView
13486
- ]);
13487
- const formActions = showDefaultActions ? entityActions.filter((a) => a.includeInForm === void 0 || a.includeInForm) : [];
13488
- const bottomActions = buildBottomActions$1({
14436
+ return buildBottomActions$1({
13489
14437
  savingError,
13490
14438
  entity,
13491
- formActions,
14439
+ formActions: useMemo(() => {
14440
+ const customEntityActions = (collection.entityActions ?? []).map((action) => resolveEntityAction(action, customizationController.entityActions)).filter(Boolean);
14441
+ const createEnabled = canCreate(collection, path);
14442
+ const deleteEnabled = entity ? canDelete(collection, path, entity) : false;
14443
+ const disableActions = collection.disableDefaultActions ?? [];
14444
+ const actions = [];
14445
+ if (createEnabled && !disableActions.includes("copy")) actions.push(copyEntityAction);
14446
+ if (deleteEnabled && !disableActions.includes("delete")) actions.push(isLinkedChildView ? unlinkEntityAction : deleteEntityAction);
14447
+ if (customEntityActions) return mergeEntityActions(actions, customEntityActions);
14448
+ return actions;
14449
+ }, [
14450
+ canCreate,
14451
+ canDelete,
14452
+ collection,
14453
+ path,
14454
+ customizationController.entityActions?.length,
14455
+ entity,
14456
+ collection.disableDefaultActions,
14457
+ isLinkedChildView
14458
+ ]).filter((a) => a.includeInForm === void 0 || a.includeInForm),
13492
14459
  collection,
13493
14460
  context,
13494
14461
  sidePanelController,
@@ -13501,35 +14468,17 @@ function EditFormActions({ collection, path, entity, layout, savingError, formex
13501
14468
  formContext,
13502
14469
  formex,
13503
14470
  t,
13504
- className: layout === "responsive" ? "@6xl:hidden" : void 0
13505
- });
13506
- const sideActions = buildSideActions$1({
13507
- savingError,
13508
- entity,
13509
- formActions,
13510
- collection,
13511
- context,
13512
- sidePanelController,
13513
- sideDialogContext,
13514
- disabled,
13515
- status,
13516
- pluginActions,
13517
- openEntityMode,
13518
- navigateBack,
13519
- formContext,
13520
- formex,
13521
- t,
13522
- className: layout === "responsive" ? "hidden @6xl:flex" : void 0
14471
+ showDefaultActions
13523
14472
  });
13524
- if (layout === "responsive") return /* @__PURE__ */ jsxs(Fragment, { children: [bottomActions, sideActions] });
13525
- return layout === "bottom" ? bottomActions : sideActions;
13526
14473
  }
13527
- function buildBottomActions$1({ savingError, entity, formActions, collection, context, sidePanelController, disabled, status, sideDialogContext, pluginActions, openEntityMode, navigateBack, formContext, formex, t, className }) {
14474
+ function buildBottomActions$1({ savingError, entity, formActions, collection, context, sidePanelController, disabled, status, sideDialogContext, pluginActions, openEntityMode, navigateBack, formContext, formex, t, showDefaultActions, className }) {
13528
14475
  const hasErrors = Object.keys(formex.errors).length > 0 && formex.submitCount > 0;
13529
14476
  const canClose = openEntityMode === "side_panel" || openEntityMode === "dialog";
14477
+ const showBackToDetail = collection.defaultEntityAction === "view" && status === "existing";
14478
+ if (!(showDefaultActions || Boolean(savingError))) return null;
13530
14479
  return /* @__PURE__ */ jsxs(DialogActions, {
13531
14480
  className,
13532
- position: "absolute",
14481
+ position: "sticky",
13533
14482
  children: [
13534
14483
  savingError && /* @__PURE__ */ jsx("div", {
13535
14484
  className: "text-right",
@@ -13567,21 +14516,20 @@ function buildBottomActions$1({ savingError, entity, formActions, collection, co
13567
14516
  size: iconSize.smallest
13568
14517
  })
13569
14518
  }) : null,
13570
- /* @__PURE__ */ jsx(Button, {
13571
- variant: "text",
13572
- color: "primary",
13573
- disabled: disabled || formex.isSubmitting,
13574
- type: "reset",
13575
- children: status === "existing" ? t("discard") : t("clear")
13576
- }),
13577
- collection.defaultEntityAction === "view" && status === "existing" && /* @__PURE__ */ jsx(Button, {
14519
+ showBackToDetail && /* @__PURE__ */ jsx(Button, {
13578
14520
  variant: "text",
13579
14521
  color: "primary",
13580
14522
  disabled,
13581
14523
  onClick: navigateBack,
13582
14524
  children: t("back_to_detail") ?? "Back to details"
13583
14525
  }),
13584
- /* @__PURE__ */ jsx(Tooltip, {
14526
+ showDefaultActions && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Button, {
14527
+ variant: "text",
14528
+ color: "primary",
14529
+ disabled: disabled || formex.isSubmitting,
14530
+ type: "reset",
14531
+ children: status === "existing" ? t("discard") : t("clear")
14532
+ }), /* @__PURE__ */ jsx(Tooltip, {
13585
14533
  title: hasErrors ? t("fix_errors_before_saving") ?? "Fix highlighted errors before saving" : void 0,
13586
14534
  children: /* @__PURE__ */ jsxs(Button, {
13587
14535
  variant: canClose ? "text" : "filled",
@@ -13597,7 +14545,7 @@ function buildBottomActions$1({ savingError, entity, formActions, collection, co
13597
14545
  status === "new" && t("create")
13598
14546
  ]
13599
14547
  })
13600
- }),
14548
+ })] }),
13601
14549
  canClose && /* @__PURE__ */ jsxs(LoadingButton, {
13602
14550
  variant: "filled",
13603
14551
  color: "primary",
@@ -13616,77 +14564,6 @@ function buildBottomActions$1({ savingError, entity, formActions, collection, co
13616
14564
  ]
13617
14565
  });
13618
14566
  }
13619
- function buildSideActions$1({ savingError, entity, formActions, collection, context, sidePanelController, disabled, status, sideDialogContext, pluginActions, openEntityMode, navigateBack, formContext, formex, t, className }) {
13620
- const hasErrors = Object.keys(formex.errors).length > 0 && formex.submitCount > 0;
13621
- return /* @__PURE__ */ jsxs("div", {
13622
- className: cls("overflow-auto h-full flex flex-col gap-2 w-80 2xl:w-96 px-4 py-16 sticky top-0 border-l", defaultBorderMixin, className),
13623
- children: [
13624
- /* @__PURE__ */ jsx(Tooltip, {
13625
- title: hasErrors ? t("fix_errors_before_saving") ?? "Fix highlighted errors before saving" : void 0,
13626
- children: /* @__PURE__ */ jsxs(LoadingButton, {
13627
- fullWidth: true,
13628
- variant: "filled",
13629
- color: "primary",
13630
- type: "submit",
13631
- loading: formex.isSubmitting,
13632
- startIcon: hasErrors ? /* @__PURE__ */ jsx(AlertCircleIcon, {}) : void 0,
13633
- disabled: disabled || formex.isSubmitting,
13634
- onClick: () => {
13635
- sideDialogContext.setPendingClose?.(false);
13636
- },
13637
- children: [
13638
- status === "existing" && t("save"),
13639
- status === "copy" && t("create_copy"),
13640
- status === "new" && t("create")
13641
- ]
13642
- })
13643
- }),
13644
- /* @__PURE__ */ jsx(Button, {
13645
- fullWidth: true,
13646
- variant: "text",
13647
- disabled: disabled || formex.isSubmitting,
13648
- type: "reset",
13649
- children: status === "existing" ? t("discard") : t("clear")
13650
- }),
13651
- collection.defaultEntityAction === "view" && status === "existing" && /* @__PURE__ */ jsx(Button, {
13652
- fullWidth: true,
13653
- variant: "text",
13654
- disabled,
13655
- onClick: navigateBack,
13656
- children: t("back_to_detail") ?? "Back to details"
13657
- }),
13658
- pluginActions,
13659
- formActions.length > 0 && /* @__PURE__ */ jsx("div", {
13660
- className: "flex flex-row flex-wrap mt-2",
13661
- children: formActions.map((action, index) => {
13662
- const props = {
13663
- view: "form",
13664
- entity,
13665
- path: getCollectionDataPath(collection),
13666
- collection,
13667
- context,
13668
- sidePanelController,
13669
- openEntityMode,
13670
- navigateBack,
13671
- formContext
13672
- };
13673
- return /* @__PURE__ */ jsx(EntityActionButton, {
13674
- action,
13675
- enabled: !action.isEnabled || action.isEnabled(props),
13676
- props
13677
- }, action.key ?? action.name ?? index);
13678
- })
13679
- }),
13680
- savingError && /* @__PURE__ */ jsx("div", {
13681
- className: "text-right",
13682
- children: /* @__PURE__ */ jsx(Typography, {
13683
- color: "error",
13684
- children: savingError.message
13685
- })
13686
- })
13687
- ]
13688
- });
13689
- }
13690
14567
  function EntityActionButton({ action, enabled, props }) {
13691
14568
  const snackbarController = useSnackbarController();
13692
14569
  const [loading, setLoading] = React.useState(false);
@@ -13728,8 +14605,68 @@ function EntityActionButton({ action, enabled, props }) {
13728
14605
  });
13729
14606
  }
13730
14607
  //#endregion
14608
+ //#region src/hooks/useRecordActions.tsx
14609
+ /**
14610
+ * The actions you can perform *on* a record — copy, delete, and whatever the
14611
+ * collection adds — resolved against the current user's permissions.
14612
+ *
14613
+ * Extracted from the form's footer so the identity bar can own them. They are
14614
+ * not ways to leave the form, so they never belonged next to Save; putting them
14615
+ * in the bar's overflow menu is what let the footer disappear instead of
14616
+ * becoming a full-width strip holding two icon buttons.
14617
+ */
14618
+ function useRecordActions({ collection, path, entity }) {
14619
+ const { canCreate, canDelete } = usePermissions();
14620
+ const customizationController = useCustomizationController();
14621
+ const childViewSource = useChildViewSource(path);
14622
+ const isLinkedChildView = childViewSource?.kind === "relation" && childViewSource.mode === "linked";
14623
+ return useMemo(() => {
14624
+ const customEntityActions = (collection.entityActions ?? []).map((action) => resolveEntityAction(action, customizationController.entityActions)).filter(Boolean);
14625
+ const createEnabled = canCreate(collection, path);
14626
+ const deleteEnabled = entity ? canDelete(collection, path, entity) : false;
14627
+ const disableActions = collection.disableDefaultActions ?? [];
14628
+ const actions = [];
14629
+ if (createEnabled && !disableActions.includes("copy")) actions.push(copyEntityAction);
14630
+ if (deleteEnabled && !disableActions.includes("delete")) actions.push(isLinkedChildView ? unlinkEntityAction : deleteEntityAction);
14631
+ return (customEntityActions.length ? mergeEntityActions(actions, customEntityActions) : actions).filter((a) => a.includeInForm === void 0 || a.includeInForm);
14632
+ }, [
14633
+ canCreate,
14634
+ canDelete,
14635
+ collection,
14636
+ path,
14637
+ customizationController.entityActions?.length,
14638
+ entity,
14639
+ collection.disableDefaultActions,
14640
+ isLinkedChildView
14641
+ ]);
14642
+ }
14643
+ //#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
13731
14669
  //#region src/components/EditViewBinding.tsx
13732
- var EntityHistoryView = lazy(() => import("./history-g9688IaK.js").then((m) => ({ default: m.EntityHistoryView })));
13733
14670
  /**
13734
14671
  * This is the default view that is used as the content of a side panel when
13735
14672
  * a record is opened.
@@ -13778,11 +14715,12 @@ function EditViewBinding({ entityId, ...props }) {
13778
14715
  children: content
13779
14716
  });
13780
14717
  }
13781
- function EditViewBindingInner({ path, entityId, selectedTab: selectedTabProp, collection, parentCollectionSlugs, parentEntityIds, onValuesModified, onSaved, onTabChange, navigateBack, entity, initialDirtyValues, dataLoading, layout = "side_panel", barActions, status, setStatus, formProps, canEdit }) {
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 }) {
13782
14719
  const ResolvedFormActions = useComponentOverride("EditView.FormActions", EditFormActions);
13783
14720
  const ResolvedEntityForm = useComponentOverride("Entity.Form", EntityFormBinding);
13784
14721
  const ResolvedCollectionView = useComponentOverride("Collection.View", CollectionViewBinding);
13785
14722
  const context = useRebaseContext();
14723
+ const adminContext = useAdminContext();
13786
14724
  const urlController = useUrlController();
13787
14725
  const navigate = useNavigate();
13788
14726
  const [usedEntity, setUsedEntity] = useState(entity);
@@ -13793,7 +14731,7 @@ function EditViewBindingInner({ path, entityId, selectedTab: selectedTabProp, co
13793
14731
  useLargeLayout();
13794
14732
  const customizationController = useCustomizationController();
13795
14733
  const plugins = customizationController.plugins;
13796
- const pluginActionsTop = useSlot("form.actions.top", useMemo(() => ({
14734
+ const formActionTopProps = useMemo(() => ({
13797
14735
  entityId,
13798
14736
  parentCollectionSlugs,
13799
14737
  parentEntityIds,
@@ -13814,7 +14752,8 @@ function EditViewBindingInner({ path, entityId, selectedTab: selectedTabProp, co
13814
14752
  context,
13815
14753
  formContext,
13816
14754
  layout
13817
- ]));
14755
+ ]);
14756
+ const pluginActionsTop = useSlot("form.actions.top", formActionTopProps);
13818
14757
  const defaultSelectedView = useMemo(() => resolveDefaultSelectedView(collection ? collection.defaultSelectedView : void 0, {
13819
14758
  status,
13820
14759
  entityId
@@ -13840,24 +14779,17 @@ function EditViewBindingInner({ path, entityId, selectedTab: selectedTabProp, co
13840
14779
  const customViewsCount = customViews?.length ?? 0;
13841
14780
  const includeJsonView = collection.includeJsonView === void 0 ? true : collection.includeJsonView;
13842
14781
  const includeHistoryView = Boolean(collection.history);
13843
- const hasAdditionalViews = customViewsCount > 0 || subcollectionsCount > 0 || includeJsonView || includeHistoryView;
14782
+ const hasAdditionalViews = customViewsCount > 0 || subcollectionsCount > 0;
13844
14783
  const { resolvedEntityViews } = resolvedSelectedEntityView(customViews, customizationController, void 0, canEdit);
14784
+ const legacyInspectorTab = selectedTab === "__json" ? "json" : selectedTab === "__rebase_history" ? "history" : void 0;
13845
14785
  const activeTab = useMemo(() => {
13846
- return new Set([
14786
+ return /* @__PURE__ */ new Set([
13847
14787
  "__main_##Q$SC^#S6",
13848
- ...includeJsonView ? ["__json"] : [],
13849
- ...includeHistoryView ? ["__rebase_history"] : [],
13850
14788
  ...resolvedEntityViews.map((v) => v.key),
13851
14789
  ...subcollections.map((s) => s.slug)
13852
14790
  ]);
13853
- }, [
13854
- includeJsonView,
13855
- includeHistoryView,
13856
- resolvedEntityViews,
13857
- subcollections
13858
- ]).has(selectedTab) ? selectedTab : MAIN_TAB_VALUE;
14791
+ }, [resolvedEntityViews, subcollections]).has(selectedTab) ? selectedTab : MAIN_TAB_VALUE;
13859
14792
  const { selectedEntityView, selectedSecondaryForm } = resolvedSelectedEntityView(customViews, customizationController, activeTab, canEdit);
13860
- const actionsAtTheBottom = layout === "side_panel" || layout === "dialog" || selectedEntityView?.includeActions === "bottom";
13861
14793
  const mainViewVisible = activeTab === "__main_##Q$SC^#S6" || Boolean(selectedSecondaryForm);
13862
14794
  const mountedTabsRef = useRef(/* @__PURE__ */ new Set());
13863
14795
  if (activeTab) mountedTabsRef.current.add(activeTab);
@@ -13926,25 +14858,46 @@ function EditViewBindingInner({ path, entityId, selectedTab: selectedTabProp, co
13926
14858
  }, `custom_view_${customView.key}`);
13927
14859
  }).filter(Boolean);
13928
14860
  const globalLoading = dataLoading && !usedEntity || canEdit === void 0 && (status === "existing" || status === "copy");
13929
- const jsonTabMounted = mountedTabsRef.current.has(JSON_TAB_VALUE);
13930
- const jsonView = activeTab === "__json" || jsonTabMounted ? /* @__PURE__ */ jsx("div", {
13931
- className: cls("relative flex-1 h-full overflow-auto w-full", { "hidden": activeTab !== JSON_TAB_VALUE }),
13932
- role: "tabpanel",
13933
- children: /* @__PURE__ */ jsx(ErrorBoundary, { children: /* @__PURE__ */ jsx(JsonPreviewBinding, { values: formContext?.values ?? entity?.values ?? {} }) })
13934
- }, "json_view") : null;
13935
- const historyView = includeHistoryView && activeTab === "__rebase_history" ? /* @__PURE__ */ jsx("div", {
13936
- className: "relative flex-1 h-full overflow-auto w-full",
13937
- role: "tabpanel",
13938
- children: /* @__PURE__ */ jsx(ErrorBoundary, { children: /* @__PURE__ */ jsx(Suspense, {
13939
- fallback: /* @__PURE__ */ jsx(CircularProgressCenter, {}),
13940
- children: /* @__PURE__ */ jsx(EntityHistoryView, {
13941
- collection,
13942
- entity: usedEntity,
13943
- formContext,
13944
- modifiedValues: formContext?.values ?? usedEntity?.values
13945
- })
13946
- }) })
13947
- }, "history_view") : null;
14861
+ const displayTitle = useEntityDisplayTitle({
14862
+ collection,
14863
+ values: formContext?.values ?? usedEntity?.values,
14864
+ entityId,
14865
+ status
14866
+ });
14867
+ const formex = formContext?.formex;
14868
+ const hasFormErrors = Boolean(formex && Object.keys(formex.errors).length > 0 && formex.submitCount > 0);
14869
+ const saveDisabled = Boolean(!formContext || formContext.disabled || formex?.isSubmitting || status === "existing" && !formex?.dirty);
14870
+ const [inspectorTab, setInspectorTab] = useState(null);
14871
+ const [savedCount, setSavedCount] = useState(0);
14872
+ const sideDialogContext = useSideDialogContext();
14873
+ const canCloseAfterSave = layout === "side_panel" || layout === "dialog";
14874
+ const recordActions = useRecordActions({
14875
+ collection,
14876
+ path,
14877
+ entity: usedEntity
14878
+ });
14879
+ const recordActionItems = usedEntity && recordActions.length ? recordActions.map((action, index) => {
14880
+ const clickProps = {
14881
+ view: "form",
14882
+ entity: usedEntity,
14883
+ path,
14884
+ collection,
14885
+ context,
14886
+ sidePanelController: adminContext.sidePanelController,
14887
+ openEntityMode: layout,
14888
+ navigateBack: navigateBack ?? (() => void 0),
14889
+ formContext
14890
+ };
14891
+ return /* @__PURE__ */ jsxs(MenuItem, {
14892
+ disabled: !(!action.isEnabled || action.isEnabled(clickProps)),
14893
+ onClick: () => action.onClick(clickProps),
14894
+ children: [getIcon(action.icon, void 0, void 0, "smallest"), action.name]
14895
+ }, action.key ?? action.name ?? index);
14896
+ }) : null;
14897
+ const formPluginActions = useSlot("form.actions", formActionTopProps);
14898
+ useEffect(() => {
14899
+ if (legacyInspectorTab) setInspectorTab(legacyInspectorTab);
14900
+ }, [legacyInspectorTab]);
13948
14901
  const subCollectionsViews = childViews && childViews.map(({ collection: subcollection }) => {
13949
14902
  const subcollectionId = subcollection.slug;
13950
14903
  const newFullPath = usedEntity ? `${path}/${usedEntity?.id}/${removeInitialAndTrailingSlashes(getCollectionDataPath(subcollection))}` : void 0;
@@ -14038,7 +14991,7 @@ function EditViewBindingInner({ path, entityId, selectedTab: selectedTabProp, co
14038
14991
  entity,
14039
14992
  initialDirtyValues,
14040
14993
  openEntityMode: layout,
14041
- forceActionsAtTheBottom: actionsAtTheBottom,
14994
+ showDefaultActions: false,
14042
14995
  initialStatus: status,
14043
14996
  className: cls((!mainViewVisible || !canEdit) && !selectedSecondaryForm ? "hidden" : "", formProps?.className),
14044
14997
  EntityFormActionsComponent: ResolvedFormActions,
@@ -14058,6 +15011,7 @@ function EditViewBindingInner({ path, entityId, selectedTab: selectedTabProp, co
14058
15011
  formProps?.onFormContextReady?.(formContext);
14059
15012
  },
14060
15013
  onSaved: (params) => {
15014
+ setSavedCount((count) => count + 1);
14061
15015
  const res = {
14062
15016
  ...params,
14063
15017
  selectedTab: "__main_##Q$SC^#S6" === activeTab ? void 0 : activeTab
@@ -14100,14 +15054,14 @@ function EditViewBindingInner({ path, entityId, selectedTab: selectedTabProp, co
14100
15054
  })
14101
15055
  }, `entity_detail_collection_tab_${view.name}`);
14102
15056
  });
14103
- const shouldShowTopBar = Boolean(barActions) || hasAdditionalViews || layout === "side_panel" || layout === "dialog";
14104
- const fullScreenButton = !barActions && (layout === "side_panel" || layout === "split" || layout === "dialog") && entityId ? /* @__PURE__ */ jsx(Tooltip, {
15057
+ const fullScreenButton = !barActions && (layout === "side_panel" || layout === "dialog") && entityId ? /* @__PURE__ */ jsx(Tooltip, {
14105
15058
  title: "Open full screen",
14106
15059
  children: /* @__PURE__ */ jsx(IconButton, {
14107
15060
  size: "small",
14108
15061
  onClick: () => {
14109
15062
  const editSuffix = collection.defaultEntityAction === "view" ? "/edit" : "";
14110
- navigate(`${urlController.buildUrlCollectionPath(`${path}/${entityId}${editSuffix}`)}#full`);
15063
+ const entityUrl = urlController.buildUrlCollectionPath(`${path}/${entityId}${editSuffix}`);
15064
+ navigate(`${entityUrl}#full`);
14111
15065
  },
14112
15066
  children: /* @__PURE__ */ jsx(Maximize2Icon, { size: iconSize.smallest })
14113
15067
  })
@@ -14115,60 +15069,86 @@ function EditViewBindingInner({ path, entityId, selectedTab: selectedTabProp, co
14115
15069
  let result = /* @__PURE__ */ jsxs("div", {
14116
15070
  className: "relative flex flex-col h-full w-full bg-white dark:bg-surface-800",
14117
15071
  children: [
14118
- shouldShowTopBar && /* @__PURE__ */ jsxs("div", {
14119
- className: cls("h-[52px] items-center flex overflow-hidden w-full border-b pl-2 pr-2 flex bg-surface-50 dark:bg-surface-900", defaultBorderMixin),
14120
- children: [
15072
+ /* @__PURE__ */ jsx(EntityIdentityBar, {
15073
+ collection,
15074
+ title: displayTitle,
15075
+ entityId: status === "existing" ? entityId : void 0,
15076
+ status,
15077
+ dirty: Boolean(formContext?.formex?.dirty),
15078
+ saving: Boolean(formContext?.isSaving),
15079
+ hasErrors: hasFormErrors,
15080
+ saveDisabled: !canEdit || saveDisabled,
15081
+ onBack: layout === "full_screen" ? () => navigate(withViewMode(urlController.buildUrlCollectionPath(path))) : void 0,
15082
+ onSave: canEdit && formContext ? () => {
15083
+ sideDialogContext.setPendingClose?.(false);
15084
+ formContext.submit();
15085
+ } : void 0,
15086
+ onSaveAndClose: canEdit && formContext && canCloseAfterSave ? () => {
15087
+ sideDialogContext.setPendingClose?.(true);
15088
+ formContext.submit();
15089
+ } : void 0,
15090
+ onDiscard: canEdit && formContext ? () => formContext.formex.resetForm() : void 0,
15091
+ onInspect: includeJsonView ? () => setInspectorTab("json") : void 0,
15092
+ onViewHistory: includeHistoryView ? () => setInspectorTab("history") : void 0,
15093
+ recordActions: recordActionItems,
15094
+ pluginActions: formPluginActions,
15095
+ leading: /* @__PURE__ */ jsxs(Fragment, { children: [barActionsStart, layout === "split" && /* @__PURE__ */ jsx(SplitListCloseButton, {})] }),
15096
+ trailing: /* @__PURE__ */ jsxs(Fragment, { children: [
15097
+ pluginActionsTop,
14121
15098
  fullScreenButton,
14122
15099
  barActions?.({
14123
15100
  path,
14124
15101
  entityId,
14125
15102
  values: formContext?.values ?? usedEntity?.values ?? {},
14126
15103
  status
14127
- }),
14128
- pluginActionsTop,
14129
- hasAdditionalViews && /* @__PURE__ */ jsx("div", {
14130
- className: "flex-1 flex justify-end min-w-0 shrink-0",
14131
- children: /* @__PURE__ */ jsxs(Tabs, {
14132
- className: "!w-fit max-w-full",
14133
- value: activeTab,
14134
- onValueChange: (value) => {
14135
- onSideTabClick(value);
14136
- },
14137
- children: [
14138
- includeJsonView && /* @__PURE__ */ jsx(Tab, {
14139
- disabled: !hasAdditionalViews,
14140
- value: "__json",
14141
- className: "text-sm",
14142
- children: /* @__PURE__ */ jsx(CodeIcon, { size: iconSize.smallest })
14143
- }),
14144
- includeHistoryView && /* @__PURE__ */ jsx(Tab, {
14145
- disabled: !hasAdditionalViews,
14146
- value: "__rebase_history",
14147
- className: "text-sm",
14148
- children: /* @__PURE__ */ jsx(HistoryIcon, { size: iconSize.smallest })
14149
- }),
14150
- /* @__PURE__ */ jsx(Tab, {
14151
- disabled: !hasAdditionalViews,
14152
- value: "__main_##Q$SC^#S6",
14153
- className: "text-sm min-w-[90px]",
14154
- children: /* @__PURE__ */ jsxs("span", {
14155
- className: "flex items-center gap-1.5",
14156
- children: [getIcon(collection.icon, void 0, void 0, "smallest"), collection.singularName ?? collection.name]
14157
- })
14158
- }),
14159
- customViewTabsStart,
14160
- customViewTabsEnd,
14161
- subcollectionTabs
14162
- ]
14163
- })
14164
15104
  })
14165
- ]
15105
+ ] })
15106
+ }),
15107
+ hasAdditionalViews && /* @__PURE__ */ jsx("div", {
15108
+ className: cls("h-10 shrink-0 flex items-stretch border-b px-2 min-w-0", defaultBorderMixin),
15109
+ children: /* @__PURE__ */ jsxs(Tabs, {
15110
+ className: "!w-full",
15111
+ innerClassName: "h-full",
15112
+ variant: "boxy",
15113
+ value: activeTab,
15114
+ onValueChange: (value) => {
15115
+ onSideTabClick(value);
15116
+ },
15117
+ children: [
15118
+ /* @__PURE__ */ jsx(Tab, {
15119
+ value: "__main_##Q$SC^#S6",
15120
+ children: /* @__PURE__ */ jsxs("span", {
15121
+ className: "flex items-center gap-1.5",
15122
+ children: [getIcon(collection.icon, void 0, void 0, "smallest"), collection.singularName ?? collection.name]
15123
+ })
15124
+ }),
15125
+ customViewTabsStart,
15126
+ customViewTabsEnd,
15127
+ subcollectionTabs
15128
+ ]
15129
+ })
14166
15130
  }),
14167
- globalLoading ? /* @__PURE__ */ jsx(EntityFormSkeleton, { collection }) : /* @__PURE__ */ jsxs(Fragment, { children: [entityReadOnlyView, entityView] }),
14168
- jsonView,
14169
- historyView,
14170
- customViewsView,
14171
- subCollectionsViews
15131
+ /* @__PURE__ */ jsxs("div", {
15132
+ className: "flex-1 min-h-0 flex",
15133
+ children: [/* @__PURE__ */ jsxs("div", {
15134
+ className: "flex-1 min-w-0 flex flex-col",
15135
+ children: [
15136
+ globalLoading ? /* @__PURE__ */ jsx(EntityFormSkeleton, { collection }) : /* @__PURE__ */ jsxs(Fragment, { children: [entityReadOnlyView, entityView] }),
15137
+ customViewsView,
15138
+ subCollectionsViews
15139
+ ]
15140
+ }), /* @__PURE__ */ jsx(EntityInspector, {
15141
+ tab: inspectorTab,
15142
+ onTabChange: setInspectorTab,
15143
+ onClose: () => setInspectorTab(null),
15144
+ collection,
15145
+ entity: usedEntity,
15146
+ formContext,
15147
+ values: formContext?.values ?? usedEntity?.values,
15148
+ refreshToken: savedCount,
15149
+ includeHistory: includeHistoryView
15150
+ })]
15151
+ })
14172
15152
  ]
14173
15153
  });
14174
15154
  if (plugins && plugins.length > 0) result = /* @__PURE__ */ jsx(PluginProviderStack, {
@@ -14397,21 +15377,25 @@ function SidePanelBinding(props) {
14397
15377
  parentCollectionSlugs,
14398
15378
  parentEntityIds,
14399
15379
  onEditClick: openEditView,
14400
- barActions: ({ status, values }) => /* @__PURE__ */ jsxs("div", {
15380
+ barActionsStart: /* @__PURE__ */ jsx(IconButton, {
15381
+ className: "self-center",
15382
+ size: "small",
15383
+ onClick: onClose,
15384
+ children: /* @__PURE__ */ jsx(XIcon, {})
15385
+ }),
15386
+ barActions: ({ status, values }) => /* @__PURE__ */ jsx("div", {
14401
15387
  className: "flex gap-1",
14402
- children: [/* @__PURE__ */ jsx(IconButton, {
14403
- className: "self-center",
14404
- size: "small",
14405
- onClick: onClose,
14406
- children: /* @__PURE__ */ jsx(XIcon, {})
14407
- }), allowFullScreen && /* @__PURE__ */ jsx(IconButton, {
15388
+ children: allowFullScreen && /* @__PURE__ */ jsx(IconButton, {
14408
15389
  className: "self-center",
14409
15390
  size: "small",
14410
15391
  onClick: () => {
14411
- if (entityId) navigate(urlController.buildUrlCollectionPath(`${path}/${entityId}`), { state: null });
15392
+ if (entityId) {
15393
+ const fullScreenUrl = urlController.buildUrlCollectionPath(`${path}/${entityId}`);
15394
+ navigate(fullScreenUrl, { state: null });
15395
+ }
14412
15396
  },
14413
15397
  children: /* @__PURE__ */ jsx(Maximize2Icon, {})
14414
- })]
15398
+ })
14415
15399
  }),
14416
15400
  onTabChange: ({ entityId: tabEntityId, selectedTab, collection: paramCollection }) => {
14417
15401
  if (collection?.openEntityMode === "dialog" || paramCollection?.openEntityMode === "dialog") return;
@@ -14432,25 +15416,31 @@ function SidePanelBinding(props) {
14432
15416
  onValuesModified,
14433
15417
  onSaved: onUpdate,
14434
15418
  navigateBack: closeEditView,
14435
- barActions: ({ status, values }) => /* @__PURE__ */ jsxs("div", {
15419
+ barActionsStart: /* @__PURE__ */ jsx(IconButton, {
15420
+ className: "self-center",
15421
+ size: "small",
15422
+ onClick: onClose,
15423
+ children: /* @__PURE__ */ jsx(XIcon, {})
15424
+ }),
15425
+ barActions: ({ status, values }) => /* @__PURE__ */ jsx("div", {
14436
15426
  className: "flex gap-1",
14437
- children: [/* @__PURE__ */ jsx(IconButton, {
14438
- className: "self-center",
14439
- size: "small",
14440
- onClick: onClose,
14441
- children: /* @__PURE__ */ jsx(XIcon, {})
14442
- }), allowFullScreen && /* @__PURE__ */ jsx(IconButton, {
15427
+ children: allowFullScreen && /* @__PURE__ */ jsx(IconButton, {
14443
15428
  className: "self-center",
14444
15429
  size: "small",
14445
15430
  onClick: () => {
14446
15431
  saveEntityToMemoryCache(status === "new" || status === "copy" ? path + "#new" : path + "/" + entityId, values);
14447
15432
  setBlocked(false);
14448
15433
  setBlockedNavigationMessage(void 0);
14449
- if (entityId) navigate(urlController.buildUrlCollectionPath(`${path}/${entityId}`), { state: null });
14450
- else navigate(urlController.buildUrlCollectionPath(path) + "#new", { state: null });
15434
+ if (entityId) {
15435
+ const fullScreenUrl = urlController.buildUrlCollectionPath(`${path}/${entityId}`);
15436
+ navigate(fullScreenUrl, { state: null });
15437
+ } else {
15438
+ const fullScreenUrl = urlController.buildUrlCollectionPath(path);
15439
+ navigate(fullScreenUrl + "#new", { state: null });
15440
+ }
14451
15441
  },
14452
15442
  children: /* @__PURE__ */ jsx(Maximize2Icon, {})
14453
- })]
15443
+ })
14454
15444
  }),
14455
15445
  onTabChange: ({ entityId: tabEntityId, selectedTab, collection: paramCollection }) => {
14456
15446
  if (collection?.openEntityMode === "dialog" || paramCollection?.openEntityMode === "dialog") return;
@@ -14485,39 +15475,25 @@ function ResolvedSidePanelBinding(props) {
14485
15475
  }
14486
15476
  var NEW_URL_HASH = "new_side";
14487
15477
  var SIDE_URL_HASH = "side";
15478
+ /**
15479
+ * How wide the side panel opens.
15480
+ *
15481
+ * One width for the whole panel, whatever tab is showing. It used to depend on
15482
+ * the selected tab — the form got one width and a subcollection or custom view
15483
+ * got `calc(55vw + 768px)` — so the panel physically resized under the cursor
15484
+ * every time you switched tabs. And the "form" width was derived from how
15485
+ * deeply nested the collection's *properties* were, which is not a thing anyone
15486
+ * can predict from looking at the panel.
15487
+ *
15488
+ * Now: `width` prop, then the collection's `sideDialogWidth`, then a default
15489
+ * sized for the content the panel actually has to hold — the form column plus
15490
+ * its metadata rail — capped so it never eats the whole window.
15491
+ */
14488
15492
  function getEntityViewWidth(props, small, customizationController) {
14489
15493
  if (small) return CONTAINER_FULL_WIDTH;
14490
- const { selectedSecondaryForm } = resolvedSelectedEntityView(props.collection?.entityViews, customizationController, props.selectedTab);
14491
- const shouldUseSmallLayout = !props.selectedTab || props.selectedTab === "edit" || props.selectedTab === "__json" || props.selectedTab === "__rebase_history" || Boolean(selectedSecondaryForm);
14492
- let resolvedWidth;
14493
- if (props.width) resolvedWidth = typeof props.width === "number" ? `${props.width}px` : props.width;
14494
- else if (props.collection?.sideDialogWidth) resolvedWidth = typeof props.collection.sideDialogWidth === "number" ? `${props.collection.sideDialogWidth}px` : props.collection.sideDialogWidth;
14495
- if (!shouldUseSmallLayout) return `calc(${ADDITIONAL_TAB_WIDTH} + ${resolvedWidth ?? FORM_CONTAINER_WIDTH})`;
14496
- else if (resolvedWidth) return resolvedWidth;
14497
- else if (!props.collection) return FORM_CONTAINER_WIDTH;
14498
- else return calculateCollectionDesiredWidth(props.collection);
14499
- }
14500
- var collectionViewWidthCache = {};
14501
- function calculateCollectionDesiredWidth(collection) {
14502
- if (collectionViewWidthCache[collection.slug]) return collectionViewWidthCache[collection.slug];
14503
- let result = FORM_CONTAINER_WIDTH;
14504
- if (collection?.properties) {
14505
- const values = Object.values(collection.properties).map((p) => getNestedPropertiesDepth(p));
14506
- const maxDepth = Math.max(...values);
14507
- if (maxDepth < 3) result = FORM_CONTAINER_WIDTH;
14508
- else result = 768 + 32 * (maxDepth - 2) + "px";
14509
- }
14510
- collectionViewWidthCache[collection.slug] = result;
14511
- return result;
14512
- }
14513
- function getNestedPropertiesDepth(property, accumulator = 0) {
14514
- if (property.type === "map" && property.properties) {
14515
- const values = Object.values(property.properties).flatMap((childProperty) => getNestedPropertiesDepth(childProperty, accumulator + 1));
14516
- return Math.max(...values);
14517
- } else if (property.type === "array" && property.oneOf) return accumulator + 3;
14518
- else if (property.type === "array" && property.of) if (Array.isArray(property.of)) return Math.max(...property.of.map((p) => getNestedPropertiesDepth(p, accumulator + 1)));
14519
- else return getNestedPropertiesDepth(property.of, accumulator + 1);
14520
- else return accumulator + 1;
15494
+ if (props.width) return typeof props.width === "number" ? `${props.width}px` : props.width;
15495
+ if (props.collection?.sideDialogWidth) return typeof props.collection.sideDialogWidth === "number" ? `${props.collection.sideDialogWidth}px` : props.collection.sideDialogWidth;
15496
+ return SIDE_PANEL_DEFAULT_WIDTH;
14521
15497
  }
14522
15498
  var useBuildSidePanel = (collectionRegistryController, urlController, navigationStateController, sideDialogsController, authController) => {
14523
15499
  const location = useLocation();
@@ -14759,8 +15735,9 @@ function useResolvedNavigationFrom({ path }) {
14759
15735
  * Replaces the old subcollection-based approach with a proper API call.
14760
15736
  */
14761
15737
  function useHistory(params) {
14762
- const { slug, entityId, enabled = true, pageSize = 10 } = params;
15738
+ const { slug, entityId, enabled = true, pageSize = 10, refreshToken } = params;
14763
15739
  const apiConfig = useApiConfig();
15740
+ const apiBase = useApiBase();
14764
15741
  const [entries, setEntries] = useState([]);
14765
15742
  const [total, setTotal] = useState(0);
14766
15743
  const [isLoading, setIsLoading] = useState(false);
@@ -14768,6 +15745,7 @@ function useHistory(params) {
14768
15745
  const [error, setError] = useState();
14769
15746
  const [offset, setOffset] = useState(0);
14770
15747
  const [refreshTrigger, setRefreshTrigger] = useState(0);
15748
+ const entityKey = `${slug}/${entityId}/${refreshToken ?? 0}`;
14771
15749
  const currentEntityRef = useRef(void 0);
14772
15750
  const fetchEntries = useCallback(async (currentOffset, signal) => {
14773
15751
  if (!apiConfig?.apiUrl || !entityId || !enabled) return;
@@ -14777,13 +15755,12 @@ function useHistory(params) {
14777
15755
  const token = await apiConfig.getAuthToken?.();
14778
15756
  const headers = { "Content-Type": "application/json" };
14779
15757
  if (token) headers["Authorization"] = `Bearer ${token}`;
14780
- const url = `${apiConfig.apiUrl}/api/data/${slug}/${entityId}/history?limit=${pageSize}&offset=${currentOffset}`;
15758
+ const url = `${apiBase}/data/${slug}/${entityId}/history?limit=${pageSize}&offset=${currentOffset}`;
14781
15759
  const response = await fetch(url, {
14782
15760
  headers,
14783
15761
  signal
14784
15762
  });
14785
15763
  if (signal?.aborted) return;
14786
- const entityKey = `${slug}/${entityId}`;
14787
15764
  if (currentEntityRef.current !== entityKey) return;
14788
15765
  if (!response.ok) {
14789
15766
  const errorData = await response.json().catch(() => ({ error: { message: response.statusText } }));
@@ -14802,13 +15779,14 @@ function useHistory(params) {
14802
15779
  }
14803
15780
  }, [
14804
15781
  apiConfig,
15782
+ apiBase,
14805
15783
  slug,
14806
15784
  entityId,
15785
+ entityKey,
14807
15786
  enabled,
14808
15787
  pageSize
14809
15788
  ]);
14810
15789
  useEffect(() => {
14811
- const entityKey = `${slug}/${entityId}`;
14812
15790
  const isNewEntity = currentEntityRef.current !== entityKey;
14813
15791
  if (isNewEntity) {
14814
15792
  currentEntityRef.current = entityKey;
@@ -14828,7 +15806,7 @@ function useHistory(params) {
14828
15806
  offset,
14829
15807
  enabled,
14830
15808
  entityId,
14831
- slug,
15809
+ entityKey,
14832
15810
  refreshTrigger
14833
15811
  ]);
14834
15812
  const loadMore = useCallback(() => {
@@ -14846,7 +15824,7 @@ function useHistory(params) {
14846
15824
  const token = await apiConfig.getAuthToken?.();
14847
15825
  const headers = { "Content-Type": "application/json" };
14848
15826
  if (token) headers["Authorization"] = `Bearer ${token}`;
14849
- const url = `${apiConfig.apiUrl}/api/data/${slug}/${entityId}/history/${historyId}/revert`;
15827
+ const url = `${apiBase}/data/${slug}/${entityId}/history/${historyId}/revert`;
14850
15828
  const response = await fetch(url, {
14851
15829
  method: "POST",
14852
15830
  headers
@@ -14861,6 +15839,7 @@ function useHistory(params) {
14861
15839
  return result.data;
14862
15840
  }, [
14863
15841
  apiConfig,
15842
+ apiBase,
14864
15843
  slug,
14865
15844
  entityId
14866
15845
  ]);
@@ -15161,6 +16140,24 @@ function filterViewsByRole(views, authController) {
15161
16140
  //#endregion
15162
16141
  //#region src/hooks/navigation/utils.ts
15163
16142
  var NAVIGATION_DEFAULT_GROUP_NAME = "Views";
16143
+ /**
16144
+ * Group names that sink to the bottom of the drawer on their name alone.
16145
+ *
16146
+ * A magic string, and it stays one for compatibility: apps have been relying on
16147
+ * `group: "Settings"` landing last since before there was a flag, and taking it
16148
+ * away would silently reshuffle their navigation.
16149
+ *
16150
+ * It is a bad way to ask for the behaviour, though — it is invisible from the
16151
+ * `group?: string` type, and it is defeated by the most ordinary edit there is:
16152
+ * translating the label. `group: "Ajustes"` is the same group to a reader and a
16153
+ * different one to this comparison, so the ordering just quietly stops
16154
+ * happening. Set {@link AppView.pinToBottom} instead, which says so.
16155
+ */
16156
+ var NAVIGATION_BOTTOM_GROUP_NAMES = ["Admin", "Settings"];
16157
+ /** Whether a group sinks to the bottom by virtue of its name. @see NAVIGATION_BOTTOM_GROUP_NAMES */
16158
+ function isBottomPinnedGroupName(groupName) {
16159
+ return groupName !== void 0 && NAVIGATION_BOTTOM_GROUP_NAMES.includes(groupName);
16160
+ }
15164
16161
  function getGroup(collectionOrView) {
15165
16162
  const trimmed = collectionOrView.group?.trim();
15166
16163
  if (!trimmed || trimmed === "") return NAVIGATION_DEFAULT_GROUP_NAME;
@@ -15562,10 +16559,9 @@ function useTopLevelNavigation(props) {
15562
16559
  return acc;
15563
16560
  }, [])
15564
16561
  ];
15565
- const groupOrderValue = (groupName) => {
15566
- if (groupName === "Admin" || groupName === "Settings" || groupName === "Admin") return 1;
15567
- return 0;
15568
- };
16562
+ const pinnedGroups = /* @__PURE__ */ new Set();
16563
+ for (const view of [...views ?? [], ...adminViews ?? []]) if (view.pinToBottom) pinnedGroups.add(getGroup(view));
16564
+ const groupOrderValue = (groupName) => isBottomPinnedGroupName(groupName) || groupName !== void 0 && pinnedGroups.has(groupName) ? 1 : 0;
15569
16565
  const usedViewsOrder = viewsOrder ?? navigationEntriesOrder;
15570
16566
  navigationEntries = navigationEntries.sort((a, b) => {
15571
16567
  const groupDiff = groupOrderValue(a.group) - groupOrderValue(b.group);
@@ -15591,8 +16587,8 @@ function useTopLevelNavigation(props) {
15591
16587
  ...additionalGroups
15592
16588
  ];
15593
16589
  const uniqueGroupsArray = [...new Set(allDefinedGroups)];
15594
- const lastGroups = uniqueGroupsArray.filter((g) => g === "Admin" || g === "Settings" || g === "Admin");
15595
- const uniqueGroups = [...uniqueGroupsArray.filter((g) => g !== "Admin" && g !== "Settings" && g !== "Admin"), ...lastGroups];
16590
+ const lastGroups = uniqueGroupsArray.filter((g) => groupOrderValue(g) === 1);
16591
+ const uniqueGroups = [...uniqueGroupsArray.filter((g) => groupOrderValue(g) === 0), ...lastGroups];
15596
16592
  const computedTopLevelNav = {
15597
16593
  allowDragAndDrop: plugins?.some((plugin) => plugin.hooks?.allowDragAndDrop) ?? false,
15598
16594
  navigationEntries,
@@ -16401,7 +17397,7 @@ function BlockEntry({ name, index, value, typeField, valueField, properties, aut
16401
17397
  * and tables to the specified properties.
16402
17398
  * @group Form fields
16403
17399
  */
16404
- function DateTimeFieldBinding({ propertyKey, value, setValue, autoFocus, error, showError, disabled, touched, property, includeDescription }) {
17400
+ function DateTimeFieldBinding({ propertyKey, value, setValue, autoFocus, error, showError, disabled, touched, property, includeDescription, hideLabel }) {
16405
17401
  const { locale } = useCustomizationController();
16406
17402
  const internalValue = value || null;
16407
17403
  useClearRestoreValue({
@@ -16419,7 +17415,7 @@ function DateTimeFieldBinding({ propertyKey, value, setValue, autoFocus, error,
16419
17415
  locale,
16420
17416
  error: showError,
16421
17417
  disabled,
16422
- label: /* @__PURE__ */ jsx(LabelWithIcon, {
17418
+ label: hideLabel ? void 0 : /* @__PURE__ */ jsx(LabelWithIcon, {
16423
17419
  icon: getIconForProperty(property, "small"),
16424
17420
  required: property.validation?.required,
16425
17421
  className: showError ? "text-red-500 dark:text-red-500" : "text-text-secondary dark:text-text-secondary-dark",
@@ -16884,7 +17880,7 @@ function MapFieldBinding({ propertyKey, value, showError, error, disabled, prope
16884
17880
  if (!property.properties) throw Error(`You need to specify a 'properties' prop (or specify a custom field) in your map property '${propertyKey}'${property.name ? ` ("${property.name}")` : ""}`);
16885
17881
  const mapProperties = property.properties;
16886
17882
  const mapFormView = /* @__PURE__ */ jsx(Fragment, { children: /* @__PURE__ */ jsx("div", {
16887
- className: cls("py-1 flex flex-col space-y-2", minimalistView && property.admin?.widthPercentage !== void 0 ? "mt-8" : void 0),
17883
+ className: "py-1 flex flex-col space-y-2",
16888
17884
  children: Object.entries(mapProperties).filter(([_, property]) => !isHidden(property)).map(([entryKey, childProperty], index) => {
16889
17885
  const thisDisabled = isReadOnly(childProperty) || Boolean(childProperty.admin?.disabled);
16890
17886
  return /* @__PURE__ */ jsx("div", {
@@ -16914,7 +17910,6 @@ function MapFieldBinding({ propertyKey, value, showError, error, disabled, prope
16914
17910
  expanded
16915
17911
  } });
16916
17912
  },
16917
- className: property.admin?.widthPercentage !== void 0 ? "mt-8" : void 0,
16918
17913
  innerClassName: "px-2 md:px-4 pb-2 md:pb-4 pt-1 md:pt-2 bg-white dark:bg-surface-900",
16919
17914
  title: /* @__PURE__ */ jsx(LabelWithIconAndTooltip, {
16920
17915
  propertyKey,
@@ -16971,7 +17966,7 @@ function canonicalizeMarkdown(md) {
16971
17966
  return normalizeMarkdown(md);
16972
17967
  }
16973
17968
  }
16974
- function MarkdownEditorFieldBinding({ property, propertyKey, value, setValue, includeDescription, showError, error, minimalistView, disabled: disabledProp, isSubmitting, context, customProps }) {
17969
+ function MarkdownEditorFieldBinding({ property, propertyKey, value, setValue, includeDescription, hideLabel, showError, error, minimalistView, disabled: disabledProp, isSubmitting, context, customProps }) {
16975
17970
  useAuthController();
16976
17971
  const disabled = disabledProp || isSubmitting;
16977
17972
  const highlight = customProps?.highlight;
@@ -17086,7 +18081,7 @@ function MarkdownEditorFieldBinding({ property, propertyKey, value, setValue, in
17086
18081
  /* @__PURE__ */ jsxs("div", {
17087
18082
  className: "flex items-center w-full",
17088
18083
  children: [
17089
- /* @__PURE__ */ jsx(LabelWithIconAndTooltip, {
18084
+ !hideLabel && /* @__PURE__ */ jsx(LabelWithIconAndTooltip, {
17090
18085
  propertyKey,
17091
18086
  icon: getIconForProperty(property, "small"),
17092
18087
  required: property.validation?.required,
@@ -17126,7 +18121,7 @@ function MarkdownEditorFieldBinding({ property, propertyKey, value, setValue, in
17126
18121
  * and tables to the specified properties.
17127
18122
  * @group Form fields
17128
18123
  */
17129
- function MultiSelectFieldBinding({ propertyKey, value, setValue, error, showError, disabled, property, includeDescription, size = "large", autoFocus }) {
18124
+ function MultiSelectFieldBinding({ propertyKey, value, setValue, error, showError, disabled, property, includeDescription, hideLabel, size = "large", autoFocus }) {
17130
18125
  const of = property.of;
17131
18126
  if (!of) throw Error("Using wrong component ArrayEnumSelect");
17132
18127
  if (Array.isArray(of)) throw Error("Using array properties instead of single one in `of` in ArrayProperty");
@@ -17143,11 +18138,12 @@ function MultiSelectFieldBinding({ propertyKey, value, setValue, error, showErro
17143
18138
  });
17144
18139
  const validValue = !!value && Array.isArray(value);
17145
18140
  const renderValue = useCallback((enumKey, list) => {
18141
+ const enumValue = enumKey !== void 0 ? getLabelOrConfigFrom(enumValues, enumKey) : void 0;
17146
18142
  return /* @__PURE__ */ jsxs(EnumValuesChip, {
17147
18143
  enumKey,
17148
18144
  enumValues,
17149
18145
  size: "medium",
17150
- children: [(enumKey !== void 0 ? getLabelOrConfigFrom(enumValues, enumKey) : void 0)?.label ?? enumKey, !list && /* @__PURE__ */ jsx("button", {
18146
+ children: [enumValue?.label ?? enumKey, !list && /* @__PURE__ */ jsx("button", {
17151
18147
  className: "ml-1 ring-offset-background rounded-full outline-hidden focus:ring-2 focus:ring-ring focus:ring-offset-2",
17152
18148
  onMouseDown: (e) => {
17153
18149
  e.preventDefault();
@@ -17172,7 +18168,7 @@ function MultiSelectFieldBinding({ propertyKey, value, setValue, error, showErro
17172
18168
  value: validValue ? value.map((v) => v?.toString()) : [],
17173
18169
  disabled,
17174
18170
  modalPopover: true,
17175
- label: /* @__PURE__ */ jsx(LabelWithIconAndTooltip, {
18171
+ label: hideLabel ? void 0 : /* @__PURE__ */ jsx(LabelWithIconAndTooltip, {
17176
18172
  propertyKey,
17177
18173
  icon: getIconForProperty(property, "small"),
17178
18174
  required: property.validation?.required,
@@ -17210,7 +18206,7 @@ function ReferenceFieldBinding(props) {
17210
18206
  if (typeof props.property.path !== "string") return /* @__PURE__ */ jsx(ReadOnlyFieldBinding, { ...props });
17211
18207
  return /* @__PURE__ */ jsx(ReferenceFieldBindingInternal, { ...props });
17212
18208
  }
17213
- function ReferenceFieldBindingInternal({ propertyKey, value, setValue, error, showError, isSubmitting, disabled, minimalistView, touched, autoFocus, property, includeDescription, size = "medium" }) {
18209
+ function ReferenceFieldBindingInternal({ propertyKey, value, setValue, error, showError, isSubmitting, disabled, minimalistView, touched, autoFocus, property, includeDescription, hideLabel, size = "medium" }) {
17214
18210
  if (!property.path) throw new Error("Property path is required for ReferenceFieldBinding");
17215
18211
  const refValue = value;
17216
18212
  useClearRestoreValue({
@@ -17240,7 +18236,7 @@ function ReferenceFieldBindingInternal({ propertyKey, value, setValue, error, sh
17240
18236
  referenceDialogController.open();
17241
18237
  };
17242
18238
  return /* @__PURE__ */ jsxs(Fragment, { children: [
17243
- !minimalistView && /* @__PURE__ */ jsx(LabelWithIconAndTooltip, {
18239
+ !minimalistView && !hideLabel && /* @__PURE__ */ jsx(LabelWithIconAndTooltip, {
17244
18240
  propertyKey,
17245
18241
  icon: getIconForProperty(property, "small"),
17246
18242
  required: property.validation?.required,
@@ -17321,6 +18317,7 @@ function RepeatFieldBinding({ propertyKey, value, error, showError, isSubmitting
17321
18317
  context,
17322
18318
  partOfArray: true,
17323
18319
  minimalistView: false,
18320
+ hideLabel: true,
17324
18321
  autoFocus: internalId === lastAddedId,
17325
18322
  index
17326
18323
  }) });
@@ -17337,8 +18334,7 @@ function RepeatFieldBinding({ propertyKey, value, error, showError, isSubmitting
17337
18334
  canAddElements,
17338
18335
  sortable,
17339
18336
  newDefaultEntry: getDefaultValueFor(property.of),
17340
- onValueChange: (value) => setFieldValue(propertyKey, value),
17341
- className: property.admin?.widthPercentage !== void 0 ? "mt-8" : void 0
18337
+ onValueChange: (value) => setFieldValue(propertyKey, value)
17342
18338
  });
17343
18339
  const title = /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(LabelWithIconAndTooltip, {
17344
18340
  propertyKey,
@@ -17382,7 +18378,7 @@ function RepeatFieldBinding({ propertyKey, value, error, showError, isSubmitting
17382
18378
  * and tables to the specified properties.
17383
18379
  * @group Form fields
17384
18380
  */
17385
- function SelectFieldBinding({ propertyKey, value, setValue, error, showError, disabled, autoFocus, touched, property, includeDescription, size = "large" }) {
18381
+ function SelectFieldBinding({ propertyKey, value, setValue, error, showError, disabled, autoFocus, touched, property, includeDescription, hideLabel, size = "large" }) {
17386
18382
  const enumValues = resolveEnumValues(property.enum ?? []);
17387
18383
  useClearRestoreValue({
17388
18384
  property,
@@ -17401,7 +18397,7 @@ function SelectFieldBinding({ propertyKey, value, setValue, error, showError, di
17401
18397
  fullWidth: true,
17402
18398
  position: "item-aligned",
17403
18399
  inputClassName: cls("w-full"),
17404
- label: /* @__PURE__ */ jsx(PropertyIdCopyTooltip, {
18400
+ label: hideLabel ? void 0 : /* @__PURE__ */ jsx(PropertyIdCopyTooltip, {
17405
18401
  propertyKey,
17406
18402
  children: /* @__PURE__ */ jsx(LabelWithIcon, {
17407
18403
  icon: getIconForProperty(property, "small"),
@@ -17482,13 +18478,13 @@ function StorageItemPreview({ name, property, value, onRemove, disabled, size, p
17482
18478
  }
17483
18479
  //#endregion
17484
18480
  //#region src/form/field_bindings/StorageUploadFieldBinding.tsx
17485
- var dropZoneClasses = "box-border relative pt-[2px] items-center border border-transparent min-h-[254px] outline-none rounded-md duration-200 ease-[cubic-bezier(0.4,0,0.2,1)] focus:border-primary-solid";
18481
+ var dropZoneClasses = "box-border relative pt-[2px] h-[172px] items-center border border-transparent outline-none rounded-md duration-200 ease-[cubic-bezier(0.4,0,0.2,1)] focus:border-primary-solid";
17486
18482
  var disabledClasses = fieldBackgroundDisabledMixin;
17487
18483
  var nonActiveDropClasses = fieldBackgroundHoverMixin;
17488
18484
  var activeDropClasses = "pt-0 border-2 border-solid";
17489
18485
  var acceptDropClasses = "transition-colors duration-200 ease-[cubic-bezier(0,0,0.2,1)] border-2 border-solid border-green-500";
17490
18486
  var rejectDropClasses = "transition-colors duration-200 ease-[cubic-bezier(0,0,0.2,1)] border-2 border-solid border-red-500";
17491
- function StorageUploadFieldBinding({ propertyKey, value, setValue, error, showError, autoFocus, minimalistView, property, includeDescription, context, isSubmitting }) {
18487
+ function StorageUploadFieldBinding({ propertyKey, value, setValue, error, showError, autoFocus, minimalistView, property, includeDescription, hideLabel, context, isSubmitting }) {
17492
18488
  useAuthController();
17493
18489
  const storageSource = useStorageSource();
17494
18490
  const disabled = isReadOnly(property) || !!property.admin?.disabled || isSubmitting || context.disabled;
@@ -17509,7 +18505,7 @@ function StorageUploadFieldBinding({ propertyKey, value, setValue, error, showEr
17509
18505
  setValue
17510
18506
  });
17511
18507
  return /* @__PURE__ */ jsxs(Fragment, { children: [
17512
- !minimalistView && /* @__PURE__ */ jsx(LabelWithIconAndTooltip, {
18508
+ !minimalistView && !hideLabel && /* @__PURE__ */ jsx(LabelWithIconAndTooltip, {
17513
18509
  propertyKey,
17514
18510
  icon: getIconForProperty(property, "small"),
17515
18511
  required: property.validation?.required,
@@ -17579,7 +18575,7 @@ function SortableStorageItem({ id, entry, property, metadata, storagePathBuilder
17579
18575
  style,
17580
18576
  ...attributes,
17581
18577
  ...listeners,
17582
- className: cls("rounded-md m-1"),
18578
+ className: cls("rounded-md shrink-0"),
17583
18579
  tabIndex: -1,
17584
18580
  children: child
17585
18581
  });
@@ -17619,7 +18615,7 @@ function FileDropComponent({ storage, disabled, onFilesAdded, multipleFilesSuppo
17619
18615
  [disabledClasses]: disabled || isDndItemDragging
17620
18616
  }),
17621
18617
  children: [/* @__PURE__ */ jsxs("div", {
17622
- className: cls("flex items-center p-1 px-4 no-scrollbar", multipleFilesSupported && internalValue.length ? "overflow-auto" : "", multipleFilesSupported && internalValue.length ? "min-h-[180px]" : "min-h-[250px]"),
18618
+ className: cls("flex items-center gap-2 p-1 px-3 no-scrollbar", multipleFilesSupported && internalValue.length ? "overflow-x-auto overflow-y-hidden" : "", "h-[126px]"),
17623
18619
  children: [/* @__PURE__ */ jsx("input", {
17624
18620
  autoFocus,
17625
18621
  ...getInputProps()
@@ -17636,10 +18632,10 @@ function FileDropComponent({ storage, disabled, onFilesAdded, multipleFilesSuppo
17636
18632
  isSortable: multipleFilesSupported
17637
18633
  }, entry.id))]
17638
18634
  }), /* @__PURE__ */ jsx("div", {
17639
- className: "flex-grow min-h-[38px] box-border m-2 text-center",
18635
+ className: "box-border mx-3 mb-2.5 h-[34px] flex items-center justify-center text-center",
17640
18636
  children: /* @__PURE__ */ jsx(Typography, {
17641
18637
  align: "center",
17642
- variant: "label",
18638
+ variant: "caption",
17643
18639
  className: disabled ? "text-surface-accent-600 dark:text-surface-accent-500" : "",
17644
18640
  children: helpText
17645
18641
  })
@@ -17742,7 +18738,7 @@ function StorageUpload({ property, name, value, setInternalValue, onChange, mult
17742
18738
  * and tables to the specified properties.
17743
18739
  * @group Form fields
17744
18740
  */
17745
- var SwitchFieldBinding = function SwitchFieldBinding({ propertyKey, value, setValue, error, showError, autoFocus, disabled, size = "large", property, includeDescription }) {
18741
+ var SwitchFieldBinding = function SwitchFieldBinding({ propertyKey, value, setValue, error, showError, autoFocus, disabled, size = "large", property, includeDescription, hideLabel }) {
17746
18742
  useClearRestoreValue({
17747
18743
  property,
17748
18744
  value,
@@ -17754,8 +18750,8 @@ var SwitchFieldBinding = function SwitchFieldBinding({ propertyKey, value, setVa
17754
18750
  value,
17755
18751
  onValueChange: (v) => setValue(v),
17756
18752
  error: showError,
17757
- className: property.admin?.widthPercentage !== void 0 ? "mt-8" : void 0,
17758
- label: /* @__PURE__ */ jsx(LabelWithIcon, {
18753
+ position: hideLabel ? "start" : void 0,
18754
+ label: hideLabel ? void 0 : /* @__PURE__ */ jsx(LabelWithIcon, {
17759
18755
  icon: getIconForProperty(property, "small"),
17760
18756
  required: property.validation?.required,
17761
18757
  title: property.name ?? propertyKey
@@ -17780,7 +18776,7 @@ var SwitchFieldBinding = function SwitchFieldBinding({ propertyKey, value, setVa
17780
18776
  * and tables to the specified properties.
17781
18777
  * @group Form fields
17782
18778
  */
17783
- function TextFieldBinding({ propertyKey, value, setValue, error, showError, disabled, autoFocus, property, includeDescription, size = "large" }) {
18779
+ function TextFieldBinding({ propertyKey, value, setValue, error, showError, disabled, autoFocus, property, includeDescription, hideLabel, size = "large" }) {
17784
18780
  let multiline;
17785
18781
  let url;
17786
18782
  if (property.type === "string") {
@@ -17813,7 +18809,7 @@ function TextFieldBinding({ propertyKey, value, setValue, error, showError, disa
17813
18809
  if (property.email) inputType = "email";
17814
18810
  else if (property.admin?.urlPreview) inputType = "url";
17815
18811
  }
17816
- const label = /* @__PURE__ */ jsx(LabelWithIcon, {
18812
+ const label = hideLabel ? void 0 : /* @__PURE__ */ jsx(LabelWithIcon, {
17817
18813
  icon: getIconForProperty(property, "small"),
17818
18814
  required: property.validation?.required || property.isId === true,
17819
18815
  title: property.name ?? propertyKey
@@ -17822,9 +18818,9 @@ function TextFieldBinding({ propertyKey, value, setValue, error, showError, disa
17822
18818
  /* @__PURE__ */ jsx(PropertyIdCopyTooltip, {
17823
18819
  propertyKey,
17824
18820
  children: isMultiline ? /* @__PURE__ */ jsxs("div", {
17825
- className: cls("rounded-md relative max-w-full min-h-[64px]", fieldBackgroundMixin, fieldBackgroundHoverMixin, showError && error ? "border border-red-500 dark:border-red-600" : "", property.admin?.widthPercentage !== void 0 ? "mt-8" : void 0),
18821
+ className: cls("rounded-md relative max-w-full min-h-[64px]", fieldBackgroundMixin, fieldBackgroundHoverMixin, showError && error ? "border border-red-500 dark:border-red-600" : ""),
17826
18822
  children: [
17827
- /* @__PURE__ */ jsx("div", {
18823
+ label && /* @__PURE__ */ jsx("div", {
17828
18824
  className: "pointer-events-none absolute top-1 text-xs font-medium px-3 text-text-secondary dark:text-text-secondary-dark",
17829
18825
  children: label
17830
18826
  }),
@@ -17833,7 +18829,7 @@ function TextFieldBinding({ propertyKey, value, setValue, error, showError, disa
17833
18829
  onChange,
17834
18830
  autoFocus,
17835
18831
  disabled,
17836
- className: cls("rounded-md resize-none w-full outline-none p-[32px] text-base bg-transparent min-h-[64px] px-3 pt-8", disabled && "outline-none opacity-50 text-surface-accent-600 dark:text-surface-accent-500", showError && error ? "text-red-500 dark:text-red-600" : "")
18832
+ className: cls("rounded-md resize-none w-full outline-none text-base bg-transparent min-h-[64px] px-3", label ? "pt-8 pb-2" : "py-2", disabled && "outline-none opacity-50 text-surface-accent-600 dark:text-surface-accent-500", showError && error ? "text-red-500 dark:text-red-600" : "")
17837
18833
  }),
17838
18834
  property.admin?.clearable && /* @__PURE__ */ jsx("div", {
17839
18835
  className: "flex flex-row justify-center items-center absolute h-full right-0 top-0 mr-4",
@@ -17848,12 +18844,7 @@ function TextFieldBinding({ propertyKey, value, setValue, error, showError, disa
17848
18844
  value: displayValue,
17849
18845
  onChange,
17850
18846
  autoFocus,
17851
- className: property.admin?.widthPercentage !== void 0 ? "mt-8" : void 0,
17852
- label: /* @__PURE__ */ jsx(LabelWithIcon, {
17853
- icon: getIconForProperty(property, "small"),
17854
- required: property.validation?.required || property.isId === true,
17855
- title: property.name ?? propertyKey
17856
- }),
18847
+ label,
17857
18848
  type: inputType,
17858
18849
  disabled,
17859
18850
  endAdornment: property.admin?.clearable && /* @__PURE__ */ jsx(IconButton, {
@@ -17892,7 +18883,7 @@ function TextFieldBinding({ propertyKey, value, setValue, error, showError, disa
17892
18883
  * Vector object structure expected by Rebase driver.
17893
18884
  * @group Form fields
17894
18885
  */
17895
- function VectorFieldBinding({ propertyKey, value, setValue, error, showError, disabled, autoFocus, property, includeDescription, size = "large" }) {
18886
+ function VectorFieldBinding({ propertyKey, value, setValue, error, showError, disabled, autoFocus, property, includeDescription, hideLabel, size = "large" }) {
17896
18887
  const isVectorObject = (val) => {
17897
18888
  return typeof val === "object" && val !== null && "value" in val;
17898
18889
  };
@@ -17922,7 +18913,7 @@ function VectorFieldBinding({ propertyKey, value, setValue, error, showError, di
17922
18913
  value: raw.split(",").map((str) => str.trim()).filter((str) => str !== "").map(Number)
17923
18914
  });
17924
18915
  };
17925
- const label = /* @__PURE__ */ jsx(LabelWithIcon, {
18916
+ const label = hideLabel ? null : /* @__PURE__ */ jsx(LabelWithIcon, {
17926
18917
  icon: getIconForProperty(property, "small"),
17927
18918
  required: property.validation?.required,
17928
18919
  title: property.name ?? propertyKey
@@ -18065,7 +19056,7 @@ var inRange = (n, [min, max]) => n >= min && n <= max;
18065
19056
  *
18066
19057
  * @group Form fields
18067
19058
  */
18068
- function GeopointFieldBinding({ propertyKey, value, setValue, error, showError, disabled, autoFocus, property, includeDescription, size = "large" }) {
19059
+ function GeopointFieldBinding({ propertyKey, value, setValue, error, showError, disabled, autoFocus, property, includeDescription, hideLabel, size = "large" }) {
18069
19060
  const coordinates = readCoordinates(value);
18070
19061
  const [latText, setLatText] = useState(() => coordinates ? String(coordinates.latitude) : "");
18071
19062
  const [lngText, setLngText] = useState(() => coordinates ? String(coordinates.longitude) : "");
@@ -18111,7 +19102,7 @@ function GeopointFieldBinding({ propertyKey, value, setValue, error, showError,
18111
19102
  children: [
18112
19103
  /* @__PURE__ */ jsxs("div", {
18113
19104
  className: "flex items-center justify-between mb-1",
18114
- children: [/* @__PURE__ */ jsx(LabelWithIcon, {
19105
+ children: [!hideLabel && /* @__PURE__ */ jsx(LabelWithIcon, {
18115
19106
  icon: getIconForProperty(property, "small"),
18116
19107
  required: property.validation?.required,
18117
19108
  title: property.name ?? propertyKey
@@ -18199,7 +19190,7 @@ function humanSize(bytes) {
18199
19190
  *
18200
19191
  * @group Form fields
18201
19192
  */
18202
- function BinaryFieldBinding({ propertyKey, value, setValue, error, showError, disabled, autoFocus, property, includeDescription, size = "large" }) {
19193
+ function BinaryFieldBinding({ propertyKey, value, setValue, error, showError, disabled, autoFocus, property, includeDescription, hideLabel, size = "large" }) {
18203
19194
  const [text, setText] = useState(typeof value === "string" ? value : "");
18204
19195
  const [isEditing, setIsEditing] = useState(false);
18205
19196
  useEffect(() => {
@@ -18230,7 +19221,7 @@ function BinaryFieldBinding({ propertyKey, value, setValue, error, showError, di
18230
19221
  children: [
18231
19222
  /* @__PURE__ */ jsx("div", {
18232
19223
  className: "flex items-center justify-between mb-1",
18233
- children: /* @__PURE__ */ jsx(LabelWithIcon, {
19224
+ children: !hideLabel && /* @__PURE__ */ jsx(LabelWithIcon, {
18234
19225
  icon: getIconForProperty(property, "small"),
18235
19226
  required: property.validation?.required,
18236
19227
  title: property.name ?? propertyKey
@@ -18426,14 +19417,14 @@ function RelationFieldBinding(props) {
18426
19417
  relation
18427
19418
  });
18428
19419
  }
18429
- function RelationSelectorBinding({ propertyKey, value, size, error, showError, disabled, isSubmitting, property, includeDescription, setValue, relation, manyRelation }) {
19420
+ function RelationSelectorBinding({ propertyKey, value, size, error, showError, disabled, isSubmitting, property, includeDescription, hideLabel, setValue, relation, manyRelation }) {
18430
19421
  const singleValue = (!manyRelation && value && !Array.isArray(value) ? normalizeToEntityRelation(value) : null) ?? null;
18431
19422
  const multipleValue = manyRelation && Array.isArray(value) ? value : [];
18432
19423
  const selectorSize = size === "large" ? "medium" : size;
18433
19424
  return /* @__PURE__ */ jsxs("div", {
18434
19425
  className: "",
18435
19426
  children: [
18436
- /* @__PURE__ */ jsx(LabelWithIconAndTooltip, {
19427
+ !hideLabel && /* @__PURE__ */ jsx(LabelWithIconAndTooltip, {
18437
19428
  propertyKey,
18438
19429
  icon: getIconForProperty(property, "small"),
18439
19430
  required: property.validation?.required,
@@ -18461,7 +19452,7 @@ function RelationSelectorBinding({ propertyKey, value, size, error, showError, d
18461
19452
  ]
18462
19453
  });
18463
19454
  }
18464
- function SingleRelationFieldBinding({ propertyKey, value, size, error, showError, disabled, isSubmitting, property, includeDescription, setValue, relation }) {
19455
+ function SingleRelationFieldBinding({ propertyKey, value, size, error, showError, disabled, isSubmitting, property, includeDescription, hideLabel, setValue, relation }) {
18465
19456
  const normalizedValue = value && !Array.isArray(value) ? normalizeToEntityRelation(value) : null;
18466
19457
  const validValue = !!normalizedValue;
18467
19458
  const collection = relation?.target();
@@ -18483,7 +19474,7 @@ function SingleRelationFieldBinding({ propertyKey, value, size, error, showError
18483
19474
  };
18484
19475
  const usedRelation = Array.isArray(value) ? void 0 : normalizedValue ?? void 0;
18485
19476
  return /* @__PURE__ */ jsxs(Fragment, { children: [
18486
- /* @__PURE__ */ jsx(LabelWithIconAndTooltip, {
19477
+ !hideLabel && /* @__PURE__ */ jsx(LabelWithIconAndTooltip, {
18487
19478
  propertyKey,
18488
19479
  icon: getIconForProperty(property, "small"),
18489
19480
  required: property.validation?.required,
@@ -18532,10 +19523,10 @@ function SingleRelationFieldBinding({ propertyKey, value, size, error, showError
18532
19523
  * and tables to the specified properties.
18533
19524
  * @group Form fields
18534
19525
  */
18535
- function UserSelectFieldBinding({ propertyKey, value, setValue, error, showError, disabled, autoFocus, touched, property, includeDescription, size = "large" }) {
19526
+ function UserSelectFieldBinding({ propertyKey, value, setValue, error, showError, disabled, autoFocus, touched, property, includeDescription, hideLabel, size = "large" }) {
18536
19527
  const selectorSize = size === "large" ? "medium" : size;
18537
19528
  return /* @__PURE__ */ jsxs(Fragment, { children: [
18538
- /* @__PURE__ */ jsx(PropertyIdCopyTooltip, {
19529
+ !hideLabel && /* @__PURE__ */ jsx(PropertyIdCopyTooltip, {
18539
19530
  propertyKey,
18540
19531
  children: /* @__PURE__ */ jsx(LabelWithIcon, {
18541
19532
  icon: getIconForProperty(property, "small"),
@@ -19047,7 +20038,7 @@ var PropertyFieldBinding = React.memo(PropertyFieldBindingInternal, (a, b) => {
19047
20038
  if (shouldPropertyReRender(b.property)) return false;
19048
20039
  return false;
19049
20040
  });
19050
- function PropertyFieldBindingInternal({ propertyKey, property, context, includeDescription, underlyingValueHasChanged, disabled: disabledProp, partOfArray, partOfBlock, minimalistView, autoFocus, index, size, onPropertyChange }) {
20041
+ function PropertyFieldBindingInternal({ propertyKey, property, context, includeDescription, hideLabel, underlyingValueHasChanged, disabled: disabledProp, partOfArray, partOfBlock, minimalistView, autoFocus, index, size, onPropertyChange }) {
19051
20042
  const authController = useAuthController();
19052
20043
  const customizationController = useCustomizationController();
19053
20044
  return /* @__PURE__ */ jsx(Field, {
@@ -19099,6 +20090,7 @@ function PropertyFieldBindingInternal({ propertyKey, property, context, includeD
19099
20090
  propertyKey,
19100
20091
  property: resolvedProperty,
19101
20092
  includeDescription,
20093
+ hideLabel,
19102
20094
  underlyingValueHasChanged,
19103
20095
  context,
19104
20096
  disabled,
@@ -19114,7 +20106,7 @@ function PropertyFieldBindingInternal({ propertyKey, property, context, includeD
19114
20106
  }
19115
20107
  }, propertyKey);
19116
20108
  }
19117
- function FieldInternal({ Component, componentProps: { propertyKey, property, includeDescription, underlyingValueHasChanged, partOfArray, partOfBlock, minimalistView, autoFocus, context, disabled, size, onPropertyChange }, formexFieldProps }) {
20109
+ function FieldInternal({ Component, componentProps: { propertyKey, property, includeDescription, hideLabel, underlyingValueHasChanged, partOfArray, partOfBlock, minimalistView, autoFocus, context, disabled, size, onPropertyChange }, formexFieldProps }) {
19118
20110
  const { plugins } = useCustomizationController();
19119
20111
  const customFieldProps = property.admin?.customProps;
19120
20112
  const value = formexFieldProps.field.value;
@@ -19148,6 +20140,7 @@ function FieldInternal({ Component, componentProps: { propertyKey, property, inc
19148
20140
  showError,
19149
20141
  isSubmitting,
19150
20142
  includeDescription: includeDescription ?? true,
20143
+ hideLabel: hideLabel ?? false,
19151
20144
  property,
19152
20145
  disabled: disabled ?? false,
19153
20146
  underlyingValueHasChanged: underlyingValueHasChanged ?? false,
@@ -19227,22 +20220,18 @@ var ErrorFocus = ({ containerRef }) => {
19227
20220
  };
19228
20221
  //#endregion
19229
20222
  //#region src/form/EntityFormActions.tsx
19230
- function EntityFormActions({ path, collection, entity, layout, savingError, formex, disabled, status, pluginActions, openEntityMode, navigateBack, formContext }) {
19231
- const bottomActionsProps = {
19232
- path,
19233
- savingError,
19234
- entity,
19235
- collection,
19236
- disabled,
19237
- status,
19238
- pluginActions,
19239
- openEntityMode,
19240
- navigateBack,
19241
- formContext,
19242
- formex,
19243
- className: layout === "responsive" ? "@6xl:hidden" : void 0
19244
- };
19245
- const sideActionsProps = {
20223
+ /**
20224
+ * The form's own action bar.
20225
+ *
20226
+ * There used to be two variants: this one, and a `w-80 2xl:w-96` sticky rail
20227
+ * holding the same two buttons above several hundred pixels of nothing — a
20228
+ * fifth of a 1440px viewport reserved for a Save button. The rail is gone. When
20229
+ * the container supplies its own actions (the entity view's identity bar does)
20230
+ * it passes `showDefaultActions={false}`, and this renders only what the bar
20231
+ * cannot host: the saving error, plugin actions, and collection form actions.
20232
+ */
20233
+ function EntityFormActions({ path, collection, entity, savingError, formex, disabled, status, pluginActions, openEntityMode, navigateBack, formContext, showDefaultActions = true }) {
20234
+ return buildBottomActions({
19246
20235
  path,
19247
20236
  savingError,
19248
20237
  entity,
@@ -19254,17 +20243,16 @@ function EntityFormActions({ path, collection, entity, layout, savingError, form
19254
20243
  navigateBack,
19255
20244
  formContext,
19256
20245
  formex,
19257
- className: layout === "responsive" ? "hidden @6xl:flex" : void 0
19258
- };
19259
- const bottomActions = buildBottomActions(bottomActionsProps);
19260
- const sideActions = buildSideActions(sideActionsProps);
19261
- if (layout === "responsive") return /* @__PURE__ */ jsxs(Fragment, { children: [bottomActions, sideActions] });
19262
- return layout === "bottom" ? bottomActions : sideActions;
20246
+ showDefaultActions
20247
+ });
19263
20248
  }
19264
- function buildBottomActions({ savingError, entity, path, formActions, collection, disabled, status, pluginActions, openEntityMode, navigateBack, formContext, formex, className }) {
20249
+ function buildBottomActions({ savingError, entity, path, formActions, collection, disabled, status, pluginActions, openEntityMode, navigateBack, formContext, formex, showDefaultActions, className }) {
19265
20250
  const hasErrors = Object.keys(formex.errors).length > 0 && formex.submitCount > 0;
20251
+ const entityActions = entity ? formActions ?? [] : [];
20252
+ const hasPluginActions = Boolean(pluginActions && pluginActions.length > 0);
20253
+ if (!showDefaultActions && !savingError && !entityActions.length && !hasPluginActions) return null;
19266
20254
  return /* @__PURE__ */ jsxs(DialogActions, {
19267
- position: "absolute",
20255
+ position: "sticky",
19268
20256
  className,
19269
20257
  children: [
19270
20258
  savingError && /* @__PURE__ */ jsx("div", {
@@ -19274,9 +20262,9 @@ function buildBottomActions({ savingError, entity, path, formActions, collection
19274
20262
  children: savingError.message
19275
20263
  })
19276
20264
  }),
19277
- entity && (formActions ?? []).length > 0 && /* @__PURE__ */ jsx("div", {
20265
+ entityActions.length > 0 && /* @__PURE__ */ jsx("div", {
19278
20266
  className: "grow flex overflow-auto no-scrollbar",
19279
- children: (formActions ?? []).map((action) => /* @__PURE__ */ jsx(IconButton, {
20267
+ children: entityActions.map((action) => /* @__PURE__ */ jsx(IconButton, {
19280
20268
  color: "primary",
19281
20269
  onClick: (event) => {
19282
20270
  event.stopPropagation();
@@ -19296,14 +20284,13 @@ function buildBottomActions({ savingError, entity, path, formActions, collection
19296
20284
  }, action.name))
19297
20285
  }),
19298
20286
  pluginActions,
19299
- /* @__PURE__ */ jsx(Button, {
20287
+ showDefaultActions && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Button, {
19300
20288
  variant: "text",
19301
20289
  disabled: disabled || formex.isSubmitting,
19302
20290
  color: "primary",
19303
20291
  type: "reset",
19304
20292
  children: status === "existing" ? "Discard" : "Clear"
19305
- }),
19306
- /* @__PURE__ */ jsx(Tooltip, {
20293
+ }), /* @__PURE__ */ jsx(Tooltip, {
19307
20294
  title: hasErrors ? "Fix highlighted errors before saving" : void 0,
19308
20295
  children: /* @__PURE__ */ jsxs(LoadingButton, {
19309
20296
  variant: "filled",
@@ -19318,47 +20305,7 @@ function buildBottomActions({ savingError, entity, path, formActions, collection
19318
20305
  status === "new" && "Create"
19319
20306
  ]
19320
20307
  })
19321
- })
19322
- ]
19323
- });
19324
- }
19325
- function buildSideActions({ savingError, entity, formActions, path, openEntityMode, collection, disabled, status, pluginActions, formex, className }) {
19326
- const hasErrors = Object.keys(formex.errors).length > 0 && formex.submitCount > 0;
19327
- return /* @__PURE__ */ jsxs("div", {
19328
- className: cls("overflow-auto h-full flex flex-col gap-2 w-80 2xl:w-96 px-4 py-16 sticky top-0 border-l", defaultBorderMixin, className),
19329
- children: [
19330
- /* @__PURE__ */ jsx(Tooltip, {
19331
- title: hasErrors ? "Fix highlighted errors before saving" : void 0,
19332
- children: /* @__PURE__ */ jsxs(LoadingButton, {
19333
- fullWidth: true,
19334
- variant: "filled",
19335
- color: "primary",
19336
- type: "submit",
19337
- loading: formex.isSubmitting,
19338
- startIcon: hasErrors ? /* @__PURE__ */ jsx(AlertCircleIcon, {}) : void 0,
19339
- disabled: disabled || formex.isSubmitting,
19340
- children: [
19341
- status === "existing" && "Save",
19342
- status === "copy" && "Create copy",
19343
- status === "new" && "Create"
19344
- ]
19345
- })
19346
- }),
19347
- /* @__PURE__ */ jsx(Button, {
19348
- fullWidth: true,
19349
- variant: "text",
19350
- disabled: disabled || formex.isSubmitting,
19351
- type: "reset",
19352
- children: status === "existing" ? "Discard" : "Clear"
19353
- }),
19354
- pluginActions,
19355
- savingError && /* @__PURE__ */ jsx("div", {
19356
- className: "text-right",
19357
- children: /* @__PURE__ */ jsx(Typography, {
19358
- color: "error",
19359
- children: savingError.message
19360
- })
19361
- })
20308
+ })] })
19362
20309
  ]
19363
20310
  });
19364
20311
  }
@@ -19520,7 +20467,7 @@ function getChanges(source, comparison) {
19520
20467
  const changes = {};
19521
20468
  if (!source) return {};
19522
20469
  if (!comparison) return source;
19523
- const allKeys = Array.from(new Set([...Object.keys(source), ...Object.keys(comparison)]));
20470
+ const allKeys = Array.from(/* @__PURE__ */ new Set([...Object.keys(source), ...Object.keys(comparison)]));
19524
20471
  for (const key of allKeys) {
19525
20472
  const sourceValue = source[key];
19526
20473
  const comparisonValue = comparison[key];
@@ -19581,8 +20528,7 @@ function zodToFormErrors(zodError) {
19581
20528
  *
19582
20529
  * @group Components
19583
20530
  */
19584
- function EntityForm({ path, entityId: entityIdProp, collection, onValuesModified, onIdChange, onSaved, entity, initialDirtyValues, onFormContextReady, forceActionsAtTheBottom, initialStatus, className, onStatusChange, onEntityChange, openEntityMode = "full_screen", formex: formexProp, disabled: disabledProp, Builder, EntityFormActionsComponent = EntityFormActions, showDefaultActions = true, showEntityPath = true, navigateBack: navigateBackProp, children, onSubmit: onSubmitProp, onValuesChangeDeferred: onValuesChangeDeferredProp, onReset: onResetProp, uniqueFieldValidator: uniqueFieldValidatorProp, beforeFields, afterFields, pluginActions: pluginActionsProp, computedInitialValues, hasLocalChanges: hasLocalChangesProp, localChangesData, manualApplyLocalChanges, localChangesCacheKey, onClearLocalChanges }) {
19585
- useCustomizationController();
20531
+ function EntityForm({ path, entityId: entityIdProp, collection, onValuesModified, onIdChange, onSaved, entity, initialDirtyValues, onFormContextReady, initialStatus, className, onStatusChange, onEntityChange, openEntityMode = "full_screen", formex: formexProp, disabled: disabledProp, Builder, EntityFormActionsComponent = EntityFormActions, showDefaultActions = true, navigateBack: navigateBackProp, children, onSubmit: onSubmitProp, onValuesChangeDeferred: onValuesChangeDeferredProp, onReset: onResetProp, uniqueFieldValidator: uniqueFieldValidatorProp, beforeFields, afterFields, pluginActions: pluginActionsProp, computedInitialValues, hasLocalChanges: hasLocalChangesProp, localChangesData, manualApplyLocalChanges, localChangesCacheKey, onClearLocalChanges }) {
19586
20532
  const navigateBack = useCallback(() => {
19587
20533
  if (navigateBackProp) navigateBackProp();
19588
20534
  }, [navigateBackProp]);
@@ -19604,7 +20550,6 @@ function EntityForm({ path, entityId: entityIdProp, collection, onValuesModified
19604
20550
  else return entityIdProp;
19605
20551
  }, [entityIdProp, status]);
19606
20552
  const [entityId, setEntityId] = useState(initialEntityId);
19607
- const [entityIdError, setEntityIdError] = useState(false);
19608
20553
  const [savingError, setSavingError] = useState();
19609
20554
  const [isSavingAutoSave, setIsSavingAutoSave] = useState(false);
19610
20555
  const [discardDialogOpen, setDiscardDialogOpen] = useState(false);
@@ -19623,7 +20568,6 @@ function EntityForm({ path, entityId: entityIdProp, collection, onValuesModified
19623
20568
  const hasLocalChanges = hasLocalChangesProp !== void 0 ? hasLocalChangesProp && !localChangesCleared : false;
19624
20569
  const onSubmit = (values, formexController) => {
19625
20570
  setSavingError(void 0);
19626
- setEntityIdError(false);
19627
20571
  if (status === "existing") {
19628
20572
  if (!entity?.id) throw Error("Form misconfiguration when saving, no id for existing entity");
19629
20573
  } else if (status !== "new" && status !== "copy") throw Error("New FormType added, check EntityForm");
@@ -19731,7 +20675,8 @@ function EntityForm({ path, entityId: entityIdProp, collection, onValuesModified
19731
20675
  savingError,
19732
20676
  status,
19733
20677
  openEntityMode,
19734
- disabled
20678
+ disabled,
20679
+ isSaving: formex.isSubmitting || isSavingAutoSave
19735
20680
  };
19736
20681
  useEffect(() => {
19737
20682
  onFormContextReady?.(formContext);
@@ -19739,13 +20684,13 @@ function EntityForm({ path, entityId: entityIdProp, collection, onValuesModified
19739
20684
  formex.version,
19740
20685
  collection,
19741
20686
  entityId,
19742
- path
20687
+ path,
20688
+ formex.dirty,
20689
+ formex.isSubmitting,
20690
+ formex.submitCount,
20691
+ isSavingAutoSave
19743
20692
  ]);
19744
20693
  const actionsDisabled = disabled || formex.isSubmitting || status === "existing" && !formex.dirty || Boolean(disabledProp);
19745
- const titlePropertyKey = getEntityTitlePropertyKeyForEntity(collection, formex.values, entityId);
19746
- const rawTitle = formex.values && titlePropertyKey ? getValueInPath(formex.values, titlePropertyKey) : void 0;
19747
- const titleUser = useResolvedUser(isUserSelectProperty(collection, titlePropertyKey) && typeof rawTitle === "string" ? rawTitle : void 0);
19748
- const title = titleUser ? getUserLabel(titleUser) : rawTitle !== void 0 && rawTitle !== null ? resolveTitleToString(rawTitle) : collection.singularName ?? collection.name;
19749
20694
  const modified = formex.dirty;
19750
20695
  useEffect(() => {
19751
20696
  if (!autoSave) onValuesModified?.(modified, formex.values);
@@ -19779,6 +20724,106 @@ function EntityForm({ path, entityId: entityIdProp, collection, onValuesModified
19779
20724
  formex.setFieldValue
19780
20725
  ]);
19781
20726
  const formFieldKeys = getFormFieldKeys(collection);
20727
+ const layout = useMemo(() => resolveFormLayout({
20728
+ collection,
20729
+ fieldKeys: formFieldKeys,
20730
+ status
20731
+ }), [
20732
+ collection,
20733
+ formFieldKeys.join(","),
20734
+ status
20735
+ ]);
20736
+ const formElementRef = useRef(null);
20737
+ const railVisible = useRailVisible(formElementRef);
20738
+ const showRail = !Builder && layout.hasRail && railVisible;
20739
+ const [collapsedSections, setCollapsedSections] = useState(() => new Set(layout.sections.filter((s) => s.collapsed).map((s) => s.key)));
20740
+ const toggleSection = useCallback((key) => {
20741
+ setCollapsedSections((prev) => {
20742
+ const next = new Set(prev);
20743
+ if (next.has(key)) next.delete(key);
20744
+ else next.add(key);
20745
+ return next;
20746
+ });
20747
+ }, []);
20748
+ const errorKeys = useMemo(() => new Set(formex.submitCount > 0 ? Object.keys(formex.errors) : []), [formex.errors, formex.submitCount]);
20749
+ const autoFocusKey = useMemo(() => {
20750
+ if (status !== "new" && status !== "copy") return void 0;
20751
+ return [...layout.sections.flatMap((s) => s.fields), ...layout.sidebar].find((field) => {
20752
+ const property = collection.properties?.[field.key];
20753
+ return property ? !isFieldDisabled(property) : false;
20754
+ })?.key;
20755
+ }, [
20756
+ layout,
20757
+ collection,
20758
+ status,
20759
+ disabledProp
20760
+ ]);
20761
+ function isFieldDisabled(property) {
20762
+ const isNew = status === "new" || status === "copy";
20763
+ const isIdAndAuto = (property.type === "string" || property.type === "number") && "isId" in property && typeof property.isId === "string" && property.isId !== "manual";
20764
+ return Boolean(disabledProp || !autoSave && formex.isSubmitting || isReadOnly(property) || Boolean(property.admin?.disabled) || !isNew && "isId" in property && Boolean(property.isId) || isNew && isIdAndAuto);
20765
+ }
20766
+ const renderField = (field) => {
20767
+ if (field.additional) {
20768
+ const additionalField = collection.additionalFields?.find((f) => f.key === field.key);
20769
+ if (!additionalField || !entity) return null;
20770
+ const AdditionalFieldBuilder = additionalField.Builder;
20771
+ if (!AdditionalFieldBuilder && !additionalField.value) throw new Error("When using additional fields you need to provide a Builder or a value");
20772
+ const additionalFieldContext = formContext;
20773
+ const child = AdditionalFieldBuilder ? /* @__PURE__ */ jsx(AdditionalFieldBuilder, {
20774
+ entity,
20775
+ context: additionalFieldContext
20776
+ }) : /* @__PURE__ */ jsx("div", {
20777
+ className: "w-full",
20778
+ children: additionalField.value?.({
20779
+ entity,
20780
+ context: additionalFieldContext
20781
+ })?.toString()
20782
+ });
20783
+ return /* @__PURE__ */ jsxs("div", {
20784
+ className: spanClass(field.span),
20785
+ children: [/* @__PURE__ */ jsx(LabelWithIconAndTooltip, {
20786
+ propertyKey: field.key,
20787
+ icon: /* @__PURE__ */ jsx(AlignLeftIcon, { size: iconSize.small }),
20788
+ title: additionalField.name,
20789
+ className: "text-text-secondary dark:text-text-secondary-dark"
20790
+ }), /* @__PURE__ */ jsx("div", {
20791
+ className: cls(paperMixin, "w-full min-h-14 p-4 md:p-6 overflow-x-auto no-scrollbar"),
20792
+ children: /* @__PURE__ */ jsx(ErrorBoundary, { children: child })
20793
+ })]
20794
+ }, `additional_${field.key}`);
20795
+ }
20796
+ const property = collection.properties?.[field.key];
20797
+ if (!property) {
20798
+ console.warn(`Property ${field.key} not found in collection ${collection.name} in properties or additional fields. Skipping.`);
20799
+ return null;
20800
+ }
20801
+ const underlyingValueHasChanged = !!underlyingChanges && Object.keys(underlyingChanges).includes(field.key) && formex.touched[field.key];
20802
+ const disabled = isFieldDisabled(property);
20803
+ const selfLabelling = isSelfLabellingProperty(property);
20804
+ const cmsFormFieldProps = {
20805
+ propertyKey: field.key,
20806
+ disabled,
20807
+ property,
20808
+ includeDescription: false,
20809
+ hideLabel: !selfLabelling,
20810
+ underlyingValueHasChanged: underlyingValueHasChanged && !autoSave,
20811
+ context: formContext,
20812
+ partOfArray: false,
20813
+ minimalistView: false,
20814
+ size: "large",
20815
+ autoFocus: autoFocusKey === field.key
20816
+ };
20817
+ return /* @__PURE__ */ jsx("div", {
20818
+ className: spanClass(field.span),
20819
+ children: /* @__PURE__ */ jsx(FieldBlock, {
20820
+ propertyKey: field.key,
20821
+ property,
20822
+ showLabel: !selfLabelling,
20823
+ children: /* @__PURE__ */ jsx(PropertyFieldBinding, { ...cmsFormFieldProps })
20824
+ })
20825
+ }, `field_${field.key}`);
20826
+ };
19782
20827
  const formFields = () => {
19783
20828
  if (Builder) return /* @__PURE__ */ jsx(Builder, {
19784
20829
  collection,
@@ -19786,99 +20831,29 @@ function EntityForm({ path, entityId: entityIdProp, collection, onValuesModified
19786
20831
  modifiedValues: formex.values,
19787
20832
  formContext
19788
20833
  });
19789
- const isNewEntity = status === "new" || status === "copy";
19790
- let firstFocusableIndex = -1;
19791
- return /* @__PURE__ */ jsx(FormLayout, { children: formFieldKeys.map((key) => {
19792
- const property = collection.properties?.[key];
19793
- if (property) {
19794
- const underlyingValueHasChanged = !!underlyingChanges && Object.keys(underlyingChanges).includes(key) && formex.touched[key];
19795
- const isNew = status === "new" || status === "copy";
19796
- const isIdAndAuto = (property.type === "string" || property.type === "number") && "isId" in property && typeof property.isId === "string" && property.isId !== "manual";
19797
- const disabled = disabledProp || !autoSave && formex.isSubmitting || isReadOnly(property) || Boolean(property.admin?.disabled) || !isNew && "isId" in property && Boolean(property.isId) || isNew && isIdAndAuto;
19798
- if (isHidden(property)) return null;
19799
- const widthPercentage = property.admin?.widthPercentage ?? 100;
19800
- const shouldAutoFocus = isNewEntity && !disabled && firstFocusableIndex === -1;
19801
- if (shouldAutoFocus) firstFocusableIndex = 0;
19802
- return /* @__PURE__ */ jsx(FormEntry, {
19803
- propertyKey: key,
19804
- widthPercentage,
19805
- children: /* @__PURE__ */ jsx(PropertyFieldBinding, {
19806
- propertyKey: key,
19807
- disabled,
19808
- property,
19809
- includeDescription: Boolean(property.description),
19810
- underlyingValueHasChanged: underlyingValueHasChanged && !autoSave,
19811
- context: formContext,
19812
- partOfArray: false,
19813
- minimalistView: false,
19814
- autoFocus: shouldAutoFocus
19815
- })
19816
- }, `field_${key}`);
19817
- }
19818
- const additionalField = collection.additionalFields?.find((f) => f.key === key);
19819
- if (additionalField && entity) {
19820
- const AdditionalFieldBuilder = additionalField.Builder;
19821
- if (!AdditionalFieldBuilder && !additionalField.value) throw new Error("When using additional fields you need to provide a Builder or a value");
19822
- const additionalFieldContext = formContext;
19823
- const child = AdditionalFieldBuilder ? /* @__PURE__ */ jsx(AdditionalFieldBuilder, {
19824
- entity,
19825
- context: additionalFieldContext
19826
- }) : /* @__PURE__ */ jsx("div", {
19827
- className: "w-full",
19828
- children: additionalField.value?.({
19829
- entity,
19830
- context: additionalFieldContext
19831
- })?.toString()
19832
- });
19833
- return /* @__PURE__ */ jsxs("div", {
19834
- className: "w-full",
19835
- children: [/* @__PURE__ */ jsx(LabelWithIconAndTooltip, {
19836
- propertyKey: key,
19837
- icon: /* @__PURE__ */ jsx(AlignLeftIcon, { size: iconSize.small }),
19838
- title: additionalField.name,
19839
- className: "text-text-secondary dark:text-text-secondary-dark ml-3.5"
19840
- }), /* @__PURE__ */ jsx("div", {
19841
- className: cls(paperMixin, "w-full min-h-14 p-4 md:p-6 overflow-x-scroll no-scrollbar"),
19842
- children: /* @__PURE__ */ jsx(ErrorBoundary, { children: child })
19843
- })]
19844
- }, `additional_${key}`);
19845
- }
19846
- console.warn(`Property ${key} not found in collection ${collection.name} in properties or additional fields. Skipping.`);
19847
- return null;
19848
- }).filter(Boolean) });
20834
+ return /* @__PURE__ */ jsx(FormSections, {
20835
+ sections: showRail || !layout.sidebar.length ? layout.sections : [...layout.sections, {
20836
+ key: "__rail",
20837
+ title: "Settings",
20838
+ collapsible: false,
20839
+ collapsed: false,
20840
+ fields: layout.sidebar
20841
+ }],
20842
+ collapsed: collapsedSections,
20843
+ onToggle: toggleSection,
20844
+ errorKeys,
20845
+ renderField
20846
+ });
19849
20847
  };
19850
20848
  const formRef = useRef(null);
19851
20849
  const hasFormErrors = Object.keys(formex.errors).length > 0 && formex.submitCount > 0;
19852
20850
  const formView = /* @__PURE__ */ jsx(ErrorBoundary, { children: /* @__PURE__ */ jsxs(Fragment, { children: [
19853
20851
  beforeFields,
19854
- !Builder && /* @__PURE__ */ jsxs("div", {
19855
- className: "w-full flex flex-col items-start my-4 lg:my-6",
19856
- children: [
19857
- /* @__PURE__ */ jsx(Typography, {
19858
- className: "my-4 grow line-clamp-1 " + (collection.hideIdFromForm ? "mb-6" : ""),
19859
- variant: "h4",
19860
- children: title ?? collection.singularName ?? collection.name
19861
- }),
19862
- !entity?.values && initialStatus === "existing" && /* @__PURE__ */ jsx(Alert, {
19863
- color: "warning",
19864
- size: "small",
19865
- outerClassName: "w-full mb-4 text-xs",
19866
- children: "This entity does not exist yet"
19867
- }),
19868
- showEntityPath && /* @__PURE__ */ jsx(Alert, {
19869
- color: "base",
19870
- outerClassName: "w-full",
19871
- size: "small",
19872
- children: /* @__PURE__ */ jsxs("code", {
19873
- className: "text-xs select-all text-text-secondary dark:text-text-secondary-dark",
19874
- children: [
19875
- entity?.path ?? path,
19876
- "/",
19877
- entityId
19878
- ]
19879
- })
19880
- })
19881
- ]
20852
+ !entity?.values && initialStatus === "existing" && /* @__PURE__ */ jsx(Alert, {
20853
+ color: "warning",
20854
+ size: "small",
20855
+ outerClassName: "w-full mb-4 text-xs",
20856
+ children: "This entity does not exist yet"
19882
20857
  }),
19883
20858
  children,
19884
20859
  initialEntityId && !entity && initialStatus !== "new" && /* @__PURE__ */ jsx(Alert, {
@@ -19893,12 +20868,10 @@ function EntityForm({ path, entityId: entityIdProp, collection, onValuesModified
19893
20868
  children: "Please fix the highlighted errors before saving."
19894
20869
  }),
19895
20870
  formContext && /* @__PURE__ */ jsx(Fragment, { children: /* @__PURE__ */ jsxs("div", {
19896
- className: "mt-12 flex flex-col gap-8",
19897
20871
  ref: formRef,
19898
20872
  children: [formFields(), /* @__PURE__ */ jsx(ErrorFocus, { containerRef: formRef })]
19899
20873
  }) }),
19900
20874
  afterFields,
19901
- forceActionsAtTheBottom && /* @__PURE__ */ jsx("div", { className: "h-16" }),
19902
20875
  /* @__PURE__ */ jsxs(Dialog, {
19903
20876
  open: discardDialogOpen,
19904
20877
  onOpenChange: setDiscardDialogOpen,
@@ -19930,7 +20903,6 @@ function EntityForm({ path, entityId: entityIdProp, collection, onValuesModified
19930
20903
  collection,
19931
20904
  path,
19932
20905
  entity,
19933
- layout: forceActionsAtTheBottom ? "bottom" : "responsive",
19934
20906
  savingError,
19935
20907
  formex,
19936
20908
  disabled: actionsDisabled,
@@ -19944,6 +20916,7 @@ function EntityForm({ path, entityId: entityIdProp, collection, onValuesModified
19944
20916
  return /* @__PURE__ */ jsx(Formex, {
19945
20917
  value: formex,
19946
20918
  children: /* @__PURE__ */ jsxs("form", {
20919
+ ref: formElementRef,
19947
20920
  onSubmit: formex.handleSubmit,
19948
20921
  onReset: (e) => {
19949
20922
  e.preventDefault();
@@ -19951,56 +20924,56 @@ function EntityForm({ path, entityId: entityIdProp, collection, onValuesModified
19951
20924
  else formex.resetForm({ values: baseInitialValues });
19952
20925
  },
19953
20926
  noValidate: true,
19954
- className: cls("@container flex-1 flex flex-row w-full overflow-y-auto justify-center", className),
19955
- children: [/* @__PURE__ */ jsx("div", {
19956
- id: `form_${path}`,
19957
- className: cls("relative flex flex-row max-w-4xl lg:max-w-3xl xl:max-w-4xl 2xl:max-w-6xl w-full h-fit"),
19958
- children: /* @__PURE__ */ jsxs("div", {
19959
- className: cls("flex flex-col w-full", openEntityMode === "dialog" ? "pt-4 pb-12 px-6 sm:px-8" : "pt-12 pb-16 px-4 sm:px-8 md:px-10"),
19960
- children: [/* @__PURE__ */ jsxs("div", {
19961
- className: "flex flex-row gap-4 justify-end h-0 overflow-visible sticky top-4 z-10",
19962
- children: [manualApplyLocalChanges && hasLocalChanges && localChangesCacheKey && /* @__PURE__ */ jsx(LocalChangesMenu, {
19963
- cacheKey: localChangesCacheKey,
19964
- properties: collection.properties,
19965
- cachedData: localChangesData,
19966
- formex,
19967
- onClearLocalChanges: () => {
19968
- setLocalChangesCleared(true);
19969
- onClearLocalChanges?.();
19970
- }
19971
- }), isSavingAutoSave ? /* @__PURE__ */ jsx(Tooltip, {
19972
- title: "Saving…",
19973
- children: /* @__PURE__ */ jsx(Chip, {
19974
- size: "small",
19975
- className: "py-1",
19976
- colorScheme: "blueDarker",
19977
- children: /* @__PURE__ */ jsx(LoaderIcon, {
19978
- size: iconSize.smallest,
19979
- className: "animate-spin"
19980
- })
19981
- })
19982
- }) : formex.dirty ? /* @__PURE__ */ jsx(Tooltip, {
19983
- title: "Form has been modified",
19984
- children: /* @__PURE__ */ jsx(Chip, {
19985
- size: "small",
19986
- className: "py-1",
19987
- colorScheme: "orangeDarker",
19988
- children: /* @__PURE__ */ jsx(PencilIcon, { size: iconSize.smallest })
19989
- })
19990
- }) : /* @__PURE__ */ jsx(Tooltip, {
19991
- title: "Form is in sync",
19992
- children: /* @__PURE__ */ jsx(Chip, {
19993
- size: "small",
19994
- className: "py-1",
19995
- children: /* @__PURE__ */ jsx(CheckIcon, { size: iconSize.smallest })
20927
+ className: cls("@container/form flex-1 flex flex-col w-full min-h-0", className),
20928
+ children: [/* @__PURE__ */ jsxs("div", {
20929
+ className: "flex-1 min-h-0 flex flex-row w-full",
20930
+ children: [/* @__PURE__ */ jsx("div", {
20931
+ className: "flex-1 min-w-0 overflow-y-auto flex justify-center items-start",
20932
+ children: /* @__PURE__ */ jsxs("div", {
20933
+ id: `form_${path}`,
20934
+ 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"),
20935
+ children: [manualApplyLocalChanges && hasLocalChanges && localChangesCacheKey && /* @__PURE__ */ jsx("div", {
20936
+ className: "flex justify-end mb-3",
20937
+ children: /* @__PURE__ */ jsx(LocalChangesMenu, {
20938
+ cacheKey: localChangesCacheKey,
20939
+ properties: collection.properties,
20940
+ cachedData: localChangesData,
20941
+ formex,
20942
+ onClearLocalChanges: () => {
20943
+ setLocalChangesCleared(true);
20944
+ onClearLocalChanges?.();
20945
+ }
19996
20946
  })
19997
- })]
19998
- }), formView]
19999
- })
20947
+ }), formView]
20948
+ })
20949
+ }), showRail && /* @__PURE__ */ jsx(FormRail, {
20950
+ fields: layout.sidebar,
20951
+ showRecordMeta: layout.showRecordMeta && status === "existing",
20952
+ entity,
20953
+ renderField
20954
+ })]
20000
20955
  }), dialogActions]
20001
20956
  })
20002
20957
  });
20003
20958
  }
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
+ }
20004
20977
  function useOnAutoSave(autoSave, formex, lastSavedValues, save) {
20005
20978
  useEffect(() => {
20006
20979
  if (!autoSave) return;
@@ -20219,7 +21192,6 @@ function EntityFormBinding({ path, entityId: entityIdProp, collection, entity, i
20219
21192
  context,
20220
21193
  openEntityMode
20221
21194
  ]);
20222
- const pluginFormActions = useSlot("form.actions", formActionProps);
20223
21195
  const pluginFormBefore = useSlot("form.before", formActionProps);
20224
21196
  const pluginFormAfter = useSlot("form.after", formActionProps);
20225
21197
  return /* @__PURE__ */ jsx(EntityForm, {
@@ -20242,7 +21214,7 @@ function EntityFormBinding({ path, entityId: entityIdProp, collection, entity, i
20242
21214
  uniqueFieldValidator,
20243
21215
  beforeFields: pluginFormBefore,
20244
21216
  afterFields: pluginFormAfter,
20245
- pluginActions: pluginFormActions ?? [],
21217
+ pluginActions: [],
20246
21218
  computedInitialValues,
20247
21219
  hasLocalChanges,
20248
21220
  localChangesData,
@@ -20270,6 +21242,30 @@ var useCollectionsConfigController = () => useContext(ConfigControllerContext);
20270
21242
  * @module
20271
21243
  */
20272
21244
  /**
21245
+ * Rebuild a serialized relation's `target` slug into the thunk consumers call.
21246
+ *
21247
+ * Shared by the relation *property* and the collection-level `relations` array.
21248
+ * They are the same shape on the wire and they need the same treatment coming
21249
+ * back, but only the property path had it — so a collection imported from an
21250
+ * existing table came off disk with `relations[n].target` still a string, which
21251
+ * typechecks (the serializable shape erases the difference) and throws "target
21252
+ * is not a function" at the first consumer.
21253
+ *
21254
+ * The lookup is consulted lazily, inside the thunk, so collections may
21255
+ * reference each other in any order, including circularly.
21256
+ */
21257
+ function fromSerializableRelation(relation, label, lookup) {
21258
+ const slug = relation.target;
21259
+ return {
21260
+ ...relation,
21261
+ target: () => {
21262
+ const found = slug ? lookup?.(slug) : void 0;
21263
+ if (!found) throw new Error(`Relation "${relation.relationName ?? label}" targets collection "${slug ?? "(none)"}", which is not among the collections it was deserialized with. Pass the whole set to \`fromSerializableCollectionConfigs\` so slugs can be resolved against each other.`);
21264
+ return found;
21265
+ }
21266
+ };
21267
+ }
21268
+ /**
20273
21269
  * Strip non-serializable fields from a `AdminPropertyOptions`.
20274
21270
  * Removes `Field` and `Preview` (ComponentRef).
20275
21271
  */
@@ -20365,7 +21361,8 @@ function toSerializableRelation(link) {
20365
21361
  case "hasMany": return {
20366
21362
  kind: link.kind,
20367
21363
  ...common,
20368
- ...link.foreignKeyOnTarget ? { foreignKeyOnTarget: link.foreignKeyOnTarget } : {}
21364
+ ...link.foreignKeyOnTarget ? { foreignKeyOnTarget: link.foreignKeyOnTarget } : {},
21365
+ ...link.sourceKey ? { sourceKey: link.sourceKey } : {}
20369
21366
  };
20370
21367
  case "manyToMany": return {
20371
21368
  kind: "manyToMany",
@@ -20676,17 +21673,7 @@ function fromSerializableProperty(serialized, lookup) {
20676
21673
  ...rest,
20677
21674
  type: "relation"
20678
21675
  };
20679
- if (relation) {
20680
- const slug = relation.target;
20681
- result.relation = {
20682
- ...relation,
20683
- target: () => {
20684
- const found = slug ? lookup?.(slug) : void 0;
20685
- if (!found) throw new Error(`Relation "${relation.relationName ?? sp.name}" targets collection "${slug ?? "(none)"}", which is not among the collections it was deserialized with. Pass the whole set to \`fromSerializableCollectionConfigs\` so slugs can be resolved against each other.`);
20686
- return found;
20687
- }
20688
- };
20689
- }
21676
+ if (relation) result.relation = fromSerializableRelation(relation, sp.name ?? "", lookup);
20690
21677
  return result;
20691
21678
  }
20692
21679
  default: return serialized;
@@ -20708,9 +21695,10 @@ function fromSerializableProperties(serialized, lookup) {
20708
21695
  * consumer if needed.
20709
21696
  */
20710
21697
  function fromSerializableCollectionConfig(serialized, lookup) {
20711
- const { properties, ...rest } = serialized;
21698
+ const { properties, relations, ...rest } = serialized;
20712
21699
  return {
20713
21700
  ...rest,
21701
+ ...relations ? { relations: relations.map((relation, index) => fromSerializableRelation(relation, `relations[${index}]`, lookup)) } : {},
20714
21702
  properties: fromSerializableProperties(properties, lookup)
20715
21703
  };
20716
21704
  }
@@ -20814,14 +21802,15 @@ function validateProperty(property, path, errors) {
20814
21802
  });
20815
21803
  const allowedByKind = {
20816
21804
  belongsTo: ["localKey"],
20817
- hasOne: ["foreignKeyOnTarget"],
20818
- hasMany: ["foreignKeyOnTarget"],
21805
+ hasOne: ["foreignKeyOnTarget", "sourceKey"],
21806
+ hasMany: ["foreignKeyOnTarget", "sourceKey"],
20819
21807
  manyToMany: ["through"],
20820
21808
  via: ["joinPath", "cardinality"]
20821
21809
  };
20822
21810
  const linkFields = [
20823
21811
  "localKey",
20824
21812
  "foreignKeyOnTarget",
21813
+ "sourceKey",
20825
21814
  "through",
20826
21815
  "joinPath",
20827
21816
  "cardinality"
@@ -21098,6 +22087,6 @@ function getFullIdPath(propertyKey, propertyNamespace) {
21098
22087
  return idToPropertiesPath(propertyNamespace ? `${propertyNamespace}.${propertyKey}` : propertyKey);
21099
22088
  }
21100
22089
  //#endregion
21101
- export { useBuildCollectionRegistryController as $, StorageThumbnailInternal as $n, CollectionTableBinding as $t, getFieldId as A, ArrayOfReferencesPreview as An, convertFileToJson as At, MapFieldBinding as B, getBracketNotation as Bn, getCollectionBySlugWithin 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, CollectionRegistryContext as Fn, FieldCaption as Ft, useSelectionDialog as G, getPropertyInPath as Gn, removeTrailingSlash$1 as Gt, DateTimeFieldBinding as H, getIconForProperty as Hn, getLastSegment$1 as Ht, RepeatFieldBinding as I, useCollectionRegistryController as In, useBreadcrumbsController as It, useBuildUrlController as J, isRelationProperty as Jn, resolveViewMode as Jt, SelectionTableBinding as K, getResolvedPropertyInPath as Kn, resolveCollectionPathIds$1 as Kt, ReferenceFieldBinding as L, getEntityPreviewKeys as Ln, BreadcrumbsProvider as Lt, TextFieldBinding as M, EntityPreviewBinding as Mn, ArrayContainer as Mt, SwitchFieldBinding as N, SidePanelControllerContext as Nn, PropertyConfigBadge as Nt, getDefaultFieldId as O, ArrayOfStorageComponentsPreview as On, ImportSaveInProgress as Ot, StorageUploadFieldBinding as P, useSidePanel as Pn, SearchIconsView as Pt, useResolvedCollections as Q, StorageThumbnail as Qn, VirtualTableInput$1 as Qt, MultiSelectFieldBinding as R, getEntityTitlePropertyKey as Rn, mergeEntityActions as Rt, getInitialEntityValues as S, KeyValuePreview as Sn, DataNewPropertiesMapping as St, PropertyFieldBinding as T, ArrayOfStringsPreview as Tn, processValueMapping as Tt, BlockFieldBinding as U, getIconForWidget as Un, removeInitialAndTrailingSlashes$1 as Ut, KeyValueFieldBinding as V, getDefaultPropertiesOrder as Vn, getCollectionPathsCombinations as Vt, ArrayOfReferencesFieldBinding as W, getPropertiesWithPropertiesOrder as Wn, removeInitialSlash as Wt, useTopLevelNavigation as X, StringPropertyPreview as Xn, resolveEntityView as Xt, useBuildNavigationStateController as Y, ArrayPropertyPreview as Yn, resolveEntityAction as Yt, useResolvedViews as Z, EnumValuesChip as Zn, useSelectionController as Zt, useCollectionsConfigController as _, getUserLabel as _n, CollectionViewActions as _t, namespaceToPropertiesPath as a, NavigationStateContext as an, UrlComponentPreview as ar, useBuildSidePanel as at, extractTouchedValues as b, BooleanPreview as bn, useCollectionEditorDialogsState as bt, buildCollectionGenerationCallback as c, SideDialogsControllerContext as cn, EmptyValue as cr, copyEntityAction as ct, fromSerializableCollectionConfigs as d, useClearRestoreValue as dn, LabelWithIcon as dr, resetPasswordAction as dt, SelectableTable as en, SkeletonPropertyComponent as er, useHistory as et, fromSerializableProperties as f, ReadOnlyFieldBinding as fn, FormLayout as fr, CreationResultDialog as ft, toSerializableProperty as g, UserPreview as gn, EntityCardBinding as gt, toSerializableProperties as h, PropertyPreview as hn, CollectionCardViewBinding as ht, namespaceToPropertiesOrderPath as i, useUrlController as in, renderSkeletonText as ir, getEntityViewWidth as it, VectorFieldBinding as j, ReferencePreview as jn, unflattenObject as jt, getFieldConfig as k, RelationPreview as kn, ImportFileUpload as kt, validateCollectionJson as l, SelectableTableContext as ln, LabelWithIconAndTooltip as lr, deleteEntityAction as lt, toSerializableCollectionConfig as m, ArrayOfMapsPreview as mn, EntityViewBinding as mt, getFullIdPath as n, useAdminContext as nn, renderSkeletonIcon as nr, useResolvedNavigationFrom as nt, CollectionGenerationApiError as o, useNavigationStateController as on, ImagePreview as or, EditViewBinding as ot, fromSerializableProperty as p, FieldHelperText as pn, FormEntry as pr, DetailViewBinding as pt, SideDialogs as q, isReferenceProperty as qn, resolveOpenEntityMode as qt, idToPropertiesPath as r, UrlContext as rn, renderSkeletonImageThumbnail as rr, buildSidePanelsFromUrl as rt, DEFAULT_COLLECTION_GENERATION_ENDPOINT as s, useSideDialogsController as sn, sanitizeUrl as sr, CollectionViewBinding as st, getFullId as t, CollectionRowActions as tn, renderSkeletonCaptionText as tr, resolveNavigationFrom as tt, fromSerializableCollectionConfig as u, ArrayCustomShapedFieldBinding as un, PropertyIdCopyTooltip 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, getEntityTitlePropertyKeyForEntity as zn, addInitialSlash as zt };
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 };
21102
22091
 
21103
- //# sourceMappingURL=util-B5fJm1FA.js.map
22092
+ //# sourceMappingURL=util-GiwPgxnL.js.map