@rebasepro/admin 0.13.1-canary.gef9608c → 0.14.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 (148) hide show
  1. package/dist/{CollectionEditorDialog-7b-kSLno.js → CollectionEditorDialog-CSheud_E.js} +19 -9
  2. package/dist/CollectionEditorDialog-CSheud_E.js.map +1 -0
  3. package/dist/{PropertyEditView-C-31uvpG.js → PropertyEditView-CWZlC1gq.js} +5 -5
  4. package/dist/PropertyEditView-CWZlC1gq.js.map +1 -0
  5. package/dist/{RouterCollectionsStudioView-BJylABq3.js → RouterCollectionsStudioView-CwTpdzWF.js} +9 -13
  6. package/dist/RouterCollectionsStudioView-CwTpdzWF.js.map +1 -0
  7. package/dist/collection_editor/serializable_types.d.ts +1 -0
  8. package/dist/collection_editor_ui.js +4 -4
  9. package/dist/components/ClearFilterSortButton.d.ts +7 -1
  10. package/dist/components/CollectionViewBinding/EntityCardBinding.d.ts +3 -1
  11. package/dist/components/CollectionViewBinding/SearchExplanation.d.ts +48 -0
  12. package/dist/components/CollectionViewBinding/SearchHighlight.d.ts +101 -0
  13. package/dist/components/CollectionViewBinding/SlotValue.d.ts +26 -0
  14. package/dist/components/CollectionViewBinding/SortButton.d.ts +7 -1
  15. package/dist/components/CollectionViewBinding/SplitListCloseButton.d.ts +17 -6
  16. package/dist/components/CollectionViewBinding/SplitListShowButton.d.ts +1 -1
  17. package/dist/components/CollectionViewBinding/usePreviewSlots.d.ts +26 -1
  18. package/dist/components/DetailViewBinding.d.ts +7 -0
  19. package/dist/components/DrawerNavigationGroup.d.ts +4 -2
  20. package/dist/components/DrawerNavigationItem.d.ts +13 -5
  21. package/dist/components/EditViewBinding.d.ts +22 -7
  22. package/dist/components/EntityIdentityBar.d.ts +24 -1
  23. package/dist/components/RelationSelector.d.ts +13 -0
  24. package/dist/data_export/export/export.d.ts +7 -0
  25. package/dist/data_export/export/fetch_export_data.d.ts +48 -0
  26. package/dist/data_export/export/index.d.ts +1 -0
  27. package/dist/data_import/utils/csv.d.ts +30 -0
  28. package/dist/data_import/utils/index.d.ts +2 -0
  29. package/dist/data_import/utils/save_entities.d.ts +38 -0
  30. package/dist/data_import/utils/transforms.d.ts +9 -0
  31. package/dist/editor.js +3 -0
  32. package/dist/editor.js.map +1 -1
  33. package/dist/{export-C-XG3aRh.js → export-L53WektO.js} +175 -39
  34. package/dist/export-L53WektO.js.map +1 -0
  35. package/dist/form/form_utils.d.ts +38 -1
  36. package/dist/{history-DfMuvPfr.js → history-D5SKygpJ.js} +2 -2
  37. package/dist/{history-DfMuvPfr.js.map → history-D5SKygpJ.js.map} +1 -1
  38. package/dist/{import-B0GyllB0.js → import-C_4edkoD.js} +5 -4
  39. package/dist/import-C_4edkoD.js.map +1 -0
  40. package/dist/index.d.ts +2 -2
  41. package/dist/index.js +96 -80
  42. package/dist/index.js.map +1 -1
  43. package/dist/preview/components/date_fns_locales.d.ts +9 -0
  44. package/dist/util/view_constants.d.ts +24 -0
  45. package/dist/util/view_mode.d.ts +18 -0
  46. package/dist/{util-DHNXodBo.js → util-C-D5yD2n.js} +2173 -673
  47. package/dist/util-C-D5yD2n.js.map +1 -0
  48. package/package.json +10 -9
  49. package/src/collection_editor/serializable_types.ts +1 -0
  50. package/src/collection_editor/ui/collection_editor/AICollectionGeneratorPopover.tsx +8 -3
  51. package/src/collection_editor/ui/collection_editor/CollectionDetailsForm.tsx +5 -2
  52. package/src/collection_editor/ui/collection_editor/CollectionJsonImportDialog.tsx +1 -0
  53. package/src/collection_editor/ui/collection_editor/CollectionRLSTab.tsx +9 -8
  54. package/src/collection_editor/ui/collection_editor/CollectionsStudioView.tsx +9 -11
  55. package/src/collection_editor/ui/collection_editor/GeneralSettingsForm.tsx +5 -2
  56. package/src/collection_editor/ui/collection_editor/properties/MapPropertyField.tsx +1 -1
  57. package/src/collection_editor/ui/collection_editor/properties/advanced/AdvancedPropertyValidation.tsx +5 -2
  58. package/src/collection_editor/ui/collection_editor/properties/conditions/ConditionsEditor.tsx +1 -0
  59. package/src/collection_editor/useJsonCollectionsConfigController.tsx +92 -84
  60. package/src/collection_editor/useLocalCollectionsConfigController.tsx +10 -3
  61. package/src/components/ClearFilterSortButton.tsx +16 -7
  62. package/src/components/CollectionTableBinding/CollectionTableBinding.tsx +2 -2
  63. package/src/components/CollectionTableBinding/PropertyTableCell.tsx +2 -2
  64. package/src/components/CollectionTableBinding/column_utils.tsx +15 -6
  65. package/src/components/CollectionTableBinding/internal/popup_field/PopupFormField.tsx +2 -2
  66. package/src/components/CollectionTableBinding/table_bindings.tsx +7 -1
  67. package/src/components/CollectionViewBinding/BoardCardBinding.tsx +18 -1
  68. package/src/components/CollectionViewBinding/CollectionBoardViewBinding.tsx +13 -7
  69. package/src/components/CollectionViewBinding/CollectionCardViewBinding.tsx +2 -1
  70. package/src/components/CollectionViewBinding/CollectionListViewBinding.tsx +816 -335
  71. package/src/components/CollectionViewBinding/CollectionViewActions.tsx +1 -1
  72. package/src/components/CollectionViewBinding/CollectionViewBinding.tsx +26 -6
  73. package/src/components/CollectionViewBinding/CollectionViewStartActions.tsx +9 -0
  74. package/src/components/CollectionViewBinding/EntityCardBinding.tsx +45 -10
  75. package/src/components/CollectionViewBinding/SearchExplanation.tsx +106 -0
  76. package/src/components/CollectionViewBinding/SearchHighlight.tsx +311 -0
  77. package/src/components/CollectionViewBinding/SlotValue.tsx +96 -0
  78. package/src/components/CollectionViewBinding/SortButton.tsx +16 -4
  79. package/src/components/CollectionViewBinding/SplitListCloseButton.tsx +23 -9
  80. package/src/components/CollectionViewBinding/SplitListShowButton.tsx +3 -3
  81. package/src/components/CollectionViewBinding/SplitListView.tsx +36 -9
  82. package/src/components/CollectionViewBinding/ViewModeToggle.tsx +10 -5
  83. package/src/components/CollectionViewBinding/useBoardDataController.tsx +11 -3
  84. package/src/components/CollectionViewBinding/usePreviewSlots.ts +76 -24
  85. package/src/components/CollectionViewBinding/utils.ts +7 -4
  86. package/src/components/DefaultDrawer.tsx +7 -0
  87. package/src/components/DetailViewBinding.tsx +33 -9
  88. package/src/components/DrawerNavigationGroup.tsx +12 -32
  89. package/src/components/DrawerNavigationItem.tsx +33 -12
  90. package/src/components/EditViewBinding.tsx +175 -28
  91. package/src/components/EntityIdentityBar.tsx +109 -17
  92. package/src/components/EntityInspector.tsx +24 -6
  93. package/src/components/EntityViewBinding.tsx +4 -2
  94. package/src/components/ReferenceTable/SelectionTableBinding.tsx +4 -2
  95. package/src/components/RelationSelector.tsx +61 -11
  96. package/src/components/SearchIconsView.tsx +14 -33
  97. package/src/components/SelectableTable/filters/StringNumberFilterField.tsx +1 -0
  98. package/src/components/SidePanelBinding.tsx +36 -25
  99. package/src/components/UserSelector.tsx +52 -9
  100. package/src/components/history/LastEditedByIndicator.tsx +7 -12
  101. package/src/data_export/export/ExportCollectionAction.tsx +101 -48
  102. package/src/data_export/export/export.ts +63 -16
  103. package/src/data_export/export/fetch_export_data.ts +90 -0
  104. package/src/data_export/export/index.ts +1 -0
  105. package/src/data_import/components/DataNewPropertiesMapping.tsx +20 -3
  106. package/src/data_import/components/ImportNewPropertyFieldPreview.tsx +1 -0
  107. package/src/data_import/components/ImportSaveInProgress.tsx +35 -36
  108. package/src/data_import/import/ImportCollectionAction.tsx +13 -2
  109. package/src/data_import/utils/csv.ts +153 -0
  110. package/src/data_import/utils/data.ts +16 -2
  111. package/src/data_import/utils/file_to_json.ts +77 -6
  112. package/src/data_import/utils/index.ts +2 -0
  113. package/src/data_import/utils/save_entities.ts +138 -0
  114. package/src/data_import/utils/transforms.ts +23 -0
  115. package/src/editor/components/image-bubble.tsx +2 -0
  116. package/src/editor/selectors/link-selector.tsx +1 -0
  117. package/src/form/EntityForm.tsx +12 -3
  118. package/src/form/EntityFormBinding.tsx +8 -12
  119. package/src/form/PropertyFieldBinding.tsx +2 -2
  120. package/src/form/field_bindings/ArrayCustomShapedFieldBinding.tsx +2 -2
  121. package/src/form/field_bindings/BinaryFieldBinding.tsx +1 -0
  122. package/src/form/field_bindings/BlockFieldBinding.tsx +3 -3
  123. package/src/form/field_bindings/DateTimeFieldBinding.tsx +4 -0
  124. package/src/form/field_bindings/KeyValueFieldBinding.tsx +3 -0
  125. package/src/form/field_bindings/MapFieldBinding.tsx +2 -2
  126. package/src/form/field_bindings/ReadOnlyFieldBinding.tsx +15 -2
  127. package/src/form/field_bindings/RepeatFieldBinding.tsx +3 -3
  128. package/src/form/field_bindings/StorageUploadFieldBinding.tsx +2 -2
  129. package/src/form/field_bindings/TextFieldBinding.tsx +18 -0
  130. package/src/form/field_bindings/VectorFieldBinding.tsx +1 -0
  131. package/src/form/form_utils.ts +78 -0
  132. package/src/index.ts +9 -1
  133. package/src/preview/PropertyPreview.tsx +7 -3
  134. package/src/preview/components/DatePreview.tsx +15 -2
  135. package/src/preview/components/ReferencePreview.tsx +5 -8
  136. package/src/preview/components/RelationPreview.tsx +8 -7
  137. package/src/preview/components/date_fns_locales.ts +125 -0
  138. package/src/preview/property_previews/ArrayOfRelationsPreview.tsx +6 -3
  139. package/src/util/navigation_utils.ts +4 -2
  140. package/src/util/property_utils.tsx +6 -1
  141. package/src/util/view_constants.ts +27 -0
  142. package/src/util/view_mode.ts +36 -0
  143. package/dist/CollectionEditorDialog-7b-kSLno.js.map +0 -1
  144. package/dist/PropertyEditView-C-31uvpG.js.map +0 -1
  145. package/dist/RouterCollectionsStudioView-BJylABq3.js.map +0 -1
  146. package/dist/export-C-XG3aRh.js.map +0 -1
  147. package/dist/import-B0GyllB0.js.map +0 -1
  148. package/dist/util-DHNXodBo.js.map +0 -1
@@ -1,18 +1,17 @@
1
1
  import * as React$1 from "react";
2
2
  import React, { Suspense, createContext, createElement, forwardRef, lazy, memo, useCallback, useContext, useEffect, useId, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
3
3
  import { deepEqual } from "fast-equals";
4
- import { Alert, AlertCircleIcon, AlertTriangleIcon, AlignLeftIcon, ArrowDownIcon, ArrowLeftIcon, ArrowRightToLineIcon, ArrowUpDownIcon, ArrowUpIcon, Badge, BooleanSwitchWithLabel, Button, CHIP_COLORS, CHIP_SEED_KEYS, CalendarIcon, Card, CardView, CenteredView, CheckCircleIcon, CheckIcon, Checkbox, ChevronDownIcon, ChevronRightIcon, ChevronUpIcon, ChevronsLeftIcon, ChevronsRightIcon, ChevronsUpDownIcon, Chip, CircleIcon, CircularProgress, CircularProgressCenter, CodeIcon, Collapse, ColumnsIcon, CopyIcon, DateTimeField, Dialog, DialogActions, DialogContent, DialogTitle, ErrorBoundary, ExpandablePanel, ExternalLinkIcon, EyeIcon, EyeOffIcon, FileTextIcon, FileUpload, FilterChip, FilterIcon, FilterXIcon, FlagIcon, FolderUpIcon, FunctionSquareIcon, GlobeIcon, HandleIcon, HashIcon, HistoryIcon, IconButton, ImageOffIcon, KanbanIcon, KanbanView, KeyRoundIcon, Label, LayoutGridIcon, Link2Icon, LinkIcon, ListIcon, ListOrderedIcon, ListView, LoadingButton, MailIcon, Markdown, Maximize2Icon, Menu, MenuItem, MinusIcon, MoreVerticalIcon, MultiSelect, MultiSelectItem, PencilIcon, PlusIcon, Popover, PopoverPrimitive, Portal, QuoteIcon, RadioGroup, RadioGroupItem, RefreshCwIcon, RepeatIcon, ResizablePanels, Rows3Icon, SearchBar, SearchIcon, Select, SelectItem, Separator, SettingsIcon, Sheet, Skeleton, Tab, Table, TableBody, TableCell, TableHeader, TableIcon, TableRow, Tabs, TextField, TextIcon, TextareaAutosize, ToggleButtonGroup, Tooltip, Trash2Icon, Typography, Unlink2Icon, UploadIcon, UserIcon, VirtualTable, VirtualTableDateField, VirtualTableInput, VirtualTableInput as VirtualTableInput$1, VirtualTableNumberInput, VirtualTableSwitch, VoteIcon, XCircleIcon, XIcon, cls, coolIconKeys, createVirtualTableSelectionStore, debounce, defaultBorderMixin, fieldBackgroundDisabledMixin, fieldBackgroundHoverMixin, fieldBackgroundInvisibleMixin, fieldBackgroundMixin, focusedDisabled, getColorSchemeForSeed, iconSize, lucideIcons, paperMixin, useDebouncedCallback, useInjectStyles, useOutsideAlerter } from "@rebasepro/ui";
5
- import { CollectionRegistry, DEFAULT_ONE_OF_TYPE, DEFAULT_ONE_OF_VALUE, canReadCollection, enumToObjectEntries, getArrayResolvedProperties, getDefaultValueFor, getDefaultValueFortype, getDefaultValuesFor, getEntityChildViews, getLabelOrConfigFrom, getReferenceFrom, getRelationFrom, getSubcollections, getTableName, isPropertyBuilder, normalizeToEntityRelation, resolveArrayProperties, resolveEnumValues, resolveProperty, resolveRelationProperty, resolveStorageFilenameString, resolveStoragePathString, resolveStorageSource } from "@rebasepro/common";
6
- import { CONTAINER_FULL_WIDTH, CollectionScopeProvider, DEFAULT_PAGE_SIZE, EntityDisplayCache, ErrorTooltip, ErrorView, IconForView, PluginProviderStack, SIDE_PANEL_DEFAULT_WIDTH, StorageSourceContext, UnsavedChangesDialog, UserDisplay, addInitialSlash, apiBaseOf, buildEnumLabel, createFormexStub, deleteEntityWithCallbacks, entityDisplayKey, flattenKeys, getAdminEntityChildViews, getCollectionBySlugWithin, getCollectionPathsCombinations, getColorScheme, getColumnKeysForProperty, getDisplayPropertyKey, getDisplayResolver, getEntityFromCache, getEntityFromMemoryCache, getEntityImagePreviewPropertyKey, getEntityPreviewKeys, getFormFieldKeys, getIcon, getLastSegment as getLastSegment$1, getLeadingRelationTitleKey, getLocalChangesBackup, getNavigationEntriesFromPath, getParentReferencesFromPath, getPropertyInPath, getRelationIncludeParams, getRowHeight, getTitlePropertyCandidates, getTitlePropertyKey, getTitlePropertyKeyForValues, iconsSearch, isFilterableRelation, isHidden, isReadOnly, looksLikeIdentifierValue, removeEntityFromCache, removeEntityFromMemoryCache, removeInitialAndTrailingSlashes, removeInitialAndTrailingSlashes as removeInitialAndTrailingSlashes$1, removeInitialSlash, removeTrailingSlash, removeTrailingSlash as removeTrailingSlash$1, resolveCollectionPathIds, resolveCollectionPathIds as resolveCollectionPathIds$1, resolveComponentRef, resolveDefaultSelectedView, resolveFilterOperators, resolveFormLayout, saveEntityToCache, saveEntityToMemoryCache, saveEntityWithCallbacks, useAnalyticsController, useApiBase, useApiConfig, useAuthController, useCollectionScope, useColumnIds, useComponentOverride, useCustomizationController, useData, useDataTableController, useFetch, useLargeLayout, useModeController, useNavigationBlocker, usePermissions, useRebaseClient, useRebaseContext, useRelationSelector, useResolvedComponent, useScrollRestoration, useSlot, useSnackbarController, useStorageSource, useStorageSources, useStorageUploadController, useTranslation, useUnsavedChangesDialog, useUserConfigurationPersistence } from "@rebasepro/app";
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, LucideIconByName, MailIcon, Markdown, Maximize2Icon, Menu, MenuItem, MinusIcon, MoreVerticalIcon, MultiSelect, MultiSelectItem, PanelLeftCloseIcon, PanelLeftOpenIcon, 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, paperMixin, useDebouncedCallback, useOutsideAlerter, usePortalContainer } from "@rebasepro/ui";
5
+ import { CollectionRegistry, DEFAULT_ONE_OF_TYPE, DEFAULT_ONE_OF_VALUE, canReadCollection, enumToObjectEntries, getArrayResolvedProperties, getDefaultValueFor, getDefaultValueFortype, getDefaultValuesFor, getEntityChildViews, getLabelOrConfigFrom, getReferenceFrom, getRelationFrom, getRelationTargetPath, getSubcollections, getTableName, isPropertyBuilder, normalizeToEntityRelation, resolveArrayProperties, resolveEnumValues, resolveProperty, resolveRelationProperty, resolveStorageFilenameString, resolveStoragePathString, resolveStorageSource } from "@rebasepro/common";
6
+ import { CONTAINER_FULL_WIDTH, CollectionScopeProvider, DEFAULT_PAGE_SIZE, EntityDisplayCache, ErrorTooltip, ErrorView, IconForView, PluginProviderStack, SIDE_PANEL_DEFAULT_WIDTH, StorageSourceContext, UnsavedChangesDialog, UserDisplay, addInitialSlash, apiBaseOf, buildEnumLabel, createFormexStub, deleteEntityWithCallbacks, entityDisplayKey, flattenKeys, getAdminEntityChildViews, getCollectionBySlugWithin, getCollectionPathsCombinations, getColorScheme, getColumnKeysForProperty, getDisplayPropertyKey, getDisplayResolver, getEntityFromCache, getEntityFromMemoryCache, getEntityImagePreviewPropertyKey, getEntityPreviewKeys, getFormFieldKeys, getIcon, getLastSegment as getLastSegment$1, getLeadingRelationTitleKey, getLocalChangesBackup, getNavigationEntriesFromPath, getParentReferencesFromPath, getPropertyInPath, getRedundantChildViewColumnIds, getRelationIncludeParams, getRowHeight, getTitlePropertyCandidates, getTitlePropertyKey, getTitlePropertyKeyForValues, hasDeclaredDisplay, iconsSearch, isDisabled, isFilterableRelation, isHidden, isReadOnly, looksLikeIdentifierValue, removeEntityFromCache, removeEntityFromMemoryCache, removeInitialAndTrailingSlashes, removeInitialAndTrailingSlashes as removeInitialAndTrailingSlashes$1, removeInitialSlash, removeTrailingSlash, removeTrailingSlash as removeTrailingSlash$1, resolveCollectionPathIds, resolveCollectionPathIds as resolveCollectionPathIds$1, resolveComponentRef, resolveDefaultSelectedView, resolveFilterOperators, resolveFormLayout, saveEntityToCache, saveEntityToMemoryCache, saveEntityWithCallbacks, useAnalyticsController, useApiBase, useApiConfig, useAuthController, useCollectionScope, useColumnIds, useComponentOverride, useCustomizationController, useData, useDataTableController, useFetch, useLargeLayout, useModeController, useNavigationBlocker, usePermissions, useRebaseClient, useRebaseContext, useRelationSelector, useResolvedComponent, useScrollRestoration, useSlot, useSnackbarController, useStorageSource, useStorageSources, useStorageUploadController, useTranslation, useUnsavedChangesDialog, useUserConfigurationPersistence } from "@rebasepro/app";
7
7
  import { Field, Formex, getIn, setIn, useCreateFormex, useFormex } from "@rebasepro/forms";
8
8
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
9
- import { defaultDateFormat, getHashValue, getValueInPath, hydrateRegExp, isObject, mergeDeep, randomString, toArray } from "@rebasepro/utils";
9
+ import { defaultDateFormat, formatRelativeTime, getHashValue, getValueInPath, hydrateRegExp, isArrayValue, isObject, isPrototypePollutingKey, mergeDeep, pathTraversesPrototype, randomString, readStoredJson, toArray, writeStoredJson } from "@rebasepro/utils";
10
10
  import { resolveAdminCollection } from "@rebasepro/admin-types";
11
11
  import { format } from "date-fns";
12
- import * as locales from "date-fns/locale";
13
12
  import { Command } from "cmdk";
14
13
  import { useDropzone } from "react-dropzone";
15
- import { EntityReference, EntityRelation, GeoPoint, Vector, getCollectionDataPath, getDataSourceCapabilities, isRelationalCollectionConfig } from "@rebasepro/types";
14
+ import { EntityReference, EntityRelation, GeoPoint, MAX_LIST_LIMIT, Vector, getCollectionDataPath, getDataSourceCapabilities, isRelationalCollectionConfig } from "@rebasepro/types";
16
15
  import { z } from "zod";
17
16
  import useMeasure from "react-use-measure";
18
17
  import { Link, useLocation, useNavigate, useSearchParams } from "react-router";
@@ -20,7 +19,6 @@ import { DndContext, KeyboardSensor, PointerSensor, closestCenter, useSensor, us
20
19
  import { restrictToVerticalAxis } from "@dnd-kit/modifiers";
21
20
  import { SortableContext, horizontalListSortingStrategy, sortableKeyboardCoordinates, useSortable, verticalListSortingStrategy } from "@dnd-kit/sortable";
22
21
  import { CSS } from "@dnd-kit/utilities";
23
- import ExcelJS from "exceljs";
24
22
  import { inferTypeFromValue } from "@rebasepro/inference";
25
23
  import { Highlight, themes } from "prism-react-renderer";
26
24
  import { generateKeyBetween, generateNKeysBetween } from "fractional-indexing";
@@ -111,7 +109,7 @@ function getResolvedPropertyInPath(properties, path) {
111
109
  if (path.includes(".")) {
112
110
  const pathSegments = path.split(".");
113
111
  const childProperty = properties[pathSegments[0]];
114
- if (childProperty.type === "map" && childProperty.properties) return getResolvedPropertyInPath(childProperty.properties, pathSegments.slice(1).join("."));
112
+ if (childProperty?.type === "map" && childProperty.properties) return getResolvedPropertyInPath(childProperty.properties, pathSegments.slice(1).join("."));
115
113
  }
116
114
  }
117
115
  }
@@ -1524,7 +1522,8 @@ function resolveCollectionSlotKeys(collection, authController, propertyConfigs)
1524
1522
  subtitleKey: getDisplayPropertyKey(collection, "subtitle") ?? (sortedPreviewKeys.length > 0 ? sortedPreviewKeys[0] : void 0),
1525
1523
  relationKeys,
1526
1524
  statusKey,
1527
- dateKey
1525
+ dateKey,
1526
+ tagsKey: getDisplayPropertyKey(collection, "tags")
1528
1527
  };
1529
1528
  }
1530
1529
  /**
@@ -1541,6 +1540,7 @@ function resolveImageSlot(collection, imageKey, entity) {
1541
1540
  const resolvedValue = ofProp ? (rawValue ?? []).length > 0 ? rawValue[0] : void 0 : rawValue;
1542
1541
  if (resolvedValue === void 0 || resolvedValue === null) return void 0;
1543
1542
  return {
1543
+ role: "image",
1544
1544
  property: resolvedProperty,
1545
1545
  propertyKey: imageKey,
1546
1546
  value: resolvedValue
@@ -1569,14 +1569,8 @@ function formatDateValue$1(value) {
1569
1569
  const date = value instanceof Date ? value : new Date(value);
1570
1570
  if (isNaN(date.getTime())) return String(value);
1571
1571
  const now = /* @__PURE__ */ new Date();
1572
- const diffMs = now.getTime() - date.getTime();
1573
- const diffMins = Math.floor(diffMs / 6e4);
1574
- const diffHours = Math.floor(diffMs / 36e5);
1575
- const diffDays = Math.floor(diffMs / 864e5);
1576
- if (diffMins < 1) return "Just now";
1577
- if (diffMins < 60) return `${diffMins}m ago`;
1578
- if (diffHours < 24) return `${diffHours}h ago`;
1579
- if (diffDays < 7) return `${diffDays}d ago`;
1572
+ const relative = formatRelativeTime(date, { now });
1573
+ if (relative) return relative;
1580
1574
  return date.toLocaleDateString(void 0, {
1581
1575
  month: "short",
1582
1576
  day: "numeric",
@@ -1603,7 +1597,7 @@ function useCollectionSlotKeys(collection, authController, propertyConfigs) {
1603
1597
  * `React.memo` render functions or loops without violating hook rules.
1604
1598
  */
1605
1599
  function resolveEntitySlots(entity, collection, slotKeys) {
1606
- const { titleKey, titleKeyCandidates, imageKey, subtitleKey, relationKeys, statusKey, dateKey } = slotKeys;
1600
+ const { titleKey, titleKeyCandidates, imageKey, subtitleKey, relationKeys, statusKey, dateKey, tagsKey } = slotKeys;
1607
1601
  const image = imageKey ? resolveImageSlot(collection, imageKey, entity) : void 0;
1608
1602
  const resolvedTitleKey = resolveEntityTitleKey(entity, titleKeyCandidates ?? (titleKey ? [titleKey] : []));
1609
1603
  let title;
@@ -1611,6 +1605,7 @@ function resolveEntitySlots(entity, collection, slotKeys) {
1611
1605
  const prop = collection.properties[resolvedTitleKey];
1612
1606
  const val = getValueInPath(entity.values, resolvedTitleKey);
1613
1607
  if (prop) title = {
1608
+ role: "title",
1614
1609
  property: prop,
1615
1610
  propertyKey: resolvedTitleKey,
1616
1611
  value: val
@@ -1621,6 +1616,7 @@ function resolveEntitySlots(entity, collection, slotKeys) {
1621
1616
  const prop = collection.properties[subtitleKey];
1622
1617
  const val = getValueInPath(entity.values, subtitleKey);
1623
1618
  if (prop && val !== void 0 && val !== null && val !== "") subtitle = {
1619
+ role: "subtitle",
1624
1620
  property: prop,
1625
1621
  propertyKey: subtitleKey,
1626
1622
  value: val
@@ -1678,6 +1674,7 @@ function resolveEntitySlots(entity, collection, slotKeys) {
1678
1674
  const prop = collection.properties[statusKey];
1679
1675
  const val = getValueInPath(entity.values, statusKey);
1680
1676
  if (prop && val !== void 0 && val !== null) status = {
1677
+ role: "status",
1681
1678
  property: prop,
1682
1679
  propertyKey: statusKey,
1683
1680
  value: val
@@ -1688,12 +1685,25 @@ function resolveEntitySlots(entity, collection, slotKeys) {
1688
1685
  const prop = collection.properties[dateKey];
1689
1686
  const val = getValueInPath(entity.values, dateKey);
1690
1687
  if (prop && val !== void 0 && val !== null) date = {
1688
+ role: "date",
1691
1689
  property: prop,
1692
1690
  propertyKey: dateKey,
1693
1691
  value: val,
1694
1692
  formatted: formatDateValue$1(val)
1695
1693
  };
1696
1694
  }
1695
+ let tags;
1696
+ if (tagsKey) {
1697
+ const prop = collection.properties[tagsKey];
1698
+ const val = getValueInPath(entity.values, tagsKey);
1699
+ const empty = val === void 0 || val === null || val === "" || Array.isArray(val) && val.length === 0;
1700
+ if (prop && !empty) tags = {
1701
+ role: "tags",
1702
+ property: prop,
1703
+ propertyKey: tagsKey,
1704
+ value: val
1705
+ };
1706
+ }
1697
1707
  return {
1698
1708
  image,
1699
1709
  title,
@@ -1701,6 +1711,7 @@ function resolveEntitySlots(entity, collection, slotKeys) {
1701
1711
  relations,
1702
1712
  status,
1703
1713
  date,
1714
+ tags,
1704
1715
  entityId: entity.id
1705
1716
  };
1706
1717
  }
@@ -1775,19 +1786,24 @@ function useEntitySlots(entity, collection, slotKeys) {
1775
1786
  const image = useEntityDisplay("image", params);
1776
1787
  const status = useEntityDisplay("status", params);
1777
1788
  const date = useEntityDisplay("date", params);
1789
+ const tags = useEntityDisplay("tags", params);
1778
1790
  return useMemo(() => {
1779
- const computed = (resolved) => resolved.value !== void 0 && resolved.source === void 0 ? { value: resolved.value } : void 0;
1780
- const computedDate = computed(date);
1791
+ const computed = (role, resolved) => resolved.value !== void 0 && resolved.source === void 0 ? {
1792
+ role,
1793
+ value: resolved.value
1794
+ } : void 0;
1795
+ const computedDate = computed("date", date);
1781
1796
  return {
1782
1797
  ...derived,
1783
- title: computed(title) ?? derived.title,
1784
- subtitle: computed(subtitle) ?? derived.subtitle,
1785
- image: computed(image) ?? derived.image,
1786
- status: computed(status) ?? derived.status,
1798
+ title: computed("title", title) ?? derived.title,
1799
+ subtitle: computed("subtitle", subtitle) ?? derived.subtitle,
1800
+ image: computed("image", image) ?? derived.image,
1801
+ status: computed("status", status) ?? derived.status,
1787
1802
  date: computedDate ? {
1788
1803
  ...computedDate,
1789
1804
  formatted: formatDateValue$1(computedDate.value)
1790
- } : derived.date
1805
+ } : derived.date,
1806
+ tags: computed("tags", tags) ?? derived.tags
1791
1807
  };
1792
1808
  }, [
1793
1809
  derived,
@@ -1795,7 +1811,8 @@ function useEntitySlots(entity, collection, slotKeys) {
1795
1811
  subtitle,
1796
1812
  image,
1797
1813
  status,
1798
- date
1814
+ date,
1815
+ tags
1799
1816
  ]);
1800
1817
  }
1801
1818
  //#endregion
@@ -2225,8 +2242,7 @@ function ReferencePreviewExisting({ reference, collection, previewProperties, si
2225
2242
  collection,
2226
2243
  useCache: true
2227
2244
  });
2228
- if (entity) referencesCache.set(reference.pathWithId, entity);
2229
- const usedEntity = entity ?? referencesCache.get(reference.pathWithId);
2245
+ const usedEntity = entity;
2230
2246
  let body;
2231
2247
  if (!reference) body = /* @__PURE__ */ jsx(ErrorView, { error: "Reference not set" });
2232
2248
  else if (usedEntity && !usedEntity.values) body = /* @__PURE__ */ jsx(ErrorView, {
@@ -2285,7 +2301,6 @@ function ReferencePreviewExisting({ reference, collection, previewProperties, si
2285
2301
  hover
2286
2302
  });
2287
2303
  }
2288
- var referencesCache = /* @__PURE__ */ new Map();
2289
2304
  //#endregion
2290
2305
  //#region src/preview/components/InlineEntityListPreview.tsx
2291
2306
  /**
@@ -2468,8 +2483,7 @@ function RelationPreviewExisting({ relation, collection, previewProperties, size
2468
2483
  collection,
2469
2484
  useCache: true
2470
2485
  });
2471
- if (entity) relationsCache.set(relation.pathWithId, entity);
2472
- const usedEntity = passedEntity ?? entity ?? relationsCache.get(relation.pathWithId);
2486
+ const usedEntity = passedEntity ?? entity;
2473
2487
  let body;
2474
2488
  if (!relation) body = /* @__PURE__ */ jsx(ErrorView, { error: "Relation not set" });
2475
2489
  else if (usedEntity && !usedEntity.values) body = /* @__PURE__ */ jsx(ErrorView, {
@@ -2528,7 +2542,6 @@ function RelationPreviewExisting({ relation, collection, previewProperties, size
2528
2542
  hover
2529
2543
  });
2530
2544
  }
2531
- var relationsCache = /* @__PURE__ */ new Map();
2532
2545
  //#endregion
2533
2546
  //#region src/preview/property_previews/ArrayOfRelationsPreview.tsx
2534
2547
  /**
@@ -2539,8 +2552,9 @@ function ArrayOfRelationsPreview({ propertyKey, value, property, size, textOnly
2539
2552
  if (Array.isArray(property?.of)) throw Error("Using array properties instead of single one in `of` in ArrayProperty");
2540
2553
  if (property?.type !== "array" || !property.of || property.of.type !== "relation") throw Error("Picked wrong preview component ArrayOfRelationsPreview");
2541
2554
  const ofProperty = property.of;
2555
+ const relationTargetPath = getRelationTargetPath(ofProperty);
2542
2556
  if (nested || textOnly) return /* @__PURE__ */ jsx(InlineEntityListPreview, {
2543
- items: (value ?? []).map((relation) => normalizeToEntityRelation(relation)).filter((relation) => Boolean(relation)),
2557
+ items: (value ?? []).map((relation) => normalizeToEntityRelation(relation, "relation", relationTargetPath)).filter((relation) => Boolean(relation)),
2544
2558
  renderItem: (relation, index) => /* @__PURE__ */ jsx(RelationPreview, {
2545
2559
  disabled: !ofProperty.relation,
2546
2560
  previewProperties: ofProperty.admin?.previewProperties,
@@ -2554,7 +2568,7 @@ function ArrayOfRelationsPreview({ propertyKey, value, property, size, textOnly
2554
2568
  return /* @__PURE__ */ jsx("div", {
2555
2569
  className: "flex flex-col w-full gap-0.5",
2556
2570
  children: value ? value.map((relation, index) => {
2557
- const entityRelation = normalizeToEntityRelation(relation);
2571
+ const entityRelation = normalizeToEntityRelation(relation, "relation", relationTargetPath);
2558
2572
  if (!entityRelation) return null;
2559
2573
  return /* @__PURE__ */ jsx("div", {
2560
2574
  className: "w-full",
@@ -2831,13 +2845,144 @@ function KeyValuePreview({ value }) {
2831
2845
  });
2832
2846
  }
2833
2847
  //#endregion
2848
+ //#region src/preview/components/date_fns_locales.ts
2849
+ /**
2850
+ * One date-fns locale, fetched when the admin is actually configured to use it.
2851
+ *
2852
+ * `DatePreview` used to open with:
2853
+ *
2854
+ * import * as locales from "date-fns/locale";
2855
+ *
2856
+ * and then index that namespace with the configured locale. A namespace import
2857
+ * of a barrel is a reference to every member of it, so all ~77 locales were
2858
+ * bundled — 640 kB, in the entry chunk's static graph, to format dates in the
2859
+ * one locale a given deployment has configured (and `undefined`, meaning
2860
+ * en-US, for nearly all of them).
2861
+ *
2862
+ * A map of thunks is written out rather than derived from the locale key,
2863
+ * because `import(`date-fns/locale/${name}`)` is not statically analysable:
2864
+ * the bundler cannot see which modules it must emit, and the import fails at
2865
+ * runtime in a build. Each entry below is its own async chunk, and exactly one
2866
+ * of them is ever fetched.
2867
+ *
2868
+ * `fil` is in the `Locale` union and has no date-fns locale to import; it
2869
+ * resolved to `undefined` through the namespace too, so it still does.
2870
+ */
2871
+ var dateFnsLocaleLoaders = {
2872
+ af: () => import("date-fns/locale/af").then((m) => m.af),
2873
+ ar: () => import("date-fns/locale/ar").then((m) => m.ar),
2874
+ arDZ: () => import("date-fns/locale/ar-DZ").then((m) => m.arDZ),
2875
+ arMA: () => import("date-fns/locale/ar-MA").then((m) => m.arMA),
2876
+ arSA: () => import("date-fns/locale/ar-SA").then((m) => m.arSA),
2877
+ az: () => import("date-fns/locale/az").then((m) => m.az),
2878
+ be: () => import("date-fns/locale/be").then((m) => m.be),
2879
+ bg: () => import("date-fns/locale/bg").then((m) => m.bg),
2880
+ bn: () => import("date-fns/locale/bn").then((m) => m.bn),
2881
+ ca: () => import("date-fns/locale/ca").then((m) => m.ca),
2882
+ cs: () => import("date-fns/locale/cs").then((m) => m.cs),
2883
+ cy: () => import("date-fns/locale/cy").then((m) => m.cy),
2884
+ da: () => import("date-fns/locale/da").then((m) => m.da),
2885
+ de: () => import("date-fns/locale/de").then((m) => m.de),
2886
+ el: () => import("date-fns/locale/el").then((m) => m.el),
2887
+ enAU: () => import("date-fns/locale/en-AU").then((m) => m.enAU),
2888
+ enCA: () => import("date-fns/locale/en-CA").then((m) => m.enCA),
2889
+ enGB: () => import("date-fns/locale/en-GB").then((m) => m.enGB),
2890
+ enIN: () => import("date-fns/locale/en-IN").then((m) => m.enIN),
2891
+ enNZ: () => import("date-fns/locale/en-NZ").then((m) => m.enNZ),
2892
+ enUS: () => import("date-fns/locale/en-US").then((m) => m.enUS),
2893
+ eo: () => import("date-fns/locale/eo").then((m) => m.eo),
2894
+ es: () => import("date-fns/locale/es").then((m) => m.es),
2895
+ et: () => import("date-fns/locale/et").then((m) => m.et),
2896
+ eu: () => import("date-fns/locale/eu").then((m) => m.eu),
2897
+ faIR: () => import("date-fns/locale/fa-IR").then((m) => m.faIR),
2898
+ fi: () => import("date-fns/locale/fi").then((m) => m.fi),
2899
+ fr: () => import("date-fns/locale/fr").then((m) => m.fr),
2900
+ frCA: () => import("date-fns/locale/fr-CA").then((m) => m.frCA),
2901
+ frCH: () => import("date-fns/locale/fr-CH").then((m) => m.frCH),
2902
+ gd: () => import("date-fns/locale/gd").then((m) => m.gd),
2903
+ gl: () => import("date-fns/locale/gl").then((m) => m.gl),
2904
+ gu: () => import("date-fns/locale/gu").then((m) => m.gu),
2905
+ he: () => import("date-fns/locale/he").then((m) => m.he),
2906
+ hi: () => import("date-fns/locale/hi").then((m) => m.hi),
2907
+ hr: () => import("date-fns/locale/hr").then((m) => m.hr),
2908
+ hu: () => import("date-fns/locale/hu").then((m) => m.hu),
2909
+ hy: () => import("date-fns/locale/hy").then((m) => m.hy),
2910
+ id: () => import("date-fns/locale/id").then((m) => m.id),
2911
+ is: () => import("date-fns/locale/is").then((m) => m.is),
2912
+ it: () => import("date-fns/locale/it").then((m) => m.it),
2913
+ ja: () => import("date-fns/locale/ja").then((m) => m.ja),
2914
+ ka: () => import("date-fns/locale/ka").then((m) => m.ka),
2915
+ kk: () => import("date-fns/locale/kk").then((m) => m.kk),
2916
+ kn: () => import("date-fns/locale/kn").then((m) => m.kn),
2917
+ ko: () => import("date-fns/locale/ko").then((m) => m.ko),
2918
+ lb: () => import("date-fns/locale/lb").then((m) => m.lb),
2919
+ lt: () => import("date-fns/locale/lt").then((m) => m.lt),
2920
+ lv: () => import("date-fns/locale/lv").then((m) => m.lv),
2921
+ mk: () => import("date-fns/locale/mk").then((m) => m.mk),
2922
+ ms: () => import("date-fns/locale/ms").then((m) => m.ms),
2923
+ mt: () => import("date-fns/locale/mt").then((m) => m.mt),
2924
+ nb: () => import("date-fns/locale/nb").then((m) => m.nb),
2925
+ nl: () => import("date-fns/locale/nl").then((m) => m.nl),
2926
+ nlBE: () => import("date-fns/locale/nl-BE").then((m) => m.nlBE),
2927
+ nn: () => import("date-fns/locale/nn").then((m) => m.nn),
2928
+ pl: () => import("date-fns/locale/pl").then((m) => m.pl),
2929
+ pt: () => import("date-fns/locale/pt").then((m) => m.pt),
2930
+ ptBR: () => import("date-fns/locale/pt-BR").then((m) => m.ptBR),
2931
+ ro: () => import("date-fns/locale/ro").then((m) => m.ro),
2932
+ ru: () => import("date-fns/locale/ru").then((m) => m.ru),
2933
+ sk: () => import("date-fns/locale/sk").then((m) => m.sk),
2934
+ sl: () => import("date-fns/locale/sl").then((m) => m.sl),
2935
+ sr: () => import("date-fns/locale/sr").then((m) => m.sr),
2936
+ srLatn: () => import("date-fns/locale/sr-Latn").then((m) => m.srLatn),
2937
+ sv: () => import("date-fns/locale/sv").then((m) => m.sv),
2938
+ ta: () => import("date-fns/locale/ta").then((m) => m.ta),
2939
+ te: () => import("date-fns/locale/te").then((m) => m.te),
2940
+ th: () => import("date-fns/locale/th").then((m) => m.th),
2941
+ tr: () => import("date-fns/locale/tr").then((m) => m.tr),
2942
+ ug: () => import("date-fns/locale/ug").then((m) => m.ug),
2943
+ uk: () => import("date-fns/locale/uk").then((m) => m.uk),
2944
+ uz: () => import("date-fns/locale/uz").then((m) => m.uz),
2945
+ vi: () => import("date-fns/locale/vi").then((m) => m.vi),
2946
+ zhCN: () => import("date-fns/locale/zh-CN").then((m) => m.zhCN),
2947
+ zhTW: () => import("date-fns/locale/zh-TW").then((m) => m.zhTW)
2948
+ };
2949
+ var cache = /* @__PURE__ */ new Map();
2950
+ /** The locale if it has already been fetched, otherwise `undefined`. Never fetches. */
2951
+ function getLoadedDateFnsLocale(locale) {
2952
+ return locale ? cache.get(locale) : void 0;
2953
+ }
2954
+ /**
2955
+ * Fetch a date-fns locale. Resolves to `undefined` for a locale date-fns does
2956
+ * not ship, which is what indexing the namespace used to yield.
2957
+ */
2958
+ async function loadDateFnsLocale(locale) {
2959
+ if (!locale) return void 0;
2960
+ const cached = cache.get(locale);
2961
+ if (cached) return cached;
2962
+ const loader = dateFnsLocaleLoaders[locale];
2963
+ if (!loader) return void 0;
2964
+ const loaded = await loader();
2965
+ cache.set(locale, loaded);
2966
+ return loaded;
2967
+ }
2968
+ //#endregion
2834
2969
  //#region src/preview/components/DatePreview.tsx
2835
2970
  /**
2836
2971
  * @group Preview components
2837
2972
  */
2838
2973
  function DatePreview({ date, mode = "date_time", timezone }) {
2839
2974
  const customizationController = useCustomizationController();
2840
- const dateUtilsLocale = customizationController?.locale ? locales[customizationController.locale] : void 0;
2975
+ const configuredLocale = customizationController?.locale;
2976
+ const [dateUtilsLocale, setDateUtilsLocale] = React.useState(() => getLoadedDateFnsLocale(configuredLocale));
2977
+ React.useEffect(() => {
2978
+ let cancelled = false;
2979
+ loadDateFnsLocale(configuredLocale).then((loaded) => {
2980
+ if (!cancelled) setDateUtilsLocale(loaded);
2981
+ }).catch((error) => console.error(`[rebase] could not load the "${configuredLocale}" date locale`, error));
2982
+ return () => {
2983
+ cancelled = true;
2984
+ };
2985
+ }, [configuredLocale]);
2841
2986
  if (!date || !(date instanceof Date) || isNaN(date.getTime())) return /* @__PURE__ */ jsx(Fragment, {});
2842
2987
  if (timezone) {
2843
2988
  const options = {
@@ -3125,42 +3270,44 @@ var PropertyPreview = React.memo(function PropertyPreview(props) {
3125
3270
  });
3126
3271
  else content = buildWrongValueType(propertyKey, property.type, value);
3127
3272
  else content = /* @__PURE__ */ jsx(EmptyValue, {});
3128
- else if (property.type === "relation") if (!value) content = /* @__PURE__ */ jsx(EmptyValue, {});
3129
- else if (Array.isArray(value)) {
3130
- const relations = value.map((item) => normalizeToEntityRelation(item, "relation")).filter(Boolean);
3131
- const renderRelation = (entityRelation, index) => /* @__PURE__ */ jsx(RelationPreview, {
3132
- disabled: !property.relation,
3133
- previewProperties: property.admin?.previewProperties,
3134
- includeId: property.admin?.includeId,
3135
- includeEntityLink: property.admin?.includeEntityLink,
3136
- size: "small",
3137
- relation: entityRelation,
3138
- textOnly: props.textOnly
3139
- }, `preview_rel_${propertyKey}_${index}`);
3140
- content = nested || props.textOnly ? /* @__PURE__ */ jsx(InlineEntityListPreview, {
3141
- items: relations,
3142
- renderItem: renderRelation
3143
- }) : /* @__PURE__ */ jsx("div", {
3144
- className: "flex flex-col w-full gap-0.5",
3145
- children: relations.map((entityRelation, index) => /* @__PURE__ */ jsx("div", {
3146
- className: "w-full",
3147
- children: renderRelation(entityRelation, index)
3148
- }, `preview_rel_wrapper_${propertyKey}_${index}`))
3149
- });
3150
- } else {
3151
- const relationValue = normalizeToEntityRelation(value, "relation");
3152
- if (relationValue) content = /* @__PURE__ */ jsx(RelationPreview, {
3153
- disabled: !property.relation,
3154
- previewProperties: property.admin?.previewProperties,
3155
- includeId: property.admin?.includeId,
3156
- includeEntityLink: property.admin?.includeEntityLink,
3157
- size: props.size,
3158
- relation: relationValue,
3159
- textOnly: props.textOnly
3160
- });
3161
- else content = buildWrongValueType(propertyKey, property.type, value);
3162
- }
3163
- else if (property.type === "boolean") if (typeof value === "boolean") content = /* @__PURE__ */ jsx(BooleanPreview, {
3273
+ else if (property.type === "relation") {
3274
+ const relationTargetPath = getRelationTargetPath(property);
3275
+ if (!value) content = /* @__PURE__ */ jsx(EmptyValue, {});
3276
+ else if (Array.isArray(value)) {
3277
+ const relations = value.map((item) => normalizeToEntityRelation(item, "relation", relationTargetPath)).filter(Boolean);
3278
+ const renderRelation = (entityRelation, index) => /* @__PURE__ */ jsx(RelationPreview, {
3279
+ disabled: !property.relation,
3280
+ previewProperties: property.admin?.previewProperties,
3281
+ includeId: property.admin?.includeId,
3282
+ includeEntityLink: property.admin?.includeEntityLink,
3283
+ size: "small",
3284
+ relation: entityRelation,
3285
+ textOnly: props.textOnly
3286
+ }, `preview_rel_${propertyKey}_${index}`);
3287
+ content = nested || props.textOnly ? /* @__PURE__ */ jsx(InlineEntityListPreview, {
3288
+ items: relations,
3289
+ renderItem: renderRelation
3290
+ }) : /* @__PURE__ */ jsx("div", {
3291
+ className: "flex flex-col w-full gap-0.5",
3292
+ children: relations.map((entityRelation, index) => /* @__PURE__ */ jsx("div", {
3293
+ className: "w-full",
3294
+ children: renderRelation(entityRelation, index)
3295
+ }, `preview_rel_wrapper_${propertyKey}_${index}`))
3296
+ });
3297
+ } else {
3298
+ const relationValue = normalizeToEntityRelation(value, "relation", relationTargetPath);
3299
+ if (relationValue) content = /* @__PURE__ */ jsx(RelationPreview, {
3300
+ disabled: !property.relation,
3301
+ previewProperties: property.admin?.previewProperties,
3302
+ includeId: property.admin?.includeId,
3303
+ includeEntityLink: property.admin?.includeEntityLink,
3304
+ size: props.size,
3305
+ relation: relationValue,
3306
+ textOnly: props.textOnly
3307
+ });
3308
+ else content = buildWrongValueType(propertyKey, property.type, value);
3309
+ }
3310
+ } else if (property.type === "boolean") if (typeof value === "boolean") content = /* @__PURE__ */ jsx(BooleanPreview, {
3164
3311
  value,
3165
3312
  size,
3166
3313
  property,
@@ -3329,14 +3476,15 @@ function ReadOnlyFieldBinding({ propertyKey, value, error, showError, minimalist
3329
3476
  icon: getIconForProperty(property, "small"),
3330
3477
  required: property.validation?.required,
3331
3478
  title: property.name ?? propertyKey,
3332
- className: "h-8 text-text-secondary dark:text-text-secondary-dark ml-3.5"
3479
+ className: "h-8 text-text-secondary dark:text-text-secondary-dark ml-3"
3333
3480
  }),
3334
3481
  /* @__PURE__ */ jsx("div", {
3335
- className: cls("w-full overflow-x-scroll no-scrollbar flex items-center", skipCardWrapper ? "" : "rounded-lg border border-surface-200 dark:border-surface-700 px-4 md:px-6 min-h-12 opacity-80"),
3482
+ className: cls("w-full overflow-x-scroll no-scrollbar flex items-center", skipCardWrapper ? "" : "rounded-lg border border-surface-200 dark:border-surface-700 px-3 min-h-12 opacity-80"),
3336
3483
  children: /* @__PURE__ */ jsx(ErrorBoundary, { children: /* @__PURE__ */ jsx(PropertyPreview, {
3337
3484
  propertyKey,
3338
3485
  value,
3339
3486
  property,
3487
+ hideLabel: hideLabel || !minimalistView,
3340
3488
  size: "medium"
3341
3489
  }) })
3342
3490
  }),
@@ -3412,7 +3560,7 @@ function ArrayCustomShapedFieldBinding({ propertyKey, value, error, showError, i
3412
3560
  ]
3413
3561
  })] });
3414
3562
  const body = (resolvedProperties ?? []).map((childProperty, index) => {
3415
- const thisDisabled = isReadOnly(childProperty) || Boolean(childProperty.admin?.disabled);
3563
+ const thisDisabled = isReadOnly(childProperty) || isDisabled(childProperty);
3416
3564
  return /* @__PURE__ */ jsx("div", {
3417
3565
  className: "pb-4",
3418
3566
  children: /* @__PURE__ */ jsx(PropertyFieldBinding, {
@@ -3562,9 +3710,13 @@ function useUserSelector({ pageSize = DEFAULT_PAGE_SIZE$2 } = {}) {
3562
3710
  const [currentSearch, setCurrentSearch] = useState("");
3563
3711
  const offsetRef = useRef(0);
3564
3712
  const userCacheRef = useRef(/* @__PURE__ */ new Map());
3713
+ const requestIdRef = useRef(0);
3714
+ const searchTimeoutRef = useRef(null);
3565
3715
  const fetchUsers = useCallback(async (searchStr, offset, append) => {
3566
3716
  const apiBase = apiBaseOf(client);
3567
3717
  if (!apiBase) return;
3718
+ const requestId = ++requestIdRef.current;
3719
+ const superseded = () => requestId !== requestIdRef.current;
3568
3720
  setIsLoading(true);
3569
3721
  try {
3570
3722
  const token = await getAuthToken?.();
@@ -3576,6 +3728,7 @@ function useUserSelector({ pageSize = DEFAULT_PAGE_SIZE$2 } = {}) {
3576
3728
  const response = await fetch(`${apiBase}/admin/users?${params}`, { headers: token ? { Authorization: `Bearer ${token}` } : {} });
3577
3729
  if (!response.ok) throw new Error("Failed to fetch users");
3578
3730
  const data = await response.json();
3731
+ if (superseded()) return;
3579
3732
  const newItems = (data.data ?? data.users ?? []).map((r) => {
3580
3733
  const user = {
3581
3734
  uid: r.uid ?? r.id ?? "",
@@ -3597,9 +3750,10 @@ function useUserSelector({ pageSize = DEFAULT_PAGE_SIZE$2 } = {}) {
3597
3750
  setHasMore(newItems.length >= pageSize);
3598
3751
  offsetRef.current = offset + newItems.length;
3599
3752
  } catch {
3753
+ if (superseded()) return;
3600
3754
  setHasMore(false);
3601
3755
  } finally {
3602
- setIsLoading(false);
3756
+ if (!superseded()) setIsLoading(false);
3603
3757
  }
3604
3758
  }, [
3605
3759
  client?.baseUrl,
@@ -3611,15 +3765,24 @@ function useUserSelector({ pageSize = DEFAULT_PAGE_SIZE$2 } = {}) {
3611
3765
  offsetRef.current = 0;
3612
3766
  fetchUsers("", 0, false);
3613
3767
  }, [fetchUsers]);
3768
+ const search = useCallback((searchStr) => {
3769
+ if (searchTimeoutRef.current) clearTimeout(searchTimeoutRef.current);
3770
+ searchTimeoutRef.current = setTimeout(() => {
3771
+ setCurrentSearch(searchStr);
3772
+ offsetRef.current = 0;
3773
+ fetchUsers(searchStr, 0, false);
3774
+ }, searchStr.trim() ? 300 : 0);
3775
+ }, [fetchUsers]);
3776
+ useEffect(() => {
3777
+ return () => {
3778
+ if (searchTimeoutRef.current) clearTimeout(searchTimeoutRef.current);
3779
+ };
3780
+ }, []);
3614
3781
  return {
3615
3782
  items,
3616
3783
  isLoading,
3617
3784
  hasMore,
3618
- search: useCallback((searchStr) => {
3619
- setCurrentSearch(searchStr);
3620
- offsetRef.current = 0;
3621
- fetchUsers(searchStr, 0, false);
3622
- }, [fetchUsers]),
3785
+ search,
3623
3786
  loadMore: useCallback(() => {
3624
3787
  fetchUsers(currentSearch, offsetRef.current, true);
3625
3788
  }, [fetchUsers, currentSearch]),
@@ -3632,6 +3795,7 @@ var UserSelector = React$1.forwardRef(({ value, size = "medium", onValueChange,
3632
3795
  const { items: availableItems, isLoading, hasMore, search, loadMore, getUser } = useUserSelector({ pageSize });
3633
3796
  const [isPopoverOpen, setIsPopoverOpen] = useState(false);
3634
3797
  const [searchString, setSearchString] = useState("");
3798
+ const contextPortalContainer = usePortalContainer();
3635
3799
  const scrollContainerRef = useRef(null);
3636
3800
  const sentinelRef = useRef(null);
3637
3801
  const observerRef = useRef(null);
@@ -3643,7 +3807,9 @@ var UserSelector = React$1.forwardRef(({ value, size = "medium", onValueChange,
3643
3807
  }, [ref]);
3644
3808
  const contentRef = useRef(null);
3645
3809
  const searchInputRef = useRef(null);
3646
- const resolvedUser = (value ? getUser(value) ?? null : null) ?? (value ? availableItems.find((i) => i.uid === value)?.user ?? null : null);
3810
+ const listedUser = (value ? getUser(value) ?? null : null) ?? (value ? availableItems.find((i) => i.uid === value)?.user ?? null : null);
3811
+ const fetchedUser = useResolvedUser(value && !listedUser ? value : void 0);
3812
+ const resolvedUser = listedUser ?? fetchedUser;
3647
3813
  const sentinelCallbackRef = useCallback((node) => {
3648
3814
  if (observerRef.current) {
3649
3815
  observerRef.current.disconnect();
@@ -3723,9 +3889,8 @@ var UserSelector = React$1.forwardRef(({ value, size = "medium", onValueChange,
3723
3889
  document.removeEventListener("keydown", handleKey, true);
3724
3890
  };
3725
3891
  }, [isPopoverOpen]);
3726
- useInjectStyles("UserSelector", " [cmdk-group] { max-height: 40vh; overflow-y: auto; } ");
3727
3892
  const resolvedPlaceholder = placeholder || /* @__PURE__ */ jsx(EmptyValue, { className: "ml-2" });
3728
- const portalContainer = typeof document !== "undefined" ? document.body : void 0;
3893
+ const portalContainer = contextPortalContainer ?? (typeof document !== "undefined" ? document.body : void 0) ?? void 0;
3729
3894
  return /* @__PURE__ */ jsx(Fragment, { children: /* @__PURE__ */ jsxs(PopoverPrimitive.Root, {
3730
3895
  open: isPopoverOpen,
3731
3896
  onOpenChange: handleRootOpenChange,
@@ -3753,6 +3918,9 @@ var UserSelector = React$1.forwardRef(({ value, size = "medium", onValueChange,
3753
3918
  children: [resolvedUser ? /* @__PURE__ */ jsx("div", {
3754
3919
  className: "flex flex-row items-center gap-1 truncate flex-1 min-w-0 mr-2",
3755
3920
  children: /* @__PURE__ */ jsx(UserDisplay, { user: resolvedUser })
3921
+ }) : value ? /* @__PURE__ */ jsx("span", {
3922
+ className: "text-sm truncate text-text-primary dark:text-text-primary-dark",
3923
+ children: value
3756
3924
  }) : /* @__PURE__ */ jsx("span", {
3757
3925
  className: "text-sm text-text-secondary dark:text-text-secondary-dark",
3758
3926
  children: resolvedPlaceholder
@@ -4430,13 +4598,15 @@ function relationCardinality$2(relation) {
4430
4598
  if (relation.kind === "belongsTo" || relation.kind === "hasOne") return "one";
4431
4599
  return relation.cardinality;
4432
4600
  }
4433
- var RelationSelector = React$1.forwardRef(({ value, size = "medium", onValueChange, invisible, disabled, placeholder, useChips = true, className, relation, multiple: multipleOverride, fixedFilter, pageSize, emptyPlaceholder, searchPlaceholder = "Search...", noResultsText = "No relations found.", loadingText = "Loading..." }, ref) => {
4601
+ var RelationSelector = React$1.forwardRef(({ value, size = "medium", onValueChange, invisible, disabled, placeholder, useChips = true, className, relation, multiple: multipleOverride, fixedFilter, pageSize, emptyPlaceholder, searchPlaceholder = "Search...", noResultsText = "No matches.", emptyText = "Select…", emptyCollectionText, loadingText = "Loading..." }, ref) => {
4434
4602
  const collection = relation.target();
4435
4603
  const dataClient = useData();
4436
4604
  const sidePanelController = useSidePanel();
4605
+ const contextPortalContainer = usePortalContainer();
4437
4606
  const multiple = multipleOverride ?? relationCardinality$2(relation) === "many";
4438
4607
  const [isPopoverOpen, setIsPopoverOpen] = useState(false);
4439
4608
  const isPopoverOpenRef = useRef(false);
4609
+ const [listRequested, setListRequested] = useState(false);
4440
4610
  const [selectedItems, setSelectedItems] = useState([]);
4441
4611
  const [isLoadingSelectedItems, setIsLoadingSelectedItems] = useState(false);
4442
4612
  const [searchString, setSearchString] = useState("");
@@ -4446,7 +4616,8 @@ var RelationSelector = React$1.forwardRef(({ value, size = "medium", onValueChan
4446
4616
  path: getCollectionDataPath(collection),
4447
4617
  collection,
4448
4618
  fixedFilter,
4449
- pageSize
4619
+ pageSize,
4620
+ enabled: listRequested
4450
4621
  });
4451
4622
  const scrollContainerRef = useRef(null);
4452
4623
  const sentinelRef = useRef(null);
@@ -4639,6 +4810,7 @@ var RelationSelector = React$1.forwardRef(({ value, size = "medium", onValueChan
4639
4810
  if (disabled) return;
4640
4811
  if (next) {
4641
4812
  pinnedIdsRef.current = new Set(selectedItems.map((i) => String(i.id)));
4813
+ setListRequested(true);
4642
4814
  setIsPopoverOpen(true);
4643
4815
  isPopoverOpenRef.current = true;
4644
4816
  }
@@ -4669,14 +4841,13 @@ var RelationSelector = React$1.forwardRef(({ value, size = "medium", onValueChan
4669
4841
  document.removeEventListener("keydown", handleKey, true);
4670
4842
  };
4671
4843
  }, [isPopoverOpen]);
4672
- useInjectStyles("RelationSelector", " [cmdk-group] { max-height: 40vh; overflow-y: auto; } ");
4673
4844
  const closePopover = useCallback(() => {
4674
4845
  setIsPopoverOpen(false);
4675
4846
  isPopoverOpenRef.current = false;
4676
4847
  pinnedIdsRef.current = null;
4677
4848
  }, []);
4678
- const resolvedPlaceholder = placeholder || emptyPlaceholder || /* @__PURE__ */ jsx(EmptyValue, { className: "ml-2" });
4679
- const portalContainer = typeof document !== "undefined" ? document.body : void 0;
4849
+ const resolvedPlaceholder = placeholder || emptyPlaceholder || emptyText;
4850
+ const portalContainer = contextPortalContainer ?? (typeof document !== "undefined" ? document.body : void 0) ?? void 0;
4680
4851
  return /* @__PURE__ */ jsx(Fragment, { children: /* @__PURE__ */ jsxs(PopoverPrimitive.Root, {
4681
4852
  open: isPopoverOpen,
4682
4853
  onOpenChange: handleRootOpenChange,
@@ -4692,6 +4863,7 @@ var RelationSelector = React$1.forwardRef(({ value, size = "medium", onValueChan
4692
4863
  disabled,
4693
4864
  onClick: () => {
4694
4865
  if (disabled) return;
4866
+ setListRequested(true);
4695
4867
  setIsPopoverOpen((o) => {
4696
4868
  const next = !o;
4697
4869
  isPopoverOpenRef.current = next;
@@ -4848,7 +5020,7 @@ var RelationSelector = React$1.forwardRef(({ value, size = "medium", onValueChan
4848
5020
  }),
4849
5021
  !isLoading && availableItems.length === 0 && /* @__PURE__ */ jsx(Command.Empty, {
4850
5022
  className: "px-4 py-6 text-center text-text-secondary dark:text-text-secondary-dark",
4851
- children: noResultsText
5023
+ children: searchString ? noResultsText : emptyCollectionText ?? `No ${collection.name ?? "entries"} yet.`
4852
5024
  }),
4853
5025
  /* @__PURE__ */ jsxs(Command.Group, { children: [
4854
5026
  (() => {
@@ -5079,14 +5251,16 @@ function getTableBindingForProperty(property, selected) {
5079
5251
  allowScroll: false
5080
5252
  };
5081
5253
  } else if (property.type === "relation") {
5082
- if (property.relation) if (property.admin?.widget === "dialog") return {
5083
- Component: RelationDialogBindingComponent,
5084
- allowScroll: false
5085
- };
5086
- else return {
5087
- Component: RelationSelectorBindingComponent,
5088
- allowScroll: false
5089
- };
5254
+ if (property.relation) {
5255
+ if (property.admin?.widget === "dialog") return {
5256
+ Component: RelationDialogBindingComponent,
5257
+ allowScroll: false
5258
+ };
5259
+ else if (selected) return {
5260
+ Component: RelationSelectorBindingComponent,
5261
+ allowScroll: false
5262
+ };
5263
+ }
5090
5264
  } else if (property.type === "array") {
5091
5265
  const arrayProperty = property;
5092
5266
  if (!arrayProperty.of && !arrayProperty.oneOf) throw Error("You need to specify an 'of' or 'oneOf' prop (or specify a custom field) in your array property");
@@ -5565,7 +5739,7 @@ var PropertyTableCell = React.memo(function PropertyTableCell({ propertyKey, cus
5565
5739
  const customPreview = Boolean(property.admin?.Preview);
5566
5740
  const readOnlyProperty = isReadOnly(property);
5567
5741
  const disabledTooltip = typeof property.admin?.disabled === "object" ? property.admin?.disabled.disabledMessage : void 0;
5568
- const disabled = readonly || disabledProp || Boolean(property.admin?.disabled);
5742
+ const disabled = readonly || disabledProp || isDisabled(property);
5569
5743
  const validation = useMemo(() => mapPropertyToZod({
5570
5744
  property,
5571
5745
  entityId: entity.id,
@@ -6049,11 +6223,14 @@ function getSubcollectionColumnId(collection) {
6049
6223
  //#region src/components/CollectionTableBinding/column_utils.tsx
6050
6224
  function propertiesToColumns({ properties, sortable, fixedFilter, engine, AdditionalHeaderWidget }) {
6051
6225
  const disabledFilter = Boolean(fixedFilter);
6052
- return Object.entries(properties).flatMap(([key, property]) => getColumnKeysForProperty(property, key)).map(({ key, disabled }) => {
6226
+ return Object.entries(properties).flatMap(([key, property]) => getColumnKeysForProperty(property, key)).flatMap(({ key, disabled }) => {
6053
6227
  const property = getResolvedPropertyInPath(properties, key);
6054
- if (!property) throw Error("Internal error: no property found in path " + key);
6228
+ if (!property) {
6229
+ console.warn(`No property found in path "${key}" — skipping that column.`);
6230
+ return [];
6231
+ }
6055
6232
  const filterable = filterableProperty(property, false, engine);
6056
- return {
6233
+ return [{
6057
6234
  key,
6058
6235
  align: getTableCellAlignment(property),
6059
6236
  icon: getIconForProperty(property, "small"),
@@ -6071,7 +6248,7 @@ function propertiesToColumns({ properties, sortable, fixedFilter, engine, Additi
6071
6248
  propertyKey: key,
6072
6249
  onHover
6073
6250
  }) : void 0
6074
- };
6251
+ }];
6075
6252
  });
6076
6253
  }
6077
6254
  /**
@@ -6242,6 +6419,7 @@ function StringNumberFilterField({ name, value, setValue, type, isArray, enumVal
6242
6419
  children: [
6243
6420
  !enumValues && /* @__PURE__ */ jsx(TextField, {
6244
6421
  size: "medium",
6422
+ "aria-label": `${title ?? name} filter value`,
6245
6423
  type: type === "number" ? "number" : void 0,
6246
6424
  value: internalValue !== void 0 && internalValue != null ? String(internalValue) : "",
6247
6425
  disabled: isNullOperation,
@@ -7076,7 +7254,7 @@ var CollectionTableBinding = function CollectionTableBinding({ className, style,
7076
7254
  propertyKey,
7077
7255
  entity
7078
7256
  }) ?? columnCustom?.resolvedProperty;
7079
- if (!property?.admin?.disabled) disabled = false;
7257
+ if (!property || !isDisabled(property)) disabled = false;
7080
7258
  if (!property) return null;
7081
7259
  return /* @__PURE__ */ jsx(ErrorBoundary, { children: entity ? /* @__PURE__ */ jsx(PropertyTableCell, {
7082
7260
  readonly: !inlineEditing,
@@ -7398,6 +7576,35 @@ function withViewMode(url, search) {
7398
7576
  return `${base}${base.includes("?") ? "&" : "?"}${VIEW_MODE_PARAM}=${viewMode}${hash}`;
7399
7577
  }
7400
7578
  /**
7579
+ * Carry the whole of the collection list's URL state onto a navigation target.
7580
+ *
7581
+ * `withViewMode` exists because losing the view mode mid-session is jarring.
7582
+ * Everything else the list writes to the URL — the search string, the filters,
7583
+ * the sort — is state the user built up deliberately, and losing *that* is
7584
+ * worse: opening a record from a search and pressing back returned an
7585
+ * unfiltered list with an empty search box, so the search had to be retyped
7586
+ * for every record they wanted to look at.
7587
+ *
7588
+ * Copies every param the list currently has rather than an allowlist, because
7589
+ * filters are encoded under the field's own name — there is no fixed set to
7590
+ * list. Params already on the target win: the caller is more specific.
7591
+ *
7592
+ * @param url the URL to decorate. May carry a query string and/or a hash.
7593
+ * @param search query string to read from. Defaults to the browser location.
7594
+ */
7595
+ function withListState(url, search) {
7596
+ const source = search ?? (typeof window !== "undefined" ? window.location.search : "");
7597
+ const carried = new URLSearchParams(source);
7598
+ if ([...carried].length === 0) return url;
7599
+ const hashIndex = url.indexOf("#");
7600
+ const hash = hashIndex >= 0 ? url.substring(hashIndex) : "";
7601
+ const [pathname, existing] = (hashIndex >= 0 ? url.substring(0, hashIndex) : url).split("?");
7602
+ const target = new URLSearchParams(existing ?? "");
7603
+ for (const [key, value] of carried) if (!target.has(key)) target.set(key, value);
7604
+ const query = target.toString();
7605
+ return query ? `${pathname}?${query}${hash}` : `${pathname}${hash}`;
7606
+ }
7607
+ /**
7401
7608
  * Resolve the view mode for a collection, honouring the documented priority:
7402
7609
  * URL param > saved user config > collection default.
7403
7610
  */
@@ -7445,7 +7652,7 @@ function navigateToEntity({ openEntityMode, collection, entityId, copy, path, se
7445
7652
  else {
7446
7653
  let to = navigation.buildUrlCollectionPath(entityId ? `${path ?? path}/${entityId}` : path ?? path);
7447
7654
  if (entityId && selectedTab) to += `/${selectedTab}`;
7448
- to = withViewMode(to);
7655
+ to = withListState(to);
7449
7656
  if (!entityId) to += "#new";
7450
7657
  if (copy) to += "#copy";
7451
7658
  const navigateOptions = {};
@@ -7575,15 +7782,6 @@ function FieldCaption({ error, children }) {
7575
7782
  //#endregion
7576
7783
  //#region src/components/SearchIconsView.tsx
7577
7784
  var UPDATE_SEARCH_INDEX_WAIT_MS = 220;
7578
- function toPascalCase(str) {
7579
- return str.split(/[-_]/).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join("");
7580
- }
7581
- function resolveIcon(iconKey) {
7582
- const allIcons = lucideIcons;
7583
- let icon = allIcons[iconKey];
7584
- if (!icon) icon = allIcons[toPascalCase(iconKey)];
7585
- return icon ?? null;
7586
- }
7587
7785
  function SearchIconsView({ selectedIcon = "", onIconSelected }) {
7588
7786
  const { t } = useTranslation();
7589
7787
  const [keys, setKeys] = React.useState(null);
@@ -7609,21 +7807,20 @@ function SearchIconsView({ selectedIcon = "", onIconSelected }) {
7609
7807
  placeholder: t("search_for_more_icons")
7610
7808
  }), /* @__PURE__ */ jsx("div", {
7611
7809
  className: "flex max-w-full flex-wrap mt-4",
7612
- children: icons.map((icon) => {
7613
- const LucideIcon = resolveIcon(icon);
7614
- if (!LucideIcon) return null;
7615
- return /* @__PURE__ */ jsx(Tooltip, {
7616
- title: icon,
7617
- asChild: true,
7618
- children: /* @__PURE__ */ jsx(IconButton, {
7619
- shape: "square",
7620
- toggled: selectedIcon === icon,
7621
- onClick: onIconSelected ? () => onIconSelected(icon) : void 0,
7622
- className: "box-content m-1",
7623
- children: /* @__PURE__ */ jsx(LucideIcon, { size: iconSize.medium })
7810
+ children: icons.map((icon) => /* @__PURE__ */ jsx(Tooltip, {
7811
+ title: icon,
7812
+ asChild: true,
7813
+ children: /* @__PURE__ */ jsx(IconButton, {
7814
+ shape: "square",
7815
+ toggled: selectedIcon === icon,
7816
+ onClick: onIconSelected ? () => onIconSelected(icon) : void 0,
7817
+ className: "box-content m-1",
7818
+ children: /* @__PURE__ */ jsx(LucideIconByName, {
7819
+ name: icon,
7820
+ size: iconSize.medium
7624
7821
  })
7625
- }, icon);
7626
- })
7822
+ })
7823
+ }, icon))
7627
7824
  })] });
7628
7825
  }
7629
7826
  //#endregion
@@ -7967,10 +8164,23 @@ function getWorksheetHeaders(worksheet) {
7967
8164
  /**
7968
8165
  * Take an object with keys of type `address.street`, `address.city` and
7969
8166
  * convert it to an object with nested objects like `{ address: { street: ..., city: ... } }`
8167
+ *
8168
+ * Keys here are the **header row of an uploaded file**, so they are attacker
8169
+ * data. `res["__proto__"] = …` is the prototype setter rather than an own
8170
+ * property, so a column named `__proto__.polluted` walked out of the accumulator
8171
+ * and wrote onto `Object.prototype` for the life of the tab — and
8172
+ * `constructor.prototype.x` threw a `TypeError` that surfaced as an unreadable
8173
+ * file. Such a column is refused, not sanitised, the way `setIn` settled it
8174
+ * (`docs/bug-classes.md` class 22).
8175
+ *
7970
8176
  * @param flatObj
7971
8177
  */
7972
8178
  function unflattenObject(flatObj) {
7973
8179
  return Object.keys(flatObj).reduce((nestedObj, key) => {
8180
+ if (pathTraversesPrototype(key)) {
8181
+ console.warn(`Skipping column "${key}": a header may not reach the prototype chain`);
8182
+ return nestedObj;
8183
+ }
7974
8184
  let currentObj = nestedObj;
7975
8185
  const keyParts = key.split(".");
7976
8186
  keyParts.forEach((keyPart, i) => {
@@ -7992,6 +8202,10 @@ function unflattenObject(flatObj) {
7992
8202
  }
7993
8203
  function mapJsonParse(obj) {
7994
8204
  return Object.keys(obj).reduce((acc, key) => {
8205
+ if (isPrototypePollutingKey(key)) {
8206
+ console.warn(`Skipping column "${key}": a header may not reach the prototype chain`);
8207
+ return acc;
8208
+ }
7995
8209
  try {
7996
8210
  acc[key] = JSON.parse(obj[key]);
7997
8211
  } catch (e) {
@@ -8001,10 +8215,183 @@ function mapJsonParse(obj) {
8001
8215
  }, {});
8002
8216
  }
8003
8217
  //#endregion
8218
+ //#region src/data_import/utils/csv.ts
8219
+ /**
8220
+ * Delimiters sniffed from the header row, most common first.
8221
+ *
8222
+ * A spreadsheet saved as "CSV" in a locale that uses the comma as a decimal
8223
+ * separator is semicolon-delimited, and a tab-separated file saved as `.csv`
8224
+ * happens often enough to be worth reading.
8225
+ */
8226
+ var CANDIDATE_DELIMITERS = [
8227
+ ",",
8228
+ ";",
8229
+ " "
8230
+ ];
8231
+ /**
8232
+ * Split CSV text into rows of raw cells, RFC 4180 style.
8233
+ *
8234
+ * Quoted fields may contain the delimiter, newlines and doubled quotes; this is
8235
+ * the exact inverse of what the export's `entryToCSVRow` writes, so a file this
8236
+ * admin produced reads back into the cells it was built from.
8237
+ */
8238
+ function parseCsvRows(text, delimiter = ",") {
8239
+ const rows = [];
8240
+ let row = [];
8241
+ let field = "";
8242
+ let quoted = false;
8243
+ let inQuotes = false;
8244
+ let i = 0;
8245
+ const endField = () => {
8246
+ row.push(field);
8247
+ field = "";
8248
+ quoted = false;
8249
+ };
8250
+ while (i < text.length) {
8251
+ const char = text[i];
8252
+ if (inQuotes) {
8253
+ if (char === "\"") {
8254
+ if (text[i + 1] === "\"") {
8255
+ field += "\"";
8256
+ i += 2;
8257
+ continue;
8258
+ }
8259
+ inQuotes = false;
8260
+ i++;
8261
+ continue;
8262
+ }
8263
+ field += char;
8264
+ i++;
8265
+ continue;
8266
+ }
8267
+ if (char === "\"" && field === "") {
8268
+ inQuotes = true;
8269
+ quoted = true;
8270
+ i++;
8271
+ continue;
8272
+ }
8273
+ if (char === delimiter) {
8274
+ endField();
8275
+ i++;
8276
+ continue;
8277
+ }
8278
+ if (char === "\r") {
8279
+ i++;
8280
+ continue;
8281
+ }
8282
+ if (char === "\n") {
8283
+ endField();
8284
+ rows.push(row);
8285
+ row = [];
8286
+ i++;
8287
+ continue;
8288
+ }
8289
+ field += char;
8290
+ i++;
8291
+ }
8292
+ if (field !== "" || quoted || row.length > 0) {
8293
+ endField();
8294
+ rows.push(row);
8295
+ }
8296
+ return rows;
8297
+ }
8298
+ /**
8299
+ * Pick the delimiter that splits the header row into the most cells. A
8300
+ * single-column file matches nothing and stays on the comma.
8301
+ */
8302
+ function detectCsvDelimiter(text) {
8303
+ const sample = text.slice(0, 64 * 1024);
8304
+ let best = CANDIDATE_DELIMITERS[0];
8305
+ let bestCount = 1;
8306
+ for (const candidate of CANDIDATE_DELIMITERS) {
8307
+ const count = parseCsvRows(sample, candidate)[0]?.length ?? 0;
8308
+ if (count > bestCount) {
8309
+ best = candidate;
8310
+ bestCount = count;
8311
+ }
8312
+ }
8313
+ return best;
8314
+ }
8315
+ /**
8316
+ * Parse CSV text into one object per row, keyed by the header row.
8317
+ *
8318
+ * The picker accepts `.csv` and the dialog promises "Upload a CSV, Excel or JSON
8319
+ * file", but every non-JSON file used to be handed to ExcelJS'
8320
+ * `workbook.xlsx.load`, which is a zip reader — so every CSV failed with
8321
+ * "Can't find end of central directory : is this a zip file ?". The export
8322
+ * writes CSV by default, so the round trip could not close.
8323
+ *
8324
+ * A blank header keeps its column rather than collapsing it, so the cells to its
8325
+ * right stay under their own names.
8326
+ */
8327
+ function parseCsvToObjects(text) {
8328
+ const cleaned = text.charCodeAt(0) === 65279 ? text.slice(1) : text;
8329
+ const rows = parseCsvRows(cleaned, detectCsvDelimiter(cleaned)).filter((row) => row.some((cell) => cell.trim() !== ""));
8330
+ if (rows.length === 0) return {
8331
+ headers: [],
8332
+ data: []
8333
+ };
8334
+ const headers = rows[0].map((header, index) => header.trim() || `Column${index + 1}`);
8335
+ return {
8336
+ headers,
8337
+ data: rows.slice(1).map((cells) => {
8338
+ const obj = {};
8339
+ headers.forEach((header, index) => {
8340
+ if (isPrototypePollutingKey(header)) return;
8341
+ const cell = cells[index];
8342
+ if (cell === void 0) return;
8343
+ obj[header] = cell;
8344
+ });
8345
+ return obj;
8346
+ })
8347
+ };
8348
+ }
8349
+ //#endregion
8004
8350
  //#region src/data_import/utils/file_to_json.ts
8351
+ var excelJsModule;
8352
+ function loadExcelJs() {
8353
+ excelJsModule ??= import("exceljs").then((mod) => mod.default ?? mod);
8354
+ return excelJsModule;
8355
+ }
8356
+ /**
8357
+ * Whether this file is delimited text rather than a workbook.
8358
+ *
8359
+ * Browsers report `text/csv`, `application/csv` or nothing at all for the same
8360
+ * `.csv`, so the extension is the reliable half of the test.
8361
+ */
8362
+ function isCsvFile(file) {
8363
+ const name = (file.name ?? "").toLowerCase();
8364
+ if (name.endsWith(".csv") || name.endsWith(".tsv")) return true;
8365
+ return file.type === "text/csv" || file.type === "application/csv";
8366
+ }
8367
+ /** Turn the parsed cells of one sheet/file into the entity-shaped rows the import maps. */
8368
+ function toImportRows(rows) {
8369
+ return rows.map(mapJsonParse).map(unflattenObject);
8370
+ }
8005
8371
  function convertFileToJson(file) {
8006
8372
  return new Promise((resolve, reject) => {
8007
- if (file.type === "application/json") {
8373
+ if (isCsvFile(file)) {
8374
+ console.debug("Converting CSV file to JSON", file.name);
8375
+ const reader = new FileReader();
8376
+ reader.onload = function(e) {
8377
+ try {
8378
+ const { headers, data } = parseCsvToObjects(e.target?.result);
8379
+ if (headers.length === 0) {
8380
+ reject(/* @__PURE__ */ new Error("The CSV file is empty"));
8381
+ return;
8382
+ }
8383
+ resolve({
8384
+ data: toImportRows(data),
8385
+ propertiesOrder: headers
8386
+ });
8387
+ } catch (err) {
8388
+ console.error("Error parsing CSV file", err);
8389
+ reject(err);
8390
+ }
8391
+ };
8392
+ reader.onerror = () => reject(reader.error ?? /* @__PURE__ */ new Error("Could not read the file"));
8393
+ reader.readAsText(file, "utf-8");
8394
+ } else if (file.type === "application/json") {
8008
8395
  console.debug("Converting JSON file to JSON", file.name);
8009
8396
  const reader = new FileReader();
8010
8397
  reader.onload = function(e) {
@@ -8028,7 +8415,7 @@ function convertFileToJson(file) {
8028
8415
  reader.onload = async function(e) {
8029
8416
  try {
8030
8417
  const buffer = e.target?.result;
8031
- const workbook = new ExcelJS.Workbook();
8418
+ const workbook = new (await (loadExcelJs())).Workbook();
8032
8419
  await workbook.xlsx.load(buffer);
8033
8420
  const worksheet = workbook.worksheets[0];
8034
8421
  if (!worksheet) {
@@ -8042,12 +8429,12 @@ function convertFileToJson(file) {
8042
8429
  const obj = {};
8043
8430
  row.eachCell({ includeEmpty: false }, (cell, colNumber) => {
8044
8431
  const header = headers[colNumber - 1];
8045
- if (header) obj[header] = cell.value;
8432
+ if (header && !isPrototypePollutingKey(header)) obj[header] = cell.value;
8046
8433
  });
8047
8434
  parsedData.push(obj);
8048
8435
  });
8049
8436
  resolve({
8050
- data: parsedData.map(mapJsonParse).map(unflattenObject),
8437
+ data: toImportRows(parsedData),
8051
8438
  propertiesOrder: headers
8052
8439
  });
8053
8440
  } catch (err) {
@@ -8096,44 +8483,153 @@ function ImportFileUpload({ onDataAdded }) {
8096
8483
  });
8097
8484
  }
8098
8485
  //#endregion
8486
+ //#region src/data_import/utils/save_entities.ts
8487
+ /** Rows written per request, and per transaction where the data source has one. */
8488
+ var IMPORT_BATCH_SIZE = 25;
8489
+ /** The row a bulk write takes: the values, with the chosen id as a column. */
8490
+ function toBulkRow(entity) {
8491
+ const values = { ...entity.values ?? {} };
8492
+ if (entity.id !== void 0 && entity.id !== null && entity.id !== "") values.id = entity.id;
8493
+ return values;
8494
+ }
8495
+ function isBulkUnsupported(error) {
8496
+ if (error?.code === "BULK_UNSUPPORTED") return true;
8497
+ const message = error?.message ?? "";
8498
+ return /bulk writes are not supported|does not support bulk/i.test(message);
8499
+ }
8500
+ function isConflict(error) {
8501
+ if ((error?.status ?? error?.statusCode) === 409) return true;
8502
+ const code = error?.code;
8503
+ return code === "CONFLICT" || code === "23505";
8504
+ }
8505
+ /**
8506
+ * Write one batch, all-or-nothing wherever the data source can.
8507
+ *
8508
+ * `POST /<collection>/bulk` validates every row before the transaction opens,
8509
+ * names a failure by row index, and takes the `upsert` flag the preview screen
8510
+ * promises — none of which one `create` per row inside a `Promise.all` could
8511
+ * offer, since `Promise.all` does not cancel its siblings and every request was
8512
+ * its own transaction.
8513
+ */
8514
+ async function writeBatch(accessor, batch, bulkUnsupported, written) {
8515
+ if (accessor.createMany && !bulkUnsupported.current) try {
8516
+ await accessor.createMany(batch.map(toBulkRow), { upsert: true });
8517
+ written.count = batch.length;
8518
+ return;
8519
+ } catch (e) {
8520
+ if (!isBulkUnsupported(e)) throw e;
8521
+ bulkUnsupported.current = true;
8522
+ }
8523
+ for (const entity of batch) {
8524
+ try {
8525
+ await accessor.create(entity.values ?? {}, entity.id);
8526
+ } catch (e) {
8527
+ if (entity.id === void 0 || !isConflict(e)) throw e;
8528
+ await accessor.update(entity.id, entity.values ?? {});
8529
+ }
8530
+ written.count++;
8531
+ }
8532
+ }
8533
+ /**
8534
+ * Write imported entities in batches, reporting what was committed.
8535
+ *
8536
+ * Resumable on purpose: a failure at row 4 000 leaves rows 1–3 999 in the table
8537
+ * whatever this does, so the only useful thing to report is where to carry on
8538
+ * from. `offset` is that point — the Retry button passes back the `committed`
8539
+ * count of the error it is retrying, instead of starting from zero and
8540
+ * conflicting on every row that already exists.
8541
+ *
8542
+ * @throws {ImportSaveError} naming the rows that were not written.
8543
+ */
8544
+ async function saveImportedEntities(dataClient, path, data, { offset = 0, batchSize = 25, onBatchCommitted = () => void 0, bulkUnsupported = { current: false } } = {}) {
8545
+ console.debug("Saving imported data", offset, batchSize);
8546
+ const accessor = dataClient.collection(path);
8547
+ for (let start = offset; start < data.length; start += batchSize) {
8548
+ const batch = data.slice(start, start + batchSize);
8549
+ const written = { count: 0 };
8550
+ try {
8551
+ await writeBatch(accessor, batch, bulkUnsupported, written);
8552
+ } catch (e) {
8553
+ throw Object.assign(e, {
8554
+ committed: start + written.count,
8555
+ failedFrom: start + written.count,
8556
+ failedTo: start + batch.length
8557
+ });
8558
+ }
8559
+ onBatchCommitted(start + batch.length);
8560
+ }
8561
+ onBatchCommitted(data.length);
8562
+ }
8563
+ //#endregion
8099
8564
  //#region src/data_import/components/ImportSaveInProgress.tsx
8100
8565
  function ImportSaveInProgress({ path, importConfig, collection, onImportSuccess }) {
8101
8566
  const [errorSaving, setErrorSaving] = useState(void 0);
8102
8567
  const dataClient = useData();
8103
8568
  const savingRef = useRef(false);
8569
+ const committedRef = useRef(0);
8570
+ const bulkUnsupportedRef = useRef({ current: false });
8104
8571
  const [processedEntities, setProcessedEntities] = useState(0);
8105
8572
  function save() {
8106
8573
  if (savingRef.current) return;
8107
8574
  savingRef.current = true;
8108
- saveDataBatch(dataClient, collection, path, importConfig.entities, 0, 25, setProcessedEntities).then(() => {
8575
+ setErrorSaving(void 0);
8576
+ saveImportedEntities(dataClient, path, importConfig.entities, {
8577
+ offset: committedRef.current,
8578
+ batchSize: 25,
8579
+ onBatchCommitted: (written) => {
8580
+ committedRef.current = written;
8581
+ setProcessedEntities(written);
8582
+ },
8583
+ bulkUnsupported: bulkUnsupportedRef.current
8584
+ }).then(() => {
8109
8585
  onImportSuccess(collection);
8110
8586
  savingRef.current = false;
8111
8587
  }).catch((e) => {
8112
- setErrorSaving(e);
8588
+ const error = e;
8589
+ committedRef.current = error.committed ?? committedRef.current;
8590
+ setProcessedEntities(committedRef.current);
8591
+ setErrorSaving(error);
8113
8592
  savingRef.current = false;
8114
8593
  });
8115
8594
  }
8116
8595
  useEffect(() => {
8117
8596
  save();
8118
8597
  }, []);
8119
- if (errorSaving) return /* @__PURE__ */ jsxs(CenteredView, {
8120
- className: "flex flex-col gap-4 items-center",
8121
- children: [
8122
- /* @__PURE__ */ jsx(Typography, {
8123
- variant: "h6",
8124
- children: "Error saving data"
8125
- }),
8126
- /* @__PURE__ */ jsx(Typography, {
8127
- variant: "body2",
8128
- color: "error",
8129
- children: errorSaving.message
8130
- }),
8131
- /* @__PURE__ */ jsx(Button, {
8132
- onClick: save,
8133
- children: "Retry"
8134
- })
8135
- ]
8136
- });
8598
+ if (errorSaving) {
8599
+ const total = importConfig.entities.length;
8600
+ const failedRows = errorSaving.failedTo - errorSaving.failedFrom > 1 ? `rows ${errorSaving.failedFrom + 1}–${errorSaving.failedTo}` : `row ${errorSaving.failedFrom + 1}`;
8601
+ return /* @__PURE__ */ jsxs(CenteredView, {
8602
+ className: "flex flex-col gap-4 items-center",
8603
+ children: [
8604
+ /* @__PURE__ */ jsx(Typography, {
8605
+ variant: "h6",
8606
+ children: "Error saving data"
8607
+ }),
8608
+ /* @__PURE__ */ jsx(Typography, {
8609
+ variant: "body2",
8610
+ color: "error",
8611
+ children: errorSaving.message
8612
+ }),
8613
+ /* @__PURE__ */ jsxs(Typography, {
8614
+ variant: "body2",
8615
+ children: [
8616
+ errorSaving.committed,
8617
+ " of ",
8618
+ total,
8619
+ " rows were imported. The import stopped on ",
8620
+ failedRows,
8621
+ ", which ",
8622
+ errorSaving.failedTo - errorSaving.failedFrom > 1 ? "were" : "was",
8623
+ " not written."
8624
+ ]
8625
+ }),
8626
+ /* @__PURE__ */ jsx(Button, {
8627
+ onClick: save,
8628
+ children: errorSaving.committed > 0 ? `Retry from row ${errorSaving.committed + 1}` : "Retry"
8629
+ })
8630
+ ]
8631
+ });
8632
+ }
8137
8633
  return /* @__PURE__ */ jsxs("div", {
8138
8634
  className: "flex flex-col gap-4 items-center",
8139
8635
  children: [
@@ -8158,18 +8654,6 @@ function ImportSaveInProgress({ path, importConfig, collection, onImportSuccess
8158
8654
  ]
8159
8655
  });
8160
8656
  }
8161
- function saveDataBatch(dataClient, collection, path, data, offset = 0, batchSize = 25, onProgressUpdate) {
8162
- console.debug("Saving imported data", offset, batchSize);
8163
- const batch = data.slice(offset, offset + batchSize);
8164
- return Promise.all(batch.map((d) => dataClient.collection(path).create(d.values, d.id))).then(() => {
8165
- if (offset + batchSize < data.length) {
8166
- onProgressUpdate(offset + batchSize);
8167
- return saveDataBatch(dataClient, collection, path, data, offset + batchSize, batchSize, onProgressUpdate);
8168
- }
8169
- onProgressUpdate(data.length);
8170
- return Promise.resolve();
8171
- });
8172
- }
8173
8657
  //#endregion
8174
8658
  //#region src/data_import/hooks/useImportConfig.tsx
8175
8659
  var useImportConfig = () => {
@@ -8241,6 +8725,10 @@ function convertDataToEntity(authController, navigation, data, idColumn, headers
8241
8725
  }
8242
8726
  function flattenEntry(obj, parent = "") {
8243
8727
  return Object.keys(obj).reduce((acc, key) => {
8728
+ if (isPrototypePollutingKey(key)) {
8729
+ console.warn(`Skipping column "${key}": a header may not reach the prototype chain`);
8730
+ return acc;
8731
+ }
8244
8732
  const prefixedKey = parent ? `${parent}.${key}` : key;
8245
8733
  if (typeof obj[key] === "object" && !(obj[key] instanceof Date) && obj[key] !== null && !Array.isArray(obj[key])) Object.assign(acc, flattenEntry(obj[key], prefixedKey));
8246
8734
  else acc[prefixedKey] = obj[key];
@@ -8269,8 +8757,12 @@ function processValueMapping(authController, value, navigation, property) {
8269
8757
  return value;
8270
8758
  }
8271
8759
  if (from === "array" && to === "array" && Array.isArray(value) && usedProperty.of && !Array.isArray(usedProperty.of) && !isPropertyBuilder(usedProperty.of)) return value.map((v) => processValueMapping(authController, v, navigation, usedProperty.of));
8272
- else if (from === "string" && to === "number" && typeof value === "string") return Number(value);
8273
- else if (from === "string" && to === "array" && typeof value === "string" && usedProperty.of && !Array.isArray(usedProperty.of) && !isPropertyBuilder(usedProperty.of)) return value.split(",").map((v) => processValueMapping(authController, v, navigation, usedProperty.of));
8760
+ else if (from === "string" && to === "number" && typeof value === "string") {
8761
+ const trimmed = value.trim();
8762
+ if (trimmed === "") return null;
8763
+ const num = Number(trimmed);
8764
+ return Number.isNaN(num) ? null : num;
8765
+ } else if (from === "string" && to === "array" && typeof value === "string" && usedProperty.of && !Array.isArray(usedProperty.of) && !isPropertyBuilder(usedProperty.of)) return value.split(",").map((v) => processValueMapping(authController, v, navigation, usedProperty.of));
8274
8766
  else if (from === "string" && to === "boolean") return value === "true";
8275
8767
  else if (from === "number" && to === "boolean") return value === 1;
8276
8768
  else if (from === "boolean" && to === "number") return value ? 1 : 0;
@@ -8311,6 +8803,7 @@ function processValueMapping(authController, value, navigation, property) {
8311
8803
  //#endregion
8312
8804
  //#region src/data_import/components/DataNewPropertiesMapping.tsx
8313
8805
  function DataNewPropertiesMapping({ importConfig, destinationProperties, buildPropertyView }) {
8806
+ const { t } = useTranslation();
8314
8807
  const headersMapping = importConfig.headersMapping;
8315
8808
  const headingsOrder = importConfig.headingsOrder;
8316
8809
  const idColumn = importConfig.idColumn;
@@ -8390,7 +8883,7 @@ function DataNewPropertiesMapping({ importConfig, destinationProperties, buildPr
8390
8883
  /* @__PURE__ */ jsx(TableCell, {
8391
8884
  header: true,
8392
8885
  style: { width: "65%" },
8393
- children: "Default value"
8886
+ children: t("default_value")
8394
8887
  })
8395
8888
  ] }), /* @__PURE__ */ jsx(TableBody, { children: destinationProperties && getAllPropertyKeys(destinationProperties).map((key) => {
8396
8889
  const property = getPropertyInPath$1(destinationProperties, key);
@@ -8470,17 +8963,22 @@ function IdSelectField({ idColumn, headersMapping, onChange }) {
8470
8963
  }) });
8471
8964
  }
8472
8965
  function DefaultValuesField({ property, onValueChange, defaultValue }) {
8966
+ const { t } = useTranslation();
8967
+ const defaultValueLabel = t("default_value");
8968
+ const ariaLabel = property.name ? `${defaultValueLabel}: ${property.name}` : defaultValueLabel;
8473
8969
  if (property.type === "string") return /* @__PURE__ */ jsx(TextField, {
8474
8970
  size: "medium",
8475
- placeholder: "Default value",
8971
+ "aria-label": ariaLabel,
8972
+ placeholder: defaultValueLabel,
8476
8973
  value: defaultValue ?? "",
8477
8974
  onChange: (event) => onValueChange(event.target.value)
8478
8975
  });
8479
8976
  else if (property.type === "number") return /* @__PURE__ */ jsx(TextField, {
8480
8977
  size: "medium",
8481
8978
  type: "number",
8979
+ "aria-label": ariaLabel,
8482
8980
  value: defaultValue ?? "",
8483
- placeholder: "Default value",
8981
+ placeholder: defaultValueLabel,
8484
8982
  onChange: (event) => onValueChange(event.target.value)
8485
8983
  });
8486
8984
  else if (property.type === "boolean") return /* @__PURE__ */ jsx(BooleanSwitchWithLabel, {
@@ -8517,6 +9015,7 @@ function ImportNewPropertyFieldPreview({ propertyKey, property, onEditClick, inc
8517
9015
  className: "flex flex-row items-center gap-2",
8518
9016
  children: [includeName && /* @__PURE__ */ jsx(TextField, {
8519
9017
  size: "medium",
9018
+ "aria-label": propertyKey ? `Name of property ${propertyKey}` : "Property name",
8520
9019
  className: "text-base grow",
8521
9020
  value: property?.name ?? "",
8522
9021
  onChange: (e) => {
@@ -8819,8 +9318,8 @@ function EditorCollectionAction({ path, parentCollectionSlugs, parentEntityIds,
8819
9318
  }
8820
9319
  //#endregion
8821
9320
  //#region src/components/CollectionViewBinding/CollectionViewActions.tsx
8822
- var ImportCollectionAction = lazy(() => import("./import-B0GyllB0.js").then((n) => n.t).then((m) => ({ default: m.ImportCollectionAction })));
8823
- var ExportCollectionAction = lazy(() => import("./export-C-XG3aRh.js").then((n) => n.t).then((m) => ({ default: m.ExportCollectionAction })));
9321
+ var ImportCollectionAction = lazy(() => import("./import-C_4edkoD.js").then((n) => n.t).then((m) => ({ default: m.ImportCollectionAction })));
9322
+ var ExportCollectionAction = lazy(() => import("./export-L53WektO.js").then((n) => n.t).then((m) => ({ default: m.ExportCollectionAction })));
8824
9323
  function CollectionViewActions({ collection, relativePath, parentCollectionSlugs, parentEntityIds, onNewClick, onAddExistingClick, onMultipleDeleteClick, selectionEnabled, path, selectionController, tableController, collectionEntitiesCount, compact, children, openNewDocument }) {
8825
9324
  const context = useAdminContext();
8826
9325
  const { canCreate, canDelete } = usePermissions();
@@ -8829,14 +9328,14 @@ function CollectionViewActions({ collection, relativePath, parentCollectionSlugs
8829
9328
  const collectionEditorController = useCollectionEditorController();
8830
9329
  const hasCollectionEditor = Boolean(collectionEditorController?.editCollection);
8831
9330
  const selectedEntities = selectionController.selectedEntities;
8832
- const addButton = canCreate(collection, path) && onNewClick && (largeLayout && !compact ? /* @__PURE__ */ jsxs(Button, {
9331
+ const addButton = canCreate(collection, path) && onNewClick && (largeLayout && !compact ? /* @__PURE__ */ jsx(Button, {
8833
9332
  id: `add_entity_${path}`,
8834
9333
  onClick: onNewClick,
8835
9334
  startIcon: /* @__PURE__ */ jsx(PlusIcon, { size: iconSize.small }),
8836
9335
  size: "small",
8837
9336
  variant: "filled",
8838
9337
  color: "primary",
8839
- children: ["Add ", collection.singularName ?? collection.name]
9338
+ children: t("add_specific", { name: collection.singularName ?? collection.name })
8840
9339
  }) : /* @__PURE__ */ jsx(Button, {
8841
9340
  id: `add_entity_${path}`,
8842
9341
  onClick: onNewClick,
@@ -8947,46 +9446,462 @@ function CollectionViewActions({ collection, relativePath, parentCollectionSlugs
8947
9446
  });
8948
9447
  }
8949
9448
  //#endregion
8950
- //#region src/components/CollectionViewBinding/SlotValue.tsx
9449
+ //#region src/components/CollectionViewBinding/SearchHighlight.tsx
8951
9450
  /**
8952
- * One display slot, rendered.
9451
+ * Showing a searcher *why* a row is in their results.
8953
9452
  *
8954
- * A slot's value arrives one of two ways and they cannot be rendered the same:
9453
+ * A ranked list answers "which rows" and never "why this one". When the term is
9454
+ * in the title that is self-evident; when it is in a field the list does not
9455
+ * show — a certification, a nested answer, a note — the row looks arbitrary and
9456
+ * the only way to find out is to open it. That is the whole reason this exists.
8955
9457
  *
8956
- * - **From a property** — render it with that property's own preview, so an
8957
- * enum status keeps its colour, a date keeps its format and a storage path
8958
- * becomes a thumbnail. This is why a declared path beats a resolver whenever
8959
- * the value is actually on the record.
8960
- * - **From a resolver** — there is no property, because the value was computed
8961
- * or fetched. All that can be said about it is that it is text.
9458
+ * Two jobs, and they are deliberately separate:
8962
9459
  *
8963
- * The branch lives here, once, rather than at each of the fourteen places the
8964
- * list row, the card and the board card render a slot. That count is the reason:
8965
- * the last time a rule like this was inlined per call site, it drifted into
8966
- * seven versions.
9460
+ * 1. {@link Highlighted} marks the term inside a value the list already shows.
9461
+ * 2. {@link offSlotMatch} finds a match in a field that has no slot, so the
9462
+ * caller can put it where the subtitle would have gone.
8967
9463
  */
8968
- function SlotValue({ slot, size, textOnly = true, fill }) {
8969
- if (!slot || slot.value === void 0 || slot.value === null) return null;
8970
- if (!slot.property || !slot.propertyKey) return /* @__PURE__ */ jsx(Fragment, { children: Array.isArray(slot.value) ? slot.value.map((item) => String(item)).join(", ") : String(slot.value) });
8971
- return /* @__PURE__ */ jsx(PropertyPreview, {
8972
- propertyKey: slot.propertyKey,
8973
- value: slot.value,
8974
- property: slot.property,
8975
- size,
8976
- textOnly,
8977
- fill
9464
+ /** `<mark>` is Postgres's delimiter, and what `ts_headline` emits. */
9465
+ var MARK = /(<mark>.*?<\/mark>)/g;
9466
+ /**
9467
+ * Render a `ts_headline` snippet: text with `<mark>` around each hit.
9468
+ *
9469
+ * Split-and-render rather than `dangerouslySetInnerHTML`. The text around the
9470
+ * marks is whatever a user typed into their own record, and the only markup it
9471
+ * is allowed to carry is the highlight Postgres put there. Anything else stays
9472
+ * inert text.
9473
+ */
9474
+ /**
9475
+ * Context kept before and after a hit, in characters.
9476
+ *
9477
+ * Asymmetric on purpose. The snippet renders on one truncated line, so anything
9478
+ * before the hit competes with the hit itself for the width available — with a
9479
+ * generous lead-in the mark is pushed off the right edge and the reader sees a
9480
+ * fragment with nothing highlighted in it, which is worse than no snippet at
9481
+ * all. Lead with just enough to not start abruptly, then run on.
9482
+ */
9483
+ var SNIPPET_LEAD = 16;
9484
+ var SNIPPET_TRAIL = 90;
9485
+ /**
9486
+ * Pull the first mark near the front of the snippet.
9487
+ *
9488
+ * The snippet renders on one truncated line whose width is not knowable here —
9489
+ * a list column is wide, the same list beside an open record is not. Whatever
9490
+ * precedes the mark competes with it for that width, and when the mark loses
9491
+ * the reader gets a fragment of their record with nothing highlighted in it,
9492
+ * which reads as the feature being broken rather than as a narrow column.
9493
+ *
9494
+ * `ts_headline` centres its fragment on the match, so roughly half the window
9495
+ * is lead-in; that is the right shape for a paragraph and the wrong one for a
9496
+ * line. Trimming here rather than asking Postgres for a smaller window keeps
9497
+ * the trailing context, which is what actually explains the hit — and applies
9498
+ * the same shape to the snippets built in the browser, so a row looks the same
9499
+ * whichever path produced it.
9500
+ */
9501
+ function trimSnippetLead(text, lead = SNIPPET_LEAD) {
9502
+ const at = text.indexOf("<mark>");
9503
+ if (at < 0 || at <= lead) return text;
9504
+ return "…" + text.slice(at - lead, at).replace(/^\S*\s/, "") + text.slice(at);
9505
+ }
9506
+ function Snippet({ text, className }) {
9507
+ return /* @__PURE__ */ jsx("span", {
9508
+ className,
9509
+ children: useMemo(() => trimSnippetLead(text).split(MARK), [text]).map((part, i) => part.startsWith("<mark>") ? /* @__PURE__ */ jsx("mark", {
9510
+ className: "bg-amber-200/70 dark:bg-amber-400/30 text-inherit rounded-[3px] px-[1px]",
9511
+ children: part.slice(6, -7)
9512
+ }, i) : /* @__PURE__ */ jsx(React.Fragment, { children: part }, i))
8978
9513
  });
8979
9514
  }
8980
- //#endregion
8981
- //#region src/components/CollectionViewBinding/EntityCardBinding.tsx
9515
+ /**
9516
+ * Mark every occurrence of the searched terms inside a plain value.
9517
+ *
9518
+ * Used for values the list already renders, where the server's snippet would be
9519
+ * the wrong thing to show: the snippet is truncated to a fragment and folded to
9520
+ * the indexed form, while the cell should keep the record's real text. So the
9521
+ * terms are re-matched here, on the display value.
9522
+ *
9523
+ * Deliberately dumber than the server's matching, and it has to be: this cannot
9524
+ * stem or unaccent without reimplementing the text search configuration in the
9525
+ * browser and getting it subtly different. It marks what it can find as a
9526
+ * prefix, case- and accent-insensitively, and marks nothing when it cannot —
9527
+ * a highlight that is merely absent reads as "no hit in this field", which is
9528
+ * the honest answer for a stem the browser cannot reproduce.
9529
+ */
9530
+ function Highlighted({ text, terms, className }) {
9531
+ const segments = useMemo(() => splitOnTerms(text, terms), [text, terms]);
9532
+ if (segments.length <= 1) return /* @__PURE__ */ jsx("span", {
9533
+ className,
9534
+ children: text
9535
+ });
9536
+ return /* @__PURE__ */ jsx("span", {
9537
+ className,
9538
+ children: segments.map((seg, i) => seg.hit ? /* @__PURE__ */ jsx("mark", {
9539
+ className: "bg-amber-200/70 dark:bg-amber-400/30 text-inherit rounded-[3px] px-[1px]",
9540
+ children: seg.text
9541
+ }, i) : /* @__PURE__ */ jsx(React.Fragment, { children: seg.text }, i))
9542
+ });
9543
+ }
9544
+ /** Strip diacritics so `gestion` marks `gestión`, mirroring the index's unaccent. */
9545
+ var fold = (value) => value.normalize("NFD").replace(/[̀-ͯ]/g, "").toLowerCase();
9546
+ /**
9547
+ * Split `text` into hit / non-hit runs.
9548
+ *
9549
+ * Matches on the *folded* form but slices the original, so the marked text
9550
+ * keeps its accents and case — `Gestión`, not `gestion`. Folding is
9551
+ * length-preserving for the diacritics this handles, which is what makes the
9552
+ * indices transferable; a fold that changed length would misalign the slices.
9553
+ */
9554
+ function splitOnTerms(text, terms) {
9555
+ const haystack = fold(text);
9556
+ const needles = terms.map(fold).filter((t) => t.length >= 2);
9557
+ if (needles.length === 0) return [{
9558
+ text,
9559
+ hit: false
9560
+ }];
9561
+ const bounds = [];
9562
+ for (const needle of needles) {
9563
+ let from = 0;
9564
+ for (;;) {
9565
+ const at = haystack.indexOf(needle, from);
9566
+ if (at === -1) break;
9567
+ const before = at === 0 ? " " : haystack[at - 1];
9568
+ if (!/[\p{L}\p{N}]/u.test(before)) bounds.push([at, at + needle.length]);
9569
+ from = at + needle.length;
9570
+ }
9571
+ }
9572
+ if (bounds.length === 0) return [{
9573
+ text,
9574
+ hit: false
9575
+ }];
9576
+ bounds.sort((a, b) => a[0] - b[0]);
9577
+ const merged = [];
9578
+ for (const [start, end] of bounds) {
9579
+ const last = merged[merged.length - 1];
9580
+ if (last && start <= last[1]) last[1] = Math.max(last[1], end);
9581
+ else merged.push([start, end]);
9582
+ }
9583
+ const segments = [];
9584
+ let cursor = 0;
9585
+ for (const [start, end] of merged) {
9586
+ if (start > cursor) segments.push({
9587
+ text: text.slice(cursor, start),
9588
+ hit: false
9589
+ });
9590
+ segments.push({
9591
+ text: text.slice(start, end),
9592
+ hit: true
9593
+ });
9594
+ cursor = end;
9595
+ }
9596
+ if (cursor < text.length) segments.push({
9597
+ text: text.slice(cursor),
9598
+ hit: false
9599
+ });
9600
+ return segments;
9601
+ }
9602
+ /**
9603
+ * The terms a searcher typed, as things to look for in a rendered value.
9604
+ *
9605
+ * Quoted phrases stay whole; `-excluded` terms are dropped, since marking a
9606
+ * word the user asked *not* to see would be actively wrong. `websearch_to_tsquery`
9607
+ * reads the same syntax on the server.
9608
+ */
9609
+ function searchTerms(searchString) {
9610
+ if (!searchString) return [];
9611
+ const terms = [];
9612
+ for (const [, phrase, bare] of searchString.matchAll(/"([^"]+)"|(\S+)/g)) {
9613
+ const term = phrase ?? bare;
9614
+ if (!term || term.startsWith("-")) continue;
9615
+ if (/^(or|and)$/i.test(term)) continue;
9616
+ terms.push(term);
9617
+ }
9618
+ return terms;
9619
+ }
9620
+ /**
9621
+ * The best match to show when the term is nowhere the list already displays.
9622
+ *
9623
+ * `shownKeys` are the property paths the row is already rendering, so a match
9624
+ * there needs no explaining — {@link Highlighted} marks it in place and the
9625
+ * subtitle keeps its own job. Anything else is invisible, and one of those is
9626
+ * what the subtitle slot gets replaced with.
9627
+ *
9628
+ * The first such match wins, and the server returns them in the order the
9629
+ * collection declared its `search` fields — so the field an author put first is
9630
+ * the one a reader sees, rather than whichever the database aggregated first.
9631
+ */
9632
+ function offSlotMatch(matches, shownKeys) {
9633
+ if (!matches || matches.length === 0) return void 0;
9634
+ const shown = new Set(shownKeys.filter((k) => Boolean(k)));
9635
+ return matches.find((m) => !shown.has(m.field));
9636
+ }
9637
+ /**
9638
+ * Where a search term appears in a row, computed from the row itself.
9639
+ *
9640
+ * The server can say this too — `_matches`, from `ts_headline` — and its answer
9641
+ * is better, because it knows the stemming the index used. This exists because
9642
+ * the admin cannot rely on getting it: rows reach the panel over three
9643
+ * different transports (REST, the realtime socket, the offline cache) and only
9644
+ * one of them carries query metadata. A feature that explains a result on some
9645
+ * loads and not others is worse than one that always gives a slightly simpler
9646
+ * answer.
9647
+ *
9648
+ * So: use the server's when it is there, fall back to this. Both produce the
9649
+ * same `{ field, snippet }` shape, and this one marks with the same `<mark>`.
9650
+ */
9651
+ function localRowMatch(values, properties, terms, shownKeys) {
9652
+ if (!values || !properties || terms.length === 0) return void 0;
9653
+ const shown = new Set(shownKeys.filter((k) => Boolean(k)));
9654
+ for (const [key, property] of Object.entries(properties)) {
9655
+ if (shown.has(key)) continue;
9656
+ const found = findInValue(values[key], terms, key, property);
9657
+ if (found) return found;
9658
+ }
9659
+ }
9660
+ function findInValue(value, terms, path, property) {
9661
+ if (property && "enum" in property && property.enum) return void 0;
9662
+ if (typeof value === "string") {
9663
+ const snippet = snippetAround(value, terms);
9664
+ return snippet ? {
9665
+ field: path,
9666
+ snippet
9667
+ } : void 0;
9668
+ }
9669
+ if (Array.isArray(value)) {
9670
+ for (const item of value) {
9671
+ const found = findInValue(item, terms, path, void 0);
9672
+ if (found) return found;
9673
+ }
9674
+ return;
9675
+ }
9676
+ if (value && typeof value === "object") for (const [key, nested] of Object.entries(value)) {
9677
+ const found = findInValue(nested, terms, `${path}.${key}`, void 0);
9678
+ if (found) return found;
9679
+ }
9680
+ }
9681
+ /**
9682
+ * The text around the first hit, with every hit in view marked.
9683
+ *
9684
+ * Trimmed to a readable window rather than the whole field: a match in a long
9685
+ * body should show the sentence it is in, not the first line of the document,
9686
+ * and certainly not the document.
9687
+ */
9688
+ function snippetAround(text, terms) {
9689
+ const segments = splitOnTerms(text, terms);
9690
+ const firstHit = segments.findIndex((s) => s.hit);
9691
+ if (firstHit === -1) return void 0;
9692
+ const before = segments.slice(0, firstHit).map((s) => s.text).join("");
9693
+ const head = before.length > SNIPPET_LEAD ? "…" + before.slice(before.length - SNIPPET_LEAD).replace(/^\S*\s/, "") : before;
9694
+ let out = head;
9695
+ for (let i = firstHit; i < segments.length; i++) {
9696
+ const seg = segments[i];
9697
+ out += seg.hit ? `<mark>${seg.text}</mark>` : seg.text;
9698
+ if (out.length > head.length + SNIPPET_TRAIL) {
9699
+ out += "…";
9700
+ break;
9701
+ }
9702
+ }
9703
+ return out.trim();
9704
+ }
9705
+ //#endregion
9706
+ //#region src/components/CollectionViewBinding/SearchExplanation.tsx
9707
+ /**
9708
+ * The one place a view asks "why is this row here?" and gets something to draw.
9709
+ *
9710
+ * List, cards and board all render the same title-and-subtitle slots over the
9711
+ * same machinery. An explanation that shows up in one of them and not the others
9712
+ * reads as randomness rather than as a feature, so the decision and the markup
9713
+ * live here and each view calls them.
9714
+ */
9715
+ /**
9716
+ * Terms to mark inside values the row already shows, plus the single match
9717
+ * worth putting where the subtitle would go — or nothing, when the row is not
9718
+ * the product of a search or the hit is already visible.
9719
+ *
9720
+ * Prefers the server's `_matches`, which knows the stemming the index used, and
9721
+ * falls back to reading the row directly. That fallback is not belt-and-braces:
9722
+ * rows reach the panel over REST, the realtime socket and the offline cache,
9723
+ * and only one of those carries query metadata.
9724
+ */
9725
+ function useSearchExplanation(entity, properties, shownKeys, searchString) {
9726
+ const terms = useMemo(() => searchTerms(searchString), [searchString]);
9727
+ const shownKey = shownKeys.map((k) => k ?? "").join("|");
9728
+ return {
9729
+ terms,
9730
+ offSlot: useMemo(() => {
9731
+ if (terms.length === 0) return void 0;
9732
+ const shown = shownKey.split("|").map((k) => k || void 0);
9733
+ return offSlotMatch(entity.searchMatches, shown) ?? localRowMatch(entity.values, properties, terms, shown);
9734
+ }, [
9735
+ entity.searchMatches,
9736
+ entity.values,
9737
+ properties,
9738
+ shownKey,
9739
+ terms
9740
+ ])
9741
+ };
9742
+ }
9743
+ /**
9744
+ * Which field the hit was in, and the text around it.
9745
+ *
9746
+ * Sits where the subtitle would have gone. Deliberately quiet — a small amber
9747
+ * caption and one line — because it is context for a result, not the result. It
9748
+ * has to survive a narrow column, so it truncates rather than wraps.
9749
+ */
9750
+ function MatchExplanation({ match, label }) {
9751
+ return /* @__PURE__ */ jsxs("div", {
9752
+ className: "mt-0.5 flex items-baseline gap-1.5 min-w-0",
9753
+ children: [/* @__PURE__ */ jsx("span", {
9754
+ className: "shrink-0 text-[10px] uppercase tracking-wide font-semibold text-amber-700 dark:text-amber-500/90",
9755
+ title: match.field,
9756
+ children: label
9757
+ }), /* @__PURE__ */ jsx("span", {
9758
+ className: "truncate text-xs text-surface-500 dark:text-surface-400 min-w-0",
9759
+ children: /* @__PURE__ */ jsx(Snippet, { text: match.snippet })
9760
+ })]
9761
+ });
9762
+ }
9763
+ /**
9764
+ * What to call a matched field on screen.
9765
+ *
9766
+ * The path is what the config and the server speak — `questionnaire.
9767
+ * certifications` — and is unreadable in a list. Resolve it to the property's
9768
+ * declared `name`, walking into `map` properties for a dotted path, and fall
9769
+ * back to the last segment humanised, so an unnamed field still reads as words
9770
+ * rather than as a path.
9771
+ */
9772
+ function fieldLabel(properties, path) {
9773
+ const segments = path.split(".");
9774
+ let node = properties;
9775
+ let named;
9776
+ for (const segment of segments) {
9777
+ const property = node?.[segment];
9778
+ if (!property) break;
9779
+ named = property.name ?? named;
9780
+ node = property.properties;
9781
+ }
9782
+ if (named) return named;
9783
+ return (segments[segments.length - 1] ?? path).replace(/[_-]+/g, " ").replace(/^./, (c) => c.toUpperCase());
9784
+ }
9785
+ //#endregion
9786
+ //#region src/components/CollectionViewBinding/SlotValue.tsx
9787
+ /**
9788
+ * One display slot, rendered.
9789
+ *
9790
+ * A slot's value arrives one of two ways and they cannot be rendered the same:
9791
+ *
9792
+ * - **From a property** — render it with that property's own preview, so an
9793
+ * enum status keeps its colour, a date keeps its format and a storage path
9794
+ * becomes a thumbnail. This is why a declared path beats a resolver whenever
9795
+ * the value is actually on the record.
9796
+ * - **From a resolver** — there is no property, because the value was computed
9797
+ * or fetched. All that can be said about it is that it is text.
9798
+ *
9799
+ * The branch lives here, once, rather than at each of the fourteen places the
9800
+ * list row, the card and the board card render a slot. That count is the reason:
9801
+ * the last time a rule like this was inlined per call site, it drifted into
9802
+ * seven versions.
9803
+ */
9804
+ function SlotValue({ slot, size, textOnly = true, fill }) {
9805
+ if (!slot || slot.value === void 0 || slot.value === null) return null;
9806
+ if (!slot.property || !slot.propertyKey) {
9807
+ if (slot.role === "image" && typeof slot.value === "string" && slot.value.trim()) return /* @__PURE__ */ jsx(ComputedImage, {
9808
+ value: slot.value.trim(),
9809
+ size,
9810
+ fill
9811
+ });
9812
+ return /* @__PURE__ */ jsx(Fragment, { children: Array.isArray(slot.value) ? slot.value.map((item) => String(item)).join(", ") : String(slot.value) });
9813
+ }
9814
+ return /* @__PURE__ */ jsx(PropertyPreview, {
9815
+ propertyKey: slot.propertyKey,
9816
+ value: slot.value,
9817
+ property: slot.property,
9818
+ size,
9819
+ textOnly,
9820
+ fill
9821
+ });
9822
+ }
9823
+ /**
9824
+ * The `tags` slot, as chips beside the status.
9825
+ *
9826
+ * Honours both arms the same way the rest of the file does. A declared **path**
9827
+ * goes through {@link SlotValue}, so an array of enum values keeps its own
9828
+ * colours — that is the stated advantage of naming a property. A **resolver**
9829
+ * returns bare strings with no property to render them as, so they become plain
9830
+ * chips; a single string is accepted as one tag, which the role's type documents.
9831
+ *
9832
+ * Lives here rather than in the list, the card and the board card because that is
9833
+ * three copies of the same decision, and this file exists to hold exactly one.
9834
+ */
9835
+ function TagChips({ slot, max = 4 }) {
9836
+ if (slot.property && slot.propertyKey) return /* @__PURE__ */ jsx(SlotValue, {
9837
+ slot,
9838
+ size: "small"
9839
+ });
9840
+ const values = (Array.isArray(slot.value) ? slot.value : [slot.value]).filter((item) => item !== void 0 && item !== null && String(item).trim() !== "").map((item) => String(item));
9841
+ if (values.length === 0) return null;
9842
+ const shown = values.slice(0, max);
9843
+ const hidden = values.length - shown.length;
9844
+ return /* @__PURE__ */ jsxs(Fragment, { children: [shown.map((value) => /* @__PURE__ */ jsx(Chip, {
9845
+ size: "smallest",
9846
+ className: "!text-[10px] !leading-tight !py-0 shrink-0 max-w-[100px] truncate",
9847
+ children: value
9848
+ }, value)), hidden > 0 && /* @__PURE__ */ jsxs("span", {
9849
+ className: "text-[10px] text-surface-400 dark:text-surface-500 shrink-0",
9850
+ children: ["+", hidden]
9851
+ })] });
9852
+ }
9853
+ /**
9854
+ * Whether a computed image string can be fetched as-is, or has to be resolved
9855
+ * through storage first.
9856
+ *
9857
+ * Exported because it is the whole decision: rendering either branch needs a
9858
+ * provider (a storage source, a DOM), and the thing worth pinning is which
9859
+ * branch a given string takes. `//host/x` is protocol-relative and equally
9860
+ * fetchable; `data:` and `blob:` are already the bytes.
9861
+ */
9862
+ function isFetchableImageUrl(value) {
9863
+ return /^(https?:)?\/\//i.test(value) || /^(data|blob):/i.test(value);
9864
+ }
9865
+ /**
9866
+ * A string a resolver returned for the `image` role, rendered as a picture.
9867
+ *
9868
+ * Two things can arrive here and they need different handling, which is the whole
9869
+ * reason a property-path image is preferable: a storage property knows which
9870
+ * backend holds it, and a bare string does not.
9871
+ *
9872
+ * - An **absolute URL** is already fetchable. Rendering it through the storage
9873
+ * layer would ask a bucket to sign a path that was never in it.
9874
+ * - Anything else is read as a **storage path** on the default source, which is
9875
+ * the only source a value with no property can be attributed to. A resolver
9876
+ * pointing at a named source should name the property instead.
9877
+ */
9878
+ function ComputedImage({ value, size, fill }) {
9879
+ if (isFetchableImageUrl(value)) return /* @__PURE__ */ jsx(UrlComponentPreview, {
9880
+ previewType: "image",
9881
+ url: value,
9882
+ size,
9883
+ fill,
9884
+ hint: value
9885
+ });
9886
+ return /* @__PURE__ */ jsx(StorageThumbnail, {
9887
+ storagePathOrDownloadUrl: value,
9888
+ storeUrl: false,
9889
+ size,
9890
+ fill
9891
+ });
9892
+ }
9893
+ //#endregion
9894
+ //#region src/components/CollectionViewBinding/EntityCardBinding.tsx
8982
9895
  /**
8983
9896
  * Card component for displaying a entity in a grid view.
8984
9897
  * Shows thumbnail, title, and preview properties via the shared slot system.
8985
9898
  */
8986
- function EntityCardBinding({ entity, collection, onClick, selected, highlighted, onSelectionChange, selectionEnabled, size = "m" }) {
9899
+ function EntityCardBinding({ entity, collection, onClick, selected, highlighted, onSelectionChange, selectionEnabled, size = "m", searchString }) {
8987
9900
  const authController = useAuthController();
8988
9901
  const analyticsController = useAnalyticsController();
8989
- const slots = useEntitySlots(entity, collection, useCollectionSlotKeys(collection, authController, useCustomizationController().propertyConfigs));
9902
+ const slotKeys = useCollectionSlotKeys(collection, authController, useCustomizationController().propertyConfigs);
9903
+ const slots = useEntitySlots(entity, collection, slotKeys);
9904
+ const { terms, offSlot } = useSearchExplanation(entity, collection.properties, [slotKeys.titleKey, slotKeys.subtitleKey], searchString);
8990
9905
  const handleClick = (e) => {
8991
9906
  if (e && (e.metaKey || e.ctrlKey) && selectionEnabled) {
8992
9907
  e.preventDefault();
@@ -9049,7 +9964,10 @@ function EntityCardBinding({ entity, collection, onClick, selected, highlighted,
9049
9964
  }),
9050
9965
  /* @__PURE__ */ jsx("div", {
9051
9966
  className: "truncate my-1 text-sm font-medium min-h-[20px]",
9052
- children: slots.title ? /* @__PURE__ */ jsx(SlotValue, {
9967
+ children: slots.title ? terms.length > 0 && typeof slots.title.value === "string" ? /* @__PURE__ */ jsx(Highlighted, {
9968
+ text: slots.title.value,
9969
+ terms
9970
+ }) : /* @__PURE__ */ jsx(SlotValue, {
9053
9971
  slot: slots.title,
9054
9972
  size: "small"
9055
9973
  }) : /* @__PURE__ */ jsx(Typography, {
@@ -9058,9 +9976,15 @@ function EntityCardBinding({ entity, collection, onClick, selected, highlighted,
9058
9976
  children: entity.id
9059
9977
  })
9060
9978
  }),
9061
- slots.subtitle && /* @__PURE__ */ jsx("div", {
9979
+ offSlot ? /* @__PURE__ */ jsx(MatchExplanation, {
9980
+ match: offSlot,
9981
+ label: fieldLabel(collection.properties, offSlot.field)
9982
+ }) : slots.subtitle && /* @__PURE__ */ jsx("div", {
9062
9983
  className: "line-clamp-3 [&_div]:line-clamp-3 text-xs text-surface-600 dark:text-surface-400 [&_p]:!my-1 [&_p:first-child]:!mt-0 [&_p:last-child]:!mb-0",
9063
- children: /* @__PURE__ */ jsx(SlotValue, {
9984
+ children: terms.length > 0 && typeof slots.subtitle.value === "string" ? /* @__PURE__ */ jsx(Highlighted, {
9985
+ text: slots.subtitle.value,
9986
+ terms
9987
+ }) : /* @__PURE__ */ jsx(SlotValue, {
9064
9988
  slot: slots.subtitle,
9065
9989
  size: "small"
9066
9990
  })
@@ -9077,12 +10001,16 @@ function EntityCardBinding({ entity, collection, onClick, selected, highlighted,
9077
10001
  children: ["+", slots.relations.reduce((acc, r) => acc + Math.max(0, r.totalCount - r.items.length), 0)]
9078
10002
  })]
9079
10003
  }),
9080
- slots.status && /* @__PURE__ */ jsx("div", {
9081
- className: "mt-1",
9082
- children: /* @__PURE__ */ jsx(SlotValue, {
10004
+ (slots.status || slots.tags) && /* @__PURE__ */ jsxs("div", {
10005
+ className: "flex items-center gap-1 mt-1 flex-wrap",
10006
+ children: [slots.status && /* @__PURE__ */ jsx(SlotValue, {
9083
10007
  slot: slots.status,
9084
10008
  size: "small"
9085
- })
10009
+ }), slots.tags && /* @__PURE__ */ jsx(TagChips, { slot: slots.tags })]
10010
+ }),
10011
+ slots.date && /* @__PURE__ */ jsx("div", {
10012
+ className: "mt-1 text-[10px] text-surface-500 dark:text-surface-400",
10013
+ children: slots.date.formatted
9086
10014
  })
9087
10015
  ]
9088
10016
  })]
@@ -9129,6 +10057,7 @@ function CollectionCardViewBinding({ collection, tableController, onEntityClick,
9129
10057
  renderCard: useCallback((entity, { selected, highlighted, onClick }) => /* @__PURE__ */ jsx(ResolvedEntityCard, {
9130
10058
  entity,
9131
10059
  collection,
10060
+ searchString: tableController.searchString,
9132
10061
  onClick,
9133
10062
  selected,
9134
10063
  highlighted,
@@ -9140,12 +10069,51 @@ function CollectionCardViewBinding({ collection, tableController, onEntityClick,
9140
10069
  selectionEnabled,
9141
10070
  size,
9142
10071
  handleRowSelectionChange,
9143
- ResolvedEntityCard
10072
+ ResolvedEntityCard,
10073
+ tableController.searchString
9144
10074
  ])
9145
10075
  });
9146
10076
  }
9147
10077
  //#endregion
9148
10078
  //#region src/components/CollectionViewBinding/CollectionListViewBinding.tsx
10079
+ /** A column left over from a sort or filter that is no longer applied. */
10080
+ var PRIORITY_STALE_REQUEST = 1e3;
10081
+ /** The collection's own columns. Slots sit just above it, in give-way order. */
10082
+ var PRIORITY_DECLARED = 2e3;
10083
+ var PRIORITY_DATE = PRIORITY_DECLARED;
10084
+ var PRIORITY_STATUS = 2001;
10085
+ var PRIORITY_TAGS = 2002;
10086
+ /** The sort or filter currently in force. */
10087
+ var PRIORITY_ACTIVE_REQUEST = 3e3;
10088
+ /**
10089
+ * How wide a column of this property wants to be — the Tailwind class the cell
10090
+ * gets, and the pixel cost of that class. The two are returned together because
10091
+ * they must not drift: the budget is only meaningful if it describes the widths
10092
+ * actually rendered. `lg:` variants key off the viewport, not this panel, so a
10093
+ * narrow split on a wide screen gets the larger of the two — the cost we budget.
10094
+ */
10095
+ function getIdealColumnWidth(prop) {
10096
+ if (prop.type === "string" && "enum" in prop) return {
10097
+ width: "flex-shrink-0 w-32",
10098
+ widthPx: 128
10099
+ };
10100
+ if (prop.type === "date" || prop.type === "number" || prop.type === "boolean") return {
10101
+ width: "flex-shrink-0 w-28",
10102
+ widthPx: 112
10103
+ };
10104
+ if (prop.type === "reference" || prop.type === "relation") return {
10105
+ width: "flex-shrink-0 w-56 lg:w-64",
10106
+ widthPx: 256
10107
+ };
10108
+ if (prop.type === "string") return {
10109
+ width: "flex-shrink-0 w-40 lg:w-48",
10110
+ widthPx: 192
10111
+ };
10112
+ return {
10113
+ width: "flex-shrink-0 w-40",
10114
+ widthPx: 160
10115
+ };
10116
+ }
9149
10117
  /**
9150
10118
  * Get row padding/spacing classes based on size
9151
10119
  */
@@ -9159,6 +10127,31 @@ function getRowClasses(size) {
9159
10127
  default: return "py-3 px-5";
9160
10128
  }
9161
10129
  }
10130
+ /** `px-5` on both sides. */
10131
+ var ROW_PADDING_WIDTH = 40;
10132
+ /** `gap-4` between every cell of the row. */
10133
+ var COLUMN_GAP = 16;
10134
+ /**
10135
+ * The title's share of a row, taken before any column bids for width.
10136
+ *
10137
+ * A title is a name, and a name truncated to "Lider Sr. en…" has stopped being
10138
+ * one. Columns are a scanner's convenience; the title is the row's identity, so
10139
+ * a narrow list spends its width on the title and simply shows fewer columns.
10140
+ */
10141
+ var TITLE_COMFORTABLE_WIDTH = 320;
10142
+ /** An `IconButton size="small"` (`w-8`), which is what a row action renders as. */
10143
+ var ACTION_BUTTON_WIDTH = 32;
10144
+ /** `gap-0.5` between row actions. */
10145
+ var ACTION_BUTTON_GAP = 2;
10146
+ /**
10147
+ * How many rows are read to size the actions cell.
10148
+ *
10149
+ * Actions are resolved per record, so the width they need is only knowable by
10150
+ * asking — and asking every loaded row would mean re-resolving thousands of
10151
+ * them on every page. The first rows are representative: a collection whose
10152
+ * actions vary at all varies within its first screenful.
10153
+ */
10154
+ var ACTIONS_SAMPLE_SIZE = 20;
9162
10155
  /** Stable empty array for when no list-view actions are available. */
9163
10156
  var EMPTY_LIST_VIEW_ACTIONS = [];
9164
10157
  /**
@@ -9237,26 +10230,33 @@ function compactValueSummary(value, property) {
9237
10230
  return null;
9238
10231
  }
9239
10232
  /**
9240
- * Format a date value for display in the list
10233
+ * Format a date value for display in the list.
10234
+ *
10235
+ * A date column holds whatever the developer put in it, so the value is as
10236
+ * likely to be ahead of now as behind it. The relative phrase therefore comes
10237
+ * from {@link formatRelativeTime}, which reads the sign: a row due this
10238
+ * afternoon says "in 2h" rather than the "-1d ago" that flooring a negative
10239
+ * difference used to produce.
9241
10240
  */
9242
- function formatDateValue(value) {
10241
+ function formatDateValue(value, now = /* @__PURE__ */ new Date()) {
9243
10242
  if (!value) return null;
9244
10243
  let date = null;
9245
10244
  if (value instanceof Date) date = value;
9246
10245
  else if (typeof value === "string" || typeof value === "number") date = new Date(value);
9247
10246
  if (!date || isNaN(date.getTime())) return null;
9248
- const diffMs = (/* @__PURE__ */ new Date()).getTime() - date.getTime();
10247
+ const diffMs = now.getTime() - date.getTime();
9249
10248
  const diffDays = Math.floor(diffMs / (1e3 * 60 * 60 * 24));
9250
10249
  if (diffDays === 0) return date.toLocaleTimeString(void 0, {
9251
10250
  hour: "2-digit",
9252
10251
  minute: "2-digit"
9253
10252
  });
9254
- else if (diffDays === 1) return "Yesterday";
9255
- else if (diffDays < 7) return `${diffDays}d ago`;
9256
- else return date.toLocaleDateString(void 0, {
10253
+ if (diffDays === 1) return "Yesterday";
10254
+ const relative = formatRelativeTime(date, { now });
10255
+ if (relative) return relative;
10256
+ return date.toLocaleDateString(void 0, {
9257
10257
  month: "short",
9258
10258
  day: "numeric",
9259
- year: diffDays > 365 ? "numeric" : void 0
10259
+ year: date.getFullYear() !== now.getFullYear() ? "numeric" : void 0
9260
10260
  });
9261
10261
  }
9262
10262
  /**
@@ -9272,148 +10272,270 @@ function CollectionListViewBinding({ collection, tableController, onEntityClick,
9272
10272
  const analyticsController = useAnalyticsController();
9273
10273
  const context = useAdminContext();
9274
10274
  const containerRef = useRef(null);
9275
- const [containerWidth, setContainerWidth] = useState(1200);
10275
+ const [containerWidth, setContainerWidth] = useState(void 0);
9276
10276
  useEffect(() => {
9277
10277
  const el = containerRef.current;
9278
10278
  if (!el) return;
10279
+ const measure = (width) => setContainerWidth(width > 0 ? width : void 0);
10280
+ measure(el.getBoundingClientRect().width);
10281
+ if (typeof ResizeObserver === "undefined") return;
9279
10282
  const ro = new ResizeObserver(([entry]) => {
9280
- if (entry) setContainerWidth(entry.contentRect.width);
10283
+ if (entry) measure(entry.contentRect.width);
9281
10284
  });
9282
10285
  ro.observe(el);
9283
10286
  return () => ro.disconnect();
9284
10287
  }, []);
9285
- const { data, dataLoading, noMoreToLoad, dataLoadingError, itemCount, setItemCount, pageSize = 50, paginationEnabled } = tableController;
9286
- const isLoadingMore = useRef(false);
9287
- useEffect(() => {
9288
- if (!dataLoading) isLoadingMore.current = false;
9289
- }, [dataLoading]);
10288
+ const { data, dataLoading, noMoreToLoad, dataLoadingError, itemCount, setItemCount, pageSize = 50, paginationEnabled, sortBy, setSortBy, filterValues, checkFilterCombination, searchString } = tableController;
9290
10289
  const resolvedCollection = collection;
9291
10290
  const slotKeys = useCollectionSlotKeys(resolvedCollection, authController, customizationController.propertyConfigs);
9292
10291
  const { titleKey: titlePropertyKey, imageKey: imagePropertyKey, subtitleKey, statusKey: statusPropertyKey, dateKey: datePropertyKey } = slotKeys;
9293
- const previewKeys = useMemo(() => getEntityPreviewKeys(authController, resolvedCollection, customizationController.propertyConfigs, void 0, 10).filter((key) => key !== titlePropertyKey && key !== imagePropertyKey && key !== statusPropertyKey && key !== datePropertyKey), [
9294
- authController,
10292
+ const columnMode = !!resolvedCollection.listProperties && resolvedCollection.listProperties.length > 0;
10293
+ /**
10294
+ * The properties an `ORDER BY` can be written for, under the same authority
10295
+ * the table headers and the sort menu ask — so a header that offers a sort
10296
+ * here offers it there, and none of the three invents one the driver would
10297
+ * silently drop.
10298
+ */
10299
+ const sortableKeys = useMemo(() => new Set(getSortablePropertyOptions(resolvedCollection.properties).map((option) => option.key)), [resolvedCollection.properties]);
10300
+ /** The row's identity cell: the leading column, or the title slot. */
10301
+ const titleColumn = useMemo(() => {
10302
+ const key = columnMode ? resolvedCollection.listProperties?.[0] : titlePropertyKey;
10303
+ if (!key) return void 0;
10304
+ const property = resolvedCollection.properties[key];
10305
+ return {
10306
+ key,
10307
+ label: property?.name || (columnMode ? key : "Name"),
10308
+ property,
10309
+ align: "left",
10310
+ width: "flex-1 min-w-0",
10311
+ widthPx: TITLE_COMFORTABLE_WIDTH,
10312
+ sortable: sortableKeys.has(key),
10313
+ priority: Number.MAX_SAFE_INTEGER
10314
+ };
10315
+ }, [
10316
+ columnMode,
9295
10317
  resolvedCollection,
9296
- customizationController.propertyConfigs,
9297
10318
  titlePropertyKey,
9298
- imagePropertyKey,
9299
- statusPropertyKey,
9300
- datePropertyKey
10319
+ sortableKeys
9301
10320
  ]);
9302
- const columns = useMemo(() => {
9303
- const cols = [];
9304
- const getIdealColumnWidth = (prop) => {
9305
- if (prop.type === "string" && "enum" in prop) return "flex-shrink-0 w-32";
9306
- if (prop.type === "date" || prop.type === "number" || prop.type === "boolean") return "flex-shrink-0 w-28";
9307
- if (prop.type === "reference" || prop.type === "relation") return "flex-shrink-0 w-56 lg:w-64";
9308
- if (prop.type === "string") return "flex-shrink-0 w-40 lg:w-48";
9309
- return "flex-shrink-0 w-40";
9310
- };
9311
- if (resolvedCollection.listProperties && resolvedCollection.listProperties.length > 0) {
9312
- resolvedCollection.listProperties.forEach((key, index) => {
9313
- const prop = resolvedCollection.properties[key];
9314
- if (!prop) return;
9315
- cols.push({
10321
+ /**
10322
+ * The trailing columns the collection itself implies: the developer's
10323
+ * `listProperties` when there are any, otherwise the display slots the row
10324
+ * has always shown to the right of the title.
10325
+ */
10326
+ const declaredColumns = useMemo(() => {
10327
+ if (columnMode) {
10328
+ const keys = resolvedCollection.listProperties.slice(1);
10329
+ return keys.flatMap((key, index) => {
10330
+ const property = resolvedCollection.properties[key];
10331
+ if (!property) return [];
10332
+ return [{
9316
10333
  key,
9317
- label: prop.name || key,
9318
- property: prop,
9319
- isTitle: index === 0,
9320
- align: prop.type === "number" || prop.type === "date" ? "right" : "left",
9321
- width: index === 0 ? "flex-1 min-w-[200px]" : getIdealColumnWidth(prop)
9322
- });
9323
- });
9324
- return cols;
9325
- }
9326
- if (titlePropertyKey) {
9327
- const prop = resolvedCollection.properties[titlePropertyKey];
9328
- if (prop) cols.push({
9329
- key: titlePropertyKey,
9330
- label: prop.name || "Name",
9331
- property: prop,
9332
- isTitle: true,
9333
- align: "left",
9334
- width: "flex-1 min-w-[200px]"
9335
- });
9336
- }
9337
- const showDate = datePropertyKey && size !== "xs";
9338
- const showStatus = statusPropertyKey && size !== "xs" && size !== "s";
9339
- const subtitleKey = previewKeys.length > 0 ? previewKeys[0] : void 0;
9340
- const allKeys = resolvedCollection.propertiesOrder || Object.keys(resolvedCollection.properties);
9341
- const usedKeys = /* @__PURE__ */ new Set([
9342
- titlePropertyKey,
9343
- imagePropertyKey,
9344
- statusPropertyKey,
9345
- datePropertyKey,
9346
- subtitleKey,
9347
- "id"
9348
- ]);
9349
- allKeys.filter((k) => {
9350
- if (usedKeys.has(k)) return false;
9351
- const prop = resolvedCollection.properties[k];
9352
- if (!prop) return false;
9353
- if (prop.type === "string" && (prop.storage || prop.admin?.urlPreview === "image")) return false;
9354
- if (prop.type === "array" && prop.of && !Array.isArray(prop.of)) {
9355
- const inner = prop.of;
9356
- if (inner.type === "string" && (inner.storage || inner.admin?.urlPreview === "image")) return false;
9357
- }
9358
- return true;
9359
- }).forEach((key) => {
9360
- const prop = resolvedCollection.properties[key];
9361
- cols.push({
9362
- key,
9363
- label: prop.name || key,
9364
- property: prop,
9365
- align: prop.type === "number" || prop.type === "date" ? "right" : "left",
9366
- width: getIdealColumnWidth(prop)
9367
- });
9368
- });
9369
- if (showStatus && statusPropertyKey) {
9370
- const prop = resolvedCollection.properties[statusPropertyKey];
9371
- if (prop) cols.push({
9372
- key: statusPropertyKey,
9373
- label: prop.name || "Status",
9374
- property: prop,
9375
- isStatus: true,
9376
- align: "center",
9377
- width: "flex-shrink-0 w-32"
9378
- });
9379
- }
9380
- if (showDate && datePropertyKey) {
9381
- const prop = resolvedCollection.properties[datePropertyKey];
9382
- if (prop) cols.push({
9383
- key: datePropertyKey,
9384
- label: prop.name || "Modified",
9385
- property: prop,
9386
- isDate: true,
9387
- align: "right",
9388
- width: "flex-shrink-0 w-28"
10334
+ label: property.name || key,
10335
+ property,
10336
+ align: property.type === "number" || property.type === "date" ? "right" : "left",
10337
+ ...getIdealColumnWidth(property),
10338
+ sortable: sortableKeys.has(key),
10339
+ priority: PRIORITY_DECLARED + (keys.length - index)
10340
+ }];
9389
10341
  });
9390
10342
  }
10343
+ const cols = [];
10344
+ const slotColumn = (slot, key, fallbackLabel, layout, priority) => {
10345
+ const declared = hasDeclaredDisplay(resolvedCollection, slot);
10346
+ if (!key && !declared) return void 0;
10347
+ const property = key ? resolvedCollection.properties[key] : void 0;
10348
+ return {
10349
+ key: key ?? `display:${slot}`,
10350
+ label: property?.name || fallbackLabel,
10351
+ slot,
10352
+ property,
10353
+ ...layout,
10354
+ sortable: !!key && sortableKeys.has(key),
10355
+ priority
10356
+ };
10357
+ };
10358
+ const tags = slotColumn("tags", slotKeys.tagsKey, "Tags", {
10359
+ align: "left",
10360
+ width: "flex-shrink-0 w-40",
10361
+ widthPx: 160
10362
+ }, PRIORITY_TAGS);
10363
+ if (tags) cols.push(tags);
10364
+ const status = slotColumn("status", statusPropertyKey, "Status", {
10365
+ align: "left",
10366
+ width: "flex-shrink-0 w-32",
10367
+ widthPx: 128
10368
+ }, PRIORITY_STATUS);
10369
+ if (status) cols.push(status);
10370
+ const date = slotColumn("date", datePropertyKey, "Modified", {
10371
+ align: "right",
10372
+ width: "flex-shrink-0 w-20",
10373
+ widthPx: 80
10374
+ }, PRIORITY_DATE);
10375
+ if (date) cols.push(date);
9391
10376
  return cols;
9392
10377
  }, [
10378
+ columnMode,
9393
10379
  resolvedCollection,
9394
- titlePropertyKey,
10380
+ slotKeys.tagsKey,
9395
10381
  statusPropertyKey,
9396
10382
  datePropertyKey,
10383
+ sortableKeys
10384
+ ]);
10385
+ /** What the row already shows without a column of its own. */
10386
+ const shownKeys = useMemo(() => /* @__PURE__ */ new Set([
10387
+ titleColumn?.key,
9397
10388
  imagePropertyKey,
9398
- previewKeys,
9399
- size
10389
+ subtitleKey,
10390
+ ...declaredColumns.map((col) => col.key),
10391
+ ...columnMode ? [] : slotKeys.relationKeys
10392
+ ]), [
10393
+ titleColumn,
10394
+ imagePropertyKey,
10395
+ subtitleKey,
10396
+ declaredColumns,
10397
+ columnMode,
10398
+ slotKeys.relationKeys
10399
+ ]);
10400
+ /** The property the sort names, when no cell already shows it. */
10401
+ const sortedKey = useMemo(() => {
10402
+ const key = sortBy?.[0];
10403
+ return key && !shownKeys.has(key) ? key : void 0;
10404
+ }, [sortBy, shownKeys]);
10405
+ /** The properties the filters name, minus the ones a cell already shows. */
10406
+ const filteredKeys = useMemo(() => Object.keys(filterValues ?? {}).filter((key) => !shownKeys.has(key)), [filterValues, shownKeys]);
10407
+ /**
10408
+ * The last property the sort named, and the last set the filters named.
10409
+ *
10410
+ * A column earns its place when a sort or a filter names it, and keeps it
10411
+ * once that sort or filter is cleared: taking it back would move every
10412
+ * column right of it at the moment the user is comparing values across rows,
10413
+ * and clearing a sort says nothing about wanting to stop seeing the property
10414
+ * it sorted on.
10415
+ *
10416
+ * What it does *not* do is accumulate. There is only ever one sort, so
10417
+ * ordering by four properties in turn is changing one's mind four times, not
10418
+ * asking for four columns — and asking for four is how the row lost its
10419
+ * status and its date to a wall of numbers. A new sort therefore replaces
10420
+ * the property the last one left behind, and a new set of filters replaces
10421
+ * the set before it. At most one sort column and one set of filter columns
10422
+ * outlive their request.
10423
+ *
10424
+ * The list remounts per collection (it is keyed by path), so this is the
10425
+ * memory of one visit to one collection, not a preference.
10426
+ */
10427
+ const [lastSortedKey, setLastSortedKey] = useState(void 0);
10428
+ const [lastFilteredKeys, setLastFilteredKeys] = useState([]);
10429
+ useEffect(() => {
10430
+ if (sortedKey) setLastSortedKey(sortedKey);
10431
+ }, [sortedKey]);
10432
+ useEffect(() => {
10433
+ if (filteredKeys.length === 0) return;
10434
+ setLastFilteredKeys((previous) => previous.length === filteredKeys.length && previous.every((key, i) => key === filteredKeys[i]) ? previous : filteredKeys);
10435
+ }, [filteredKeys]);
10436
+ /**
10437
+ * Columns for what the user asked to sort or filter by and cannot see.
10438
+ *
10439
+ * Ordering a list by a property no cell shows produces a shuffle with no
10440
+ * explanation in it — the rows move and nothing on screen says why. So the
10441
+ * property joins the row as its own column, last: the filters' first, in
10442
+ * their own order, then the sort's.
10443
+ */
10444
+ const requestedColumns = useMemo(() => {
10445
+ const active = /* @__PURE__ */ new Set([...filteredKeys, ...sortedKey ? [sortedKey] : []]);
10446
+ const keys = [];
10447
+ const request = (key) => {
10448
+ if (!key || keys.includes(key) || shownKeys.has(key)) return;
10449
+ keys.push(key);
10450
+ };
10451
+ (filteredKeys.length > 0 ? filteredKeys : lastFilteredKeys).forEach(request);
10452
+ request(sortedKey ?? lastSortedKey);
10453
+ return keys.flatMap((key, index) => {
10454
+ const property = getResolvedPropertyInPath(resolvedCollection.properties, key);
10455
+ if (!property) return [];
10456
+ return [{
10457
+ key,
10458
+ label: property.name || key,
10459
+ property,
10460
+ align: property.type === "number" || property.type === "date" ? "right" : "left",
10461
+ ...getIdealColumnWidth(property),
10462
+ sortable: sortableKeys.has(key),
10463
+ priority: (active.has(key) ? PRIORITY_ACTIVE_REQUEST : PRIORITY_STALE_REQUEST) - index
10464
+ }];
10465
+ });
10466
+ }, [
10467
+ filteredKeys,
10468
+ sortedKey,
10469
+ lastFilteredKeys,
10470
+ lastSortedKey,
10471
+ shownKeys,
10472
+ resolvedCollection,
10473
+ sortableKeys
10474
+ ]);
10475
+ const getListViewActions = useCallback((entity) => {
10476
+ if (!getActionsForEntity) return EMPTY_LIST_VIEW_ACTIONS;
10477
+ return getActionsForEntity({
10478
+ entity,
10479
+ customEntityActions: (collection.entityActions ?? []).map((action) => resolveEntityAction(action, customizationController.entityActions)).filter(Boolean)
10480
+ }).filter((a) => a.showActionsInListView);
10481
+ }, [
10482
+ getActionsForEntity,
10483
+ collection.entityActions,
10484
+ customizationController.entityActions
9400
10485
  ]);
9401
10486
  const showImage = size !== "xs";
10487
+ /**
10488
+ * Does the checkbox ride on the thumbnail rather than sit beside it?
10489
+ *
10490
+ * Only in the split's compact list — the one layout where the list is a
10491
+ * column beside a record rather than the page itself, and where the 48px of
10492
+ * checkbox cell is 48px the title does not get. Everywhere else the list has
10493
+ * the room, and a checkbox that is always there is easier to find than one
10494
+ * that appears under the pointer.
10495
+ *
10496
+ * Needs a thumbnail to ride on: at `xs` there is none, so the checkbox keeps
10497
+ * its cell.
10498
+ */
10499
+ const combineSelection = openEntityMode === "split" && selectedEntityId !== void 0 && showImage;
10500
+ /**
10501
+ * How much room the action buttons take, reserved identically in the header
10502
+ * and in every row so the columns line up above one another whether or not a
10503
+ * given record has actions.
10504
+ */
10505
+ const actionsWidth = useMemo(() => {
10506
+ if (!getActionsForEntity) return 0;
10507
+ let most = 0;
10508
+ for (const entity of data.slice(0, ACTIONS_SAMPLE_SIZE)) most = Math.max(most, getListViewActions(entity).length);
10509
+ return most === 0 ? 0 : most * ACTION_BUTTON_WIDTH + (most - 1) * ACTION_BUTTON_GAP;
10510
+ }, [
10511
+ data,
10512
+ getActionsForEntity,
10513
+ getListViewActions
10514
+ ]);
9402
10515
  const visibleColumns = useMemo(() => {
9403
- if (columns.length <= 1) return columns;
9404
- const titleCol = columns.find((c) => c.isTitle);
9405
- const extraCols = columns.filter((c) => !c.isTitle);
9406
- const availableForExtra = Math.max(0, containerWidth - (40 + (selectionEnabled ? 40 : 0) + (showImage ? 56 : 0) + 200));
9407
- const FIRST_COL_THRESHOLD = 500;
9408
- const EXTRA_COL_WIDTH = 160;
9409
- let maxExtraCols = 0;
9410
- if (availableForExtra >= FIRST_COL_THRESHOLD) maxExtraCols = 1 + Math.floor((availableForExtra - FIRST_COL_THRESHOLD) / EXTRA_COL_WIDTH);
9411
- return [...titleCol ? [titleCol] : [], ...extraCols.slice(0, maxExtraCols)];
10516
+ const columns = [...declaredColumns, ...requestedColumns];
10517
+ if (columns.length === 0) return columns;
10518
+ if (containerWidth === void 0) return [];
10519
+ const chrome = ROW_PADDING_WIDTH + (selectionEnabled && !combineSelection ? 48 : 0) + (showImage ? 56 : 0) + (actionsWidth > 0 ? actionsWidth + COLUMN_GAP : 0);
10520
+ const available = containerWidth - chrome - TITLE_COMFORTABLE_WIDTH;
10521
+ const cost = (col) => col.widthPx + COLUMN_GAP;
10522
+ let total = columns.reduce((acc, col) => acc + cost(col), 0);
10523
+ if (total <= available) return columns;
10524
+ const dropped = /* @__PURE__ */ new Set();
10525
+ for (const col of [...columns].sort((a, b) => a.priority - b.priority)) {
10526
+ if (total <= available) break;
10527
+ dropped.add(col.key);
10528
+ total -= cost(col);
10529
+ }
10530
+ return columns.filter((col) => !dropped.has(col.key));
9412
10531
  }, [
9413
- columns,
10532
+ declaredColumns,
10533
+ requestedColumns,
9414
10534
  containerWidth,
9415
10535
  selectionEnabled,
9416
- showImage
10536
+ combineSelection,
10537
+ showImage,
10538
+ actionsWidth
9417
10539
  ]);
9418
10540
  const handleEntityClick = useCallback((entity) => {
9419
10541
  analyticsController.onAnalyticsEvent?.("entity_click", {
@@ -9431,82 +10553,126 @@ function CollectionListViewBinding({ collection, tableController, onEntityClick,
9431
10553
  useCallback((entity) => {
9432
10554
  return highlightedEntities?.some((e) => e.id === entity.id && e.path === entity.path) ?? false;
9433
10555
  }, [highlightedEntities]);
9434
- const getListViewActions = useCallback((entity) => {
9435
- if (!getActionsForEntity) return EMPTY_LIST_VIEW_ACTIONS;
9436
- return getActionsForEntity({
9437
- entity,
9438
- customEntityActions: (collection.entityActions ?? []).map((action) => resolveEntityAction(action, customizationController.entityActions)).filter(Boolean)
9439
- }).filter((a) => a.showActionsInListView);
9440
- }, [
9441
- getActionsForEntity,
9442
- collection.entityActions,
9443
- customizationController.entityActions
9444
- ]);
9445
10556
  const rowClasses = getRowClasses(size);
9446
10557
  const selectedIds = useMemo(() => new Set(selectionController?.selectedEntities.map((e) => e.id)), [selectionController?.selectedEntities]);
9447
10558
  const highlightedIds = useMemo(() => new Set(highlightedEntities?.map((e) => e.id)), [highlightedEntities]);
9448
10559
  const handleRowSelectionChange = useCallback((entity, selected) => {
9449
10560
  handleSelectionChange(entity, selected);
9450
10561
  }, [handleSelectionChange]);
9451
- return /* @__PURE__ */ jsx(ListView, {
9452
- data,
9453
- dataLoading,
9454
- noMoreToLoad,
9455
- dataLoadingError,
9456
- itemCount,
10562
+ /**
10563
+ * Order by a column, on the same cycle a table header runs: unordered →
10564
+ * ascending → descending → unordered. Clicking a header here and clicking it
10565
+ * in the table view have to mean the same thing, because they are the same
10566
+ * collection under the same controller.
10567
+ */
10568
+ const onColumnSort = useCallback((key) => {
10569
+ if (!setSortBy) return;
10570
+ const active = sortBy?.[0] === key ? sortBy[1] : void 0;
10571
+ setSortBy(active === "asc" ? [key, "desc"] : active === "desc" ? void 0 : [key, "asc"]);
10572
+ setItemCount?.(pageSize);
10573
+ }, [
10574
+ setSortBy,
10575
+ sortBy,
9457
10576
  setItemCount,
9458
- pageSize,
9459
- paginationEnabled,
9460
- onItemClick: handleEntityClick,
9461
- selectedIds,
9462
- highlightedIds,
9463
- selectionEnabled,
9464
- emptyComponent,
9465
- size,
9466
- selectedEntityId,
9467
- renderRow: useCallback(({ item: entity, style, className, selected, highlighted, isLast, onClick, onSelectionChange }) => {
9468
- return /* @__PURE__ */ jsx("div", {
9469
- style,
9470
- className,
9471
- children: /* @__PURE__ */ jsx(ListRow, {
9472
- entity,
9473
- collection: resolvedCollection,
9474
- onClick: handleEntityClick,
9475
- selected,
9476
- highlighted,
9477
- onSelectionChange: handleRowSelectionChange,
9478
- selectionEnabled,
9479
- columns: visibleColumns,
9480
- slotKeys,
9481
- rowClasses,
9482
- showImage,
9483
- size,
9484
- isLast,
9485
- isActive: selectedEntityId !== void 0 && entity.id === selectedEntityId,
9486
- listViewActions: getListViewActions(entity),
9487
- context,
9488
- path,
9489
- selectionController,
9490
- openEntityMode
9491
- })
9492
- }, entity.id);
9493
- }, [
9494
- resolvedCollection,
10577
+ pageSize
10578
+ ]);
10579
+ /**
10580
+ * Whether a sort can be offered at all next to the filter already applied.
10581
+ * A driver may refuse the combination; the table clears the filter to make
10582
+ * room, which is a heavier answer than a header click asks for, so the
10583
+ * header simply stops offering it — the same choice the sort menu makes.
10584
+ */
10585
+ const sortIsAvailable = useCallback((key) => {
10586
+ if (!setSortBy) return false;
10587
+ if (!checkFilterCombination) return true;
10588
+ const active = sortBy?.[0] === key ? sortBy[1] : void 0;
10589
+ if (active === "desc") return true;
10590
+ return checkFilterCombination(filterValues ?? {}, [key, active === "asc" ? "desc" : "asc"]);
10591
+ }, [
10592
+ setSortBy,
10593
+ checkFilterCombination,
10594
+ filterValues,
10595
+ sortBy
10596
+ ]);
10597
+ return /* @__PURE__ */ jsx("div", {
10598
+ ref: containerRef,
10599
+ className: "w-full",
10600
+ children: /* @__PURE__ */ jsx(ListView, {
10601
+ data,
10602
+ dataLoading,
10603
+ noMoreToLoad,
10604
+ dataLoadingError,
10605
+ itemCount,
10606
+ setItemCount,
10607
+ pageSize,
10608
+ paginationEnabled,
10609
+ onItemClick: handleEntityClick,
10610
+ selectedIds,
10611
+ highlightedIds,
9495
10612
  selectionEnabled,
9496
- visibleColumns,
9497
- slotKeys,
9498
- rowClasses,
9499
- showImage,
10613
+ emptyComponent,
9500
10614
  size,
9501
10615
  selectedEntityId,
9502
- getListViewActions,
9503
- context,
9504
- path,
9505
- selectionController,
9506
- openEntityMode,
9507
- handleRowSelectionChange,
9508
- handleEntityClick
9509
- ])
10616
+ header: (titleColumn || visibleColumns.length > 0) && /* @__PURE__ */ jsx(ListHeader, {
10617
+ titleColumn,
10618
+ columns: visibleColumns,
10619
+ selectionEnabled: selectionEnabled && !combineSelection,
10620
+ showImage,
10621
+ actionsWidth,
10622
+ sortBy,
10623
+ onColumnSort,
10624
+ sortIsAvailable
10625
+ }),
10626
+ renderRow: useCallback(({ item: entity, style, className, selected, highlighted, isLast, onClick, onSelectionChange }) => {
10627
+ return /* @__PURE__ */ jsx("div", {
10628
+ style,
10629
+ className,
10630
+ children: /* @__PURE__ */ jsx(ListRow, {
10631
+ entity,
10632
+ collection: resolvedCollection,
10633
+ searchString,
10634
+ onClick: handleEntityClick,
10635
+ selected,
10636
+ highlighted,
10637
+ onSelectionChange: handleRowSelectionChange,
10638
+ selectionEnabled,
10639
+ combineSelection,
10640
+ columns: visibleColumns,
10641
+ slotKeys,
10642
+ rowClasses,
10643
+ showImage,
10644
+ size,
10645
+ isLast,
10646
+ isActive: selectedEntityId !== void 0 && entity.id === selectedEntityId,
10647
+ listViewActions: getListViewActions(entity),
10648
+ actionsWidth,
10649
+ context,
10650
+ path,
10651
+ selectionController,
10652
+ openEntityMode
10653
+ })
10654
+ }, entity.id);
10655
+ }, [
10656
+ resolvedCollection,
10657
+ selectionEnabled,
10658
+ combineSelection,
10659
+ visibleColumns,
10660
+ slotKeys,
10661
+ rowClasses,
10662
+ showImage,
10663
+ size,
10664
+ selectedEntityId,
10665
+ getListViewActions,
10666
+ actionsWidth,
10667
+ context,
10668
+ path,
10669
+ selectionController,
10670
+ openEntityMode,
10671
+ handleRowSelectionChange,
10672
+ handleEntityClick,
10673
+ searchString
10674
+ ])
10675
+ })
9510
10676
  });
9511
10677
  }
9512
10678
  /**
@@ -9517,8 +10683,9 @@ function CollectionListViewBinding({ collection, tableController, onEntityClick,
9517
10683
  * When collection.listProperties is explicitly set, falls back to
9518
10684
  * the column system for developer-defined table layouts.
9519
10685
  */
9520
- var ListRow = React.memo(function ListRow({ entity, collection, onClick, selected, highlighted, onSelectionChange, selectionEnabled, columns, slotKeys, rowClasses, showImage, size, isLast, isActive = false, listViewActions = [], context, path, selectionController, openEntityMode }) {
10686
+ var ListRow = React.memo(function ListRow({ entity, collection, onClick, selected, highlighted, onSelectionChange, selectionEnabled, combineSelection = false, columns, slotKeys, rowClasses, showImage, size, isLast, isActive = false, listViewActions = [], actionsWidth = 0, context, path, selectionController, openEntityMode, searchString }) {
9521
10687
  const slots = useEntitySlots(entity, collection, slotKeys);
10688
+ const { terms, offSlot } = useSearchExplanation(entity, collection.properties, [slotKeys.titleKey, slotKeys.subtitleKey], searchString);
9522
10689
  const handleClick = useCallback((e) => {
9523
10690
  if ((e.metaKey || e.ctrlKey) && selectionEnabled) {
9524
10691
  e.preventDefault();
@@ -9539,14 +10706,22 @@ var ListRow = React.memo(function ListRow({ entity, collection, onClick, selecte
9539
10706
  const handleCheckboxChange = useCallback((checked) => {
9540
10707
  onSelectionChange?.(entity, checked);
9541
10708
  }, [entity, onSelectionChange]);
10709
+ /** The thumbnail-as-checkbox cell: the square toggles, and never opens the record. */
10710
+ const handleSelectionCellClick = useCallback((e) => {
10711
+ e.stopPropagation();
10712
+ onSelectionChange?.(entity, !selected);
10713
+ }, [
10714
+ entity,
10715
+ onSelectionChange,
10716
+ selected
10717
+ ]);
9542
10718
  const useColumnMode = !!collection.listProperties && collection.listProperties.length > 0;
9543
- const extraColumns = useColumnMode ? columns.filter((c) => !c.isTitle) : [];
9544
10719
  return /* @__PURE__ */ jsxs("div", {
9545
10720
  className: cls("@container flex items-center gap-4 cursor-pointer group transition-colors duration-200 relative h-full", rowClasses, isActive ? "bg-surface-accent-100 dark:bg-surface-accent-950 hover:bg-surface-accent-200 dark:hover:bg-surface-accent-950" : selected ? "bg-surface-accent-50 dark:bg-surface-accent-900 hover:bg-surface-accent-100 dark:hover:bg-surface-accent-950" : highlighted ? "bg-surface-accent-50 dark:bg-surface-accent-900 hover:bg-surface-50 dark:hover:bg-surface-800/40" : "bg-white dark:bg-surface-900 hover:bg-surface-50 dark:hover:bg-surface-800/40"),
9546
10721
  onClick: handleClick,
9547
10722
  children: [
9548
10723
  selected && !isActive && /* @__PURE__ */ jsx("div", { className: "absolute left-0 top-0 bottom-0 w-0.5 bg-primary-500 rounded-r-full" }),
9549
- selectionEnabled && /* @__PURE__ */ jsx("div", {
10724
+ selectionEnabled && !combineSelection && /* @__PURE__ */ jsx("div", {
9550
10725
  className: "flex-shrink-0 w-8",
9551
10726
  onClick: handleCheckboxClick,
9552
10727
  children: /* @__PURE__ */ jsx(Checkbox, {
@@ -9555,9 +10730,9 @@ var ListRow = React.memo(function ListRow({ entity, collection, onClick, selecte
9555
10730
  size: "smallest"
9556
10731
  })
9557
10732
  }),
9558
- showImage && /* @__PURE__ */ jsx("div", {
9559
- className: "flex-shrink-0",
9560
- children: slots.image ? /* @__PURE__ */ jsx("div", {
10733
+ showImage && /* @__PURE__ */ jsxs("div", {
10734
+ className: "flex-shrink-0 relative w-10 h-10",
10735
+ children: [slots.image ? /* @__PURE__ */ jsx("div", {
9561
10736
  className: cls("w-10 h-10 rounded-lg border relative overflow-hidden bg-surface-100 dark:bg-surface-900", defaultBorderMixin),
9562
10737
  children: /* @__PURE__ */ jsx(SlotValue, {
9563
10738
  slot: slots.image,
@@ -9571,10 +10746,21 @@ var ListRow = React.memo(function ListRow({ entity, collection, onClick, selecte
9571
10746
  className: "text-surface-500 dark:text-surface-400",
9572
10747
  size: "small"
9573
10748
  })
9574
- })
10749
+ }), selectionEnabled && combineSelection && /* @__PURE__ */ jsx("div", {
10750
+ className: cls("absolute inset-0 flex items-center justify-center rounded-lg transition-opacity duration-150", "bg-surface-950/55", selected ? "opacity-100" : "opacity-0 group-hover:opacity-100 focus-within:opacity-100"),
10751
+ onClick: handleSelectionCellClick,
10752
+ children: /* @__PURE__ */ jsx("div", {
10753
+ className: "pointer-events-none",
10754
+ children: /* @__PURE__ */ jsx(Checkbox, {
10755
+ checked: selected ?? false,
10756
+ onCheckedChange: handleCheckboxChange,
10757
+ size: "smallest"
10758
+ })
10759
+ })
10760
+ })]
9575
10761
  }),
9576
10762
  /* @__PURE__ */ jsxs("div", {
9577
- className: cls("min-w-0 overflow-hidden", useColumnMode ? "flex-1" : "flex-1"),
10763
+ className: "flex-1 min-w-0 overflow-hidden",
9578
10764
  children: [
9579
10765
  /* @__PURE__ */ jsx("div", {
9580
10766
  className: "truncate",
@@ -9582,7 +10768,10 @@ var ListRow = React.memo(function ListRow({ entity, collection, onClick, selecte
9582
10768
  component: "div",
9583
10769
  variant: "body2",
9584
10770
  className: "font-semibold text-surface-900 dark:text-surface-50 truncate transition-colors group-hover:text-primary-600 dark:group-hover:text-primary-400",
9585
- children: /* @__PURE__ */ jsx(SlotValue, {
10771
+ children: terms.length > 0 && typeof slots.title?.value === "string" ? /* @__PURE__ */ jsx(Highlighted, {
10772
+ text: slots.title.value,
10773
+ terms
10774
+ }) : /* @__PURE__ */ jsx(SlotValue, {
9586
10775
  slot: slots.title,
9587
10776
  size: "small"
9588
10777
  })
@@ -9593,13 +10782,19 @@ var ListRow = React.memo(function ListRow({ entity, collection, onClick, selecte
9593
10782
  children: entity.id
9594
10783
  })
9595
10784
  }),
9596
- slots.subtitle && /* @__PURE__ */ jsx("div", {
10785
+ offSlot ? /* @__PURE__ */ jsx(MatchExplanation, {
10786
+ match: offSlot,
10787
+ label: fieldLabel(collection.properties, offSlot.field)
10788
+ }) : slots.subtitle && /* @__PURE__ */ jsx("div", {
9597
10789
  className: "truncate mt-0.5",
9598
10790
  children: /* @__PURE__ */ jsx(Typography, {
9599
10791
  variant: "caption",
9600
10792
  component: "div",
9601
10793
  className: "text-surface-500 dark:text-surface-400 truncate",
9602
- children: /* @__PURE__ */ jsx(SlotValue, {
10794
+ children: terms.length > 0 && typeof slots.subtitle.value === "string" ? /* @__PURE__ */ jsx(Highlighted, {
10795
+ text: slots.subtitle.value,
10796
+ terms
10797
+ }) : /* @__PURE__ */ jsx(SlotValue, {
9603
10798
  slot: slots.subtitle,
9604
10799
  size: "small"
9605
10800
  })
@@ -9619,72 +10814,20 @@ var ListRow = React.memo(function ListRow({ entity, collection, onClick, selecte
9619
10814
  })
9620
10815
  ]
9621
10816
  }),
9622
- useColumnMode ? /* @__PURE__ */ jsx(Fragment, { children: extraColumns.map((col) => {
9623
- const value = getValueInPath(entity.values, col.key);
9624
- if (col.isStatus || col.property.type === "string" && "enum" in col.property) return /* @__PURE__ */ jsx("div", {
9625
- className: cls(col.width, "flex", col.align === "center" ? "justify-center" : col.align === "right" ? "justify-end" : "justify-start"),
9626
- children: value ? /* @__PURE__ */ jsx(PropertyPreview, {
9627
- propertyKey: col.key,
9628
- value,
9629
- property: col.property,
9630
- size: "small"
9631
- }) : /* @__PURE__ */ jsx("span", {
9632
- className: "text-surface-400",
9633
- children: "—"
9634
- })
9635
- }, col.key);
9636
- if (col.isDate || col.property.type === "date") return /* @__PURE__ */ jsx("div", {
9637
- className: cls(col.width, col.align === "center" ? "text-center" : col.align === "right" ? "text-right" : "text-left"),
9638
- children: /* @__PURE__ */ jsx(Typography, {
9639
- variant: "caption",
9640
- className: "whitespace-nowrap text-surface-400 dark:text-surface-500 font-medium",
9641
- children: formatDateValue(value) ?? "—"
9642
- })
9643
- }, col.key);
9644
- if (isComplexPropertyType(col.property)) {
9645
- const summary = compactValueSummary(value, col.property);
9646
- return /* @__PURE__ */ jsx("div", {
9647
- className: cls(col.width, "truncate", col.align === "center" ? "text-center" : col.align === "right" ? "text-right" : "text-left"),
9648
- children: /* @__PURE__ */ jsx(Typography, {
9649
- variant: "caption",
9650
- className: "text-surface-500 dark:text-surface-400 truncate",
9651
- children: summary ?? "—"
9652
- })
9653
- }, col.key);
9654
- }
9655
- return /* @__PURE__ */ jsx("div", {
9656
- className: cls(col.width, "truncate", col.align === "center" ? "text-center" : col.align === "right" ? "text-right" : "text-left"),
9657
- children: /* @__PURE__ */ jsx(Typography, {
9658
- component: "div",
9659
- variant: "body2",
9660
- className: "text-surface-600 dark:text-surface-300 truncate",
9661
- children: value !== void 0 ? /* @__PURE__ */ jsx(PropertyPreview, {
9662
- propertyKey: col.key,
9663
- value,
9664
- property: col.property,
9665
- size: "small"
9666
- }) : "—"
9667
- })
9668
- }, col.key);
9669
- }) }) : /* @__PURE__ */ jsxs("div", {
10817
+ columns.length > 0 && /* @__PURE__ */ jsx("div", {
9670
10818
  className: "flex items-center gap-4 flex-shrink-0 ml-auto",
9671
- children: [slots.status && /* @__PURE__ */ jsx("div", {
9672
- className: "flex-shrink-0 @max-[400px]:hidden",
9673
- children: /* @__PURE__ */ jsx(SlotValue, {
9674
- slot: slots.status,
9675
- size: "small"
9676
- })
9677
- }), slots.date && /* @__PURE__ */ jsx("div", {
9678
- className: "flex-shrink-0 text-right w-[80px] @max-[500px]:hidden",
9679
- children: /* @__PURE__ */ jsx(Typography, {
9680
- variant: "caption",
9681
- className: "whitespace-nowrap text-surface-400 dark:text-surface-500 font-medium",
9682
- children: slots.date.formatted ?? "—"
10819
+ children: columns.map((col) => /* @__PURE__ */ jsx("div", {
10820
+ className: cls(col.width, "flex items-center overflow-hidden", col.align === "center" ? "justify-center" : col.align === "right" ? "justify-end" : "justify-start"),
10821
+ children: /* @__PURE__ */ jsx(ListCell, {
10822
+ column: col,
10823
+ entity,
10824
+ slots
9683
10825
  })
9684
- })]
10826
+ }, col.key))
9685
10827
  }),
9686
- listViewActions.length > 0 && /* @__PURE__ */ jsx("div", {
9687
- className: "flex items-center gap-0.5 flex-shrink-0 ml-auto",
10828
+ actionsWidth > 0 && /* @__PURE__ */ jsx("div", {
10829
+ className: "flex items-center justify-end gap-0.5 flex-shrink-0 ml-auto",
10830
+ style: { minWidth: actionsWidth },
9688
10831
  onClick: (e) => e.stopPropagation(),
9689
10832
  children: listViewActions.map((action, index) => /* @__PURE__ */ jsx(Tooltip, {
9690
10833
  title: action.name,
@@ -9711,6 +10854,143 @@ var ListRow = React.memo(function ListRow({ entity, collection, onClick, selecte
9711
10854
  ]
9712
10855
  });
9713
10856
  });
10857
+ /**
10858
+ * One cell of one row: whatever the column says fills it.
10859
+ *
10860
+ * A slot column renders the slot — a status keeps its coloured chip and a date
10861
+ * its relative wording wherever the value came from — and a property column
10862
+ * reads the record. Both go through here so the header above them is labelling
10863
+ * one thing, not two that happen to line up.
10864
+ */
10865
+ function ListCell({ column, entity, slots }) {
10866
+ if (column.slot === "tags") {
10867
+ if (!slots.tags) return null;
10868
+ return /* @__PURE__ */ jsx("div", {
10869
+ className: "flex items-center gap-1 overflow-hidden",
10870
+ children: /* @__PURE__ */ jsx(TagChips, {
10871
+ slot: slots.tags,
10872
+ max: 3
10873
+ })
10874
+ });
10875
+ }
10876
+ if (column.slot === "status") {
10877
+ if (!slots.status) return /* @__PURE__ */ jsx(EmptyCell, {});
10878
+ return /* @__PURE__ */ jsx(SlotValue, {
10879
+ slot: slots.status,
10880
+ size: "small"
10881
+ });
10882
+ }
10883
+ if (column.slot === "date") return /* @__PURE__ */ jsx(Typography, {
10884
+ variant: "caption",
10885
+ className: "whitespace-nowrap text-surface-400 dark:text-surface-500 font-medium",
10886
+ children: slots.date?.formatted ?? "—"
10887
+ });
10888
+ const property = column.property;
10889
+ if (!property) return null;
10890
+ const value = getValueInPath(entity.values, column.key);
10891
+ if (property.type === "date") return /* @__PURE__ */ jsx(Typography, {
10892
+ variant: "caption",
10893
+ className: "whitespace-nowrap text-surface-400 dark:text-surface-500 font-medium",
10894
+ children: formatDateValue(value) ?? "—"
10895
+ });
10896
+ if (isComplexPropertyType(property)) return /* @__PURE__ */ jsx(Typography, {
10897
+ variant: "caption",
10898
+ className: "text-surface-500 dark:text-surface-400 truncate",
10899
+ children: compactValueSummary(value, property) ?? "—"
10900
+ });
10901
+ if (value === void 0 || value === null) return /* @__PURE__ */ jsx(EmptyCell, {});
10902
+ return /* @__PURE__ */ jsx(Typography, {
10903
+ component: "div",
10904
+ variant: "body2",
10905
+ className: "text-surface-600 dark:text-surface-300 truncate",
10906
+ children: /* @__PURE__ */ jsx(PropertyPreview, {
10907
+ propertyKey: column.key,
10908
+ value,
10909
+ property,
10910
+ size: "small"
10911
+ })
10912
+ });
10913
+ }
10914
+ /** What a column shows for a record that has nothing in it. */
10915
+ function EmptyCell() {
10916
+ return /* @__PURE__ */ jsx("span", {
10917
+ className: "text-surface-400 dark:text-surface-600",
10918
+ children: "—"
10919
+ });
10920
+ }
10921
+ /**
10922
+ * The list's column header.
10923
+ *
10924
+ * Quiet by design: a list is read row by row, and the header is there to name
10925
+ * the columns and to be clicked, not to compete with the rows. It lays out
10926
+ * against the same widths the rows do, and skips the checkbox and the image —
10927
+ * neither is a column, and neither has anything to be called.
10928
+ */
10929
+ function ListHeader({ titleColumn, columns, selectionEnabled, showImage, actionsWidth, sortBy, onColumnSort, sortIsAvailable }) {
10930
+ const headerCell = (column) => /* @__PURE__ */ jsx(ListHeaderLabel, {
10931
+ column,
10932
+ direction: sortBy?.[0] === column.key ? sortBy[1] : void 0,
10933
+ onSort: column.sortable && sortIsAvailable(column.key) ? () => onColumnSort(column.key) : void 0
10934
+ });
10935
+ return /* @__PURE__ */ jsxs("div", {
10936
+ className: cls("flex items-center gap-4 px-5 py-1.5 select-none border-b bg-surface-50 dark:bg-surface-900", defaultBorderMixin),
10937
+ children: [
10938
+ selectionEnabled && /* @__PURE__ */ jsx("div", { className: "flex-shrink-0 w-8" }),
10939
+ showImage && /* @__PURE__ */ jsx("div", { className: "flex-shrink-0 w-10" }),
10940
+ /* @__PURE__ */ jsx("div", {
10941
+ className: "flex-1 min-w-0 flex items-center",
10942
+ children: titleColumn && headerCell(titleColumn)
10943
+ }),
10944
+ columns.length > 0 && /* @__PURE__ */ jsx("div", {
10945
+ className: "flex items-center gap-4 flex-shrink-0 ml-auto",
10946
+ children: columns.map((column) => /* @__PURE__ */ jsx("div", {
10947
+ className: cls(column.width, "flex items-center overflow-hidden", column.align === "center" ? "justify-center" : column.align === "right" ? "justify-end" : "justify-start"),
10948
+ children: headerCell(column)
10949
+ }, column.key))
10950
+ }),
10951
+ actionsWidth > 0 && /* @__PURE__ */ jsx("div", {
10952
+ className: "flex-shrink-0 ml-auto",
10953
+ style: { minWidth: actionsWidth }
10954
+ })
10955
+ ]
10956
+ });
10957
+ }
10958
+ /**
10959
+ * One header label, clickable when the collection can be ordered by it.
10960
+ *
10961
+ * The arrow shows only on the column currently ordering the list: three arrows
10962
+ * in a header row are three invitations, and only one of them is ever the
10963
+ * answer to "how is this sorted".
10964
+ */
10965
+ function ListHeaderLabel({ column, direction, onSort }) {
10966
+ const content = /* @__PURE__ */ jsxs(Fragment, { children: [
10967
+ /* @__PURE__ */ jsx("span", {
10968
+ className: "truncate",
10969
+ children: column.label
10970
+ }),
10971
+ direction === "asc" && /* @__PURE__ */ jsx(ArrowUpIcon, {
10972
+ size: 12,
10973
+ className: "flex-shrink-0"
10974
+ }),
10975
+ direction === "desc" && /* @__PURE__ */ jsx(ArrowDownIcon, {
10976
+ size: 12,
10977
+ className: "flex-shrink-0"
10978
+ })
10979
+ ] });
10980
+ const base = "inline-flex items-center gap-1 max-w-full text-[11px] leading-none uppercase tracking-wider font-medium";
10981
+ const tone = direction ? "text-surface-600 dark:text-surface-300" : "text-surface-400 dark:text-surface-500";
10982
+ if (!onSort) return /* @__PURE__ */ jsx("span", {
10983
+ className: cls(base, tone),
10984
+ children: content
10985
+ });
10986
+ return /* @__PURE__ */ jsx("button", {
10987
+ type: "button",
10988
+ title: `Sort by ${column.label}`,
10989
+ onClick: onSort,
10990
+ className: cls(base, tone, "cursor-pointer hover:text-surface-700 dark:hover:text-surface-200 transition-colors"),
10991
+ children: content
10992
+ });
10993
+ }
9714
10994
  //#endregion
9715
10995
  //#region src/components/EntityDisplayHeader.tsx
9716
10996
  /**
@@ -10056,11 +11336,11 @@ function EntityViewBinding({ entity, collection, className, asPage = false, open
10056
11336
  });
10057
11337
  return /* @__PURE__ */ jsxs("div", {
10058
11338
  ref: containerRef,
10059
- className: cls("flex-1 flex flex-row w-full min-h-0", className),
11339
+ className: cls("@container/form flex-1 flex flex-row w-full min-h-0", className),
10060
11340
  children: [/* @__PURE__ */ jsx("div", {
10061
11341
  className: "flex-1 min-w-0 overflow-y-auto flex justify-center items-start",
10062
11342
  children: /* @__PURE__ */ jsx("div", {
10063
- 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"),
11343
+ className: cls("@container/col w-full max-w-3xl @7xl/form:max-w-4xl 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"),
10064
11344
  children: content
10065
11345
  })
10066
11346
  }), showRail && /* @__PURE__ */ jsx(FormRail, {
@@ -10083,11 +11363,14 @@ function EntityViewBinding({ entity, collection, className, asPage = false, open
10083
11363
  * editing. They live here instead, and the left half of a bar that was
10084
11364
  * previously empty now does the work.
10085
11365
  */
10086
- function EntityIdentityBar({ collection, collectionUrl, title, entityId, status, dirty, saving, onSave, onDiscard, saveDisabled, hasErrors, onSaveAndClose, onBack, onInspect, onViewHistory, externalLink, recordActions, pluginActions, trailing, leading }) {
11366
+ function EntityIdentityBar({ collection, collectionUrl, title, entityId, status, dirty, saving, onSave, onDiscard, saveDisabled, hasErrors, onSaveAndClose, saveAndClosePlacement = "button", onClose, onBack, onInspect, onViewHistory, externalLink, recordActions, pluginActions, trailing, leading }) {
10087
11367
  const { t } = useTranslation();
10088
11368
  const saveLabel = status === "existing" ? t("save") : status === "copy" ? t("create_copy") : t("create");
10089
11369
  const closeLabel = status === "existing" ? t("save_and_close") : status === "copy" ? t("create_copy_and_close") : t("create_and_close");
10090
11370
  const hasMenu = Boolean(recordActions) || Boolean(onInspect) || Boolean(onViewHistory) || Boolean(externalLink);
11371
+ const saveAndCloseButton = Boolean(onSaveAndClose) && saveAndClosePlacement === "button";
11372
+ const saveAndCloseMenu = Boolean(onSaveAndClose) && saveAndClosePlacement === "menu";
11373
+ const saveTooltip = hasErrors ? t("fix_errors_before_saving") ?? "Fix highlighted errors before saving" : void 0;
10091
11374
  return /* @__PURE__ */ jsxs("div", {
10092
11375
  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),
10093
11376
  children: [
@@ -10134,19 +11417,39 @@ function EntityIdentityBar({ collection, collectionUrl, title, entityId, status,
10134
11417
  onClick: onDiscard,
10135
11418
  children: status === "existing" ? t("discard") : t("clear")
10136
11419
  }),
10137
- onSave && /* @__PURE__ */ jsx(Tooltip, {
10138
- title: hasErrors ? t("fix_errors_before_saving") ?? "Fix highlighted errors before saving" : void 0,
10139
- children: /* @__PURE__ */ jsx(LoadingButton, {
10140
- variant: onSaveAndClose ? "text" : "filled",
10141
- color: "primary",
10142
- size: "small",
10143
- loading: saving && !onSaveAndClose,
10144
- disabled: saveDisabled,
10145
- onClick: onSave,
10146
- children: saveLabel
10147
- })
11420
+ onSave && /* @__PURE__ */ jsxs("div", {
11421
+ className: "flex items-stretch rounded-lg overflow-hidden",
11422
+ children: [/* @__PURE__ */ jsx(Tooltip, {
11423
+ title: saveTooltip,
11424
+ children: /* @__PURE__ */ jsx(LoadingButton, {
11425
+ variant: saveAndCloseButton ? "text" : "filled",
11426
+ color: "primary",
11427
+ size: "small",
11428
+ loading: saving && !saveAndCloseButton,
11429
+ disabled: saveDisabled,
11430
+ onClick: onSave,
11431
+ className: saveAndCloseMenu ? "rounded-none" : void 0,
11432
+ children: saveLabel
11433
+ })
11434
+ }), saveAndCloseMenu && /* @__PURE__ */ jsx(Fragment, { children: /* @__PURE__ */ jsx(Menu, {
11435
+ align: "end",
11436
+ trigger: /* @__PURE__ */ jsx(Button, {
11437
+ variant: "filled",
11438
+ color: "primary",
11439
+ size: "small",
11440
+ disabled: saveDisabled,
11441
+ "aria-label": closeLabel,
11442
+ style: { borderLeftColor: "color-mix(in oklab, currentColor 15%, transparent)" },
11443
+ className: "rounded-none px-1.5",
11444
+ children: /* @__PURE__ */ jsx(ChevronDownIcon, { size: iconSize.smallest })
11445
+ }),
11446
+ children: /* @__PURE__ */ jsxs(MenuItem, {
11447
+ onClick: onSaveAndClose,
11448
+ children: [/* @__PURE__ */ jsx(CheckIcon, { size: iconSize.smallest }), closeLabel]
11449
+ })
11450
+ }) })]
10148
11451
  }),
10149
- onSaveAndClose && /* @__PURE__ */ jsx(LoadingButton, {
11452
+ saveAndCloseButton && /* @__PURE__ */ jsx(LoadingButton, {
10150
11453
  variant: "filled",
10151
11454
  color: "primary",
10152
11455
  size: "small",
@@ -10180,7 +11483,19 @@ function EntityIdentityBar({ collection, collectionUrl, title, entityId, status,
10180
11483
  })
10181
11484
  ]
10182
11485
  }),
10183
- trailing
11486
+ trailing,
11487
+ onClose && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Separator, {
11488
+ orientation: "vertical",
11489
+ className: "h-5 mx-1 shrink-0"
11490
+ }), /* @__PURE__ */ jsx(Tooltip, {
11491
+ title: `${t("close")} · Esc`,
11492
+ children: /* @__PURE__ */ jsx(IconButton, {
11493
+ size: "small",
11494
+ onClick: onClose,
11495
+ "aria-label": t("close"),
11496
+ children: /* @__PURE__ */ jsx(XIcon, { size: iconSize.smallest })
11497
+ })
11498
+ })] })
10184
11499
  ]
10185
11500
  });
10186
11501
  }
@@ -10250,13 +11565,17 @@ function SplitViewProvider({ value, children }) {
10250
11565
  * Closes the list of a split view, leaving the open record on its own.
10251
11566
  *
10252
11567
  * It leads the record's own app bar, where the breadcrumb starts — this is the
10253
- * entity pane's old "Open full screen" button, moved to that edge and wearing a
10254
- * double chevron pointing at the list it folds away: closing the list and
10255
- * opening the record full screen are the same thing, so there is one control
10256
- * and one route (`#full`) rather than two. Full screen carries the mirrored
10257
- * chevron to bring the list back.
11568
+ * entity pane's old "Open full screen" button, moved to that edge: closing the
11569
+ * list and opening the record full screen are the same thing, so there is one
11570
+ * control and one route (`#full`) rather than two. Full screen carries the
11571
+ * mirrored icon to bring the list back.
11572
+ *
11573
+ * A pane glyph rather than the double chevron it wore first. Now that the bar
11574
+ * ends in a ✕ that dismisses the record, an arrow pointing off the left edge of
11575
+ * the record's own bar was the other thing on it that read as "close me" — and
11576
+ * it is the one control here that does not act on the record at all.
10258
11577
  */
10259
- function SplitListCloseButton() {
11578
+ function SplitListCloseButton({ onBeforeHide } = {}) {
10260
11579
  const { t } = useTranslation();
10261
11580
  const splitView = useSplitView();
10262
11581
  if (!splitView?.detailOpen) return null;
@@ -10264,9 +11583,12 @@ function SplitListCloseButton() {
10264
11583
  title: t("hide_list"),
10265
11584
  children: /* @__PURE__ */ jsx(IconButton, {
10266
11585
  size: "small",
10267
- onClick: splitView.openFullScreen,
11586
+ onClick: () => {
11587
+ onBeforeHide?.();
11588
+ splitView.openFullScreen();
11589
+ },
10268
11590
  "aria-label": t("hide_list"),
10269
- children: /* @__PURE__ */ jsx(ChevronsLeftIcon, { size: iconSize.smallest })
11591
+ children: /* @__PURE__ */ jsx(PanelLeftCloseIcon, { size: iconSize.smallest })
10270
11592
  })
10271
11593
  });
10272
11594
  }
@@ -10276,7 +11598,7 @@ function SplitListCloseButton() {
10276
11598
  * Brings the list back beside a record that was opened full screen.
10277
11599
  *
10278
11600
  * The mirror of {@link SplitListCloseButton}: the same edge of the same bar,
10279
- * the same double chevron pointing the other way, and the same single route —
11601
+ * the same pane glyph opening rather than closing, and the same single route —
10280
11602
  * showing the list is dropping the `#full` hash. It replaces the back arrow
10281
11603
  * where it applies, because the list it unfolds already holds the record.
10282
11604
  */
@@ -10288,7 +11610,7 @@ function SplitListShowButton({ onClick }) {
10288
11610
  size: "small",
10289
11611
  onClick,
10290
11612
  "aria-label": t("show_list"),
10291
- children: /* @__PURE__ */ jsx(ChevronsRightIcon, { size: iconSize.smallest })
11613
+ children: /* @__PURE__ */ jsx(PanelLeftOpenIcon, { size: iconSize.smallest })
10292
11614
  })
10293
11615
  });
10294
11616
  }
@@ -10322,7 +11644,7 @@ function JsonPreviewBinding({ values }) {
10322
11644
  }
10323
11645
  //#endregion
10324
11646
  //#region src/components/EntityInspector.tsx
10325
- var EntityHistoryView = lazy(() => import("./history-DfMuvPfr.js").then((m) => ({ default: m.EntityHistoryView })));
11647
+ var EntityHistoryView = lazy(() => import("./history-D5SKygpJ.js").then((m) => ({ default: m.EntityHistoryView })));
10326
11648
  /**
10327
11649
  * Raw values and revision history, as an inspector rather than as tabs.
10328
11650
  *
@@ -10340,13 +11662,13 @@ function EntityInspector({ tab, onTabChange, onClose, collection, entity, formCo
10340
11662
  useEffect(() => {
10341
11663
  if (!open) return;
10342
11664
  const onKeyDown = (e) => {
10343
- if (e.key === "Escape") {
10344
- e.stopPropagation();
10345
- onClose();
10346
- }
11665
+ if (e.key !== "Escape") return;
11666
+ if (document.querySelector("[role=\"dialog\"][data-state=\"open\"]")) return;
11667
+ e.stopPropagation();
11668
+ onClose();
10347
11669
  };
10348
- window.addEventListener("keydown", onKeyDown);
10349
- return () => window.removeEventListener("keydown", onKeyDown);
11670
+ document.addEventListener("keydown", onKeyDown, true);
11671
+ return () => document.removeEventListener("keydown", onKeyDown, true);
10350
11672
  }, [open, onClose]);
10351
11673
  if (!open) return null;
10352
11674
  return /* @__PURE__ */ jsxs("div", {
@@ -10433,6 +11755,15 @@ function useEntityDisplayTitle({ collection, entity, values, status }) {
10433
11755
  * Shared tab value constants used by EditViewBinding and DetailViewBinding.
10434
11756
  */
10435
11757
  var MAIN_TAB_VALUE = "__main_##Q$SC^#S6";
11758
+ /**
11759
+ * Every tab in that row, a custom `tabComponent` included.
11760
+ *
11761
+ * `h-full` over the variant's `h-9`: the row is 40px and the tab 36px, so a
11762
+ * centred tab floated ~2px clear of the row's bottom border and the active
11763
+ * tab's underline stopped short of the record it opens. Paired with
11764
+ * `items-stretch` on the Tabs root, which the variant leaves centred.
11765
+ */
11766
+ var ENTITY_VIEW_TAB_CLASS = "h-full data-[state=active]:bg-white dark:data-[state=active]:bg-surface-800";
10436
11767
  //#endregion
10437
11768
  //#region src/components/DetailViewBinding.tsx
10438
11769
  function DetailViewBinding({ entityId, ...props }) {
@@ -10458,7 +11789,7 @@ function DetailViewBinding({ entityId, ...props }) {
10458
11789
  children: content
10459
11790
  });
10460
11791
  }
10461
- function DetailViewBindingInner({ path, entityId, selectedTab: selectedTabProp, collection, parentCollectionSlugs, parentEntityIds, onTabChange, onEditClick, entity, dataLoading, layout = "full_screen", barActions, barActionsStart, onShowList }) {
11792
+ function DetailViewBindingInner({ path, entityId, selectedTab: selectedTabProp, collection, parentCollectionSlugs, parentEntityIds, onTabChange, onEditClick, onCloseRequest, entity, dataLoading, layout = "full_screen", barActions, barActionsStart, onShowList }) {
10462
11793
  const ResolvedCollectionView = useComponentOverride("Collection.View", CollectionViewBinding);
10463
11794
  const { t } = useTranslation();
10464
11795
  const context = useRebaseContext();
@@ -10494,7 +11825,7 @@ function DetailViewBindingInner({ path, entityId, selectedTab: selectedTabProp,
10494
11825
  const target = val === "edit" ? MAIN_TAB_VALUE : val;
10495
11826
  if (target !== selectedTab) setSelectedTab(target);
10496
11827
  }, [selectedTabProp, defaultSelectedView]);
10497
- const childViews = getAdminEntityChildViews(collection).filter((v) => !v.collection.hideFromNavigation);
11828
+ const childViews = getAdminEntityChildViews(collection).filter((v) => !v.collection.hideFromEntityViews);
10498
11829
  const subcollections = childViews.map((v) => v.collection);
10499
11830
  const subcollectionsCount = subcollections?.length ?? 0;
10500
11831
  const customViews = collection.entityViews ?? [];
@@ -10645,9 +11976,9 @@ function DetailViewBindingInner({ path, entityId, selectedTab: selectedTabProp,
10645
11976
  ]);
10646
11977
  const propertyDetailView = () => {
10647
11978
  if (FormViewBuilder && usedEntity) return /* @__PURE__ */ jsx("div", {
10648
- className: "flex-1 min-w-0 overflow-y-auto flex justify-center items-start",
11979
+ className: "@container/form flex-1 min-w-0 overflow-y-auto flex justify-center items-start",
10649
11980
  children: /* @__PURE__ */ jsx("div", {
10650
- 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"),
11981
+ className: cls("w-full max-w-3xl @7xl/form:max-w-4xl 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"),
10651
11982
  children: /* @__PURE__ */ jsx(ErrorBoundary, { children: /* @__PURE__ */ jsx(Suspense, {
10652
11983
  fallback: /* @__PURE__ */ jsx(CircularProgressCenter, {}),
10653
11984
  children: /* @__PURE__ */ jsx(FormViewBuilder, {
@@ -10673,7 +12004,7 @@ function DetailViewBindingInner({ path, entityId, selectedTab: selectedTabProp,
10673
12004
  const subcollectionTabs = subcollections && subcollections.map((subcollection) => {
10674
12005
  const icon = getIcon(subcollection.icon, void 0, void 0, "smallest");
10675
12006
  return /* @__PURE__ */ jsx(Tab, {
10676
- className: "text-sm min-w-[90px]",
12007
+ className: cls("h-full data-[state=active]:bg-white dark:data-[state=active]:bg-surface-800", "text-sm min-w-[90px]"),
10677
12008
  value: subcollection.slug,
10678
12009
  children: /* @__PURE__ */ jsxs("span", {
10679
12010
  className: "flex items-center gap-1.5",
@@ -10684,7 +12015,7 @@ function DetailViewBindingInner({ path, entityId, selectedTab: selectedTabProp,
10684
12015
  const customViewTabsStart = resolvedEntityViews.filter((view) => view.position === "start").map((view) => {
10685
12016
  const icon = getIcon(view.icon, void 0, void 0, "smallest");
10686
12017
  return /* @__PURE__ */ jsx(Tab, {
10687
- className: !view.tabComponent ? "text-sm min-w-[90px]" : void 0,
12018
+ className: cls(ENTITY_VIEW_TAB_CLASS, !view.tabComponent && "text-sm min-w-[90px]"),
10688
12019
  value: view.key,
10689
12020
  children: view.tabComponent ?? /* @__PURE__ */ jsxs("span", {
10690
12021
  className: "flex items-center gap-1.5",
@@ -10695,7 +12026,7 @@ function DetailViewBindingInner({ path, entityId, selectedTab: selectedTabProp,
10695
12026
  const customViewTabsEnd = resolvedEntityViews.filter((view) => !view.position || view.position === "end").map((view) => {
10696
12027
  const icon = getIcon(view.icon, void 0, void 0, "smallest");
10697
12028
  return /* @__PURE__ */ jsx(Tab, {
10698
- className: !view.tabComponent ? "text-sm min-w-[90px]" : void 0,
12029
+ className: cls(ENTITY_VIEW_TAB_CLASS, !view.tabComponent && "text-sm min-w-[90px]"),
10699
12030
  value: view.key,
10700
12031
  children: view.tabComponent ?? /* @__PURE__ */ jsxs("span", {
10701
12032
  className: "flex items-center gap-1.5",
@@ -10740,6 +12071,7 @@ function DetailViewBindingInner({ path, entityId, selectedTab: selectedTabProp,
10740
12071
  dirty: false,
10741
12072
  saving: false,
10742
12073
  onBack: backToCollection,
12074
+ onClose: onCloseRequest,
10743
12075
  onInspect: includeJsonView ? () => setInspectorTab("json") : void 0,
10744
12076
  onViewHistory: includeHistoryView ? () => setInspectorTab("history") : void 0,
10745
12077
  externalLink: usedEntity ? customizationController?.entityLinkBuilder?.({ entity: usedEntity }) : void 0,
@@ -10762,9 +12094,9 @@ function DetailViewBindingInner({ path, entityId, selectedTab: selectedTabProp,
10762
12094
  ] })
10763
12095
  }),
10764
12096
  hasAdditionalViews && /* @__PURE__ */ jsx("div", {
10765
- className: cls("h-10 shrink-0 flex items-stretch border-b px-2 min-w-0", defaultBorderMixin),
12097
+ className: cls("h-10 shrink-0 flex items-stretch border-b px-2 min-w-0", "bg-surface-50 dark:bg-surface-900", defaultBorderMixin),
10766
12098
  children: /* @__PURE__ */ jsxs(Tabs, {
10767
- className: "!w-full",
12099
+ className: "!w-full items-stretch",
10768
12100
  innerClassName: "h-full",
10769
12101
  variant: "boxy",
10770
12102
  value: activeTab,
@@ -10774,6 +12106,7 @@ function DetailViewBindingInner({ path, entityId, selectedTab: selectedTabProp,
10774
12106
  children: [
10775
12107
  /* @__PURE__ */ jsx(Tab, {
10776
12108
  value: "__main_##Q$SC^#S6",
12109
+ className: cls("h-full data-[state=active]:bg-white dark:data-[state=active]:bg-surface-800", "text-sm min-w-[90px]"),
10777
12110
  children: /* @__PURE__ */ jsxs("span", {
10778
12111
  className: "flex items-center gap-1.5",
10779
12112
  children: [getIcon(collection.icon, void 0, void 0, "smallest"), collection.singularName ?? collection.name]
@@ -10909,12 +12242,14 @@ function isListShortcutTarget(target, detailPanel) {
10909
12242
  * The list only has to identify a record — a title, a subtitle and a status
10910
12243
  * chip — while the detail beside it carries the whole record and its rail. At
10911
12244
  * 30% the list was taking width from the side that had four columns of fields
10912
- * to lay out.
12245
+ * to lay out; 26% still was, by about the width of one field.
10913
12246
  *
10914
12247
  * A default, not a constraint: {@link savePanelSize} remembers the drag per
10915
- * collection, so anyone who has already sized this list keeps their size.
12248
+ * collection, so anyone who has already sized this list keeps their size. The
12249
+ * drag floor is `minPanelSizePx`, and it holds below ~1100px of split — at that
12250
+ * width this percentage is already the floor.
10916
12251
  */
10917
- var DEFAULT_PANEL_SIZE = 26;
12252
+ var DEFAULT_PANEL_SIZE = 22;
10918
12253
  function getSavedPanelSize(path) {
10919
12254
  try {
10920
12255
  const saved = localStorage.getItem(`${PANEL_SIZE_KEY}_${path}`);
@@ -10957,7 +12292,10 @@ function SplitListView({ collection, tableController, onEntityClick: externalOnE
10957
12292
  const setDirty = useCallback((modified) => {
10958
12293
  dirty.current = modified;
10959
12294
  }, []);
12295
+ const previousEntityId = useRef(selectedEntityId);
10960
12296
  useEffect(() => {
12297
+ if (previousEntityId.current === selectedEntityId) return;
12298
+ previousEntityId.current = selectedEntityId;
10961
12299
  dirty.current = false;
10962
12300
  }, [selectedEntityId]);
10963
12301
  const blocker = useNavigationBlocker(useCallback(({ currentLocation, nextLocation }) => {
@@ -10970,9 +12308,10 @@ function SplitListView({ collection, tableController, onEntityClick: externalOnE
10970
12308
  return true;
10971
12309
  }, []));
10972
12310
  const [panelSize, setPanelSize] = useState(() => getSavedPanelSize(path));
10973
- useEffect(() => {
10974
- savePanelSize(path, panelSize);
10975
- }, [panelSize, path]);
12311
+ const handlePanelSizeChange = useCallback((size) => {
12312
+ setPanelSize(size);
12313
+ savePanelSize(path, size);
12314
+ }, [path]);
10976
12315
  const [renderedEntityId, setRenderedEntityId] = useState(selectedEntityId);
10977
12316
  const [animationPhase, setAnimationPhase] = useState(selectedEntityId !== void 0 ? "entered" : "idle");
10978
12317
  const animationTimer = useRef(null);
@@ -11100,6 +12439,7 @@ function SplitListView({ collection, tableController, onEntityClick: externalOnE
11100
12439
  parentEntityIds: usedParentEntityIds,
11101
12440
  selectedTab,
11102
12441
  layout: "split",
12442
+ onCloseRequest: handleCloseDetail,
11103
12443
  onEditClick: () => {
11104
12444
  navigate(buildUrl(`${path}/${renderedEntityId}/edit`));
11105
12445
  },
@@ -11115,6 +12455,7 @@ function SplitListView({ collection, tableController, onEntityClick: externalOnE
11115
12455
  parentEntityIds: usedParentEntityIds,
11116
12456
  selectedTab,
11117
12457
  layout: "split",
12458
+ onCloseRequest: handleCloseDetail,
11118
12459
  onValuesModified: setDirty,
11119
12460
  onSaved: () => {
11120
12461
  setDirty(false);
@@ -11150,7 +12491,7 @@ function SplitListView({ collection, tableController, onEntityClick: externalOnE
11150
12491
  secondPanel: detailPanel,
11151
12492
  showSecondPanel: isDetailVisible,
11152
12493
  panelSizePercent: animationPhase === "entering" ? 100 : panelSize,
11153
- onPanelSizeChange: setPanelSize,
12494
+ onPanelSizeChange: handlePanelSizeChange,
11154
12495
  minPanelSizePx: 240
11155
12496
  }), /* @__PURE__ */ jsx(UnsavedChangesDialog, {
11156
12497
  open: blocker?.state === "blocked",
@@ -11297,6 +12638,17 @@ function BoardCardBindingInner({ item, isDragging, isGroupedOver, style, collect
11297
12638
  className: "text-[10px] text-surface-400 dark:text-surface-500 shrink-0",
11298
12639
  children: ["+", slots.relations.reduce((acc, r) => acc + Math.max(0, r.totalCount - r.items.length), 0)]
11299
12640
  })]
12641
+ }),
12642
+ slots.tags && /* @__PURE__ */ jsx("div", {
12643
+ className: "flex items-center gap-1 mt-1 flex-wrap",
12644
+ children: /* @__PURE__ */ jsx(TagChips, {
12645
+ slot: slots.tags,
12646
+ max: 3
12647
+ })
12648
+ }),
12649
+ slots.date && /* @__PURE__ */ jsx("div", {
12650
+ className: "text-[10px] text-surface-500 dark:text-surface-400 mt-1",
12651
+ children: slots.date.formatted
11300
12652
  })
11301
12653
  ]
11302
12654
  })]
@@ -11414,6 +12766,7 @@ function useBoardDataController({ fullPath, collection, columnProperty, columns,
11414
12766
  }, []);
11415
12767
  const subscribeToColumn = useCallback((column, itemCount) => {
11416
12768
  if (isCleaningUpRef.current) return;
12769
+ const requestedCount = Math.min(itemCount, MAX_LIST_LIMIT);
11417
12770
  const currentDataClient = dataClientRef.current;
11418
12771
  const currentCollection = collectionRef.current;
11419
12772
  const currentContext = contextRef.current;
@@ -11543,7 +12896,7 @@ function useBoardDataController({ fullPath, collection, columnProperty, columns,
11543
12896
  const includeParams = getRelationIncludeParams(currentCollection);
11544
12897
  const fetchOnce = () => accessor.find({
11545
12898
  where: whereFilter,
11546
- limit: itemCount,
12899
+ limit: requestedCount,
11547
12900
  orderBy: orderByParam,
11548
12901
  include: includeParams,
11549
12902
  searchString: currentSearchString
@@ -11569,7 +12922,7 @@ function useBoardDataController({ fullPath, collection, columnProperty, columns,
11569
12922
  let liveDataReceived = false;
11570
12923
  const unsubscribe = accessor.listen({
11571
12924
  where: whereFilter,
11572
- limit: itemCount,
12925
+ limit: requestedCount,
11573
12926
  orderBy: orderByParam,
11574
12927
  include: includeParams,
11575
12928
  searchString: currentSearchString
@@ -12152,7 +13505,7 @@ function CollectionBoardViewBinding({ collection, tableController, fullPath, par
12152
13505
  analyticsController.onAnalyticsEvent?.("kanban_backfill_order", { path: fullPath });
12153
13506
  setBackfillLoading(true);
12154
13507
  try {
12155
- const entitiesToUpdate = (await dataClient.collection(fullPath).find({ limit: 1e4 })).data.filter((entity) => !isValidOrderKey(entity.values?.[orderProperty]));
13508
+ const entitiesToUpdate = (await dataClient.collection(fullPath).find({ limit: MAX_LIST_LIMIT })).data.filter((entity) => !isValidOrderKey(entity.values?.[orderProperty]));
12156
13509
  const keys = generateNKeysBetween(null, null, entitiesToUpdate.length, ORDER_KEY_DIGITS);
12157
13510
  const updates = [];
12158
13511
  entitiesToUpdate.forEach((entity, index) => {
@@ -12392,10 +13745,10 @@ var ALL_VIEW_MODES = [
12392
13745
  function ViewModeToggle({ viewMode = "table", onViewModeChange, enabledViews = ALL_VIEW_MODES, size, onSizeChanged, open, onOpenChange, kanbanPropertyOptions, selectedKanbanProperty, onKanbanPropertyChange }) {
12393
13746
  const { t } = useTranslation();
12394
13747
  const getViewModeIcon = () => {
12395
- if (viewMode === "kanban") return /* @__PURE__ */ jsx(KanbanIcon, { size: iconSize.smallest });
12396
- if (viewMode === "cards") return /* @__PURE__ */ jsx(LayoutGridIcon, { size: iconSize.smallest });
12397
- if (viewMode === "table") return /* @__PURE__ */ jsx(TableIcon, { size: iconSize.smallest });
12398
- return /* @__PURE__ */ jsx(ListIcon, { size: iconSize.smallest });
13748
+ if (viewMode === "kanban") return /* @__PURE__ */ jsx(KanbanIcon, { size: iconSize.small });
13749
+ if (viewMode === "cards") return /* @__PURE__ */ jsx(LayoutGridIcon, { size: iconSize.small });
13750
+ if (viewMode === "table") return /* @__PURE__ */ jsx(TableIcon, { size: iconSize.small });
13751
+ return /* @__PURE__ */ jsx(ListIcon, { size: iconSize.small });
12399
13752
  };
12400
13753
  const getViewModeName = () => {
12401
13754
  if (viewMode === "kanban") return t("board");
@@ -12664,12 +14017,13 @@ function addRecentId(collectionId, id) {
12664
14017
  return newRecentIds;
12665
14018
  }
12666
14019
  function saveSearchedIdsLocally(collectionId, ids) {
12667
- localStorage.setItem("recent_id_searches::" + collectionId, JSON.stringify(ids));
14020
+ writeStoredJson("recent_id_searches::" + collectionId, ids);
12668
14021
  }
12669
14022
  function getRecentIds(collectionId) {
12670
- const stored = localStorage.getItem("recent_id_searches::" + collectionId);
12671
- if (!stored) return [];
12672
- return JSON.parse(stored);
14023
+ return readStoredJson("recent_id_searches::" + collectionId, {
14024
+ fallback: [],
14025
+ accept: isArrayValue
14026
+ });
12673
14027
  }
12674
14028
  //#endregion
12675
14029
  //#region src/components/admin/CreationResultDialog.tsx
@@ -13346,7 +14700,7 @@ function PopupFormFieldInternal({ tableKey, entityId, customFieldValidator, prop
13346
14700
  const property = propertyKey && getPropertyInPath$1(collection?.properties ?? {}, propertyKey);
13347
14701
  const fieldProps = propertyKey && property ? {
13348
14702
  propertyKey,
13349
- disabled: isSubmitting || isReadOnly(property) || !!property.admin?.disabled,
14703
+ disabled: isSubmitting || isReadOnly(property) || isDisabled(property),
13350
14704
  property,
13351
14705
  includeDescription: false,
13352
14706
  underlyingValueHasChanged: false,
@@ -13433,7 +14787,7 @@ function PopupFormFieldInternal({ tableKey, entityId, customFieldValidator, prop
13433
14787
  }
13434
14788
  //#endregion
13435
14789
  //#region src/components/ClearFilterSortButton.tsx
13436
- function ClearFilterSortButton({ tableController, enabled }) {
14790
+ function ClearFilterSortButton({ tableController, enabled, compact }) {
13437
14791
  const { t } = useTranslation();
13438
14792
  if (!enabled) return null;
13439
14793
  const filterIsSet = !!tableController.filterValues && Object.keys(tableController.filterValues).length > 0;
@@ -13445,15 +14799,14 @@ function ClearFilterSortButton({ tableController, enabled }) {
13445
14799
  else label = t("clear_sort");
13446
14800
  return /* @__PURE__ */ jsx(Tooltip, {
13447
14801
  title: label,
13448
- children: /* @__PURE__ */ jsx(Button, {
14802
+ children: /* @__PURE__ */ jsx(IconButton, {
13449
14803
  size: "small",
13450
- variant: "text",
13451
14804
  "aria-label": label,
13452
14805
  onClick: () => {
13453
14806
  tableController.clearFilter?.();
13454
14807
  tableController.setSortBy?.(void 0);
13455
14808
  },
13456
- children: /* @__PURE__ */ jsx(FilterXIcon, { size: iconSize.small })
14809
+ children: /* @__PURE__ */ jsx(FilterXIcon, { size: compact ? iconSize.smallest : iconSize.small })
13457
14810
  })
13458
14811
  });
13459
14812
  }
@@ -13811,7 +15164,7 @@ function FilterPresetsButton({ filterPresets, tableController, compact }) {
13811
15164
  * is one click rather than a trip through a direction selector; clicking a
13812
15165
  * different one starts it ascending. The popover stays open through both.
13813
15166
  */
13814
- function SortButton({ tableController, properties }) {
15167
+ function SortButton({ tableController, properties, compact }) {
13815
15168
  const { t } = useTranslation();
13816
15169
  const [open, setOpen] = useState(false);
13817
15170
  const options = useMemo(() => getSortablePropertyOptions(properties), [properties]);
@@ -13837,6 +15190,7 @@ function SortButton({ tableController, properties }) {
13837
15190
  if (!setSortBy || options.length === 0) return null;
13838
15191
  const activeOption = options.find((option) => option.key === activeKey);
13839
15192
  const label = activeOption ? `${t("sort_by")}: ${activeOption.title}` : t("sort");
15193
+ const arrowSize = compact ? iconSize.smallest : iconSize.small;
13840
15194
  return /* @__PURE__ */ jsx(Popover, {
13841
15195
  open,
13842
15196
  onOpenChange: setOpen,
@@ -13848,7 +15202,7 @@ function SortButton({ tableController, properties }) {
13848
15202
  "aria-label": label,
13849
15203
  title: label,
13850
15204
  className: cls(activeOption && "text-primary"),
13851
- children: activeDirection === "desc" ? /* @__PURE__ */ jsx(ArrowDownIcon, { size: iconSize.smallest }) : activeDirection === "asc" ? /* @__PURE__ */ jsx(ArrowUpIcon, { size: iconSize.smallest }) : /* @__PURE__ */ jsx(ArrowUpDownIcon, { size: iconSize.smallest })
15205
+ children: activeDirection === "desc" ? /* @__PURE__ */ jsx(ArrowDownIcon, { size: arrowSize }) : activeDirection === "asc" ? /* @__PURE__ */ jsx(ArrowUpIcon, { size: arrowSize }) : /* @__PURE__ */ jsx(ArrowUpDownIcon, { size: arrowSize })
13852
15206
  }),
13853
15207
  children: /* @__PURE__ */ jsxs("div", {
13854
15208
  className: "py-2 min-w-[240px] flex flex-col",
@@ -13945,6 +15299,7 @@ function CollectionViewStartActions({ collection, relativePath, parentCollection
13945
15299
  children: /* @__PURE__ */ jsx(XIcon, { size: iconSize.smallest })
13946
15300
  })
13947
15301
  }, "close_tooltip");
15302
+ const iconOnlyToolbar = !largeLayout || Boolean(compact);
13948
15303
  const filtersButton = resolvedProperties && tableController.setFilterValues && /* @__PURE__ */ jsx(Tooltip, {
13949
15304
  title: t("filters"),
13950
15305
  children: /* @__PURE__ */ jsx(Badge, {
@@ -13967,7 +15322,8 @@ function CollectionViewStartActions({ collection, relativePath, parentCollection
13967
15322
  }, "filters_tooltip");
13968
15323
  const sortButton = resolvedProperties && (viewMode === "list" || viewMode === "cards") && /* @__PURE__ */ jsx(SortButton, {
13969
15324
  tableController,
13970
- properties: resolvedProperties
15325
+ properties: resolvedProperties,
15326
+ compact: iconOnlyToolbar
13971
15327
  }, "sort_button");
13972
15328
  const filterPresetsButton = !compact && collection.filterPresets?.length ? /* @__PURE__ */ jsx(FilterPresetsButton, {
13973
15329
  filterPresets: collection.filterPresets,
@@ -13981,6 +15337,7 @@ function CollectionViewStartActions({ collection, relativePath, parentCollection
13981
15337
  sortButton,
13982
15338
  /* @__PURE__ */ jsx(ClearFilterSortButton, {
13983
15339
  tableController,
15340
+ compact: iconOnlyToolbar,
13984
15341
  enabled: !collection.fixedFilter
13985
15342
  }, "clear_filter"),
13986
15343
  filterPresetsButton
@@ -14139,16 +15496,16 @@ var CollectionViewBindingInner = React.memo(function CollectionViewBindingInner(
14139
15496
  viewMode
14140
15497
  });
14141
15498
  useEffect(() => {
14142
- if (!urlView && viewMode !== defaultViewMode) setSearchParams((prev) => {
14143
- const next = new URLSearchParams(prev);
15499
+ if (!urlView && viewMode !== defaultViewMode) setSearchParams(() => {
15500
+ const next = new URLSearchParams(window.location.search);
14144
15501
  next.set(VIEW_MODE_PARAM, viewMode);
14145
15502
  return next;
14146
15503
  }, { replace: true });
14147
15504
  }, []);
14148
15505
  const setViewMode = useCallback((newMode) => {
14149
15506
  setViewModeState(newMode);
14150
- setSearchParams((prev) => {
14151
- const next = new URLSearchParams(prev);
15507
+ setSearchParams(() => {
15508
+ const next = new URLSearchParams(window.location.search);
14152
15509
  if (newMode === defaultViewMode) next.delete(VIEW_MODE_PARAM);
14153
15510
  else next.set(VIEW_MODE_PARAM, newMode);
14154
15511
  return next;
@@ -14452,15 +15809,17 @@ var CollectionViewBindingInner = React.memo(function CollectionViewBindingInner(
14452
15809
  if (!property) property = getPropertyInPath$1(resolvedCollection.properties, propertyKey);
14453
15810
  return property ?? null;
14454
15811
  }, [collection.properties, resolvedCollection.properties]);
14455
- const displayedColumnIds = useColumnIds(useMemo(() => {
15812
+ const collectionWithLocalOrder = useMemo(() => {
14456
15813
  if (localPropertiesOrder && localPropertiesOrder !== resolvedCollection.propertiesOrder) return {
14457
15814
  ...resolvedCollection,
14458
15815
  propertiesOrder: localPropertiesOrder
14459
15816
  };
14460
15817
  return resolvedCollection;
14461
- }, [resolvedCollection, localPropertiesOrder]), true);
15818
+ }, [resolvedCollection, localPropertiesOrder]);
15819
+ const displayedColumnIds = useColumnIds(collectionWithLocalOrder, true);
14462
15820
  const additionalFields = useMemo(() => {
14463
- const subcollectionColumns = getEntityChildViews(collection).map((v) => v.collection).map((subcollection) => {
15821
+ const redundant = getRedundantChildViewColumnIds(collectionWithLocalOrder);
15822
+ const subcollectionColumns = getEntityChildViews(collection).map((v) => v.collection).filter((child) => !redundant.has(getSubcollectionColumnId(child))).map((subcollection) => {
14464
15823
  return {
14465
15824
  key: getSubcollectionColumnId(subcollection),
14466
15825
  name: subcollection.name,
@@ -14488,6 +15847,7 @@ var CollectionViewBindingInner = React.memo(function CollectionViewBindingInner(
14488
15847
  return [...collection.additionalFields ?? EMPTY_ARRAY, ...subcollectionColumns];
14489
15848
  }, [
14490
15849
  collection,
15850
+ collectionWithLocalOrder,
14491
15851
  path,
14492
15852
  sidePanelController
14493
15853
  ]);
@@ -14993,6 +16353,7 @@ function EntityIdHeaderWidget({ collection, path, idPath, openEntityMode }) {
14993
16353
  className: "flex p-2 w-full gap-2",
14994
16354
  children: [/* @__PURE__ */ jsx(TextField, {
14995
16355
  autoFocus: openPopup,
16356
+ "aria-label": t("find_entity_by_id"),
14996
16357
  placeholder: t("find_entity_by_id"),
14997
16358
  size: "small",
14998
16359
  onChange: (e) => {
@@ -15036,6 +16397,137 @@ function EntityIdHeaderWidget({ collection, path, idPath, openEntityMode }) {
15036
16397
  });
15037
16398
  }
15038
16399
  //#endregion
16400
+ //#region src/form/form_utils.ts
16401
+ function extractTouchedValues(values, touched) {
16402
+ let acc = {};
16403
+ if (!touched || typeof touched !== "object") return acc;
16404
+ Object.entries(touched).forEach(([key, value]) => {
16405
+ if (value) acc = setIn(acc, key, getIn(values, key));
16406
+ });
16407
+ return acc;
16408
+ }
16409
+ /**
16410
+ * Recursively removes empty plain objects `{}` and empty arrays `[]` from a value tree.
16411
+ * This prevents ghost containers created by `setIn` intermediate path construction
16412
+ * (e.g. `{ address: {} }` when only `address.city` was touched but value is undefined)
16413
+ * from falsely triggering the unsaved local changes indicator.
16414
+ */
16415
+ /**
16416
+ * Check if a value is semantically empty (null, undefined, or empty string).
16417
+ */
16418
+ function isSemanticEmpty(v) {
16419
+ return v === null || v === void 0 || v === "";
16420
+ }
16421
+ function removeEmptyContainers(obj) {
16422
+ if (Array.isArray(obj)) return obj.map(removeEmptyContainers);
16423
+ if (obj && typeof obj === "object" && Object.getPrototypeOf(obj) === Object.prototype) {
16424
+ const result = {};
16425
+ for (const key of Object.keys(obj)) {
16426
+ const cleaned = removeEmptyContainers(obj[key]);
16427
+ if (cleaned && typeof cleaned === "object" && !Array.isArray(cleaned) && Object.getPrototypeOf(cleaned) === Object.prototype && Object.keys(cleaned).length === 0) continue;
16428
+ result[key] = cleaned;
16429
+ }
16430
+ if (Object.keys(result).length > 0 && Object.values(result).every(isSemanticEmpty)) return {};
16431
+ return result;
16432
+ }
16433
+ return obj;
16434
+ }
16435
+ function getChanges(source, comparison) {
16436
+ const changes = {};
16437
+ if (!source) return {};
16438
+ if (!comparison) return source;
16439
+ const allKeys = Array.from(/* @__PURE__ */ new Set([...Object.keys(source), ...Object.keys(comparison)]));
16440
+ for (const key of allKeys) {
16441
+ const sourceValue = source[key];
16442
+ const comparisonValue = comparison[key];
16443
+ if (deepEqual(sourceValue, comparisonValue)) continue;
16444
+ const sourceHasKey = source && typeof source === "object" && Object.prototype.hasOwnProperty.call(source, key);
16445
+ if (comparison && typeof comparison === "object" && Object.prototype.hasOwnProperty.call(comparison, key) && !sourceHasKey) changes[key] = void 0;
16446
+ else if (Array.isArray(sourceValue)) {
16447
+ const comparisonArray = Array.isArray(comparisonValue) ? comparisonValue : [];
16448
+ if (sourceValue.length !== comparisonArray.length) {
16449
+ changes[key] = sourceValue;
16450
+ continue;
16451
+ }
16452
+ if (sourceValue.some((item, index) => !deepEqual(item, comparisonArray[index]))) changes[key] = sourceValue;
16453
+ } else if (isObject(sourceValue) && sourceValue && isObject(comparisonValue) && comparisonValue) {
16454
+ const nestedChanges = getChanges(sourceValue, comparisonValue);
16455
+ if (Object.keys(nestedChanges).length > 0) changes[key] = nestedChanges;
16456
+ } else changes[key] = sourceValue;
16457
+ }
16458
+ return changes;
16459
+ }
16460
+ /**
16461
+ * What the local-changes backup still has to offer, given what the form is
16462
+ * already showing — the answer the "unsaved local changes" banner is asking for.
16463
+ * `undefined` means there is nothing left to apply, and no banner.
16464
+ *
16465
+ * Measured against what the form *opens showing*, not against the stored
16466
+ * record. The banner exists to offer a draft the form is not already displaying,
16467
+ * and an edit carried across a change of layout is in both the backup and the
16468
+ * form: measuring against the stored values alone offered to restore the values
16469
+ * already on screen, over an edit the user had never left.
16470
+ */
16471
+ function getUnappliedLocalChanges(backupValues, openingValues) {
16472
+ const cleaned = removeEmptyContainers(getChanges(backupValues, openingValues));
16473
+ if (!cleaned || typeof cleaned !== "object" || Object.keys(cleaned).length === 0) return;
16474
+ return cleaned;
16475
+ }
16476
+ /**
16477
+ * What travels when the same record changes layout — the split's "hide list",
16478
+ * full screen's "show list", the side panel's "open full screen". One mounted
16479
+ * form is replaced by another showing the same record, so the edit in progress
16480
+ * has to be handed over; left to the local-changes backup alone, the new form
16481
+ * met it as a draft from a closed tab and raised the "unsaved local changes"
16482
+ * banner over changes the user had made a second earlier and never left.
16483
+ *
16484
+ * Returns `undefined` when there is nothing to carry, which is its own answer: a
16485
+ * record nobody edited must open in the next layout exactly as clean as it was.
16486
+ *
16487
+ * Only what was edited here travels, keyed by `touched` — the same subset the
16488
+ * backup stores. Carrying the whole record instead marks every field touched in
16489
+ * the receiving form, which lights up every empty required field as an error and
16490
+ * writes the entire record back out as a "local change".
16491
+ */
16492
+ function getEditHandoffValues({ status, dirty, values, touched, storedValues }) {
16493
+ if (!values) return void 0;
16494
+ if (status === "new" || status === "copy") return Object.keys(values).length > 0 ? values : void 0;
16495
+ if (!dirty) return void 0;
16496
+ const touchedValues = removeEmptyContainers(extractTouchedValues(values, touched ?? {}));
16497
+ if (touchedValues && Object.keys(touchedValues).length > 0) return touchedValues;
16498
+ const changes = getChanges(values, storedValues ?? {});
16499
+ return Object.keys(changes).length > 0 ? changes : void 0;
16500
+ }
16501
+ function getInitialEntityValues(authController, collection, path, status, entity, propertyConfigs) {
16502
+ const properties = collection.properties;
16503
+ if ((status === "existing" || status === "copy") && entity) {
16504
+ let values;
16505
+ if (!collection.alwaysApplyDefaultValues) values = entity.values ?? getDefaultValuesFor(properties);
16506
+ else values = mergeDeep(getDefaultValuesFor(properties), entity.values ?? {});
16507
+ if (status === "copy") {
16508
+ const result = { ...values };
16509
+ for (const [key, property] of Object.entries(properties)) if (property && "isId" in property && property.isId) delete result[key];
16510
+ return result;
16511
+ }
16512
+ return values;
16513
+ } else if (status === "new") return getDefaultValuesFor(properties);
16514
+ else {
16515
+ console.error({
16516
+ status,
16517
+ entity
16518
+ });
16519
+ throw new Error("Form has not been initialised with the correct parameters");
16520
+ }
16521
+ }
16522
+ function zodToFormErrors(zodError) {
16523
+ let errors = {};
16524
+ for (const issue of zodError.issues) {
16525
+ const path = issue.path.join(".");
16526
+ if (path && !getIn(errors, path)) errors = setIn(errors, path, issue.message);
16527
+ }
16528
+ return errors;
16529
+ }
16530
+ //#endregion
15039
16531
  //#region src/components/EditFormActions.tsx
15040
16532
  /**
15041
16533
  * The entity form's footer.
@@ -15278,7 +16770,17 @@ function EditViewBinding({ entityId, ...props }) {
15278
16770
  databaseId: props.databaseId,
15279
16771
  useCache: false
15280
16772
  });
15281
- const initialDirtyValues = entityId ? getEntityFromMemoryCache(props.path + "/" + entityId) : props.defaultValues ?? getEntityFromMemoryCache(props.path + "#new");
16773
+ const carriesAsNew = props.copy || entityId === void 0;
16774
+ const handoffKey = carriesAsNew ? props.path + "#new" : props.path + "/" + entityId;
16775
+ const handoff = useRef(null);
16776
+ if (handoff.current?.key !== handoffKey) handoff.current = {
16777
+ key: handoffKey,
16778
+ values: getEntityFromMemoryCache(handoffKey)
16779
+ };
16780
+ useEffect(() => {
16781
+ removeEntityFromMemoryCache(handoffKey);
16782
+ }, [handoffKey]);
16783
+ const initialDirtyValues = carriesAsNew ? props.defaultValues ?? handoff.current.values : handoff.current.values;
15282
16784
  const { canEdit: canEditHook } = usePermissions();
15283
16785
  const [status, setStatus] = useState(props.copy ? "copy" : entityId ? "existing" : "new");
15284
16786
  const canEdit = useMemo(() => {
@@ -15314,7 +16816,7 @@ function EditViewBinding({ entityId, ...props }) {
15314
16816
  children: content
15315
16817
  });
15316
16818
  }
15317
- function EditViewBindingInner({ path, entityId, selectedTab: selectedTabProp, collection, parentCollectionSlugs, parentEntityIds, onValuesModified, onSaved, onTabChange, navigateBack, entity, initialDirtyValues, dataLoading, layout = "side_panel", barActions, barActionsStart, onShowList, status, setStatus, formProps, canEdit }) {
16819
+ function EditViewBindingInner({ path, entityId, selectedTab: selectedTabProp, collection, parentCollectionSlugs, parentEntityIds, onValuesModified, onSaved, onTabChange, navigateBack, onCloseRequest, entity, initialDirtyValues, dataLoading, layout = "side_panel", barActions, barActionsStart, onShowList, status, setStatus, formProps, canEdit }) {
15318
16820
  const ResolvedFormActions = useComponentOverride("EditView.FormActions", EditFormActions);
15319
16821
  const ResolvedEntityForm = useComponentOverride("Entity.Form", EntityFormBinding);
15320
16822
  const ResolvedCollectionView = useComponentOverride("Collection.View", CollectionViewBinding);
@@ -15371,7 +16873,7 @@ function EditViewBindingInner({ path, entityId, selectedTab: selectedTabProp, co
15371
16873
  const target = val === "edit" ? MAIN_TAB_VALUE : val;
15372
16874
  if (target !== selectedTab) setSelectedTab(target);
15373
16875
  }, [selectedTabProp, defaultSelectedView]);
15374
- const childViews = getAdminEntityChildViews(collection).filter((v) => !v.collection.hideFromNavigation);
16876
+ const childViews = getAdminEntityChildViews(collection).filter((v) => !v.collection.hideFromEntityViews);
15375
16877
  const subcollections = childViews.map((v) => v.collection);
15376
16878
  const subcollectionsCount = subcollections?.length ?? 0;
15377
16879
  const customViews = collection.entityViews ?? [];
@@ -15466,10 +16968,55 @@ function EditViewBindingInner({ path, entityId, selectedTab: selectedTabProp, co
15466
16968
  const formex = formContext?.formex;
15467
16969
  const hasFormErrors = Boolean(formex && Object.keys(formex.errors).length > 0 && formex.submitCount > 0);
15468
16970
  const saveDisabled = Boolean(!formContext || formContext.disabled || formex?.isSubmitting || status === "existing" && !formex?.dirty);
16971
+ /**
16972
+ * Hand this record's edit in progress to the layout about to replace this
16973
+ * one — the split's "hide list", full screen's "show list", the side panel's
16974
+ * "open full screen". None of them is a navigation *away* from the record:
16975
+ * the same record is about to be shown by a different form, and what the
16976
+ * user has typed belongs to the record, not to the pane it was typed in.
16977
+ *
16978
+ * Every control that changes layout has to call this, and none of them is
16979
+ * blocked by the unsaved-changes prompt — the prompt asks about leaving, and
16980
+ * these do not leave. What used to happen instead is that the edit reached
16981
+ * the next layout only as its local-changes backup, which is the channel for
16982
+ * a draft left behind by a closed tab: the record reopened *clean*, with a
16983
+ * banner offering to apply changes the user had never walked away from.
16984
+ */
16985
+ const carryEdit = useCallback(() => {
16986
+ const carried = formex ? getEditHandoffValues({
16987
+ status,
16988
+ dirty: formex.dirty,
16989
+ values: formex.values,
16990
+ touched: formex.touched,
16991
+ storedValues: usedEntity?.values
16992
+ }) : void 0;
16993
+ if (!carried) return;
16994
+ saveEntityToMemoryCache(status === "new" || status === "copy" ? path + "#new" : path + "/" + entityId, carried);
16995
+ }, [
16996
+ formex,
16997
+ status,
16998
+ path,
16999
+ entityId,
17000
+ usedEntity
17001
+ ]);
15469
17002
  const [inspectorTab, setInspectorTab] = useState(null);
15470
17003
  const [savedCount, setSavedCount] = useState(0);
15471
17004
  const sideDialogContext = useSideDialogContext();
15472
- const canCloseAfterSave = layout === "side_panel" || layout === "dialog";
17005
+ /**
17006
+ * Whether the save in flight was asked to dismiss the record afterwards.
17007
+ *
17008
+ * A ref, and for the reason {@link SideDialogs} keeps its own as one: the
17009
+ * flag is raised and the form submitted in a single click handler, and the
17010
+ * `onSaved` that has to act on it was captured before React could re-render.
17011
+ * As state it read one save late — the first "save and close" saved and
17012
+ * stayed open, and the *next* save closed the record.
17013
+ *
17014
+ * The side panel and the dialog use the side-dialog controller's flag,
17015
+ * which their own close path consumes; this one covers the layouts that
17016
+ * close by calling {@link onCloseRequest} instead.
17017
+ */
17018
+ const pendingCloseRef = useRef(false);
17019
+ const canCloseAfterSave = layout === "side_panel" || layout === "dialog" || layout === "split" && Boolean(onCloseRequest);
15473
17020
  const recordActions = useRecordActions({
15474
17021
  collection,
15475
17022
  path,
@@ -15540,9 +17087,9 @@ function EditViewBindingInner({ path, entityId, selectedTab: selectedTabProp, co
15540
17087
  const FormViewBuilder = formViewConfig?.Builder ? resolveComponentRef(formViewConfig.Builder) : null;
15541
17088
  formViewConfig?.includeActions;
15542
17089
  const entityReadOnlyView = !canEdit && entity ? FormViewBuilder && readOnlyFormContext ? /* @__PURE__ */ jsx("div", {
15543
- className: cls("flex-1 flex flex-row w-full overflow-y-auto justify-center", canEdit || !mainViewVisible || selectedSecondaryForm ? "hidden" : ""),
17090
+ className: cls("@container/form flex-1 flex flex-row w-full overflow-y-auto justify-center", canEdit || !mainViewVisible || selectedSecondaryForm ? "hidden" : ""),
15544
17091
  children: /* @__PURE__ */ jsx("div", {
15545
- className: "w-full max-w-3xl 2xl:max-w-4xl flex flex-col pt-6 pb-16 px-5 sm:px-8",
17092
+ className: "w-full max-w-3xl @7xl/form:max-w-4xl 2xl:max-w-4xl flex flex-col pt-6 pb-16 px-5 sm:px-8",
15546
17093
  children: /* @__PURE__ */ jsx(ErrorBoundary, { children: /* @__PURE__ */ jsx(Suspense, {
15547
17094
  fallback: /* @__PURE__ */ jsx(CircularProgressCenter, {}),
15548
17095
  children: /* @__PURE__ */ jsx(FormViewBuilder, {
@@ -15612,13 +17159,17 @@ function EditViewBindingInner({ path, entityId, selectedTab: selectedTabProp, co
15612
17159
  };
15613
17160
  onSaved?.(res);
15614
17161
  formProps?.onSaved?.(res);
17162
+ if (pendingCloseRef.current) {
17163
+ pendingCloseRef.current = false;
17164
+ setTimeout(() => onCloseRequest?.(), 0);
17165
+ }
15615
17166
  },
15616
17167
  Builder: resolveComponentRef(selectedSecondaryForm?.Builder)
15617
17168
  });
15618
17169
  const subcollectionTabs = subcollections && subcollections.map((subcollection) => {
15619
17170
  const icon = getIcon(subcollection.icon, void 0, void 0, "smallest");
15620
17171
  return /* @__PURE__ */ jsx(Tab, {
15621
- className: "text-sm min-w-[90px]",
17172
+ className: cls("h-full data-[state=active]:bg-white dark:data-[state=active]:bg-surface-800", "text-sm min-w-[90px]"),
15622
17173
  value: subcollection.slug,
15623
17174
  children: /* @__PURE__ */ jsxs("span", {
15624
17175
  className: "flex items-center gap-1.5",
@@ -15629,7 +17180,7 @@ function EditViewBindingInner({ path, entityId, selectedTab: selectedTabProp, co
15629
17180
  const customViewTabsStart = resolvedEntityViews.filter((view) => view.position === "start").map((view) => {
15630
17181
  const icon = getIcon(view.icon, void 0, void 0, "smallest");
15631
17182
  return /* @__PURE__ */ jsx(Tab, {
15632
- className: !view.tabComponent ? "text-sm min-w-[90px]" : void 0,
17183
+ className: cls(ENTITY_VIEW_TAB_CLASS, !view.tabComponent && "text-sm min-w-[90px]"),
15633
17184
  value: view.key,
15634
17185
  children: view.tabComponent ?? /* @__PURE__ */ jsxs("span", {
15635
17186
  className: "flex items-center gap-1.5",
@@ -15640,7 +17191,7 @@ function EditViewBindingInner({ path, entityId, selectedTab: selectedTabProp, co
15640
17191
  const customViewTabsEnd = resolvedEntityViews.filter((view) => !view.position || view.position === "end").map((view) => {
15641
17192
  const icon = getIcon(view.icon, void 0, void 0, "smallest");
15642
17193
  return /* @__PURE__ */ jsx(Tab, {
15643
- className: !view.tabComponent ? "text-sm min-w-[90px]" : void 0,
17194
+ className: cls(ENTITY_VIEW_TAB_CLASS, !view.tabComponent && "text-sm min-w-[90px]"),
15644
17195
  value: view.key,
15645
17196
  children: view.tabComponent ?? /* @__PURE__ */ jsxs("span", {
15646
17197
  className: "flex items-center gap-1.5",
@@ -15653,6 +17204,7 @@ function EditViewBindingInner({ path, entityId, selectedTab: selectedTabProp, co
15653
17204
  children: /* @__PURE__ */ jsx(IconButton, {
15654
17205
  size: "small",
15655
17206
  onClick: () => {
17207
+ carryEdit();
15656
17208
  const editSuffix = collection.defaultEntityAction === "view" ? "/edit" : "";
15657
17209
  const entityUrl = urlController.buildUrlCollectionPath(`${path}/${entityId}${editSuffix}`);
15658
17210
  navigate(`${entityUrl}#full`);
@@ -15665,7 +17217,7 @@ function EditViewBindingInner({ path, entityId, selectedTab: selectedTabProp, co
15665
17217
  children: [
15666
17218
  /* @__PURE__ */ jsx(EntityIdentityBar, {
15667
17219
  collection,
15668
- collectionUrl: layout === "full_screen" || layout === "split" ? withViewMode(urlController.buildUrlCollectionPath(path)) : void 0,
17220
+ collectionUrl: layout === "full_screen" || layout === "split" ? withListState(urlController.buildUrlCollectionPath(path)) : void 0,
15669
17221
  title: displayTitle,
15670
17222
  entityId: status === "existing" ? entityId : void 0,
15671
17223
  status,
@@ -15673,15 +17225,25 @@ function EditViewBindingInner({ path, entityId, selectedTab: selectedTabProp, co
15673
17225
  saving: Boolean(formContext?.isSaving),
15674
17226
  hasErrors: hasFormErrors,
15675
17227
  saveDisabled: !canEdit || saveDisabled,
15676
- onBack: layout === "full_screen" && !onShowList ? () => navigate(withViewMode(urlController.buildUrlCollectionPath(path))) : void 0,
17228
+ onBack: layout === "full_screen" && !onShowList ? () => navigate(withListState(urlController.buildUrlCollectionPath(path))) : void 0,
15677
17229
  onSave: canEdit && formContext ? () => {
15678
17230
  sideDialogContext.setPendingClose?.(false);
17231
+ pendingCloseRef.current = false;
15679
17232
  formContext.submit();
15680
17233
  } : void 0,
15681
17234
  onSaveAndClose: canEdit && formContext && canCloseAfterSave ? () => {
17235
+ if (layout === "split") {
17236
+ pendingCloseRef.current = true;
17237
+ Promise.resolve(formContext.submit()).finally(() => {
17238
+ pendingCloseRef.current = false;
17239
+ });
17240
+ return;
17241
+ }
15682
17242
  sideDialogContext.setPendingClose?.(true);
15683
17243
  Promise.resolve(formContext.submit()).finally(() => sideDialogContext.setPendingClose?.(false));
15684
17244
  } : void 0,
17245
+ saveAndClosePlacement: layout === "split" ? "menu" : "button",
17246
+ onClose: onCloseRequest,
15685
17247
  onDiscard: canEdit && formContext ? () => formContext.formex.resetForm() : void 0,
15686
17248
  onInspect: includeJsonView ? () => setInspectorTab("json") : void 0,
15687
17249
  onViewHistory: includeHistoryView ? () => setInspectorTab("history") : void 0,
@@ -15690,8 +17252,11 @@ function EditViewBindingInner({ path, entityId, selectedTab: selectedTabProp, co
15690
17252
  pluginActions: formPluginActions,
15691
17253
  leading: /* @__PURE__ */ jsxs(Fragment, { children: [
15692
17254
  barActionsStart,
15693
- layout === "split" && /* @__PURE__ */ jsx(SplitListCloseButton, {}),
15694
- layout === "full_screen" && onShowList && /* @__PURE__ */ jsx(SplitListShowButton, { onClick: onShowList })
17255
+ layout === "split" && /* @__PURE__ */ jsx(SplitListCloseButton, { onBeforeHide: carryEdit }),
17256
+ layout === "full_screen" && onShowList && /* @__PURE__ */ jsx(SplitListShowButton, { onClick: () => {
17257
+ carryEdit();
17258
+ onShowList();
17259
+ } })
15695
17260
  ] }),
15696
17261
  trailing: /* @__PURE__ */ jsxs(Fragment, { children: [
15697
17262
  pluginActionsTop,
@@ -15701,14 +17266,15 @@ function EditViewBindingInner({ path, entityId, selectedTab: selectedTabProp, co
15701
17266
  entityId,
15702
17267
  values: formContext?.values ?? usedEntity?.values ?? {},
15703
17268
  status,
15704
- dirty: Boolean(formContext?.formex?.dirty)
17269
+ dirty: Boolean(formContext?.formex?.dirty),
17270
+ carryEdit
15705
17271
  })
15706
17272
  ] })
15707
17273
  }),
15708
17274
  hasAdditionalViews && /* @__PURE__ */ jsx("div", {
15709
- className: cls("h-10 shrink-0 flex items-stretch border-b px-2 min-w-0", defaultBorderMixin),
17275
+ className: cls("h-10 shrink-0 flex items-stretch border-b px-2 min-w-0", "bg-surface-50 dark:bg-surface-900", defaultBorderMixin),
15710
17276
  children: /* @__PURE__ */ jsxs(Tabs, {
15711
- className: "!w-full",
17277
+ className: "!w-full items-stretch",
15712
17278
  innerClassName: "h-full",
15713
17279
  variant: "boxy",
15714
17280
  value: activeTab,
@@ -15718,6 +17284,7 @@ function EditViewBindingInner({ path, entityId, selectedTab: selectedTabProp, co
15718
17284
  children: [
15719
17285
  /* @__PURE__ */ jsx(Tab, {
15720
17286
  value: "__main_##Q$SC^#S6",
17287
+ className: cls("h-full data-[state=active]:bg-white dark:data-[state=active]:bg-surface-800", "text-sm min-w-[90px]"),
15721
17288
  children: /* @__PURE__ */ jsxs("span", {
15722
17289
  className: "flex items-center gap-1.5",
15723
17290
  children: [getIcon(collection.icon, void 0, void 0, "smallest"), collection.singularName ?? collection.name]
@@ -15880,10 +17447,28 @@ function SidePanelBinding(props) {
15880
17447
  setBlocked(false);
15881
17448
  close(true);
15882
17449
  };
17450
+ /**
17451
+ * The panel's response to a save, and the one place it decides where the
17452
+ * panel goes next.
17453
+ *
17454
+ * Exactly **one** panel navigation is raised here. These branches used to
17455
+ * run independently — a `replace` onto the saved record's address *and* a
17456
+ * close — and because the last navigation is the one that survives, which
17457
+ * of them the user got was settled by statement order rather than by
17458
+ * intent. Closing wins by construction now: moving a panel to a new address
17459
+ * it is about to leave is work whose only effect is to fight the close.
17460
+ *
17461
+ * `props.onUpdate` runs last for the same reason. It belongs to whoever
17462
+ * opened the panel and it is free to navigate — the reference picker's
17463
+ * selects the record and closes the picker — so the opener's intent should
17464
+ * be the final word, not something this panel overwrites on its way out.
17465
+ */
15883
17466
  const onUpdate = (params) => {
15884
- if (props.onUpdate) props.onUpdate(params);
15885
17467
  setEditInDialog(false);
15886
- if (params.status !== "existing") sidePanelController.replace({
17468
+ if (sideDialogsController.pendingClose || props.closeOnSave) {
17469
+ sideDialogsController.setPendingClose(false);
17470
+ closeAfterSave();
17471
+ } else if (params.status !== "existing") sidePanelController.replace({
15887
17472
  path: params.path,
15888
17473
  entityId: params.entityId,
15889
17474
  selectedTab: params.selectedTab,
@@ -15891,10 +17476,7 @@ function SidePanelBinding(props) {
15891
17476
  collection: params.collection
15892
17477
  });
15893
17478
  else closeEditView();
15894
- if (sideDialogsController.pendingClose) {
15895
- sideDialogsController.setPendingClose(false);
15896
- closeAfterSave();
15897
- }
17479
+ props.onUpdate?.(params);
15898
17480
  };
15899
17481
  const parentCollectionSlugs = useMemo(() => {
15900
17482
  return collectionRegistryController.getParentCollectionSlugs(path);
@@ -16060,11 +17642,11 @@ function SidePanelBinding(props) {
16060
17642
  onSaved: onUpdate,
16061
17643
  navigateBack: closeEditView,
16062
17644
  barActionsStart,
16063
- barActions: ({ status, values, dirty }) => windowControls(allowFullScreen && /* @__PURE__ */ jsx(IconButton, {
17645
+ barActions: ({ carryEdit }) => windowControls(allowFullScreen && /* @__PURE__ */ jsx(IconButton, {
16064
17646
  className: "self-center",
16065
17647
  size: "small",
16066
17648
  onClick: () => {
16067
- if (dirty || status === "new" || status === "copy") saveEntityToMemoryCache(status === "new" || status === "copy" ? path + "#new" : path + "/" + entityId, values);
17649
+ carryEdit?.();
16068
17650
  setBlocked(false);
16069
17651
  setBlockedNavigationMessage(void 0);
16070
17652
  if (entityId) {
@@ -17695,14 +19277,15 @@ function SelectionTableBindingInternal({ onSingleEntitySelected, onMultipleEntit
17695
19277
  function EntitySelectionDialogActions({ collection, path, onClear, onNewClick }) {
17696
19278
  const { canCreate } = usePermissions();
17697
19279
  const largeLayout = useLargeLayout();
19280
+ const { t } = useTranslation();
17698
19281
  const onClick = onNewClick ? (e) => {
17699
19282
  e.preventDefault();
17700
19283
  onNewClick();
17701
19284
  } : void 0;
17702
- const addButton = canCreate(collection, path) && onClick && (largeLayout ? /* @__PURE__ */ jsxs(Button, {
19285
+ const addButton = canCreate(collection, path) && onClick && (largeLayout ? /* @__PURE__ */ jsx(Button, {
17703
19286
  onClick,
17704
19287
  startIcon: /* @__PURE__ */ jsx(PlusIcon, {}),
17705
- children: ["Add ", collection.singularName ?? collection.name]
19288
+ children: t("add_specific", { name: collection.singularName ?? collection.name })
17706
19289
  }) : /* @__PURE__ */ jsx(Button, {
17707
19290
  onClick,
17708
19291
  children: /* @__PURE__ */ jsx(PlusIcon, {})
@@ -17924,8 +19507,8 @@ function BlockFieldBinding({ propertyKey, value, error, showError, isSubmitting,
17924
19507
  addLabel: property.name ? t("add_to_field", { fieldName: property.name }) : t("add_entry"),
17925
19508
  buildEntry,
17926
19509
  onInternalIdAdded: setLastAddedId,
17927
- disabled: isSubmitting || Boolean(property.admin?.disabled),
17928
- canAddElements: !property.admin?.disabled,
19510
+ disabled: isSubmitting || isDisabled(property),
19511
+ canAddElements: !isDisabled(property),
17929
19512
  onValueChange: (value) => setFieldValue(propertyKey, value),
17930
19513
  newDefaultEntry: {
17931
19514
  [property.oneOf.typeField ?? DEFAULT_ONE_OF_TYPE]: firstOneOfKey,
@@ -18061,7 +19644,8 @@ function DateTimeFieldBinding({ propertyKey, value, setValue, autoFocus, error,
18061
19644
  required: property.validation?.required,
18062
19645
  className: showError ? "text-red-500 dark:text-red-500" : "text-text-secondary dark:text-text-secondary-dark",
18063
19646
  title: property.name ?? propertyKey
18064
- })
19647
+ }),
19648
+ "aria-label": hideLabel ? property.name ?? propertyKey : void 0
18065
19649
  })
18066
19650
  }), /* @__PURE__ */ jsx(FieldHelperText, {
18067
19651
  includeDescription,
@@ -18218,6 +19802,7 @@ function MapKeyValueRow({ id, fieldKey, value, onFieldKeyChange, onDeleteClick,
18218
19802
  const { t } = useTranslation();
18219
19803
  function buildInput(entryValue, fieldKey, type) {
18220
19804
  if (type === "string" || type === "number") return /* @__PURE__ */ jsx(TextField, {
19805
+ "aria-label": fieldKey ? `${t("value")}: ${fieldKey}` : t("value"),
18221
19806
  placeholder: "value",
18222
19807
  value: entryValue,
18223
19808
  type: type === "number" ? "number" : "text",
@@ -18333,6 +19918,7 @@ function MapKeyValueRow({ id, fieldKey, value, onFieldKeyChange, onDeleteClick,
18333
19918
  className: "w-[300px] max-w-[30%]",
18334
19919
  children: /* @__PURE__ */ jsx(TextField, {
18335
19920
  value: fieldKey,
19921
+ "aria-label": t("key"),
18336
19922
  placeholder: "key",
18337
19923
  disabled: disabled || entryValue !== void 0 && entryValue !== null && entryValue !== "",
18338
19924
  size: "medium",
@@ -18403,6 +19989,7 @@ function ArrayKeyValueRow({ id, index, value, setValue }) {
18403
19989
  function buildInput(entryValue, type) {
18404
19990
  if (type === "string" || type === "number") return /* @__PURE__ */ jsx(TextField, {
18405
19991
  value: entryValue,
19992
+ "aria-label": `Value ${index + 1}`,
18406
19993
  type: type === "number" ? "number" : "text",
18407
19994
  size: "medium",
18408
19995
  onChange: (event) => {
@@ -18523,7 +20110,7 @@ function MapFieldBinding({ propertyKey, value, showError, error, disabled, prope
18523
20110
  const mapFormView = /* @__PURE__ */ jsx(Fragment, { children: /* @__PURE__ */ jsx("div", {
18524
20111
  className: "py-1 flex flex-col space-y-2",
18525
20112
  children: Object.entries(mapProperties).filter(([_, property]) => !isHidden(property)).map(([entryKey, childProperty], index) => {
18526
- const thisDisabled = isReadOnly(childProperty) || Boolean(childProperty.admin?.disabled);
20113
+ const thisDisabled = isReadOnly(childProperty) || isDisabled(childProperty);
18527
20114
  return /* @__PURE__ */ jsx("div", {
18528
20115
  className: "relative",
18529
20116
  children: /* @__PURE__ */ jsx(ErrorBoundary, { children: /* @__PURE__ */ jsx(PropertyFieldBinding, {
@@ -18960,7 +20547,7 @@ function RepeatFieldBinding({ propertyKey, value, error, showError, isSubmitting
18960
20547
  index
18961
20548
  }) });
18962
20549
  };
18963
- const canAddElements = !property.admin?.disabled && !isSubmitting && !disabled && (property.admin?.canAddElements || property.admin?.canAddElements === void 0);
20550
+ const canAddElements = !isDisabled(property) && !isSubmitting && !disabled && (property.admin?.canAddElements || property.admin?.canAddElements === void 0);
18964
20551
  const sortable = property.admin?.sortable === void 0 ? true : property.admin.sortable;
18965
20552
  const arrayContainer = /* @__PURE__ */ jsx(ArrayContainer, {
18966
20553
  droppableId: propertyKey,
@@ -18968,7 +20555,7 @@ function RepeatFieldBinding({ propertyKey, value, error, showError, isSubmitting
18968
20555
  value: value ?? [],
18969
20556
  buildEntry,
18970
20557
  onInternalIdAdded: setLastAddedId,
18971
- disabled: isSubmitting || Boolean(property.admin?.disabled),
20558
+ disabled: isSubmitting || isDisabled(property),
18972
20559
  canAddElements,
18973
20560
  sortable,
18974
20561
  newDefaultEntry: getDefaultValueFor(property.of),
@@ -19125,7 +20712,7 @@ var rejectDropClasses = "transition-colors duration-200 ease-[cubic-bezier(0,0,0
19125
20712
  function StorageUploadFieldBinding({ propertyKey, value, setValue, error, showError, autoFocus, minimalistView, property, includeDescription, hideLabel, context, isSubmitting }) {
19126
20713
  useAuthController();
19127
20714
  const storageSource = useStorageSource();
19128
- const disabled = isReadOnly(property) || !!property.admin?.disabled || isSubmitting || context.disabled;
20715
+ const disabled = isReadOnly(property) || isDisabled(property) || isSubmitting || context.disabled;
19129
20716
  const { internalValue, setInternalValue, onFilesAdded, storage, onFileUploadComplete, storagePathBuilder, multipleFilesSupported, resolvedStorageSource } = useStorageUploadController({
19130
20717
  entityValues: context.values,
19131
20718
  entityId: context.entityId,
@@ -19452,6 +21039,7 @@ function TextFieldBinding({ propertyKey, value, setValue, error, showError, disa
19452
21039
  required: property.validation?.required || property.isId === true,
19453
21040
  title: property.name ?? propertyKey
19454
21041
  });
21042
+ const accessibleName = property.name ?? propertyKey;
19455
21043
  return /* @__PURE__ */ jsxs(Fragment, { children: [
19456
21044
  /* @__PURE__ */ jsx(PropertyIdCopyTooltip, {
19457
21045
  propertyKey,
@@ -19467,6 +21055,7 @@ function TextFieldBinding({ propertyKey, value, setValue, error, showError, disa
19467
21055
  onChange,
19468
21056
  autoFocus,
19469
21057
  disabled,
21058
+ "aria-label": accessibleName,
19470
21059
  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" : "")
19471
21060
  }),
19472
21061
  property.admin?.clearable && /* @__PURE__ */ jsx("div", {
@@ -19483,6 +21072,7 @@ function TextFieldBinding({ propertyKey, value, setValue, error, showError, disa
19483
21072
  onChange,
19484
21073
  autoFocus,
19485
21074
  label,
21075
+ "aria-label": label ? void 0 : accessibleName,
19486
21076
  type: inputType,
19487
21077
  disabled,
19488
21078
  endAdornment: property.admin?.clearable && /* @__PURE__ */ jsx(IconButton, {
@@ -19618,6 +21208,7 @@ function VectorFieldBinding({ propertyKey, value, setValue, error, showError, di
19618
21208
  className: cls("flex flex-col gap-2 p-4 rounded-xl border bg-surface-50/20 dark:bg-surface-800/10", defaultBorderMixin),
19619
21209
  children: [/* @__PURE__ */ jsx(TextField, {
19620
21210
  size,
21211
+ "aria-label": `${property.name ?? propertyKey} vector values`,
19621
21212
  value: textValue,
19622
21213
  onChange,
19623
21214
  autoFocus: true,
@@ -19901,6 +21492,7 @@ function BinaryFieldBinding({ propertyKey, value, setValue, error, showError, di
19901
21492
  className: cls("flex flex-col gap-2 p-4 rounded-xl border", defaultBorderMixin),
19902
21493
  children: [/* @__PURE__ */ jsx(TextField, {
19903
21494
  size,
21495
+ "aria-label": `${property.name ?? propertyKey} base64 value`,
19904
21496
  value: text,
19905
21497
  onChange,
19906
21498
  autoFocus: autoFocus ?? true,
@@ -20698,7 +22290,7 @@ function PropertyFieldBindingInternal({ propertyKey, property, context, includeD
20698
22290
  authController
20699
22291
  });
20700
22292
  const readOnly = resolvedProperty ? isReadOnly(resolvedProperty) : true;
20701
- const disabled = disabledProp || readOnly || Boolean(resolvedProperty?.admin?.disabled) || context.disabled;
22293
+ const disabled = disabledProp || readOnly || (resolvedProperty ? isDisabled(resolvedProperty) : false) || context.disabled;
20702
22294
  if (resolvedProperty === null || isHidden(resolvedProperty)) return /* @__PURE__ */ jsx(Fragment, {});
20703
22295
  else if (readOnly) Component = ReadOnlyFieldBinding;
20704
22296
  else if (resolvedProperty.admin?.Field) {
@@ -21277,96 +22869,6 @@ function LocalChangesMenu({ cachedData, formex, onClearLocalChanges, cacheKey, p
21277
22869
  })] });
21278
22870
  }
21279
22871
  //#endregion
21280
- //#region src/form/form_utils.ts
21281
- function extractTouchedValues(values, touched) {
21282
- let acc = {};
21283
- if (!touched || typeof touched !== "object") return acc;
21284
- Object.entries(touched).forEach(([key, value]) => {
21285
- if (value) acc = setIn(acc, key, getIn(values, key));
21286
- });
21287
- return acc;
21288
- }
21289
- /**
21290
- * Recursively removes empty plain objects `{}` and empty arrays `[]` from a value tree.
21291
- * This prevents ghost containers created by `setIn` intermediate path construction
21292
- * (e.g. `{ address: {} }` when only `address.city` was touched but value is undefined)
21293
- * from falsely triggering the unsaved local changes indicator.
21294
- */
21295
- /**
21296
- * Check if a value is semantically empty (null, undefined, or empty string).
21297
- */
21298
- function isSemanticEmpty(v) {
21299
- return v === null || v === void 0 || v === "";
21300
- }
21301
- function removeEmptyContainers(obj) {
21302
- if (Array.isArray(obj)) return obj.map(removeEmptyContainers);
21303
- if (obj && typeof obj === "object" && Object.getPrototypeOf(obj) === Object.prototype) {
21304
- const result = {};
21305
- for (const key of Object.keys(obj)) {
21306
- const cleaned = removeEmptyContainers(obj[key]);
21307
- if (cleaned && typeof cleaned === "object" && !Array.isArray(cleaned) && Object.getPrototypeOf(cleaned) === Object.prototype && Object.keys(cleaned).length === 0) continue;
21308
- result[key] = cleaned;
21309
- }
21310
- if (Object.keys(result).length > 0 && Object.values(result).every(isSemanticEmpty)) return {};
21311
- return result;
21312
- }
21313
- return obj;
21314
- }
21315
- function getChanges(source, comparison) {
21316
- const changes = {};
21317
- if (!source) return {};
21318
- if (!comparison) return source;
21319
- const allKeys = Array.from(/* @__PURE__ */ new Set([...Object.keys(source), ...Object.keys(comparison)]));
21320
- for (const key of allKeys) {
21321
- const sourceValue = source[key];
21322
- const comparisonValue = comparison[key];
21323
- if (deepEqual(sourceValue, comparisonValue)) continue;
21324
- const sourceHasKey = source && typeof source === "object" && Object.prototype.hasOwnProperty.call(source, key);
21325
- if (comparison && typeof comparison === "object" && Object.prototype.hasOwnProperty.call(comparison, key) && !sourceHasKey) changes[key] = void 0;
21326
- else if (Array.isArray(sourceValue)) {
21327
- const comparisonArray = Array.isArray(comparisonValue) ? comparisonValue : [];
21328
- if (sourceValue.length !== comparisonArray.length) {
21329
- changes[key] = sourceValue;
21330
- continue;
21331
- }
21332
- if (sourceValue.some((item, index) => !deepEqual(item, comparisonArray[index]))) changes[key] = sourceValue;
21333
- } else if (isObject(sourceValue) && sourceValue && isObject(comparisonValue) && comparisonValue) {
21334
- const nestedChanges = getChanges(sourceValue, comparisonValue);
21335
- if (Object.keys(nestedChanges).length > 0) changes[key] = nestedChanges;
21336
- } else changes[key] = sourceValue;
21337
- }
21338
- return changes;
21339
- }
21340
- function getInitialEntityValues(authController, collection, path, status, entity, propertyConfigs) {
21341
- const properties = collection.properties;
21342
- if ((status === "existing" || status === "copy") && entity) {
21343
- let values;
21344
- if (!collection.alwaysApplyDefaultValues) values = entity.values ?? getDefaultValuesFor(properties);
21345
- else values = mergeDeep(getDefaultValuesFor(properties), entity.values ?? {});
21346
- if (status === "copy") {
21347
- const result = { ...values };
21348
- for (const [key, property] of Object.entries(properties)) if (property && "isId" in property && property.isId) delete result[key];
21349
- return result;
21350
- }
21351
- return values;
21352
- } else if (status === "new") return getDefaultValuesFor(properties);
21353
- else {
21354
- console.error({
21355
- status,
21356
- entity
21357
- });
21358
- throw new Error("Form has not been initialised with the correct parameters");
21359
- }
21360
- }
21361
- function zodToFormErrors(zodError) {
21362
- let errors = {};
21363
- for (const issue of zodError.issues) {
21364
- const path = issue.path.join(".");
21365
- if (path && !getIn(errors, path)) errors = setIn(errors, path, issue.message);
21366
- }
21367
- return errors;
21368
- }
21369
- //#endregion
21370
22872
  //#region src/form/EntityForm.tsx
21371
22873
  /**
21372
22874
  * Headless entity form component.
@@ -21623,7 +23125,7 @@ function EntityForm({ path, entityId: entityIdProp, collection, onValuesModified
21623
23125
  function isFieldDisabled(property) {
21624
23126
  const isNew = status === "new" || status === "copy";
21625
23127
  const isIdAndAuto = (property.type === "string" || property.type === "number") && "isId" in property && typeof property.isId === "string" && property.isId !== "manual";
21626
- return Boolean(disabledProp || !autoSave && formex.isSubmitting || isReadOnly(property) || Boolean(property.admin?.disabled) || !isNew && "isId" in property && Boolean(property.isId) || isNew && isIdAndAuto);
23128
+ return Boolean(disabledProp || !autoSave && formex.isSubmitting || isReadOnly(property) || isDisabled(property) || !isNew && "isId" in property && Boolean(property.isId) || isNew && isIdAndAuto);
21627
23129
  }
21628
23130
  const renderField = (field) => {
21629
23131
  if (field.additional) {
@@ -21798,7 +23300,7 @@ function EntityForm({ path, entityId: entityIdProp, collection, onValuesModified
21798
23300
  className: "flex-1 min-w-0 overflow-y-auto flex justify-center items-start",
21799
23301
  children: /* @__PURE__ */ jsxs("div", {
21800
23302
  id: `form_${path}`,
21801
- 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"),
23303
+ className: cls("@container/col w-full max-w-3xl @7xl/form:max-w-4xl 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"),
21802
23304
  children: [manualApplyLocalChanges && hasLocalChanges && localChangesCacheKey && /* @__PURE__ */ jsx("div", {
21803
23305
  className: "flex justify-end mb-3",
21804
23306
  children: /* @__PURE__ */ jsx(LocalChangesMenu, {
@@ -21886,9 +23388,7 @@ function EntityFormBinding({ path, entityId: entityIdProp, collection, entity, i
21886
23388
  const localChangesData = useMemo(() => {
21887
23389
  if (!localChangesDataRaw) return void 0;
21888
23390
  const openingValues = initialDirtyValues ? mergeDeep(computedInitialValues, initialDirtyValues) : computedInitialValues;
21889
- const cleaned = removeEmptyContainers(getChanges(localChangesDataRaw, openingValues));
21890
- if (cleaned && typeof cleaned === "object" && Object.keys(cleaned).length === 0) return;
21891
- return cleaned;
23391
+ return getUnappliedLocalChanges(localChangesDataRaw, openingValues);
21892
23392
  }, [
21893
23393
  localChangesDataRaw,
21894
23394
  computedInitialValues,
@@ -22939,6 +24439,6 @@ function getFullIdPath(propertyKey, propertyNamespace) {
22939
24439
  return idToPropertiesPath(propertyNamespace ? `${propertyNamespace}.${propertyKey}` : propertyKey);
22940
24440
  }
22941
24441
  //#endregion
22942
- export { useBuildCollectionRegistryController as $, UrlComponentPreview as $n, CollectionTableBinding as $t, getFieldId as A, ArrayOfReferencesPreview as An, convertFileToJson as At, MapFieldBinding as B, getEntityPreviewKeys 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, EnumValuesChip as Gn, removeInitialAndTrailingSlashes$1 as Gt, DateTimeFieldBinding as H, getEntityTitlePropertyKeyForEntity as Hn, getCollectionBySlugWithin as Ht, RepeatFieldBinding as I, CollectionRegistryContext as In, useBreadcrumbsController as It, useBuildUrlController as J, SkeletonPropertyComponent as Jn, resolveCollectionPathIds$1 as Jt, SelectionTableBinding as K, StorageThumbnail 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, renderSkeletonText as Qn, VirtualTableInput$1 as Qt, MultiSelectFieldBinding as R, getUserLabel 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, ArrayPropertyPreview as Un, getCollectionPathsCombinations as Ut, KeyValueFieldBinding as V, getEntityTitlePropertyKey as Vn, addInitialSlash as Vt, ArrayOfReferencesFieldBinding as W, StringPropertyPreview as Wn, getLastSegment$1 as Wt, useTopLevelNavigation as X, renderSkeletonIcon as Xn, resolveViewMode as Xt, useBuildNavigationStateController as Y, renderSkeletonCaptionText as Yn, resolveOpenEntityMode as Yt, useResolvedViews as Z, renderSkeletonImageThumbnail as Zn, useSelectionController as Zt, useCollectionsConfigController as _, PropertyPreview as _n, getResolvedPropertyInPath as _r, CollectionViewActions as _t, namespaceToPropertiesPath as a, NavigationStateContext as an, RecordMeta as ar, useBuildSidePanel as at, extractTouchedValues as b, BooleanPreview as bn, useCollectionEditorDialogsState as bt, buildCollectionGenerationCallback as c, SideDialogsControllerContext as cn, isSelfLabellingProperty as cr, copyEntityAction as ct, fromSerializableCollectionConfigs as d, useClearRestoreValue as dn, getBracketNotation as dr, resetPasswordAction as dt, SelectableTable as en, ImagePreview as er, useHistory as et, fromSerializableProperties as f, ReadOnlyFieldBinding as fn, getDefaultPropertiesOrder as fr, CreationResultDialog as ft, toSerializableProperty as g, ArrayOfMapsPreview as gn, getPropertyInPath$1 as gr, EntityCardBinding as gt, toSerializableProperties as h, FieldHelperText as hn, getPropertiesWithPropertiesOrder 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, spanClass as lr, deleteEntityAction as lt, toSerializableCollectionConfig as m, LabelWithIcon as mn, getIconForWidget as mr, EntityViewBinding as mt, getFullIdPath as n, useAdminContext as nn, EmptyValue as nr, useResolvedNavigationFrom as nt, CollectionGenerationApiError as o, useNavigationStateController as on, FieldBlock as or, EditViewBinding as ot, fromSerializableProperty as p, LabelWithIconAndTooltip as pn, getIconForProperty as pr, DetailViewBinding as pt, SideDialogs as q, StorageThumbnailInternal 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, LABEL_ICON_SIZE as sr, CollectionViewBinding as st, getFullId as t, CollectionRowActions as tn, sanitizeUrl as tr, resolveNavigationFrom as tt, fromSerializableCollectionConfig as u, ArrayCustomShapedFieldBinding as un, PropertyIdCopyTooltip as ur, editEntityAction as ut, EntityFormBinding as v, UserPreview as vn, isReferenceProperty as vr, 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, isRelationProperty as yr, ConfigControllerProvider as yt, MarkdownEditorFieldBinding as z, useResolvedUser as zn, resolveEntityView as zt };
24442
+ export { getEntityViewWidth as $, SkeletonPropertyComponent as $n, resolveCollectionPathIds$1 as $t, SelectFieldBinding as A, ArrayOfStringsPreview as An, saveImportedEntities as At, useSelectionDialog as B, useSidePanel as Bn, FieldCaption as Bt, getDefaultFieldId as C, UserPreview as Cn, isReferenceProperty as Cr, convertDataToEntity as Ct, TextFieldBinding as D, KeyValuePreview as Dn, useImportConfig as Dt, VectorFieldBinding as E, DatePreview as En, getInferenceType as Et, MapFieldBinding as F, ArrayOfReferencesPreview as Fn, parseCsvToObjects as Ft, useTopLevelNavigation as G, getEntityPreviewKeys as Gn, mergeEntityActions as Gt, SideDialogs as H, useCollectionRegistryController as Hn, BreadcrumbsProvider as Ht, KeyValueFieldBinding as I, InlineEntityListPreview as In, unflattenObject as It, useBuildCollectionRegistryController as J, ArrayPropertyPreview as Jn, getCollectionPathsCombinations as Jt, useResolvedViews as K, getEntityTitlePropertyKey as Kn, addInitialSlash as Kt, DateTimeFieldBinding as L, ReferencePreview as Ln, ArrayContainer as Lt, ReferenceFieldBinding as M, ArrayEnumPreview as Mn, convertFileToJson as Mt, MultiSelectFieldBinding as N, ArrayOfStorageComponentsPreview as Nn, detectCsvDelimiter as Nt, SwitchFieldBinding as O, MapPropertyPreview as On, ImportSaveInProgress as Ot, MarkdownEditorFieldBinding as P, RelationPreview as Pn, parseCsvRows as Pt, buildSidePanelsFromUrl as Q, StorageThumbnailInternal as Qn, removeTrailingSlash$1 as Qt, BlockFieldBinding as R, EntityPreviewBinding as Rn, PropertyConfigBadge as Rt, getDefaultFieldConfig as S, PropertyPreview as Sn, getResolvedPropertyInPath as Sr, DataNewPropertiesMapping as St, getFieldId as T, BooleanPreview as Tn, processValueMapping as Tt, useBuildUrlController as U, getUserLabel as Un, resolveEntityAction as Ut, SelectionTableBinding as V, CollectionRegistryContext as Vn, useBreadcrumbsController as Vt, useBuildNavigationStateController as W, useResolvedUser as Wn, resolveEntityView as Wt, resolveNavigationFrom as X, EnumValuesChip as Xn, removeInitialAndTrailingSlashes$1 as Xt, useHistory as Y, StringPropertyPreview as Yn, getLastSegment$1 as Yt, useResolvedNavigationFrom as Z, StorageThumbnail as Zn, removeInitialSlash as Zt, useCollectionsConfigController as _, ReadOnlyFieldBinding as _n, getDefaultPropertiesOrder as _r, CollectionViewActions as _t, namespaceToPropertiesPath as a, SelectableTable as an, ImagePreview as ar, removeEmptyContainers as at, PropertyFieldBinding as b, FieldHelperText as bn, getPropertiesWithPropertiesOrder as br, useCollectionEditorDialogsState as bt, buildCollectionGenerationCallback as c, UrlContext as cn, FormSections as cr, copyEntityAction as ct, fromSerializableCollectionConfigs as d, useNavigationStateController as dn, FieldBlock as dr, resetPasswordAction as dt, resolveOpenEntityMode as en, renderSkeletonCaptionText as er, useBuildSidePanel as et, fromSerializableProperties as f, useSideDialogsController as fn, LABEL_ICON_SIZE as fr, CreationResultDialog as ft, toSerializableProperty as g, useClearRestoreValue as gn, getBracketNotation as gr, EntityCardBinding as gt, toSerializableProperties as h, ArrayCustomShapedFieldBinding as hn, PropertyIdCopyTooltip as hr, CollectionCardViewBinding as ht, namespaceToPropertiesOrderPath as i, CollectionTableBinding as in, UrlComponentPreview as ir, getInitialEntityValues as it, RepeatFieldBinding as j, ArrayPropertyEnumPreview as jn, ImportFileUpload as jt, StorageUploadFieldBinding as k, ArrayOneOfPreview as kn, IMPORT_BATCH_SIZE as kt, validateCollectionJson as l, useUrlController as ln, FormRail as lr, deleteEntityAction as lt, toSerializableCollectionConfig as m, SelectableTableContext as mn, spanClass as mr, EntityViewBinding as mt, getFullIdPath as n, useSelectionController as nn, renderSkeletonImageThumbnail as nr, extractTouchedValues as nt, CollectionGenerationApiError as o, CollectionRowActions as on, sanitizeUrl as or, zodToFormErrors as ot, fromSerializableProperty as p, SideDialogsControllerContext as pn, isSelfLabellingProperty as pr, DetailViewBinding as pt, useResolvedCollections as q, getEntityTitlePropertyKeyForEntity as qn, getCollectionBySlugWithin as qt, idToPropertiesPath as r, VirtualTableInput$1 as rn, renderSkeletonText as rr, getChanges as rt, DEFAULT_COLLECTION_GENERATION_ENDPOINT as s, useAdminContext as sn, EmptyValue as sr, CollectionViewBinding as st, getFullId as t, resolveViewMode as tn, renderSkeletonIcon as tr, EditViewBinding as tt, fromSerializableCollectionConfig as u, NavigationStateContext as un, RecordMeta as ur, editEntityAction as ut, EntityFormBinding as v, LabelWithIconAndTooltip as vn, getIconForProperty as vr, useCollectionEditorController as vt, getFieldConfig as w, NumberPropertyPreview as wn, isRelationProperty as wr, flattenEntry as wt, DEFAULT_FIELD_CONFIGS as x, ArrayOfMapsPreview as xn, getPropertyInPath$1 as xr, ImportNewPropertyFieldPreview as xt, EntityForm as y, LabelWithIcon as yn, getIconForWidget as yr, ConfigControllerProvider as yt, ArrayOfReferencesFieldBinding as z, SidePanelControllerContext as zn, SearchIconsView as zt };
22943
24443
 
22944
- //# sourceMappingURL=util-DHNXodBo.js.map
24444
+ //# sourceMappingURL=util-C-D5yD2n.js.map