@rebasepro/cms 0.21.2-canary.g1ea48be → 0.23.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 (58) hide show
  1. package/dist/{CollectionEditorDialog-B1mG1cfx.js → CollectionEditorDialog-DLHCka00.js} +103 -58
  2. package/dist/CollectionEditorDialog-DLHCka00.js.map +1 -0
  3. package/dist/{PropertyEditView-gKNUHxLY.js → PropertyEditView-_pu5hzZn.js} +53 -21
  4. package/dist/PropertyEditView-_pu5hzZn.js.map +1 -0
  5. package/dist/{RouterCollectionsStudioView-qgDWC6E2.js → RouterCollectionsStudioView-ChPNl_uU.js} +34 -10
  6. package/dist/RouterCollectionsStudioView-ChPNl_uU.js.map +1 -0
  7. package/dist/collection_editor/_cms_internals.d.ts +1 -0
  8. package/dist/collection_editor/relation_kinds.d.ts +11 -0
  9. package/dist/collection_editor/ui/collection_editor/CollectionStudioView.d.ts +8 -1
  10. package/dist/collection_editor_ui.js +4 -4
  11. package/dist/components/CollectionTableBinding/fields/CompactEntityCellField.d.ts +12 -13
  12. package/dist/components/CollectionTableBinding/fields/TableMultipleRelationField.d.ts +6 -3
  13. package/dist/components/CollectionTableBinding/fields/TableReferenceField.d.ts +8 -1
  14. package/dist/components/CollectionTableBinding/fields/TableRelationField.d.ts +8 -1
  15. package/dist/components/CollectionTableBinding/fields/TableRelationSelectorField.d.ts +13 -3
  16. package/dist/components/CollectionTableBinding/fields/VirtualTableSelect.d.ts +10 -0
  17. package/dist/components/CollectionTableBinding/fields/VirtualTableUserSelect.d.ts +10 -0
  18. package/dist/components/CollectionTableBinding/internal/EntityTableCell.d.ts +21 -0
  19. package/dist/components/CollectionTableBinding/internal/TableCellOpener.d.ts +45 -0
  20. package/dist/components/CollectionTableBinding/table_bindings.d.ts +24 -0
  21. package/dist/components/CollectionViewBinding/CollectionViewBinding.d.ts +7 -1
  22. package/dist/components/EntityPreviewBinding.d.ts +8 -1
  23. package/dist/components/EntityPreviewNesting.d.ts +15 -0
  24. package/dist/components/PickerTrigger.d.ts +17 -0
  25. package/dist/components/ReferenceTable/SelectionTableBinding.d.ts +12 -0
  26. package/dist/components/RelationSelector.d.ts +24 -7
  27. package/dist/components/UserSelector.d.ts +8 -1
  28. package/dist/data_export/export/additional_fields.d.ts +13 -0
  29. package/dist/data_export/export/export.d.ts +6 -0
  30. package/dist/data_import/utils/csv.d.ts +3 -2
  31. package/dist/data_import/utils/id_column.d.ts +16 -0
  32. package/dist/editor.js +1 -1
  33. package/dist/{export-DIyk1720.js → export-4bJWdlDK.js} +63 -209
  34. package/dist/export-4bJWdlDK.js.map +1 -0
  35. package/dist/form/form_utils.d.ts +13 -0
  36. package/dist/form/validation.d.ts +13 -0
  37. package/dist/{history-CrZEzQ5b.js → history-BElrZ5Rw.js} +2 -2
  38. package/dist/{history-CrZEzQ5b.js.map → history-BElrZ5Rw.js.map} +1 -1
  39. package/dist/hooks/useEntityDisplay.d.ts +4 -5
  40. package/dist/{import-ByaJMZuT.js → import-TCgkTVlS.js} +3 -4
  41. package/dist/import-TCgkTVlS.js.map +1 -0
  42. package/dist/index.js +16 -10
  43. package/dist/index.js.map +1 -1
  44. package/dist/preview/util.d.ts +25 -0
  45. package/dist/util/navigation_utils.d.ts +14 -2
  46. package/dist/util/view_mode.d.ts +12 -18
  47. package/dist/{util-DB31IoJE.js → util-CC0ozHoD.js} +35 -2
  48. package/dist/util-CC0ozHoD.js.map +1 -0
  49. package/dist/{util-qfCPxJzB.js → util-DybQBkug.js} +1482 -717
  50. package/dist/util-DybQBkug.js.map +1 -0
  51. package/package.json +9 -9
  52. package/dist/CollectionEditorDialog-B1mG1cfx.js.map +0 -1
  53. package/dist/PropertyEditView-gKNUHxLY.js.map +0 -1
  54. package/dist/RouterCollectionsStudioView-qgDWC6E2.js.map +0 -1
  55. package/dist/export-DIyk1720.js.map +0 -1
  56. package/dist/import-ByaJMZuT.js.map +0 -1
  57. package/dist/util-DB31IoJE.js.map +0 -1
  58. package/dist/util-qfCPxJzB.js.map +0 -1
@@ -1,14 +1,14 @@
1
- import { n as getThumbnailMeasure, r as sanitizeUrl, t as getPreviewSizeFrom } from "./util-DB31IoJE.js";
1
+ import { a as isEmptyPreviewValue, i as getThumbnailMeasure, n as getDatePropertyTimezone, o as sanitizeUrl, r as getPreviewSizeFrom, t as getDatePropertyMode } from "./util-CC0ozHoD.js";
2
2
  import * as React$1 from "react";
3
3
  import React, { Suspense, createContext, createElement, forwardRef, memo, useCallback, useContext, useEffect, useId, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
4
4
  import { deepEqual, shallowEqual } from "fast-equals";
5
5
  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, lazyChunk, paperMixin, useDebouncedCallback, useOutsideAlerter, usePortalContainer } from "@rebasepro/ui";
6
6
  import { CollectionRegistry, DEFAULT_ONE_OF_TYPE, DEFAULT_ONE_OF_VALUE, canReadCollection, enumToObjectEntries, getArrayResolvedProperties, getDefaultValueFor, getDefaultValueFortype, getDefaultValuesFor, getEntityChildViews, getLabelOrConfigFrom, getReferenceFrom, getRelationFrom, getRelationTargetPath, getSubcollections, getTableName, isPropertyBuilder, normalizeOrderBy, normalizeToEntityRelation, resolveArrayProperties, resolveEnumValues, resolveProperty, resolveRelationProperty, resolveStorageFilenameString, resolveStoragePathString, resolveStorageSource, serializeOrderBy } from "@rebasepro/common";
7
- 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, useBridgeRegistration, useCollectionScope, useColumnIds, useComponentOverride, useCustomizationController, useData, useDataTableController, useFetch, useLargeLayout, useNavigationBlocker, usePermissions, useRebaseClient, useRebaseContext, useRelationSelector, useResolvedComponent, useScrollRestoration, useSlot, useSnackbarController, useStorageSource, useStorageSources, useStorageUploadController, useTranslation, useUnsavedChangesDialog, useUserConfigurationPersistence } from "@rebasepro/app";
7
+ import { CONTAINER_FULL_WIDTH, CollectionScopeProvider, DEFAULT_PAGE_SIZE, 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, getSharedEntityDisplayCache, 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, useBridgeRegistration, useCollectionScope, useColumnIds, useComponentOverride, useCustomizationController, useData, useDataTableController, useFetch, useLargeLayout, useNavigationBlocker, usePermissions, useRebaseClient, useRebaseContext, useRelationSelector, useResolvedComponent, useScrollRestoration, useSlot, useSnackbarController, useStorageSource, useStorageSources, useStorageUploadController, useTranslation, useUnsavedChangesDialog, useUserConfigurationPersistence } from "@rebasepro/app";
8
8
  import { Field, Formex, getIn, setIn, useCreateFormex, useFormex } from "@rebasepro/forms";
9
9
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
10
10
  import { EntityReference, EntityRelation, GeoPoint, MAX_LIST_LIMIT, Vector, getCollectionDataPath, getDataSourceCapabilities, isRelationalCollectionConfig, resourceKeyOf } from "@rebasepro/types";
11
- import { defaultDateFormat, formatRelativeTime, getHashValue, getValueInPath, hydrateRegExp, isArrayValue, isObject, isPrototypePollutingKey, mergeDeep, pathTraversesPrototype, prettifyIdentifier, randomString, readStoredJson, toArray, writeStoredJson } from "@rebasepro/utils";
11
+ import { defaultDateFormat, formatRelativeTime, getArrayValuesCount, getHashValue, getValueInPath, hydrateRegExp, isArrayValue, isPlainObject, isPrototypePollutingKey, mergeDeep, pathTraversesPrototype, prettifyIdentifier, randomString, readStoredJson, toArray, writeStoredJson } from "@rebasepro/utils";
12
12
  import { resolveAdminCollection } from "@rebasepro/cms-types";
13
13
  import { format } from "date-fns";
14
14
  import { Command } from "cmdk";
@@ -1353,25 +1353,14 @@ function getUserLabel(user) {
1353
1353
  //#endregion
1354
1354
  //#region src/hooks/useEntityDisplay.tsx
1355
1355
  /**
1356
- * One shared store, so every surface agrees on a record and resolves it once.
1357
- *
1358
- * Module-level rather than a provider: two app instances on one page share a
1359
- * cache keyed by collection path and record id, which *is* the same record.
1360
- * Nothing here is user-specific — a resolver that reads per-user data must
1361
- * invalidate on sign-out, which {@link useEntityDisplayCache} exposes.
1362
- */
1363
- var sharedCache;
1364
- /**
1365
1356
  * The store behind {@link useEntityDisplay}, for invalidating after a write.
1366
1357
  *
1367
- * Built on first use rather than at module scope. Importing a module should not
1368
- * construct anything: it runs during SSR where there is nothing to cache, and it
1369
- * made this module unimportable from any test that stubs `@rebasepro/app` —
1370
- * which several do, and which is a legitimate thing for them to do.
1358
+ * The app's one shared store, so every surface agrees on a record and resolves
1359
+ * it once, and so it is emptied with the rest of the session's caches when the
1360
+ * user signs out.
1371
1361
  */
1372
1362
  function useEntityDisplayCache() {
1373
- if (!sharedCache) sharedCache = new EntityDisplayCache();
1374
- return sharedCache;
1363
+ return getSharedEntityDisplayCache();
1375
1364
  }
1376
1365
  /**
1377
1366
  * What fills one display role of one record.
@@ -1936,13 +1925,24 @@ function NestedEntityPreviewBoundary({ children }) {
1936
1925
  children
1937
1926
  });
1938
1927
  }
1928
+ var EntityPreviewLayoutContext = React$1.createContext("card");
1929
+ function useEntityPreviewLayout() {
1930
+ return React$1.useContext(EntityPreviewLayoutContext);
1931
+ }
1932
+ /** Lays out the previews below it as rows. See {@link EntityPreviewLayout}. */
1933
+ function RowEntityPreviewLayout({ children }) {
1934
+ return /* @__PURE__ */ jsx(EntityPreviewLayoutContext.Provider, {
1935
+ value: "row",
1936
+ children
1937
+ });
1938
+ }
1939
1939
  //#endregion
1940
1940
  //#region src/components/EntityPreviewBinding.tsx
1941
1941
  /**
1942
1942
  * This component contains the main logic and content for displaying a entity preview,
1943
1943
  * without any container wrapper. Used internally by EntityPreviewBinding.
1944
1944
  */
1945
- function EntityPreviewBindingData({ actions, collection: collectionProp, previewKeys, size = "medium", includeId = true, onSidePanelClick, includeTitle = true, includeEntityLink = true, includeImage = true, entity }) {
1945
+ function EntityPreviewBindingData({ actions, collection: collectionProp, previewKeys, size = "medium", includeId = true, onSidePanelClick, includeTitle = true, includeEntityLink = true, includeImage = true, layout = "card", entity }) {
1946
1946
  const authController = useAuthController();
1947
1947
  const analyticsController = useAnalyticsController();
1948
1948
  const sidePanelController = useSidePanel();
@@ -1972,8 +1972,32 @@ function EntityPreviewBindingData({ actions, collection: collectionProp, preview
1972
1972
  const ofProp = imageProperty && "of" in imageProperty ? imageProperty.of : void 0;
1973
1973
  const usedImageProperty = ofProp ? Array.isArray(ofProp) ? ofProp[0] : ofProp : imageProperty;
1974
1974
  const restProperties = listProperties.filter((p) => p !== titleProperty && p !== imagePropertyKey);
1975
+ const openInSidePanel = () => {
1976
+ onSidePanelClick?.(entity);
1977
+ analyticsController.onAnalyticsEvent?.("entity_click_from_reference", {
1978
+ path: entity.path,
1979
+ entityId: entity.id
1980
+ });
1981
+ sidePanelController.open({
1982
+ entityId: entity.id,
1983
+ path: entity.path,
1984
+ collection,
1985
+ updateUrl: true
1986
+ });
1987
+ };
1975
1988
  const imageValue = imagePropertyKey ? getValueInPath(entity.values, imagePropertyKey) : void 0;
1976
1989
  const usedImageValue = imageProperty !== void 0 ? "of" in imageProperty ? (imageValue ?? []).length > 0 ? imageValue[0] : void 0 : imageValue : void 0;
1990
+ if (layout === "row") return /* @__PURE__ */ jsx(EntityRowPreviewContent, {
1991
+ size,
1992
+ entity,
1993
+ collection,
1994
+ title: title.value ?? String(entity.id),
1995
+ imageProperty: usedImageProperty,
1996
+ imagePropertyKey,
1997
+ imageValue: usedImageValue,
1998
+ supportingKeys: restProperties,
1999
+ onOpen: includeEntityLink ? openInSidePanel : void 0
2000
+ });
1977
2001
  return /* @__PURE__ */ jsxs(NestedEntityPreviewBoundary, { children: [
1978
2002
  /* @__PURE__ */ jsxs("div", {
1979
2003
  className: cls("flex shrink-0 overflow-hidden", {
@@ -2048,17 +2072,7 @@ function EntityPreviewBindingData({ actions, collection: collectionProp, preview
2048
2072
  onClick: (e) => {
2049
2073
  e.stopPropagation();
2050
2074
  e.preventDefault();
2051
- onSidePanelClick?.(entity);
2052
- analyticsController.onAnalyticsEvent?.("entity_click_from_reference", {
2053
- path: entity.path,
2054
- entityId: entity.id
2055
- });
2056
- sidePanelController.open({
2057
- entityId: entity.id,
2058
- path: entity.path,
2059
- collection,
2060
- updateUrl: true
2061
- });
2075
+ openInSidePanel();
2062
2076
  },
2063
2077
  children: /* @__PURE__ */ jsx(ArrowRightToLineIcon, {})
2064
2078
  })
@@ -2071,6 +2085,75 @@ function EntityPreviewBindingData({ actions, collection: collectionProp, preview
2071
2085
  ] });
2072
2086
  }
2073
2087
  /**
2088
+ * A record as a line of a table row: its image, a title that opens it, and the
2089
+ * supporting lines that fit — one at `medium`, two at `large`. Supporting
2090
+ * values that are empty are skipped rather than drawn as an empty bar, so the
2091
+ * line under a name is the first thing the record actually has to say.
2092
+ */
2093
+ function EntityRowPreviewContent({ size, entity, collection, title, imageProperty, imagePropertyKey, imageValue, supportingKeys, onOpen }) {
2094
+ const maxLines = size === "large" ? 2 : 1;
2095
+ const lines = supportingKeys.map((key) => ({
2096
+ key,
2097
+ property: getPropertyInPath$1(collection.properties, key),
2098
+ value: getValueInPath(entity.values, key)
2099
+ })).filter((line) => Boolean(line.property) && !isEmptyPreviewValue(line.value)).slice(0, maxLines);
2100
+ const titleNode = /* @__PURE__ */ jsx("span", {
2101
+ className: "truncate",
2102
+ children: title
2103
+ });
2104
+ return /* @__PURE__ */ jsx(NestedEntityPreviewBoundary, { children: /* @__PURE__ */ jsxs("div", {
2105
+ className: "flex items-center gap-2.5 min-w-0 w-full text-left",
2106
+ children: [imagePropertyKey && /* @__PURE__ */ jsx("div", {
2107
+ className: cls("flex shrink-0 items-center justify-center overflow-hidden rounded-md", size === "large" ? "w-10 h-10" : "w-8 h-8"),
2108
+ children: imageProperty && imageValue ? /* @__PURE__ */ jsx(PropertyPreview, {
2109
+ property: imageProperty,
2110
+ propertyKey: imagePropertyKey,
2111
+ size: "small",
2112
+ value: imageValue
2113
+ }) : /* @__PURE__ */ jsx(IconForView, {
2114
+ collectionOrView: collection,
2115
+ color: "primary",
2116
+ size: "small"
2117
+ })
2118
+ }), /* @__PURE__ */ jsxs("div", {
2119
+ className: "flex flex-col min-w-0 flex-1",
2120
+ children: [/* @__PURE__ */ jsx("div", {
2121
+ className: "flex min-w-0 text-sm font-medium text-text-primary dark:text-text-primary-dark",
2122
+ children: onOpen ? /* @__PURE__ */ jsx(Tooltip, {
2123
+ title: `See details for ${title}`,
2124
+ asChild: true,
2125
+ children: /* @__PURE__ */ jsx("span", {
2126
+ role: "button",
2127
+ tabIndex: 0,
2128
+ className: "inline-flex min-w-0 max-w-full cursor-pointer hover:underline underline-offset-2",
2129
+ onMouseDown: (event) => event.preventDefault(),
2130
+ onClick: (event) => {
2131
+ event.preventDefault();
2132
+ event.stopPropagation();
2133
+ onOpen();
2134
+ },
2135
+ onKeyDown: (event) => {
2136
+ if (event.key !== "Enter" && event.key !== " ") return;
2137
+ event.preventDefault();
2138
+ event.stopPropagation();
2139
+ onOpen();
2140
+ },
2141
+ children: titleNode
2142
+ })
2143
+ }) : titleNode
2144
+ }), lines.map(({ key, property, value }) => /* @__PURE__ */ jsx("div", {
2145
+ className: "truncate min-w-0 max-h-5 overflow-hidden text-xs text-text-secondary dark:text-text-secondary-dark",
2146
+ children: /* @__PURE__ */ jsx(PropertyPreview, {
2147
+ propertyKey: key,
2148
+ value,
2149
+ property,
2150
+ size: "small"
2151
+ })
2152
+ }, key))]
2153
+ })]
2154
+ }) });
2155
+ }
2156
+ /**
2074
2157
  * This view is used to display a preview of a entity.
2075
2158
  * It is used by default in reference fields and whenever a reference is displayed.
2076
2159
  */
@@ -2167,6 +2250,7 @@ function InlineEntityPreview({ entity, collection: collectionProp, label, disabl
2167
2250
  const content = /* @__PURE__ */ jsxs("span", {
2168
2251
  tabIndex: interactive ? 0 : void 0,
2169
2252
  role: interactive ? "button" : void 0,
2253
+ onMouseDown: interactive ? (event) => event.preventDefault() : void 0,
2170
2254
  onClick: interactive ? handleClick : void 0,
2171
2255
  onKeyDown: interactive ? (event) => {
2172
2256
  if (event.key === "Enter" || event.key === " ") handleClick(event);
@@ -2292,7 +2376,9 @@ function ReferencePreviewInternal(props) {
2292
2376
  function ReferencePreviewExisting({ reference, collection, previewProperties, size, disabled, includeEntityLink, includeId, onClick, hover, textOnly }) {
2293
2377
  const ResolvedEntityPreview = useComponentOverride("EntityPreview", EntityPreviewBinding);
2294
2378
  useCustomizationController();
2295
- const inline = useIsNestedEntityPreview() || Boolean(textOnly) || size === "small";
2379
+ const nested = useIsNestedEntityPreview();
2380
+ const layout = useEntityPreviewLayout();
2381
+ const inline = nested || Boolean(textOnly) || size === "small";
2296
2382
  const { entity, dataLoading, dataLoadingError } = useFetch({
2297
2383
  path: reference.path,
2298
2384
  entityId: reference.id,
@@ -2346,6 +2432,14 @@ function ReferencePreviewExisting({ reference, collection, previewProperties, si
2346
2432
  onClick,
2347
2433
  includeEntityLink: !textOnly && includeEntityLink !== false
2348
2434
  });
2435
+ if (layout === "row" && ResolvedEntityPreview === EntityPreviewBinding) return /* @__PURE__ */ jsx(EntityPreviewBindingData, {
2436
+ layout: "row",
2437
+ size,
2438
+ entity: usedEntity,
2439
+ collection,
2440
+ previewKeys: previewProperties,
2441
+ includeEntityLink: !disabled && includeEntityLink !== false
2442
+ });
2349
2443
  return /* @__PURE__ */ jsx(ResolvedEntityPreview, {
2350
2444
  size,
2351
2445
  previewKeys: previewProperties,
@@ -2394,7 +2488,7 @@ function ArrayOfReferencesPreview({ propertyKey, value, property, size, textOnly
2394
2488
  if (property?.type !== "array" || !property.of || property.of.type !== "reference") throw Error("Picked wrong preview component ArrayOfReferencesPreview");
2395
2489
  const childSize = size === "medium" ? "medium" : "small";
2396
2490
  const ofProperty = property.of;
2397
- if (nested || textOnly) return /* @__PURE__ */ jsx(InlineEntityListPreview, {
2491
+ if (nested || textOnly || size === "small") return /* @__PURE__ */ jsx(InlineEntityListPreview, {
2398
2492
  items: value ?? [],
2399
2493
  renderItem: (reference, index) => /* @__PURE__ */ jsx(ReferencePreview, {
2400
2494
  disabled: !ofProperty.path,
@@ -2533,7 +2627,9 @@ function RelationPreviewExisting({ relation, collection, previewProperties, size
2533
2627
  const passedEntity = relation.data;
2534
2628
  const ResolvedEntityPreview = useComponentOverride("EntityPreview", EntityPreviewBinding);
2535
2629
  useCustomizationController();
2536
- const inline = useIsNestedEntityPreview() || Boolean(textOnly) || size === "small";
2630
+ const nested = useIsNestedEntityPreview();
2631
+ const layout = useEntityPreviewLayout();
2632
+ const inline = nested || Boolean(textOnly) || size === "small";
2537
2633
  const { entity, dataLoading, dataLoadingError } = useFetch({
2538
2634
  path: relation.path,
2539
2635
  entityId: passedEntity ? void 0 : relation.id,
@@ -2587,6 +2683,14 @@ function RelationPreviewExisting({ relation, collection, previewProperties, size
2587
2683
  onClick,
2588
2684
  includeEntityLink: !textOnly && includeEntityLink !== false
2589
2685
  });
2686
+ if (layout === "row" && ResolvedEntityPreview === EntityPreviewBinding) return /* @__PURE__ */ jsx(EntityPreviewBindingData, {
2687
+ layout: "row",
2688
+ size,
2689
+ entity: usedEntity,
2690
+ collection,
2691
+ previewKeys: previewProperties,
2692
+ includeEntityLink: !disabled && includeEntityLink !== false
2693
+ });
2590
2694
  return /* @__PURE__ */ jsx(ResolvedEntityPreview, {
2591
2695
  size,
2592
2696
  previewKeys: previewProperties,
@@ -2610,7 +2714,7 @@ function ArrayOfRelationsPreview({ propertyKey, value, property, size, textOnly
2610
2714
  if (property?.type !== "array" || !property.of || property.of.type !== "relation") throw Error("Picked wrong preview component ArrayOfRelationsPreview");
2611
2715
  const ofProperty = property.of;
2612
2716
  const relationTargetPath = getRelationTargetPath(ofProperty);
2613
- if (nested || textOnly) return /* @__PURE__ */ jsx(InlineEntityListPreview, {
2717
+ if (nested || textOnly || size === "small") return /* @__PURE__ */ jsx(InlineEntityListPreview, {
2614
2718
  items: (value ?? []).map((relation) => normalizeToEntityRelation(relation, "relation", relationTargetPath)).filter((relation) => Boolean(relation)),
2615
2719
  renderItem: (relation, index) => /* @__PURE__ */ jsx(RelationPreview, {
2616
2720
  disabled: !ofProperty.relation,
@@ -3053,10 +3157,11 @@ function DatePreview({ date, mode = "date_time", timezone }) {
3053
3157
  options.minute = "2-digit";
3054
3158
  }
3055
3159
  const formattedDate = new Intl.DateTimeFormat(customizationController?.locale ?? "en-US", options).format(date);
3056
- const tzAbbrev = new Intl.DateTimeFormat("en-US", {
3160
+ const parts = new Intl.DateTimeFormat("en-US", {
3057
3161
  timeZone: timezone,
3058
3162
  timeZoneName: "short"
3059
- }).formatToParts(date).find((p) => p.type === "timeZoneName")?.value ?? "";
3163
+ }).formatToParts(date);
3164
+ const tzAbbrev = mode === "date_time" ? parts.find((p) => p.type === "timeZoneName")?.value ?? "" : "";
3060
3165
  return /* @__PURE__ */ jsxs("span", {
3061
3166
  className: "flex items-center gap-1 font-mono tabular-nums",
3062
3167
  children: [formattedDate, tzAbbrev && /* @__PURE__ */ jsxs("span", {
@@ -3316,7 +3421,8 @@ var PropertyPreview = React.memo(function PropertyPreview(props) {
3316
3421
  const date = asDate(value);
3317
3422
  if (date) content = /* @__PURE__ */ jsx(DatePreview, {
3318
3423
  date,
3319
- mode: property.mode
3424
+ mode: getDatePropertyMode(property),
3425
+ timezone: getDatePropertyTimezone(property)
3320
3426
  });
3321
3427
  else content = buildWrongValueType(propertyKey, property.type, value);
3322
3428
  } else if (property.type === "reference") if (typeof property.path === "string") if (typeof value === "object" && value !== null && "isEntityReference" in value && value.isEntityReference()) content = /* @__PURE__ */ jsx(ReferencePreview, {
@@ -3344,7 +3450,7 @@ var PropertyPreview = React.memo(function PropertyPreview(props) {
3344
3450
  relation: entityRelation,
3345
3451
  textOnly: props.textOnly
3346
3452
  }, `preview_rel_${propertyKey}_${index}`);
3347
- content = nested || props.textOnly ? /* @__PURE__ */ jsx(InlineEntityListPreview, {
3453
+ content = nested || props.textOnly || size === "small" ? /* @__PURE__ */ jsx(InlineEntityListPreview, {
3348
3454
  items: relations,
3349
3455
  renderItem: renderRelation
3350
3456
  }) : /* @__PURE__ */ jsx("div", {
@@ -3562,16 +3668,16 @@ function ReadOnlyFieldBinding({ propertyKey, value, error, showError, minimalist
3562
3668
  * @ignore
3563
3669
  */
3564
3670
  function useClearRestoreValue({ property, value, setValue }) {
3565
- const clearedValueRef = useRef(null);
3671
+ const clearedRef = useRef(null);
3566
3672
  useEffect(() => {
3567
3673
  if (typeof property.admin?.disabled === "object" && Boolean(property.admin?.disabled.clearOnDisabled)) {
3568
3674
  if (value != null) {
3569
- clearedValueRef.current = value;
3675
+ clearedRef.current = { value };
3570
3676
  setValue(null);
3571
3677
  }
3572
- } else if (clearedValueRef.current) {
3573
- setValue(clearedValueRef.current);
3574
- clearedValueRef.current = null;
3678
+ } else if (clearedRef.current) {
3679
+ setValue(clearedRef.current.value);
3680
+ clearedRef.current = null;
3575
3681
  }
3576
3682
  }, [property]);
3577
3683
  }
@@ -3679,7 +3785,7 @@ function useCellSelected(store, propertyKey, entityPath, entityId) {
3679
3785
  //#endregion
3680
3786
  //#region src/components/CollectionTableBinding/fields/VirtualTableSelect.tsx
3681
3787
  function VirtualTableSelect(props) {
3682
- const { enumValues, internalValue, disabled, small, focused, updateValue, multiple, valueType } = props;
3788
+ const { enumValues, internalValue, disabled, small, focused, updateValue, multiple, valueType, open, onOpenChange, preview, anchorRef } = props;
3683
3789
  const validValue = Array.isArray(internalValue) && multiple || !Array.isArray(internalValue) && !multiple;
3684
3790
  const ref = React.useRef(null);
3685
3791
  useEffect(() => {
@@ -3700,25 +3806,33 @@ function VirtualTableSelect(props) {
3700
3806
  ]);
3701
3807
  const resolvedEnumValues = resolveEnumValues(enumValues);
3702
3808
  const renderValue = (enumKey) => {
3809
+ if (preview !== void 0) return preview;
3703
3810
  return /* @__PURE__ */ jsx(EnumValuesChip, {
3704
3811
  enumKey: String(enumKey),
3705
3812
  enumValues,
3706
3813
  size: small ? "small" : "medium"
3707
3814
  }, `${enumKey}`);
3708
3815
  };
3709
- const handleOpenChange = useCallback((open) => {
3710
- if (!open && ref.current) setTimeout(() => {
3816
+ const handleOpenChange = useCallback((nextOpen) => {
3817
+ onOpenChange?.(nextOpen);
3818
+ if (!nextOpen && ref.current) setTimeout(() => {
3711
3819
  ref.current?.focus({ preventScroll: true });
3712
3820
  }, 0);
3713
- }, []);
3821
+ }, [onOpenChange]);
3714
3822
  return multiple ? /* @__PURE__ */ jsx(MultiSelect, {
3715
3823
  invisible: true,
3716
3824
  inputRef: ref,
3825
+ size: "smallest",
3717
3826
  className: "w-full h-full p-0 bg-transparent outline-none",
3718
3827
  position: "item-aligned",
3719
3828
  disabled,
3720
3829
  includeClear: false,
3830
+ chevron: preview === void 0,
3831
+ anchorRef,
3721
3832
  useChips: false,
3833
+ open,
3834
+ renderValues: preview !== void 0 ? () => preview : void 0,
3835
+ placeholder: preview,
3722
3836
  value: validValue ? internalValue.map((v) => v.toString()) : [],
3723
3837
  onValueChange: onChange,
3724
3838
  onOpenChange: handleOpenChange,
@@ -3739,7 +3853,11 @@ function VirtualTableSelect(props) {
3739
3853
  position: "item-aligned",
3740
3854
  disabled,
3741
3855
  padding: false,
3856
+ size: "smallest",
3857
+ chevron: preview === void 0,
3858
+ open,
3742
3859
  value: validValue ? internalValue?.toString() : "",
3860
+ placeholder: preview,
3743
3861
  onValueChange: onChange,
3744
3862
  onOpenChange: handleOpenChange,
3745
3863
  renderValue,
@@ -3754,6 +3872,27 @@ function VirtualTableSelect(props) {
3754
3872
  });
3755
3873
  }
3756
3874
  //#endregion
3875
+ //#region src/components/PickerTrigger.tsx
3876
+ /**
3877
+ * The trigger of a picker's popover, or — given an anchor — a plain child
3878
+ * beside a `Popover.Anchor` on that element.
3879
+ *
3880
+ * Both at once does not work: Radix's `Trigger` registers itself as the
3881
+ * anchor through a ref callback, and a custom anchor through an effect that
3882
+ * only reports a change. Under StrictMode the ref callback runs again after
3883
+ * the effect, the effect sees nothing new, and the popover stays anchored to
3884
+ * a trigger that the custom anchor then unmounts — it opened at the window's
3885
+ * top-left corner. The pickers open and close themselves, so they lose
3886
+ * nothing by leaving the `Trigger` out.
3887
+ */
3888
+ function PickerTrigger({ anchorRef, children }) {
3889
+ if (!anchorRef) return /* @__PURE__ */ jsx(PopoverPrimitive.Trigger, {
3890
+ asChild: true,
3891
+ children
3892
+ });
3893
+ return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(PopoverPrimitive.Anchor, { virtualRef: anchorRef }), children] });
3894
+ }
3895
+ //#endregion
3757
3896
  //#region src/components/UserSelector.tsx
3758
3897
  var DEFAULT_PAGE_SIZE$2 = 10;
3759
3898
  /**
@@ -3849,9 +3988,30 @@ function useUserSelector({ pageSize = DEFAULT_PAGE_SIZE$2 } = {}) {
3849
3988
  }, [])
3850
3989
  };
3851
3990
  }
3852
- var UserSelector = React$1.forwardRef(({ value, size = "medium", onValueChange, invisible, disabled, placeholder, clearable = true, className, pageSize, searchPlaceholder = "Search users...", noResultsText = "No users found.", loadingText = "Loading..." }, ref) => {
3991
+ var UserSelector = React$1.forwardRef(({ value, size = "medium", onValueChange, invisible, disabled, placeholder, clearable = true, className, pageSize, searchPlaceholder = "Search users...", noResultsText = "No users found.", loadingText = "Loading...", open: openProp, onOpenChange, triggerContent, anchorRef }, ref) => {
3853
3992
  const { items: availableItems, isLoading, hasMore, search, loadMore, getUser } = useUserSelector({ pageSize });
3854
- const [isPopoverOpen, setIsPopoverOpen] = useState(false);
3993
+ const [isPopoverOpen, setIsPopoverOpenState] = useState(false);
3994
+ const isPopoverOpenRef = useRef(false);
3995
+ const onOpenChangeRef = useRef(onOpenChange);
3996
+ onOpenChangeRef.current = onOpenChange;
3997
+ const setIsPopoverOpen = useCallback((next, notify = true) => {
3998
+ const changed = isPopoverOpenRef.current !== next;
3999
+ isPopoverOpenRef.current = next;
4000
+ setIsPopoverOpenState(next);
4001
+ if (changed && notify) onOpenChangeRef.current?.(next);
4002
+ }, []);
4003
+ useEffect(() => {
4004
+ if (openProp === void 0 || openProp === isPopoverOpenRef.current) return;
4005
+ if (openProp && disabled) {
4006
+ onOpenChangeRef.current?.(false);
4007
+ return;
4008
+ }
4009
+ setIsPopoverOpen(openProp, false);
4010
+ }, [
4011
+ openProp,
4012
+ disabled,
4013
+ setIsPopoverOpen
4014
+ ]);
3855
4015
  const [searchString, setSearchString] = useState("");
3856
4016
  const contextPortalContainer = usePortalContainer();
3857
4017
  const scrollContainerRef = useRef(null);
@@ -3917,16 +4077,24 @@ var UserSelector = React$1.forwardRef(({ value, size = "medium", onValueChange,
3917
4077
  if (value === item.uid) onValueChange?.(null);
3918
4078
  else onValueChange?.(item.uid);
3919
4079
  setIsPopoverOpen(false);
3920
- }, [value, onValueChange]);
4080
+ }, [
4081
+ value,
4082
+ onValueChange,
4083
+ setIsPopoverOpen
4084
+ ]);
3921
4085
  const handleClear = useCallback((e) => {
3922
4086
  e.stopPropagation();
3923
4087
  e.preventDefault();
3924
4088
  onValueChange?.(null);
3925
4089
  }, [onValueChange]);
4090
+ const toggleOpen = useCallback(() => {
4091
+ if (disabled) return;
4092
+ setIsPopoverOpen(!isPopoverOpenRef.current);
4093
+ }, [disabled, setIsPopoverOpen]);
3926
4094
  const handleRootOpenChange = useCallback((next) => {
3927
4095
  if (disabled) return;
3928
4096
  if (next) setIsPopoverOpen(true);
3929
- }, [disabled]);
4097
+ }, [disabled, setIsPopoverOpen]);
3930
4098
  useEffect(() => {
3931
4099
  if (!isPopoverOpen) return;
3932
4100
  function handlePointerDown(ev) {
@@ -3946,26 +4114,39 @@ var UserSelector = React$1.forwardRef(({ value, size = "medium", onValueChange,
3946
4114
  document.removeEventListener("mousedown", handlePointerDown, true);
3947
4115
  document.removeEventListener("keydown", handleKey, true);
3948
4116
  };
3949
- }, [isPopoverOpen]);
4117
+ }, [isPopoverOpen, setIsPopoverOpen]);
3950
4118
  const resolvedPlaceholder = placeholder || /* @__PURE__ */ jsx(EmptyValue, { className: "ml-2" });
3951
4119
  const portalContainer = contextPortalContainer ?? (typeof document !== "undefined" ? document.body : void 0) ?? void 0;
3952
4120
  return /* @__PURE__ */ jsx(Fragment, { children: /* @__PURE__ */ jsxs(PopoverPrimitive.Root, {
3953
4121
  open: isPopoverOpen,
3954
4122
  onOpenChange: handleRootOpenChange,
3955
4123
  modal: false,
3956
- children: [/* @__PURE__ */ jsx(PopoverPrimitive.Trigger, {
3957
- asChild: true,
3958
- children: /* @__PURE__ */ jsx("button", {
4124
+ children: [/* @__PURE__ */ jsx(PickerTrigger, {
4125
+ anchorRef,
4126
+ children: triggerContent !== void 0 ? /* @__PURE__ */ jsx("div", {
4127
+ ref: handleButtonRef,
4128
+ role: "button",
4129
+ tabIndex: disabled ? -1 : 0,
4130
+ "aria-disabled": disabled || void 0,
4131
+ "aria-haspopup": "listbox",
4132
+ "aria-expanded": isPopoverOpen,
4133
+ "data-user-selector-trigger": true,
4134
+ onClick: toggleOpen,
4135
+ onKeyDown: (event) => {
4136
+ if (event.key !== "Enter" && event.key !== " ") return;
4137
+ event.preventDefault();
4138
+ toggleOpen();
4139
+ },
4140
+ className: cls("w-full select-none text-left text-sm relative flex items-center outline-none", className),
4141
+ children: triggerContent
4142
+ }) : /* @__PURE__ */ jsx("button", {
3959
4143
  ref: handleButtonRef,
3960
4144
  type: "button",
3961
4145
  "aria-haspopup": "listbox",
3962
4146
  "aria-expanded": isPopoverOpen,
3963
4147
  "data-user-selector-trigger": true,
3964
4148
  disabled,
3965
- onClick: () => {
3966
- if (disabled) return;
3967
- setIsPopoverOpen((o) => !o);
3968
- },
4149
+ onClick: toggleOpen,
3969
4150
  className: cls({
3970
4151
  "min-h-[42px] py-1 px-2": size === "small",
3971
4152
  "min-h-[56px] py-2 px-4": size === "medium",
@@ -4094,8 +4275,19 @@ UserSelector.displayName = "UserSelector";
4094
4275
  //#endregion
4095
4276
  //#region src/components/CollectionTableBinding/fields/VirtualTableUserSelect.tsx
4096
4277
  function VirtualTableUserSelect(props) {
4097
- const { internalValue, disabled, small, focused, updateValue, multiple } = props;
4278
+ const { internalValue, disabled, focused, updateValue, open, onOpenChange, preview, anchorRef } = props;
4279
+ const ref = useRef(null);
4280
+ useEffect(() => {
4281
+ if (focused) ref.current?.focus({ preventScroll: true });
4282
+ }, [focused]);
4283
+ const handleOpenChange = (nextOpen) => {
4284
+ onOpenChange?.(nextOpen);
4285
+ if (!nextOpen) setTimeout(() => {
4286
+ if (!document.activeElement || document.activeElement === document.body) ref.current?.focus({ preventScroll: true });
4287
+ }, 0);
4288
+ };
4098
4289
  return /* @__PURE__ */ jsx(UserSelector, {
4290
+ ref,
4099
4291
  value: Array.isArray(internalValue) ? internalValue[0] ?? null : internalValue ?? null,
4100
4292
  onValueChange: (userId) => {
4101
4293
  updateValue(userId);
@@ -4103,7 +4295,11 @@ function VirtualTableUserSelect(props) {
4103
4295
  disabled,
4104
4296
  size: "small",
4105
4297
  clearable: true,
4106
- invisible: true
4298
+ invisible: true,
4299
+ open,
4300
+ onOpenChange: handleOpenChange,
4301
+ triggerContent: preview,
4302
+ anchorRef
4107
4303
  });
4108
4304
  }
4109
4305
  //#endregion
@@ -4388,55 +4584,38 @@ function TableStorageItemPreview({ propertyKey, property, value, size, entity })
4388
4584
  //#endregion
4389
4585
  //#region src/components/CollectionTableBinding/fields/CompactEntityCellField.tsx
4390
4586
  /**
4391
- * The one-line editor a reference or relation cell shows while selected, at
4392
- * the row sizes that carry small previews (`xs`, `s`, `m`).
4587
+ * A reference or relation cell, at the row sizes that carry small previews
4588
+ * (`xs`, `s`, `m`), for the pickers that open a selection dialog.
4393
4589
  *
4394
- * The flow it replaces: at 48px the selected cell swapped its resting inline
4395
- * line for the card editor built for 140px rows, which was clipped to its top
4396
- * third and whose only click opened the picker — there was no way to look at
4397
- * the record. Here the line stays (its title opens the record in the side
4398
- * panel, exactly as it does at rest), a pencil opens the picker and a cross
4399
- * clears the value. Click the name to see it, click the pencil to change it.
4590
+ * The value is one line of inline previews, the same at rest and selected:
4591
+ * each title opens its record in the side panel. The cell's opener — the
4592
+ * chevron it reveals on hover — opens the dialog, and a cross beside it clears
4593
+ * the value. The cross follows the opener: hidden at rest, shown on hover and
4594
+ * while the cell is selected. Unlike the opener it saves at once, so hidden it
4595
+ * cannot be hit either: a tap where nothing is hovered selects the cell.
4400
4596
  */
4401
- function CompactEntityCellField({ children, empty, disabled, onEdit, onClear, emptyLabel }) {
4597
+ function CompactEntityCellField({ children, empty, disabled, selected, onClear }) {
4402
4598
  const { t } = useTranslation();
4403
4599
  return /* @__PURE__ */ jsxs("div", {
4404
4600
  className: "w-full min-w-0 flex items-center gap-0.5 min-h-8",
4405
4601
  children: [/* @__PURE__ */ jsx("div", {
4406
4602
  className: "grow min-w-0 flex items-center gap-x-3 overflow-hidden text-sm",
4407
- children: empty ? /* @__PURE__ */ jsx("button", {
4408
- type: "button",
4409
- disabled,
4410
- onClick: (e) => {
4411
- e.stopPropagation();
4412
- onEdit();
4413
- },
4414
- className: cls("truncate text-left text-text-secondary dark:text-text-secondary-dark", !disabled && "cursor-pointer hover:text-text-primary dark:hover:text-text-primary-dark"),
4415
- children: emptyLabel ?? t("select_reference")
4416
- }) : children
4417
- }), !disabled && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Tooltip, {
4418
- title: t("edit"),
4419
- children: /* @__PURE__ */ jsx(IconButton, {
4420
- size: "small",
4421
- "aria-label": t("edit"),
4422
- onClick: (e) => {
4423
- e.stopPropagation();
4424
- onEdit();
4425
- },
4426
- children: /* @__PURE__ */ jsx(PencilIcon, {})
4427
- })
4428
- }), !empty && /* @__PURE__ */ jsx(Tooltip, {
4603
+ children: empty ? /* @__PURE__ */ jsx(EmptyValue, {}) : children
4604
+ }), !disabled && !empty && /* @__PURE__ */ jsx(Tooltip, {
4429
4605
  title: t("clear"),
4430
4606
  children: /* @__PURE__ */ jsx(IconButton, {
4431
- size: "small",
4607
+ size: "smallest",
4608
+ shape: "square",
4432
4609
  "aria-label": t("clear"),
4610
+ className: cls("w-6 !h-6 min-w-6 min-h-6 p-0 hover:scale-100 transition-opacity duration-100", selected ? "opacity-100" : "opacity-0 pointer-events-none group-hover/cell:opacity-100 group-hover/cell:pointer-events-auto focus-visible:opacity-100"),
4611
+ onMouseDown: (e) => e.preventDefault(),
4433
4612
  onClick: (e) => {
4434
4613
  e.stopPropagation();
4435
4614
  onClear();
4436
4615
  },
4437
- children: /* @__PURE__ */ jsx(XIcon, {})
4616
+ children: /* @__PURE__ */ jsx(XIcon, { size: 14 })
4438
4617
  })
4439
- })] })]
4618
+ })]
4440
4619
  });
4441
4620
  }
4442
4621
  //#endregion
@@ -4467,7 +4646,7 @@ function TableReferenceField(props) {
4467
4646
  return content;
4468
4647
  }
4469
4648
  var TableReferenceFieldInternal = React.memo(function TableReferenceFieldInternal(props) {
4470
- const { name, internalValue, updateValue, multiselect, path, size, previewProperties, title, disabled, fixedFilter, collection, includeId, includeEntityLink } = props;
4649
+ const { name, internalValue, updateValue, multiselect, path, size, previewProperties, disabled, fixedFilter, collection, includeId, includeEntityLink, selected, open, onOpenChange } = props;
4471
4650
  const onSingleEntitySelected = useCallback((entity) => {
4472
4651
  updateValue(entity ? getReferenceFrom(entity) : null);
4473
4652
  }, [updateValue]);
@@ -4486,72 +4665,27 @@ var TableReferenceFieldInternal = React.memo(function TableReferenceFieldInterna
4486
4665
  if (disabled) return;
4487
4666
  referenceDialogController.open();
4488
4667
  };
4668
+ useEffect(() => {
4669
+ if (!open) return;
4670
+ onOpenChange?.(false);
4671
+ handleOpen();
4672
+ }, [open]);
4489
4673
  const valueNotSet = !internalValue || Array.isArray(internalValue) && internalValue.length === 0;
4490
- const buildSingleReferenceField = () => {
4491
- if (internalValue && !Array.isArray(internalValue) && internalValue.isEntityReference && internalValue.isEntityReference()) return /* @__PURE__ */ jsx(ReferencePreview, {
4492
- onClick: disabled ? void 0 : handleOpen,
4493
- size: getPreviewSizeFrom(size),
4494
- reference: internalValue,
4495
- hover: !disabled,
4496
- disabled: !path,
4497
- previewProperties,
4498
- includeId,
4499
- includeEntityLink
4500
- });
4501
- else return /* @__PURE__ */ jsx(EntityPreviewContainer, {
4502
- onClick: disabled ? void 0 : handleOpen,
4503
- size: getPreviewSizeFrom(size),
4504
- children: /* @__PURE__ */ jsx(ErrorView, {
4505
- title: "Value is not a reference.",
4506
- error: "Click to edit"
4507
- })
4508
- });
4509
- };
4510
- const buildMultipleReferenceField = () => {
4511
- if (Array.isArray(internalValue)) return /* @__PURE__ */ jsx(Fragment, { children: internalValue.map((reference, index) => /* @__PURE__ */ jsx("div", {
4512
- className: "w-full my-0.5",
4513
- children: /* @__PURE__ */ jsx(ReferencePreview, {
4514
- onClick: disabled ? void 0 : handleOpen,
4515
- size: "small",
4516
- reference,
4517
- hover: !disabled,
4518
- disabled: !path,
4519
- previewProperties,
4520
- includeId,
4521
- includeEntityLink
4522
- })
4523
- }, `preview_array_ref_${name}_${index}`)) });
4524
- else return /* @__PURE__ */ jsx(ErrorView, { error: "Data is not an array of references" });
4525
- };
4526
4674
  if (!collection) return /* @__PURE__ */ jsx(ErrorView, { error: "The specified collection does not exist" });
4527
- if (getPreviewSizeFrom(size) === "small") return /* @__PURE__ */ jsx(CompactEntityCellField, {
4675
+ return /* @__PURE__ */ jsx(CompactEntityCellField, {
4528
4676
  empty: valueNotSet,
4529
4677
  disabled,
4530
- onEdit: handleOpen,
4678
+ selected,
4531
4679
  onClear: () => updateValue(multiselect ? [] : null),
4532
- emptyLabel: title,
4533
4680
  children: (!internalValue ? [] : Array.isArray(internalValue) ? internalValue : [internalValue]).map((reference, index) => reference && reference.isEntityReference && reference.isEntityReference() ? /* @__PURE__ */ jsx(ReferencePreview, {
4534
- size: "small",
4681
+ size: multiselect ? "small" : getPreviewSizeFrom(size),
4535
4682
  reference,
4536
4683
  hover: false,
4537
4684
  disabled: !path,
4538
4685
  previewProperties,
4539
4686
  includeId,
4540
4687
  includeEntityLink
4541
- }, `compact_ref__`) : /* @__PURE__ */ jsx(ErrorView, { error: "Value is not a reference" }, `compact_ref__`))
4542
- });
4543
- return /* @__PURE__ */ jsxs("div", {
4544
- className: "w-full group",
4545
- children: [
4546
- internalValue && !multiselect && buildSingleReferenceField(),
4547
- internalValue && multiselect && buildMultipleReferenceField(),
4548
- valueNotSet && /* @__PURE__ */ jsxs(EntityPreviewContainer, {
4549
- className: cls("px-3 py-2 text-sm font-medium flex items-center", multiselect ? "gap-4" : "gap-6", disabled ? "text-surface-accent-500" : "cursor-pointer text-text-secondary dark:text-text-secondary-dark hover:bg-surface-hover group-hover:bg-surface-hover"),
4550
- onClick: handleOpen,
4551
- size: "medium",
4552
- children: [/* @__PURE__ */ jsx(PencilIcon, { className: "ml-2 mr-1 text-surface-300 dark:text-surface-600" }), title]
4553
- })
4554
- ]
4688
+ }, `compact_ref_${name}_${index}`) : /* @__PURE__ */ jsx(ErrorView, { error: "Value is not a reference" }, `compact_ref_${name}_${index}`))
4555
4689
  });
4556
4690
  }, deepEqual);
4557
4691
  //#endregion
@@ -4564,7 +4698,7 @@ function TableMultipleRelationField(props) {
4564
4698
  });
4565
4699
  }
4566
4700
  var TableMultipleRelationFieldInternal = React.memo(function TableMultipleRelationFieldInternal(props) {
4567
- const { name, internalValue, updateValue, previewProperties, title, disabled, fixedFilter, collection, includeId, includeEntityLink, size } = props;
4701
+ const { name, internalValue, updateValue, previewProperties, disabled, fixedFilter, collection, includeId, includeEntityLink, selected, open, onOpenChange } = props;
4568
4702
  const value = Array.isArray(internalValue) ? internalValue : [];
4569
4703
  const onMultipleEntitiesSelected = useCallback((entities) => {
4570
4704
  updateValue(entities.map((e) => getRelationFrom(e)));
@@ -4582,33 +4716,18 @@ var TableMultipleRelationFieldInternal = React.memo(function TableMultipleRelati
4582
4716
  if (disabled) return;
4583
4717
  relationDialogController.open();
4584
4718
  };
4719
+ useEffect(() => {
4720
+ if (!open) return;
4721
+ onOpenChange?.(false);
4722
+ handleOpen();
4723
+ }, [open]);
4585
4724
  const valueNotSet = !internalValue || Array.isArray(internalValue) && internalValue.length === 0;
4586
- const buildMultipleRelationField = () => {
4587
- if (Array.isArray(internalValue)) return /* @__PURE__ */ jsx(Fragment, { children: internalValue.map((item, index) => {
4588
- const relationItem = normalizeToEntityRelation(item);
4589
- if (!relationItem) return null;
4590
- return /* @__PURE__ */ jsx("div", {
4591
- className: "w-full my-0.5",
4592
- children: /* @__PURE__ */ jsx(RelationPreview, {
4593
- onClick: disabled ? void 0 : handleOpen,
4594
- size: "small",
4595
- relation: relationItem,
4596
- hover: !disabled,
4597
- previewProperties,
4598
- includeId,
4599
- includeEntityLink
4600
- })
4601
- }, `preview_array_ref_${name}_${index}`);
4602
- }) });
4603
- else return /* @__PURE__ */ jsx(ErrorView, { error: "Data is not an array of relations" });
4604
- };
4605
4725
  if (!collection) return /* @__PURE__ */ jsx(ErrorView, { error: "The specified collection does not exist" });
4606
- if (getPreviewSizeFrom(size) === "small") return /* @__PURE__ */ jsx(CompactEntityCellField, {
4726
+ return /* @__PURE__ */ jsx(CompactEntityCellField, {
4607
4727
  empty: valueNotSet,
4608
4728
  disabled,
4609
- onEdit: handleOpen,
4729
+ selected,
4610
4730
  onClear: () => updateValue([]),
4611
- emptyLabel: title,
4612
4731
  children: value.map((item, index) => {
4613
4732
  const relationItem = normalizeToEntityRelation(item);
4614
4733
  if (!relationItem) return null;
@@ -4619,18 +4738,9 @@ var TableMultipleRelationFieldInternal = React.memo(function TableMultipleRelati
4619
4738
  previewProperties,
4620
4739
  includeId,
4621
4740
  includeEntityLink
4622
- }, `compact_rel__`);
4741
+ }, `compact_rel_${name}_${index}`);
4623
4742
  })
4624
4743
  });
4625
- return /* @__PURE__ */ jsxs("div", {
4626
- className: "w-full group",
4627
- children: [internalValue && buildMultipleRelationField(), valueNotSet && /* @__PURE__ */ jsxs(EntityPreviewContainer, {
4628
- className: cls("px-3 py-2 text-sm font-medium flex items-center gap-4", disabled ? "text-surface-accent-500" : "cursor-pointer text-text-secondary dark:text-text-secondary-dark hover:bg-surface-hover group-hover:bg-surface-hover"),
4629
- onClick: handleOpen,
4630
- size: "medium",
4631
- children: [/* @__PURE__ */ jsx(PencilIcon, { className: "ml-2 mr-1 text-surface-300 dark:text-surface-600" }), title]
4632
- })]
4633
- });
4634
4744
  }, deepEqual);
4635
4745
  //#endregion
4636
4746
  //#region src/components/CollectionTableBinding/fields/TableRelationField.tsx
@@ -4649,13 +4759,14 @@ function TableRelationField(props) {
4649
4759
  disabled: props.disabled,
4650
4760
  internalValue: Array.isArray(props.internalValue) ? props.internalValue : [],
4651
4761
  updateValue: (newValue) => props.updateValue(newValue),
4652
- size: props.size,
4653
4762
  previewProperties: props.previewProperties,
4654
- title: props.title,
4655
4763
  relation: props.relation,
4656
4764
  fixedFilter: props.fixedFilter,
4657
4765
  includeId: props.includeId,
4658
- includeEntityLink: props.includeEntityLink
4766
+ includeEntityLink: props.includeEntityLink,
4767
+ selected: props.selected,
4768
+ open: props.open,
4769
+ onOpenChange: props.onOpenChange
4659
4770
  });
4660
4771
  return /* @__PURE__ */ jsx(TableRelationFieldInternal, {
4661
4772
  ...props,
@@ -4663,7 +4774,7 @@ function TableRelationField(props) {
4663
4774
  });
4664
4775
  }
4665
4776
  var TableRelationFieldInternal = React.memo(function TableRelationFieldInternal(props) {
4666
- const { name, internalValue, updateValue, multiselect, relation, size, previewProperties, title, disabled, fixedFilter, collection, includeId, includeEntityLink } = props;
4777
+ const { name, internalValue, updateValue, multiselect, relation, size, previewProperties, disabled, fixedFilter, collection, includeId, includeEntityLink, selected, open, onOpenChange } = props;
4667
4778
  const onSingleEntitySelected = useCallback((entity) => {
4668
4779
  updateValue(entity ? getRelationFrom(entity) : null);
4669
4780
  }, [updateValue]);
@@ -4684,75 +4795,31 @@ var TableRelationFieldInternal = React.memo(function TableRelationFieldInternal(
4684
4795
  if (disabled) return;
4685
4796
  relationDialogController.open();
4686
4797
  };
4798
+ useEffect(() => {
4799
+ if (!open) return;
4800
+ onOpenChange?.(false);
4801
+ handleOpen();
4802
+ }, [open]);
4687
4803
  const valueNotSet = !internalValue || Array.isArray(internalValue) && internalValue.length === 0;
4688
- const buildSingleRelationField = () => {
4689
- const normalizedRelation = normalizeToEntityRelation(internalValue);
4690
- if (normalizedRelation) return /* @__PURE__ */ jsx(RelationPreview, {
4691
- onClick: disabled ? void 0 : handleOpen,
4692
- size: getPreviewSizeFrom(size),
4693
- relation: normalizedRelation,
4694
- hover: !disabled,
4695
- previewProperties,
4696
- includeId,
4697
- includeEntityLink
4698
- });
4699
- else return /* @__PURE__ */ jsx(EntityPreviewContainer, {
4700
- onClick: disabled ? void 0 : handleOpen,
4701
- size: getPreviewSizeFrom(size),
4702
- children: /* @__PURE__ */ jsx(ErrorView, {
4703
- title: "Value is not a relation.",
4704
- error: "Click to edit"
4705
- })
4706
- });
4707
- };
4708
- const buildMultipleRelationField = () => {
4709
- if (Array.isArray(internalValue)) return /* @__PURE__ */ jsx(Fragment, { children: internalValue.map((relationItem, index) => /* @__PURE__ */ jsx("div", {
4710
- className: "w-full my-0.5",
4711
- children: /* @__PURE__ */ jsx(RelationPreview, {
4712
- onClick: disabled ? void 0 : handleOpen,
4713
- size: "small",
4714
- relation: relationItem,
4715
- hover: !disabled,
4716
- previewProperties,
4717
- includeId,
4718
- includeEntityLink
4719
- })
4720
- }, `preview_array_ref_${name}_${index}`)) });
4721
- else return /* @__PURE__ */ jsx(ErrorView, { error: "Data is not an array of relations" });
4722
- };
4723
4804
  if (!collection) return /* @__PURE__ */ jsx(ErrorView, { error: "The specified collection does not exist" });
4724
- if (getPreviewSizeFrom(size) === "small") return /* @__PURE__ */ jsx(CompactEntityCellField, {
4805
+ return /* @__PURE__ */ jsx(CompactEntityCellField, {
4725
4806
  empty: valueNotSet,
4726
4807
  disabled,
4727
- onEdit: handleOpen,
4808
+ selected,
4728
4809
  onClear: () => updateValue(multiselect ? [] : null),
4729
- emptyLabel: title,
4730
4810
  children: (!internalValue ? [] : Array.isArray(internalValue) ? internalValue : [internalValue]).map((item, index) => {
4731
4811
  const relationItem = normalizeToEntityRelation(item);
4732
4812
  if (!relationItem) return null;
4733
4813
  return /* @__PURE__ */ jsx(RelationPreview, {
4734
- size: "small",
4814
+ size: multiselect ? "small" : getPreviewSizeFrom(size),
4735
4815
  relation: relationItem,
4736
4816
  hover: false,
4737
4817
  previewProperties,
4738
4818
  includeId,
4739
4819
  includeEntityLink
4740
- }, `compact_rel__`);
4820
+ }, `compact_rel_${name}_${index}`);
4741
4821
  })
4742
4822
  });
4743
- return /* @__PURE__ */ jsxs("div", {
4744
- className: "w-full group",
4745
- children: [
4746
- internalValue && !multiselect && buildSingleRelationField(),
4747
- internalValue && multiselect && buildMultipleRelationField(),
4748
- valueNotSet && /* @__PURE__ */ jsxs(EntityPreviewContainer, {
4749
- className: cls("px-3 py-2 text-sm font-medium flex items-center", multiselect ? "gap-4" : "gap-6", disabled ? "text-surface-accent-500" : "cursor-pointer text-text-secondary dark:text-text-secondary-dark hover:bg-surface-hover group-hover:bg-surface-hover"),
4750
- onClick: handleOpen,
4751
- size: "medium",
4752
- children: [/* @__PURE__ */ jsx(PencilIcon, { className: "ml-2 mr-1 text-surface-300 dark:text-surface-600" }), title]
4753
- })
4754
- ]
4755
- });
4756
4823
  }, deepEqual);
4757
4824
  //#endregion
4758
4825
  //#region src/components/RelationSelector.tsx
@@ -4764,7 +4831,7 @@ function relationCardinality$2(relation) {
4764
4831
  if (relation.kind === "belongsTo" || relation.kind === "hasOne") return "one";
4765
4832
  return relation.cardinality;
4766
4833
  }
4767
- 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...", allowCreate = true }, ref) => {
4834
+ 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...", allowCreate = true, open: openProp, onOpenChange, triggerContent, anchorRef }, ref) => {
4768
4835
  const collection = relation.target();
4769
4836
  const dataPath = getCollectionDataPath(collection);
4770
4837
  const dataClient = useData();
@@ -4773,7 +4840,7 @@ var RelationSelector = React$1.forwardRef(({ value, size = "medium", onValueChan
4773
4840
  const { t } = useTranslation();
4774
4841
  const contextPortalContainer = usePortalContainer();
4775
4842
  const multiple = multipleOverride ?? relationCardinality$2(relation) === "many";
4776
- const [isPopoverOpen, setIsPopoverOpen] = useState(false);
4843
+ const [isPopoverOpen, setIsPopoverOpenState] = useState(false);
4777
4844
  const isPopoverOpenRef = useRef(false);
4778
4845
  const [listRequested, setListRequested] = useState(false);
4779
4846
  const [selectedItems, setSelectedItems] = useState([]);
@@ -4781,6 +4848,32 @@ var RelationSelector = React$1.forwardRef(({ value, size = "medium", onValueChan
4781
4848
  const [searchString, setSearchString] = useState("");
4782
4849
  const localSelectionIdsRef = useRef(null);
4783
4850
  const pinnedIdsRef = useRef(null);
4851
+ const selectedItemsForPinRef = useRef([]);
4852
+ selectedItemsForPinRef.current = selectedItems;
4853
+ const onOpenChangeRef = useRef(onOpenChange);
4854
+ onOpenChangeRef.current = onOpenChange;
4855
+ const setPopoverOpen = useCallback((next, notify = true) => {
4856
+ if (next) {
4857
+ pinnedIdsRef.current = /* @__PURE__ */ new Set([...selectedItemsForPinRef.current.map((i) => String(i.id)), ...relationsArrayRef.current.map((rel) => String(typeof rel === "string" || typeof rel === "number" ? rel : rel.id))]);
4858
+ setListRequested(true);
4859
+ } else pinnedIdsRef.current = null;
4860
+ const changed = isPopoverOpenRef.current !== next;
4861
+ isPopoverOpenRef.current = next;
4862
+ setIsPopoverOpenState(next);
4863
+ if (changed && notify) onOpenChangeRef.current?.(next);
4864
+ }, []);
4865
+ useEffect(() => {
4866
+ if (openProp === void 0 || openProp === isPopoverOpenRef.current) return;
4867
+ if (openProp && disabled) {
4868
+ onOpenChangeRef.current?.(false);
4869
+ return;
4870
+ }
4871
+ setPopoverOpen(openProp, false);
4872
+ }, [
4873
+ openProp,
4874
+ disabled,
4875
+ setPopoverOpen
4876
+ ]);
4784
4877
  const { items: availableItems, isLoading, hasMore, search, loadMore, entityToRelationItem } = useRelationSelector({
4785
4878
  path: dataPath,
4786
4879
  collection,
@@ -4826,7 +4919,9 @@ var RelationSelector = React$1.forwardRef(({ value, size = "medium", onValueChan
4826
4919
  let active = true;
4827
4920
  const currentRelationsArray = relationsArrayRef.current;
4828
4921
  if (incomingIds === resolvedIdsRef.current && (allHaveData || hasResolvedItemsRef.current)) return;
4829
- if (localSelectionIdsRef.current !== null && incomingIds === localSelectionIdsRef.current) {
4922
+ const localSelectionIds = localSelectionIdsRef.current;
4923
+ localSelectionIdsRef.current = null;
4924
+ if (localSelectionIds !== null && incomingIds === localSelectionIds) {
4830
4925
  resolvedIdsRef.current = incomingIds;
4831
4926
  hasResolvedItemsRef.current = true;
4832
4927
  return;
@@ -4949,9 +5044,7 @@ var RelationSelector = React$1.forwardRef(({ value, size = "medium", onValueChan
4949
5044
  if (multiple) newSelected = selectedItems.some((v) => String(v.id) === String(item.id)) ? selectedItems.filter((v) => String(v.id) !== String(item.id)) : [...selectedItems, item];
4950
5045
  else {
4951
5046
  newSelected = [item];
4952
- setIsPopoverOpen(false);
4953
- isPopoverOpenRef.current = false;
4954
- pinnedIdsRef.current = null;
5047
+ setPopoverOpen(false);
4955
5048
  }
4956
5049
  setSelectedItems(newSelected);
4957
5050
  localSelectionIdsRef.current = computeSelectionFingerprint(newSelected);
@@ -4960,7 +5053,8 @@ var RelationSelector = React$1.forwardRef(({ value, size = "medium", onValueChan
4960
5053
  multiple,
4961
5054
  selectedItems,
4962
5055
  emitValueChange,
4963
- computeSelectionFingerprint
5056
+ computeSelectionFingerprint,
5057
+ setPopoverOpen
4964
5058
  ]);
4965
5059
  const handleClear = useCallback(() => {
4966
5060
  setSelectedItems([]);
@@ -4979,13 +5073,8 @@ var RelationSelector = React$1.forwardRef(({ value, size = "medium", onValueChan
4979
5073
  ]);
4980
5074
  const handleRootOpenChange = useCallback((next) => {
4981
5075
  if (disabled) return;
4982
- if (next) {
4983
- pinnedIdsRef.current = new Set(selectedItems.map((i) => String(i.id)));
4984
- setListRequested(true);
4985
- setIsPopoverOpen(true);
4986
- isPopoverOpenRef.current = true;
4987
- }
4988
- }, [disabled, selectedItems]);
5076
+ if (next) setPopoverOpen(true);
5077
+ }, [disabled, setPopoverOpen]);
4989
5078
  useEffect(() => {
4990
5079
  if (!isPopoverOpen) return;
4991
5080
  function handlePointerDown(ev) {
@@ -4994,16 +5083,10 @@ var RelationSelector = React$1.forwardRef(({ value, size = "medium", onValueChan
4994
5083
  const contentEl = contentRef.current;
4995
5084
  if (triggerEl?.contains(target)) return;
4996
5085
  if (contentEl?.contains(target)) return;
4997
- setIsPopoverOpen(false);
4998
- isPopoverOpenRef.current = false;
4999
- pinnedIdsRef.current = null;
5086
+ setPopoverOpen(false);
5000
5087
  }
5001
5088
  function handleKey(ev) {
5002
- if (ev.key === "Escape") {
5003
- setIsPopoverOpen(false);
5004
- isPopoverOpenRef.current = false;
5005
- pinnedIdsRef.current = null;
5006
- }
5089
+ if (ev.key === "Escape") setPopoverOpen(false);
5007
5090
  }
5008
5091
  document.addEventListener("mousedown", handlePointerDown, true);
5009
5092
  document.addEventListener("keydown", handleKey, true);
@@ -5011,12 +5094,12 @@ var RelationSelector = React$1.forwardRef(({ value, size = "medium", onValueChan
5011
5094
  document.removeEventListener("mousedown", handlePointerDown, true);
5012
5095
  document.removeEventListener("keydown", handleKey, true);
5013
5096
  };
5014
- }, [isPopoverOpen]);
5015
- const closePopover = useCallback(() => {
5016
- setIsPopoverOpen(false);
5017
- isPopoverOpenRef.current = false;
5018
- pinnedIdsRef.current = null;
5019
- }, []);
5097
+ }, [isPopoverOpen, setPopoverOpen]);
5098
+ const closePopover = useCallback(() => setPopoverOpen(false), [setPopoverOpen]);
5099
+ const toggleOpen = useCallback(() => {
5100
+ if (disabled) return;
5101
+ setPopoverOpen(!isPopoverOpenRef.current);
5102
+ }, [disabled, setPopoverOpen]);
5020
5103
  const canCreateTarget = allowCreate && canCreate(collection, dataPath);
5021
5104
  const buildDefaultValues = useCallback((searchText) => {
5022
5105
  const trimmed = searchText.trim();
@@ -5062,28 +5145,39 @@ var RelationSelector = React$1.forwardRef(({ value, size = "medium", onValueChan
5062
5145
  open: isPopoverOpen,
5063
5146
  onOpenChange: handleRootOpenChange,
5064
5147
  modal: false,
5065
- children: [/* @__PURE__ */ jsx(PopoverPrimitive.Trigger, {
5066
- asChild: true,
5067
- children: /* @__PURE__ */ jsx("button", {
5148
+ children: [/* @__PURE__ */ jsx(PickerTrigger, {
5149
+ anchorRef,
5150
+ children: triggerContent !== void 0 ? /* @__PURE__ */ jsx("div", {
5151
+ ref: handleButtonRef,
5152
+ role: "button",
5153
+ tabIndex: disabled ? -1 : 0,
5154
+ "aria-disabled": disabled || void 0,
5155
+ "aria-haspopup": "listbox",
5156
+ "aria-expanded": isPopoverOpen,
5157
+ "data-relation-selector-trigger": true,
5158
+ onClick: toggleOpen,
5159
+ onClickCapture: (event) => {
5160
+ const target = event.target;
5161
+ if (!(target instanceof Element) || !isPopoverOpenRef.current) return;
5162
+ const interactive = target.closest("a, button, [role=\"button\"]");
5163
+ if (interactive && interactive !== event.currentTarget) setPopoverOpen(false);
5164
+ },
5165
+ onKeyDown: (event) => {
5166
+ if (event.key !== "Enter" && event.key !== " ") return;
5167
+ event.preventDefault();
5168
+ toggleOpen();
5169
+ },
5170
+ className: cls("w-full select-none text-left text-sm relative flex items-center outline-none", className),
5171
+ children: triggerContent
5172
+ }) : /* @__PURE__ */ jsx("button", {
5068
5173
  ref: handleButtonRef,
5069
5174
  type: "button",
5070
5175
  "aria-haspopup": "listbox",
5071
5176
  "aria-expanded": isPopoverOpen,
5072
5177
  "data-relation-selector-trigger": true,
5073
5178
  disabled,
5074
- onClick: () => {
5075
- if (disabled) return;
5076
- setListRequested(true);
5077
- setIsPopoverOpen((o) => {
5078
- const next = !o;
5079
- isPopoverOpenRef.current = next;
5080
- if (next) pinnedIdsRef.current = new Set(selectedItems.map((i) => String(i.id)));
5081
- else pinnedIdsRef.current = null;
5082
- return next;
5083
- });
5084
- },
5179
+ onClick: toggleOpen,
5085
5180
  className: cls({
5086
- "min-h-8 py-0.5 px-2": size === "compact",
5087
5181
  "min-h-[42px] py-1 px-2": size === "small",
5088
5182
  "min-h-[56px] py-2 px-4": size === "medium",
5089
5183
  "min-h-[48px] py-1 px-4": size === "large"
@@ -5099,36 +5193,19 @@ var RelationSelector = React$1.forwardRef(({ value, size = "medium", onValueChan
5099
5193
  }) : selectedItems.length > 0 ? /* @__PURE__ */ jsx("div", {
5100
5194
  className: "flex flex-wrap items-center gap-1.5 text-start flex-1 min-w-0 mr-2",
5101
5195
  children: selectedItems.map((item) => {
5102
- if (!useChips || !multiple) {
5103
- if (size === "compact") return item.data ? /* @__PURE__ */ jsx(InlineEntityPreview, {
5196
+ if (!useChips || !multiple) return /* @__PURE__ */ jsx("div", {
5197
+ className: "flex flex-row items-center gap-1 truncate",
5198
+ children: item.data ? /* @__PURE__ */ jsx(EntityPreviewBindingData, {
5199
+ size: "medium",
5104
5200
  entity: item.data,
5105
- onClick: () => {
5106
- closePopover();
5107
- sidePanelController.open({
5108
- entityId: item.data.id,
5109
- path: item.data.path,
5110
- collection,
5111
- updateUrl: true
5112
- });
5113
- }
5114
- }, String(item.id)) : /* @__PURE__ */ jsx("span", {
5201
+ includeEntityLink: false,
5202
+ includeId: false,
5203
+ onSidePanelClick: closePopover
5204
+ }) : /* @__PURE__ */ jsx("span", {
5115
5205
  className: "text-sm truncate",
5116
5206
  children: item.label
5117
- }, String(item.id));
5118
- return /* @__PURE__ */ jsx("div", {
5119
- className: "flex flex-row items-center gap-1 truncate",
5120
- children: item.data ? /* @__PURE__ */ jsx(EntityPreviewBindingData, {
5121
- size: "medium",
5122
- entity: item.data,
5123
- includeEntityLink: false,
5124
- includeId: false,
5125
- onSidePanelClick: closePopover
5126
- }) : /* @__PURE__ */ jsx("span", {
5127
- className: "text-sm truncate",
5128
- children: item.label
5129
- })
5130
- }, String(item.id));
5131
- }
5207
+ })
5208
+ }, String(item.id));
5132
5209
  return /* @__PURE__ */ jsxs(Chip, {
5133
5210
  size: "small",
5134
5211
  className: cls("flex flex-row items-center gap-1 truncate"),
@@ -5154,7 +5231,7 @@ var RelationSelector = React$1.forwardRef(({ value, size = "medium", onValueChan
5154
5231
  children: resolvedPlaceholder
5155
5232
  }), /* @__PURE__ */ jsxs("div", {
5156
5233
  className: "flex items-center flex-shrink-0",
5157
- children: [size !== "compact" && !multiple && selectedItems.length === 1 && selectedItems[0]?.data && /* @__PURE__ */ jsx(Tooltip, {
5234
+ children: [!multiple && selectedItems.length === 1 && selectedItems[0]?.data && /* @__PURE__ */ jsx(Tooltip, {
5158
5235
  title: `Open ${selectedItems[0].label}`,
5159
5236
  children: /* @__PURE__ */ jsx(IconButton, {
5160
5237
  component: "div",
@@ -5165,7 +5242,7 @@ var RelationSelector = React$1.forwardRef(({ value, size = "medium", onValueChan
5165
5242
  e.stopPropagation();
5166
5243
  e.preventDefault();
5167
5244
  const entity = selectedItems[0].data;
5168
- setIsPopoverOpen(false);
5245
+ setPopoverOpen(false);
5169
5246
  sidePanelController.open({
5170
5247
  entityId: entity.id,
5171
5248
  path: entity.path,
@@ -5176,7 +5253,7 @@ var RelationSelector = React$1.forwardRef(({ value, size = "medium", onValueChan
5176
5253
  children: /* @__PURE__ */ jsx(ArrowRightToLineIcon, { size: iconSize.small })
5177
5254
  })
5178
5255
  }), /* @__PURE__ */ jsx(ChevronDownIcon, {
5179
- size: size === "small" || size === "compact" ? iconSize.small : iconSize.medium,
5256
+ size: size === "small" ? iconSize.small : iconSize.medium,
5180
5257
  className: cls("transition", isPopoverOpen ? "rotate-180" : "")
5181
5258
  })]
5182
5259
  })]
@@ -5337,14 +5414,28 @@ function InnerCheckBox({ checked }) {
5337
5414
  //#endregion
5338
5415
  //#region src/components/CollectionTableBinding/fields/TableRelationSelectorField.tsx
5339
5416
  /** Thin wrapper around RelationSelector for table cells */
5340
- function TableRelationSelectorField({ disabled = false, internalValue, updateValue, relation, fixedFilter, size = "medium" }) {
5417
+ function TableRelationSelectorField({ disabled = false, internalValue, updateValue, relation, fixedFilter, focused, open, onOpenChange, preview, anchorRef }) {
5418
+ const ref = useRef(null);
5419
+ useEffect(() => {
5420
+ if (focused) ref.current?.focus({ preventScroll: true });
5421
+ }, [focused]);
5422
+ const handleOpenChange = (nextOpen) => {
5423
+ onOpenChange?.(nextOpen);
5424
+ if (!nextOpen) setTimeout(() => {
5425
+ if (!document.activeElement || document.activeElement === document.body) ref.current?.focus({ preventScroll: true });
5426
+ }, 0);
5427
+ };
5341
5428
  return /* @__PURE__ */ jsx(RelationSelector, {
5429
+ ref,
5342
5430
  disabled,
5343
- size,
5344
5431
  value: internalValue || null,
5345
5432
  onValueChange: (newVal) => updateValue(newVal ?? null),
5346
5433
  relation,
5347
- fixedFilter
5434
+ fixedFilter,
5435
+ open,
5436
+ onOpenChange: handleOpenChange,
5437
+ triggerContent: preview,
5438
+ anchorRef
5348
5439
  });
5349
5440
  }
5350
5441
  //#endregion
@@ -5380,7 +5471,7 @@ function getTableBindingForProperty(property, selected) {
5380
5471
  else if (selected && property.type === "number") {
5381
5472
  const numberProperty = property;
5382
5473
  if (numberProperty.enum) return {
5383
- Component: ({ propertyKey, disabled, selected, size, error, validationError, internalValue, updateValue }) => /* @__PURE__ */ jsx(VirtualTableSelect, {
5474
+ Component: ({ propertyKey, disabled, selected, size, error, validationError, internalValue, updateValue, open, onOpenChange, preview, anchorRef }) => /* @__PURE__ */ jsx(VirtualTableSelect, {
5384
5475
  name: propertyKey,
5385
5476
  multiple: false,
5386
5477
  disabled,
@@ -5390,9 +5481,13 @@ function getTableBindingForProperty(property, selected) {
5390
5481
  enumValues: numberProperty.enum,
5391
5482
  error: validationError ?? error,
5392
5483
  internalValue,
5393
- updateValue
5484
+ updateValue,
5485
+ open,
5486
+ onOpenChange,
5487
+ preview,
5488
+ anchorRef
5394
5489
  }),
5395
- fullHeight: true
5490
+ opener: "dropdown"
5396
5491
  };
5397
5492
  else return {
5398
5493
  Component: ({ align, error, validationError, selected, disabled, internalValue, updateValue }) => /* @__PURE__ */ jsx(VirtualTableNumberInput, {
@@ -5408,7 +5503,7 @@ function getTableBindingForProperty(property, selected) {
5408
5503
  } else if (selected && property.type === "string") {
5409
5504
  const stringProperty = property;
5410
5505
  if (stringProperty.enum) return {
5411
- Component: ({ propertyKey, disabled, selected, size, error, validationError, internalValue, updateValue }) => /* @__PURE__ */ jsx(VirtualTableSelect, {
5506
+ Component: ({ propertyKey, disabled, selected, size, error, validationError, internalValue, updateValue, open, onOpenChange, preview, anchorRef }) => /* @__PURE__ */ jsx(VirtualTableSelect, {
5412
5507
  name: propertyKey,
5413
5508
  multiple: false,
5414
5509
  focused: selected,
@@ -5418,12 +5513,16 @@ function getTableBindingForProperty(property, selected) {
5418
5513
  enumValues: stringProperty.enum,
5419
5514
  error: validationError ?? error,
5420
5515
  internalValue,
5421
- updateValue
5516
+ updateValue,
5517
+ open,
5518
+ onOpenChange,
5519
+ preview,
5520
+ anchorRef
5422
5521
  }),
5423
- fullHeight: true
5522
+ opener: "dropdown"
5424
5523
  };
5425
5524
  else if (stringProperty.userSelect) return {
5426
- Component: ({ propertyKey, disabled, selected, size, error, validationError, internalValue, updateValue }) => /* @__PURE__ */ jsx(VirtualTableUserSelect, {
5525
+ Component: ({ propertyKey, disabled, selected, size, error, validationError, internalValue, updateValue, open, onOpenChange, preview, anchorRef }) => /* @__PURE__ */ jsx(VirtualTableUserSelect, {
5427
5526
  name: propertyKey,
5428
5527
  multiple: false,
5429
5528
  focused: selected,
@@ -5431,9 +5530,13 @@ function getTableBindingForProperty(property, selected) {
5431
5530
  small: getPreviewSizeFrom(size) !== "medium",
5432
5531
  error: validationError ?? error,
5433
5532
  internalValue,
5434
- updateValue
5533
+ updateValue,
5534
+ open,
5535
+ onOpenChange,
5536
+ preview,
5537
+ anchorRef
5435
5538
  }),
5436
- fullHeight: true
5539
+ opener: "dropdown"
5437
5540
  };
5438
5541
  else if (stringProperty.admin?.markdown || !stringProperty.storage) {
5439
5542
  const multiline = Boolean(stringProperty.admin?.multiline) || Boolean(stringProperty.admin?.markdown);
@@ -5457,29 +5560,20 @@ function getTableBindingForProperty(property, selected) {
5457
5560
  updateValue
5458
5561
  }) };
5459
5562
  else if (property.type === "date") return {
5460
- Component: ({ propertyKey, error, validationError, disabled, selected, size, property, internalValue, updateValue }) => {
5461
- const { locale } = useCustomizationController();
5462
- return /* @__PURE__ */ jsx(VirtualTableDateField, {
5463
- name: propertyKey,
5464
- error: validationError ?? error,
5465
- disabled,
5466
- small: getPreviewSizeFrom(size) !== "medium",
5467
- mode: property.mode,
5468
- focused: selected,
5469
- internalValue,
5470
- updateValue,
5471
- locale
5472
- });
5473
- },
5563
+ Component: DateBindingComponent,
5564
+ opener: "calendar",
5474
5565
  fullHeight: true,
5475
5566
  hideOverflow: false,
5476
5567
  allowScroll: false
5477
5568
  };
5478
5569
  else if (property.type === "reference") {
5479
5570
  if (property.path) return {
5480
- Component: ({ propertyKey, internalValue, updateValue, disabled, size, property }) => {
5571
+ Component: ({ propertyKey, internalValue, updateValue, disabled, size, property, selected, open, onOpenChange }) => {
5481
5572
  return /* @__PURE__ */ jsx(TableReferenceField, {
5482
5573
  name: propertyKey,
5574
+ selected,
5575
+ open,
5576
+ onOpenChange,
5483
5577
  internalValue,
5484
5578
  updateValue,
5485
5579
  disabled,
@@ -5489,20 +5583,22 @@ function getTableBindingForProperty(property, selected) {
5489
5583
  previewProperties: property.admin?.previewProperties,
5490
5584
  includeId: property.admin?.includeId,
5491
5585
  includeEntityLink: property.admin?.includeEntityLink,
5492
- title: property.name ?? propertyKey,
5493
5586
  fixedFilter: property.admin?.fixedFilter
5494
5587
  });
5495
5588
  },
5589
+ opener: "dialog",
5496
5590
  allowScroll: false
5497
5591
  };
5498
5592
  } else if (property.type === "relation") {
5499
5593
  if (property.relation) {
5500
5594
  if (property.admin?.widget === "dialog") return {
5501
5595
  Component: RelationDialogBindingComponent,
5596
+ opener: "dialog",
5502
5597
  allowScroll: false
5503
5598
  };
5504
5599
  else if (selected) return {
5505
5600
  Component: RelationSelectorBindingComponent,
5601
+ opener: "dropdown",
5506
5602
  allowScroll: false
5507
5603
  };
5508
5604
  }
@@ -5513,7 +5609,7 @@ function getTableBindingForProperty(property, selected) {
5513
5609
  const ofProp = arrayProperty.of;
5514
5610
  if (ofProp.type === "string" || ofProp.type === "number") {
5515
5611
  if (selected && ofProp.enum) return {
5516
- Component: ({ propertyKey, disabled, selected, size, error, validationError, internalValue, updateValue }) => /* @__PURE__ */ jsx(VirtualTableSelect, {
5612
+ Component: ({ propertyKey, disabled, selected, size, error, validationError, internalValue, updateValue, open, onOpenChange, preview, anchorRef }) => /* @__PURE__ */ jsx(VirtualTableSelect, {
5517
5613
  name: propertyKey,
5518
5614
  multiple: true,
5519
5615
  disabled,
@@ -5523,17 +5619,22 @@ function getTableBindingForProperty(property, selected) {
5523
5619
  enumValues: ofProp.enum,
5524
5620
  error: validationError ?? error,
5525
5621
  internalValue,
5526
- updateValue
5527
- }),
5528
- allowScroll: true,
5529
- fullHeight: true,
5530
- hideOverflow: false
5622
+ updateValue,
5623
+ open,
5624
+ onOpenChange,
5625
+ preview,
5626
+ anchorRef
5627
+ }),
5628
+ opener: "dropdown"
5531
5629
  };
5532
5630
  } else if (ofProp.type === "reference") {
5533
5631
  const refOfProp = ofProp;
5534
5632
  if (refOfProp.path) return {
5535
- Component: ({ propertyKey, disabled, internalValue, updateValue, size }) => /* @__PURE__ */ jsx(TableReferenceField, {
5633
+ Component: ({ propertyKey, disabled, internalValue, updateValue, size, selected, open, onOpenChange }) => /* @__PURE__ */ jsx(TableReferenceField, {
5536
5634
  name: propertyKey,
5635
+ selected,
5636
+ open,
5637
+ onOpenChange,
5537
5638
  disabled,
5538
5639
  internalValue,
5539
5640
  updateValue,
@@ -5541,22 +5642,58 @@ function getTableBindingForProperty(property, selected) {
5541
5642
  multiselect: true,
5542
5643
  path: refOfProp.path,
5543
5644
  previewProperties: refOfProp.admin?.previewProperties,
5544
- title: arrayProperty.name,
5545
5645
  fixedFilter: refOfProp.admin?.fixedFilter,
5546
5646
  includeId: refOfProp.admin?.includeId,
5547
5647
  includeEntityLink: refOfProp.admin?.includeEntityLink
5548
5648
  }),
5649
+ opener: "dialog",
5549
5650
  allowScroll: false
5550
5651
  };
5551
5652
  }
5552
5653
  }
5553
5654
  }
5554
5655
  }
5656
+ /**
5657
+ * Stable component for date cells. The input is always there, at rest too, so
5658
+ * a date can be typed into; the cell's opener shows the picker through it.
5659
+ */
5660
+ function DateBindingComponent({ propertyKey, error, validationError, disabled, selected, size, property, internalValue, updateValue, open, onOpenChange }) {
5661
+ const { locale } = useCustomizationController();
5662
+ const inputRef = useRef(null);
5663
+ useEffect(() => {
5664
+ if (!open) return;
5665
+ onOpenChange?.(false);
5666
+ const input = inputRef.current;
5667
+ if (!input) return;
5668
+ try {
5669
+ input.showPicker();
5670
+ } catch {
5671
+ input.focus();
5672
+ }
5673
+ }, [open]);
5674
+ return /* @__PURE__ */ jsx(VirtualTableDateField, {
5675
+ name: propertyKey,
5676
+ error: validationError ?? error,
5677
+ disabled,
5678
+ small: getPreviewSizeFrom(size) !== "medium",
5679
+ mode: getDatePropertyMode(property),
5680
+ timezone: getDatePropertyTimezone(property),
5681
+ focused: selected,
5682
+ internalValue,
5683
+ updateValue,
5684
+ locale,
5685
+ pickerButton: onOpenChange === void 0,
5686
+ inputRef
5687
+ });
5688
+ }
5555
5689
  /** Stable component for relation fields rendered with the dialog widget */
5556
- function RelationDialogBindingComponent({ propertyKey, internalValue, updateValue, disabled, size, property }) {
5690
+ function RelationDialogBindingComponent({ propertyKey, internalValue, updateValue, disabled, size, property, selected, open, onOpenChange }) {
5557
5691
  const relProp = property;
5558
5692
  return /* @__PURE__ */ jsx(TableRelationField, {
5559
5693
  name: propertyKey,
5694
+ selected,
5695
+ open,
5696
+ onOpenChange,
5560
5697
  internalValue,
5561
5698
  updateValue,
5562
5699
  disabled,
@@ -5566,21 +5703,24 @@ function RelationDialogBindingComponent({ propertyKey, internalValue, updateValu
5566
5703
  previewProperties: relProp.admin?.previewProperties,
5567
5704
  includeId: relProp.admin?.includeId,
5568
5705
  includeEntityLink: relProp.admin?.includeEntityLink,
5569
- title: relProp.name ?? propertyKey,
5570
5706
  fixedFilter: relProp.admin?.fixedFilter
5571
5707
  });
5572
5708
  }
5573
5709
  /** Stable component for relation fields rendered with the inline selector */
5574
- function RelationSelectorBindingComponent({ propertyKey, internalValue, updateValue, disabled, property, size }) {
5710
+ function RelationSelectorBindingComponent({ propertyKey, internalValue, updateValue, disabled, property, selected, open, onOpenChange, preview, anchorRef }) {
5575
5711
  const relProp = property;
5576
5712
  return /* @__PURE__ */ jsx(TableRelationSelectorField, {
5577
- size: getPreviewSizeFrom(size) === "small" ? "compact" : "medium",
5578
5713
  name: propertyKey,
5579
5714
  internalValue,
5580
5715
  updateValue,
5581
5716
  disabled,
5582
5717
  relation: relProp.relation,
5583
- fixedFilter: relProp.admin?.fixedFilter
5718
+ fixedFilter: relProp.admin?.fixedFilter,
5719
+ focused: selected,
5720
+ open,
5721
+ onOpenChange,
5722
+ preview,
5723
+ anchorRef
5584
5724
  });
5585
5725
  }
5586
5726
  //#endregion
@@ -5683,23 +5823,72 @@ function getZodStringSchema({ property, parentProperty, customFieldValidator, na
5683
5823
  const regExp = typeof validation.matches === "string" ? hydrateRegExp(validation.matches) : validation.matches;
5684
5824
  if (regExp) schema = schema.refine((value) => value == null || typeof value === "string" && regExp.test(value), { message: validation.matchesMessage ?? "Invalid format" });
5685
5825
  }
5686
- if (validation.trim) schema = z.preprocess((v) => typeof v === "string" ? v.trim() : v, schema);
5687
- if (validation.lowercase) schema = z.preprocess((v) => typeof v === "string" ? v.toLowerCase() : v, schema);
5688
- if (validation.uppercase) schema = z.preprocess((v) => typeof v === "string" ? v.toUpperCase() : v, schema);
5689
- if (property.email) schema = schema.refine((value) => value == null || typeof value === "string" && /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value), { message: `${fieldLabel$1(property, name)} must be an email` });
5690
- if (property.url) if (!property.storage || property.storage?.storeUrl) schema = schema.refine((value) => {
5691
- if (value == null) return true;
5692
- try {
5693
- new URL(value);
5694
- return true;
5695
- } catch {
5696
- return false;
5697
- }
5698
- }, { message: `${fieldLabel$1(property, name)} must be a url` });
5699
- else console.warn(`Property ${fieldLabel$1(property, name)} has a url validation but its storage configuration is not set to store urls`);
5826
+ if (validation.length !== void 0) schema = schema.refine((value) => value == null || typeof value === "string" && value.length === validation.length, { message: `${fieldLabel$1(property, name)} must be exactly ${validation.length} characters long` });
5827
+ if (validation.trim || validation.lowercase || validation.uppercase) schema = z.preprocess((v) => typeof v === "string" ? transformString(v, validation) : v, schema);
5700
5828
  }
5829
+ if (property.email) schema = schema.refine((value) => value == null || value === "" || typeof value === "string" && /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value), { message: `${fieldLabel$1(property, name)} must be an email` });
5830
+ if (property.url) if (!property.storage || property.storage?.storeUrl) schema = schema.refine((value) => {
5831
+ if (value == null || value === "") return true;
5832
+ try {
5833
+ new URL(value);
5834
+ return true;
5835
+ } catch {
5836
+ return false;
5837
+ }
5838
+ }, { message: `${fieldLabel$1(property, name)} must be a url` });
5839
+ else console.warn(`Property ${fieldLabel$1(property, name)} has a url validation but its storage configuration is not set to store urls`);
5701
5840
  return schema;
5702
5841
  }
5842
+ /**
5843
+ * A string's declared transforms — `trim`, `lowercase`, `uppercase` — applied.
5844
+ * The one definition of them: validation judges its result, and
5845
+ * {@link applyValueTransforms} writes it.
5846
+ */
5847
+ function transformString(value, validation) {
5848
+ let result = value;
5849
+ if (validation.trim) result = result.trim();
5850
+ if (validation.lowercase) result = result.toLowerCase();
5851
+ if (validation.uppercase) result = result.toUpperCase();
5852
+ return result;
5853
+ }
5854
+ /**
5855
+ * The values as they are to be written: every `trim`, `lowercase` and
5856
+ * `uppercase` a string property declares applied to its value, inside maps and
5857
+ * arrays too.
5858
+ *
5859
+ * They are documented as changing the value that is written, and only the
5860
+ * panel applies them — the server checks what it receives. Validation judges
5861
+ * the transformed value, so without this the form passed "my-slug" and sent
5862
+ * " My-Slug ", which a `matches` on the server then refused.
5863
+ *
5864
+ * Returns `values` itself when nothing changes.
5865
+ */
5866
+ function applyValueTransforms(values, properties) {
5867
+ let result = values;
5868
+ for (const [key, property] of Object.entries(properties)) {
5869
+ if (!(key in values)) continue;
5870
+ const transformed = transformValue(property, values[key]);
5871
+ if (transformed !== values[key]) result = {
5872
+ ...result,
5873
+ [key]: transformed
5874
+ };
5875
+ }
5876
+ return result;
5877
+ }
5878
+ function transformValue(property, value) {
5879
+ if (!property || typeof property !== "object") return value;
5880
+ if (property.type === "string") return typeof value === "string" && property.validation ? transformString(value, property.validation) : value;
5881
+ if (property.type === "map" && property.properties && isPlainObject(value)) return applyValueTransforms(value, property.properties);
5882
+ if (property.type === "array" && property.of && Array.isArray(value)) {
5883
+ const of = property.of;
5884
+ const items = value.map((item, index) => {
5885
+ const itemProperty = Array.isArray(of) ? of[index] : of;
5886
+ return itemProperty ? transformValue(itemProperty, item) : item;
5887
+ });
5888
+ return items.some((item, index) => item !== value[index]) ? items : value;
5889
+ }
5890
+ return value;
5891
+ }
5703
5892
  function getZodNumberSchema({ property, parentProperty, customFieldValidator, name, entityId }) {
5704
5893
  const validation = property.validation;
5705
5894
  let schema = z.preprocess((val) => {
@@ -5712,8 +5901,8 @@ function getZodNumberSchema({ property, parentProperty, customFieldValidator, na
5712
5901
  if (validation) {
5713
5902
  if (validation.min || validation.min === 0) schema = schema.refine((value) => value == null || typeof value === "number" && value >= validation.min, { message: `${fieldLabel$1(property, name)} must be higher or equal to ${validation.min}` });
5714
5903
  if (validation.max || validation.max === 0) schema = schema.refine((value) => value == null || typeof value === "number" && value <= validation.max, { message: `${fieldLabel$1(property, name)} must be lower or equal to ${validation.max}` });
5715
- if (validation.lessThan || validation.lessThan === 0) schema = schema.refine((value) => value == null || typeof value === "number" && value < validation.lessThan, { message: `${fieldLabel$1(property, name)} must be higher than ${validation.lessThan}` });
5716
- if (validation.moreThan || validation.moreThan === 0) schema = schema.refine((value) => value == null || typeof value === "number" && value > validation.moreThan, { message: `${fieldLabel$1(property, name)} must be lower than ${validation.moreThan}` });
5904
+ if (validation.lessThan || validation.lessThan === 0) schema = schema.refine((value) => value == null || typeof value === "number" && value < validation.lessThan, { message: `${fieldLabel$1(property, name)} must be lower than ${validation.lessThan}` });
5905
+ if (validation.moreThan || validation.moreThan === 0) schema = schema.refine((value) => value == null || typeof value === "number" && value > validation.moreThan, { message: `${fieldLabel$1(property, name)} must be higher than ${validation.moreThan}` });
5717
5906
  if (validation.positive) schema = schema.refine((value) => value == null || typeof value === "number" && value > 0, { message: `${fieldLabel$1(property, name)} must be positive` });
5718
5907
  if (validation.negative) schema = schema.refine((value) => value == null || typeof value === "number" && value < 0, { message: `${fieldLabel$1(property, name)} must be negative` });
5719
5908
  if (validation.integer) schema = schema.refine((value) => value == null || typeof value === "number" && Number.isInteger(value), { message: `${fieldLabel$1(property, name)} must be an integer` });
@@ -5733,7 +5922,7 @@ function getZodDateSchema({ property, parentProperty, customFieldValidator, name
5733
5922
  if (validation) {
5734
5923
  if (validation.required) schema = schema.refine((value) => value !== void 0 && value !== null, { message: validation?.requiredMessage ? validation.requiredMessage : "Required" });
5735
5924
  if (validation.min) schema = schema.refine((value) => value == null || value instanceof Date && value >= validation.min, { message: `${fieldLabel$1(property, name)} must be after ${validation.min}` });
5736
- if (validation.max) schema = schema.refine((value) => value == null || value instanceof Date && value <= validation.max, { message: `${fieldLabel$1(property, name)} must be before ${validation.min}` });
5925
+ if (validation.max) schema = schema.refine((value) => value == null || value instanceof Date && value <= validation.max, { message: `${fieldLabel$1(property, name)} must be before ${validation.max}` });
5737
5926
  }
5738
5927
  return schema;
5739
5928
  }
@@ -5773,30 +5962,30 @@ function hasUniqueInArrayModifier(property) {
5773
5962
  function getZodArraySchema({ property, parentProperty, customFieldValidator, name, entityId }) {
5774
5963
  let arraySchema = z.array(z.any()).nullable().optional();
5775
5964
  if (property.of) if (Array.isArray(property.of)) {
5776
- const zodProperties = {};
5777
- property.of.forEach((p, index) => {
5965
+ const positionSchemas = property.of.map((p, index) => {
5778
5966
  try {
5779
- zodProperties[`${name}[${index}]`] = mapPropertyToZod({
5967
+ return mapPropertyToZod({
5780
5968
  property: p,
5781
5969
  parentProperty: property,
5782
5970
  entityId
5783
5971
  });
5784
5972
  } catch (e) {
5785
5973
  console.error(`Error creating validation schema for array item ${index}:`, e);
5786
- zodProperties[`${name}[${index}]`] = z.any().refine(() => false, { message: `Validation error: ${e instanceof Error ? e.message : "Unknown error"}` });
5974
+ return z.any().refine(() => false, { message: `Validation error: ${e instanceof Error ? e.message : "Unknown error"}` });
5787
5975
  }
5788
5976
  });
5789
- arraySchema = z.array(z.any().superRefine(async (object, ctx) => {
5790
- for (const [key, zodProperty] of Object.entries(zodProperties)) if (zodProperty) {
5791
- const result = await zodProperty.safeParseAsync(object);
5977
+ arraySchema = z.array(z.any()).superRefine(async (items, ctx) => {
5978
+ for (let index = 0; index < items.length && index < positionSchemas.length; index++) {
5979
+ const result = await positionSchemas[index].safeParseAsync(items[index]);
5792
5980
  if (!result.success) result.error.issues.forEach((issue) => {
5793
5981
  ctx.addIssue({
5794
5982
  code: "custom",
5795
- message: issue.message
5983
+ message: issue.message,
5984
+ path: [index, ...issue.path]
5796
5985
  });
5797
5986
  });
5798
5987
  }
5799
- })).nullable().optional();
5988
+ }).nullable().optional();
5800
5989
  } else {
5801
5990
  try {
5802
5991
  const ofSchema = mapPropertyToZod({
@@ -5843,6 +6032,53 @@ function getZodBinarySchema({ property }) {
5843
6032
  return schema;
5844
6033
  }
5845
6034
  //#endregion
6035
+ //#region src/components/CollectionTableBinding/internal/TableCellOpener.tsx
6036
+ /**
6037
+ * The one control a cell reveals to open its editor.
6038
+ *
6039
+ * A cell looks the same at rest, hovered and selected: the value, in the same
6040
+ * place. This is the only thing that appears — on hover, and for as long as
6041
+ * the cell is selected — in a slot the cell keeps for it whether it is shown
6042
+ * or not, so revealing it moves nothing.
6043
+ *
6044
+ * One click on it opens the editor, from rest: it selects the cell on the way.
6045
+ * It never takes the focus on a mouse press, because the cell selects itself
6046
+ * on focus and doing so first would re-render the cell under the pointer.
6047
+ */
6048
+ function TableCellOpener({ kind, open, selected, focusOnSelect, label, onToggle }) {
6049
+ const ref = useRef(null);
6050
+ const openAtPressRef = useRef(void 0);
6051
+ useEffect(() => {
6052
+ if (selected && focusOnSelect) ref.current?.focus({ preventScroll: true });
6053
+ }, [selected, focusOnSelect]);
6054
+ const Icon = kind === "calendar" ? CalendarIcon : kind === "expand" ? Maximize2Icon : ChevronDownIcon;
6055
+ return /* @__PURE__ */ jsx(IconButton, {
6056
+ ref,
6057
+ size: "smallest",
6058
+ shape: "square",
6059
+ "aria-label": label,
6060
+ "aria-haspopup": kind === "calendar" || kind === "expand" ? "dialog" : "listbox",
6061
+ "aria-expanded": kind === "dropdown" ? open : void 0,
6062
+ tabIndex: -1,
6063
+ "data-table-cell-opener": kind,
6064
+ className: cls("w-6 !h-6 min-w-6 min-h-6 p-0 hover:scale-100", "transition-opacity duration-100", selected || open ? "opacity-100" : "opacity-0 group-hover/cell:opacity-100 focus-visible:opacity-100"),
6065
+ onPointerDown: () => {
6066
+ openAtPressRef.current = open;
6067
+ },
6068
+ onMouseDown: (event) => event.preventDefault(),
6069
+ onClick: (event) => {
6070
+ event.stopPropagation();
6071
+ const wasOpen = openAtPressRef.current ?? open;
6072
+ openAtPressRef.current = void 0;
6073
+ onToggle(!wasOpen);
6074
+ },
6075
+ children: /* @__PURE__ */ jsx(Icon, {
6076
+ size: iconSize.smallest,
6077
+ className: cls(kind === "dropdown" || kind === "dialog" ? "transition-transform duration-150" : "", open && (kind === "dropdown" || kind === "dialog") ? "rotate-180" : "")
6078
+ })
6079
+ });
6080
+ }
6081
+ //#endregion
5846
6082
  //#region src/components/CollectionTableBinding/internal/EntityTableCell.tsx
5847
6083
  var TableCellInner = ({ justifyContent, scrollable, faded, fullHeight, children }) => {
5848
6084
  return /* @__PURE__ */ jsx("div", {
@@ -5857,9 +6093,10 @@ var TableCellInner = ({ justifyContent, scrollable, faded, fullHeight, children
5857
6093
  children
5858
6094
  });
5859
6095
  };
5860
- var EntityTableCell = React.memo(function EntityTableCell({ children, actions, size, selected, disabled, disabledTooltip, savedTimestamp, error, align, allowScroll, removePadding, fullHeight, onSelect, width, hideOverflow = true, showExpandIcon = true, sortableNodeRef, sortableStyle, sortableAttributes, isDragging, isDraggable, frozen }) {
6096
+ var EntityTableCell = React.memo(function EntityTableCell({ children, actions, size, selected, disabled, disabledTooltip, savedTimestamp, error, align, allowScroll, removePadding, fullHeight, opener, cellRef, onSelect, width, hideOverflow = true, showExpandIcon = true, sortableNodeRef, sortableStyle, sortableAttributes, isDragging, isDraggable, frozen }) {
5861
6097
  const [measureRef, bounds] = useMeasure();
5862
- const ref = useRef(null);
6098
+ const internalRef = useRef(null);
6099
+ const ref = cellRef ?? internalRef;
5863
6100
  const maxHeight = useMemo(() => getRowHeight(size), [size]);
5864
6101
  const [onHover, setOnHover] = useState(false);
5865
6102
  const [showSaved, setShowSaved] = useState(false);
@@ -5909,6 +6146,16 @@ var EntityTableCell = React.memo(function EntityTableCell({ children, actions, s
5909
6146
  selected,
5910
6147
  disabled
5911
6148
  ]);
6149
+ const onOpenerToggle = useCallback((open) => {
6150
+ if (!opener) return;
6151
+ const cellRect = ref.current?.getBoundingClientRect();
6152
+ if (open) onSelectCallback();
6153
+ opener.onToggle(open, cellRect);
6154
+ }, [
6155
+ opener,
6156
+ onSelectCallback,
6157
+ ref
6158
+ ]);
5912
6159
  const onFocus = useCallback((event) => {
5913
6160
  event.stopPropagation();
5914
6161
  event.preventDefault();
@@ -5924,8 +6171,24 @@ var EntityTableCell = React.memo(function EntityTableCell({ children, actions, s
5924
6171
  const setOnHoverTrue = useCallback(() => setOnHover(true), []);
5925
6172
  const setOnHoverFalse = useCallback(() => setOnHover(false), []);
5926
6173
  const borderClass = showError ? "border-red-500" : isSelected ? "border-primary" : "border-transparent";
6174
+ const content = /* @__PURE__ */ jsx(RowEntityPreviewLayout, { children: /* @__PURE__ */ jsxs(ErrorBoundary, { children: [fullHeight && !faded && children, (!fullHeight || faded) && /* @__PURE__ */ jsx(TableCellInner, {
6175
+ fullHeight: fullHeight ?? false,
6176
+ justifyContent,
6177
+ scrollable: scrollable ?? false,
6178
+ faded,
6179
+ children: !fullHeight && /* @__PURE__ */ jsx("div", {
6180
+ ref: measureRef,
6181
+ style: {
6182
+ display: "flex",
6183
+ width: "100%",
6184
+ justifyContent,
6185
+ height: fullHeight ? "100%" : void 0
6186
+ },
6187
+ children
6188
+ })
6189
+ })] }) });
5927
6190
  const result = /* @__PURE__ */ jsx(Fragment, { children: /* @__PURE__ */ jsxs("div", {
5928
- className: cls("transition-colors duration-500", `flex relative h-full rounded-md p-${p} border-4`, showSaved ? "bg-primary/20 dark:bg-primary/20" : onHover && !disabled ? "bg-surface-hover" : "", hideOverflow ? "overflow-hidden" : "", isSelected && !showSaved ? "bg-surface-raised" : "", borderClass),
6191
+ className: cls("group/cell transition-colors duration-500", `flex relative h-full rounded-md p-${p} border-4`, showSaved ? "bg-primary/20 dark:bg-primary/20" : onHover && !disabled ? "bg-surface-hover" : "", hideOverflow ? "overflow-hidden" : "", isSelected && !showSaved ? "bg-surface-raised" : "", borderClass),
5929
6192
  ref,
5930
6193
  style: {
5931
6194
  justifyContent,
@@ -5938,22 +6201,28 @@ var EntityTableCell = React.memo(function EntityTableCell({ children, actions, s
5938
6201
  onMouseEnter: setOnHoverTrue,
5939
6202
  onMouseMove: setOnHoverTrue,
5940
6203
  onMouseLeave: setOnHoverFalse,
5941
- children: [/* @__PURE__ */ jsxs(ErrorBoundary, { children: [fullHeight && !faded && children, (!fullHeight || faded) && /* @__PURE__ */ jsx(TableCellInner, {
5942
- fullHeight: fullHeight ?? false,
5943
- justifyContent,
5944
- scrollable: scrollable ?? false,
5945
- faded,
5946
- children: !fullHeight && /* @__PURE__ */ jsx("div", {
5947
- ref: measureRef,
6204
+ children: [
6205
+ opener ? /* @__PURE__ */ jsx("div", {
6206
+ className: "flex flex-1 min-w-0 self-stretch",
5948
6207
  style: {
5949
- display: "flex",
5950
- width: "100%",
5951
6208
  justifyContent,
5952
- height: fullHeight ? "100%" : void 0
6209
+ alignItems: disabled || !isOverflowing ? "center" : void 0
5953
6210
  },
5954
- children
5955
- })
5956
- })] }), actions]
6211
+ children: content
6212
+ }) : content,
6213
+ opener && /* @__PURE__ */ jsx("div", {
6214
+ className: "shrink-0 self-center flex items-center pl-1",
6215
+ children: /* @__PURE__ */ jsx(TableCellOpener, {
6216
+ kind: opener.kind,
6217
+ open: opener.open,
6218
+ selected: Boolean(selected),
6219
+ focusOnSelect: opener.focusOnSelect,
6220
+ label: opener.label,
6221
+ onToggle: onOpenerToggle
6222
+ })
6223
+ }),
6224
+ actions
6225
+ ]
5957
6226
  }) });
5958
6227
  const { tabIndex: _tabIndex, ...sortableAttrsWithoutTabIndex } = sortableAttributes ?? {};
5959
6228
  return sortableNodeRef ? /* @__PURE__ */ jsx("div", {
@@ -5974,7 +6243,7 @@ var EntityTableCell = React.memo(function EntityTableCell({ children, actions, s
5974
6243
  children: result
5975
6244
  }) : result;
5976
6245
  }, (a, b) => {
5977
- return a.error === b.error && a.value === b.value && a.disabled === b.disabled && a.savedTimestamp === b.savedTimestamp && a.allowScroll === b.allowScroll && a.align === b.align && a.size === b.size && a.disabledTooltip === b.disabledTooltip && a.width === b.width && a.showExpandIcon === b.showExpandIcon && a.removePadding === b.removePadding && a.fullHeight === b.fullHeight && a.selected === b.selected && a.isDragging === b.isDragging && a.isDraggable === b.isDraggable && a.frozen === b.frozen;
6246
+ return a.error === b.error && a.value === b.value && a.disabled === b.disabled && a.savedTimestamp === b.savedTimestamp && a.allowScroll === b.allowScroll && a.align === b.align && a.size === b.size && a.disabledTooltip === b.disabledTooltip && a.width === b.width && a.showExpandIcon === b.showExpandIcon && a.removePadding === b.removePadding && a.fullHeight === b.fullHeight && a.selected === b.selected && a.opener?.kind === b.opener?.kind && a.opener?.open === b.opener?.open && a.opener?.focusOnSelect === b.opener?.focusOnSelect && a.opener?.label === b.opener?.label && a.isDragging === b.isDragging && a.isDraggable === b.isDraggable && a.frozen === b.frozen;
5978
6247
  });
5979
6248
  //#endregion
5980
6249
  //#region src/components/SelectableTable/SelectableTableContext.tsx
@@ -5984,6 +6253,7 @@ var useSelectableTableController = () => useContext(SelectableTableContext);
5984
6253
  //#region src/components/CollectionTableBinding/PropertyTableCell.tsx
5985
6254
  var PropertyTableCell = React.memo(function PropertyTableCell({ propertyKey, customFieldValidator, value, property, align, width, height, path, entity, readonly, disabled: disabledProp, enablePopupIcon = true, sortableNodeRef, sortableStyle, sortableAttributes, isDragging, isDraggable, frozen }) {
5986
6255
  const { onValueChange, size, selectionStore, select, setPopupCell } = useSelectableTableController();
6256
+ const { t } = useTranslation();
5987
6257
  const dummySelectionStore = useMemo(() => createSelectionStore(), []);
5988
6258
  const selected = useCellSelected(selectionStore || dummySelectionStore, propertyKey, entity.path, entity.id);
5989
6259
  const [internalValue, setInternalValue] = useState(value);
@@ -6017,15 +6287,29 @@ var PropertyTableCell = React.memo(function PropertyTableCell({ propertyKey, cus
6017
6287
  onValueUpdated();
6018
6288
  }
6019
6289
  }, [onValueUpdated, value]);
6290
+ const toWrittenValue = useCallback((typed) => applyValueTransforms({ value: typed }, { value: property }).value, [property]);
6291
+ const [transformedWrite, setTransformedWrite] = useState();
6292
+ useEffect(() => {
6293
+ if (selected || !transformedWrite) return;
6294
+ if (deepEqual(toWrittenValue(internalValue), transformedWrite.value)) setInternalValue(transformedWrite.value);
6295
+ setTransformedWrite(void 0);
6296
+ }, [
6297
+ selected,
6298
+ transformedWrite,
6299
+ internalValue,
6300
+ toWrittenValue
6301
+ ]);
6020
6302
  const saveValues = async (value) => {
6021
- if (deepEqual(value, internalValueRef.current)) return;
6303
+ const writtenValue = toWrittenValue(value);
6304
+ if (deepEqual(writtenValue, internalValueRef.current)) return;
6022
6305
  const result = await validation.safeParseAsync(value);
6023
6306
  if (result.success) {
6024
6307
  setValidationError(void 0);
6025
- internalValueRef.current = value;
6308
+ internalValueRef.current = writtenValue;
6309
+ if (!deepEqual(writtenValue, value)) setTransformedWrite({ value: writtenValue });
6026
6310
  if (onValueChange) try {
6027
6311
  onValueChange({
6028
- value,
6312
+ value: writtenValue,
6029
6313
  propertyKey,
6030
6314
  setError,
6031
6315
  onValueUpdated,
@@ -6056,11 +6340,6 @@ var PropertyTableCell = React.memo(function PropertyTableCell({ propertyKey, cus
6056
6340
  setInternalValue(updatedValue);
6057
6341
  saveValues(updatedValue);
6058
6342
  };
6059
- useClearRestoreValue({
6060
- property,
6061
- value: internalValue,
6062
- setValue: updateValue
6063
- });
6064
6343
  const onSelect = useCallback((cellRect) => {
6065
6344
  if (!cellRect) select(void 0);
6066
6345
  else select({
@@ -6091,6 +6370,39 @@ var PropertyTableCell = React.memo(function PropertyTableCell({ propertyKey, cus
6091
6370
  });
6092
6371
  };
6093
6372
  const tableBinding = useMemo(() => getTableBindingForProperty(property, selected), [property, selected]);
6373
+ const editorBinding = useMemo(() => getTableBindingForProperty(property, true), [property]);
6374
+ const [editorOpen, setEditorOpen] = useState(false);
6375
+ useEffect(() => {
6376
+ if (!selected) setEditorOpen(false);
6377
+ }, [selected]);
6378
+ const cellRef = useRef(null);
6379
+ const preview = /* @__PURE__ */ jsx(PropertyPreview, {
6380
+ width,
6381
+ height,
6382
+ propertyKey,
6383
+ value: internalValue,
6384
+ property,
6385
+ size: getPreviewSizeFrom(size)
6386
+ });
6387
+ let openerKind;
6388
+ if (!disabled && !readOnlyProperty) {
6389
+ if (!customField && editorBinding) openerKind = editorBinding.opener;
6390
+ else if (enablePopupIcon && setPopupCell) openerKind = "expand";
6391
+ }
6392
+ const onOpenerToggleRef = useRef(() => void 0);
6393
+ onOpenerToggleRef.current = (open, cellRect) => {
6394
+ if (openerKind === "expand") {
6395
+ if (open) openPopup(cellRect);
6396
+ } else setEditorOpen(open);
6397
+ };
6398
+ const onOpenerToggle = useCallback((open, cellRect) => onOpenerToggleRef.current(open, cellRect), []);
6399
+ const opener = openerKind ? {
6400
+ kind: openerKind,
6401
+ open: editorOpen,
6402
+ focusOnSelect: openerKind !== "dropdown",
6403
+ label: openerKind === "expand" ? t("expand") : t("edit_name", { name: property.name ?? propertyKey }),
6404
+ onToggle: onOpenerToggle
6405
+ } : void 0;
6094
6406
  let innerComponent;
6095
6407
  let allowScroll = false;
6096
6408
  let showExpandIcon = false;
@@ -6139,7 +6451,11 @@ var PropertyTableCell = React.memo(function PropertyTableCell({ propertyKey, cus
6139
6451
  align,
6140
6452
  entity,
6141
6453
  path,
6142
- openPopup: setPopupCell ? openPopup : void 0
6454
+ openPopup: setPopupCell ? openPopup : void 0,
6455
+ open: editorOpen,
6456
+ onOpenChange: setEditorOpen,
6457
+ preview,
6458
+ anchorRef: cellRef
6143
6459
  });
6144
6460
  allowScroll = tableBinding.allowScroll ?? false;
6145
6461
  includeActions = tableBinding.includeActions ?? true;
@@ -6151,15 +6467,7 @@ var PropertyTableCell = React.memo(function PropertyTableCell({ propertyKey, cus
6151
6467
  }
6152
6468
  if (!innerComponent) {
6153
6469
  allowScroll = false;
6154
- showExpandIcon = enablePopupIcon && selected && !innerComponent && !disabled && !readOnlyProperty;
6155
- innerComponent = /* @__PURE__ */ jsx(PropertyPreview, {
6156
- width,
6157
- height,
6158
- propertyKey,
6159
- value: internalValue,
6160
- property,
6161
- size: getPreviewSizeFrom(size)
6162
- });
6470
+ innerComponent = preview;
6163
6471
  }
6164
6472
  return /* @__PURE__ */ jsx(EntityTableCell, {
6165
6473
  size,
@@ -6177,6 +6485,8 @@ var PropertyTableCell = React.memo(function PropertyTableCell({ propertyKey, cus
6177
6485
  showExpandIcon,
6178
6486
  value: internalValue,
6179
6487
  hideOverflow,
6488
+ opener,
6489
+ cellRef,
6180
6490
  sortableNodeRef,
6181
6491
  sortableStyle,
6182
6492
  sortableAttributes,
@@ -6194,7 +6504,7 @@ var PropertyTableCell = React.memo(function PropertyTableCell({ propertyKey, cus
6194
6504
  }, `cell_${propertyKey}_${entity.path}_${entity.id}`);
6195
6505
  }, areEqual);
6196
6506
  function areEqual(prevProps, nextProps) {
6197
- return prevProps.height === nextProps.height && prevProps.propertyKey === nextProps.propertyKey && prevProps.align === nextProps.align && prevProps.width === nextProps.width && deepEqual(prevProps.property, nextProps.property) && deepEqual(prevProps.value, nextProps.value) && prevProps.entity.id === nextProps.entity.id && prevProps.entity.path === nextProps.entity.path && prevProps.isDragging === nextProps.isDragging && prevProps.isDraggable === nextProps.isDraggable && prevProps.frozen === nextProps.frozen;
6507
+ return prevProps.height === nextProps.height && prevProps.propertyKey === nextProps.propertyKey && prevProps.align === nextProps.align && prevProps.width === nextProps.width && deepEqual(prevProps.property, nextProps.property) && deepEqual(prevProps.value, nextProps.value) && prevProps.entity.id === nextProps.entity.id && prevProps.entity.path === nextProps.entity.path && (prevProps.entity === nextProps.entity || deepEqual(prevProps.entity.values, nextProps.entity.values)) && prevProps.isDragging === nextProps.isDragging && prevProps.isDraggable === nextProps.isDraggable && prevProps.frozen === nextProps.frozen;
6198
6508
  }
6199
6509
  //#endregion
6200
6510
  //#region src/contexts/SideDialogsControllerContext.tsx
@@ -6285,15 +6595,43 @@ var CollectionRowActions = function CollectionRowActions({ entity, collection, p
6285
6595
  const context = useAdminContext();
6286
6596
  const sidePanelCtrl = context.sidePanelController;
6287
6597
  const { t } = useTranslation();
6598
+ const slotActions = useSlot("entity.row.actions", React.useMemo(() => ({
6599
+ entity,
6600
+ entityId: entity.id,
6601
+ path,
6602
+ collection,
6603
+ selectionController,
6604
+ context
6605
+ }), [
6606
+ entity,
6607
+ path,
6608
+ collection,
6609
+ selectionController,
6610
+ context
6611
+ ]));
6288
6612
  const onCheckedChange = useCallback((checked) => {
6289
6613
  selectionController?.toggleEntitySelection(entity, checked);
6290
6614
  }, [entity, selectionController?.toggleEntitySelection]);
6291
- const hasActions = actions.length > 0;
6615
+ const hasActions = actions.length > 0 || slotActions.length > 0;
6292
6616
  const hasCollapsedActions = actions.some((a) => a.collapsed || a.collapsed === void 0);
6293
6617
  const collapsedActions = actions.filter((a) => a.collapsed || a.collapsed === void 0);
6294
6618
  const uncollapsedActions = actions.filter((a) => a.collapsed === false);
6295
6619
  const hasDraft = (collection ? getLocalChangesBackup(collection) : false) ? getEntityFromCache(path + "/" + entity.id) : false;
6296
6620
  const iconSize = "small";
6621
+ const clickProps = {
6622
+ view: "collection",
6623
+ entity,
6624
+ path,
6625
+ collection,
6626
+ context,
6627
+ sidePanelController: sidePanelCtrl,
6628
+ selectionController,
6629
+ highlightEntity,
6630
+ unhighlightEntity,
6631
+ onCollectionChange,
6632
+ openEntityMode: openEntityMode ?? collection?.openEntityMode
6633
+ };
6634
+ const isActionEnabled = (action) => !action.isEnabled || action.isEnabled(clickProps);
6297
6635
  const content = /* @__PURE__ */ jsxs("div", {
6298
6636
  className: cls("h-full flex items-center justify-center flex-col z-10", isSelected ? "bg-surface-raised" : "bg-surface-card group-hover:bg-surface-card-hover", frozen ? "sticky left-0" : ""),
6299
6637
  onClick: useCallback((event) => {
@@ -6312,21 +6650,10 @@ var CollectionRowActions = function CollectionRowActions({ entity, collection, p
6312
6650
  const isEditAction = action.key === "edit";
6313
6651
  const tooltip = isEditAction && hasDraft ? t("unsaved_local_changes") : action.name;
6314
6652
  let iconButton = /* @__PURE__ */ jsx(IconButton, {
6653
+ disabled: !isActionEnabled(action),
6315
6654
  onClick: (event) => {
6316
6655
  event.stopPropagation();
6317
- action.onClick({
6318
- view: "collection",
6319
- entity,
6320
- path,
6321
- collection,
6322
- context,
6323
- sidePanelController: sidePanelCtrl,
6324
- selectionController,
6325
- highlightEntity,
6326
- unhighlightEntity,
6327
- onCollectionChange,
6328
- openEntityMode: openEntityMode ?? collection?.openEntityMode
6329
- });
6656
+ action.onClick(clickProps);
6330
6657
  },
6331
6658
  size: iconSize,
6332
6659
  children: getIcon(action.icon, void 0, void 0, "smallest")
@@ -6341,27 +6668,17 @@ var CollectionRowActions = function CollectionRowActions({ entity, collection, p
6341
6668
  children: iconButton
6342
6669
  }, index);
6343
6670
  }),
6671
+ slotActions,
6344
6672
  hasCollapsedActions && /* @__PURE__ */ jsx(Menu, {
6345
6673
  trigger: /* @__PURE__ */ jsx(IconButton, {
6346
6674
  size: iconSize,
6347
6675
  children: /* @__PURE__ */ jsx(MoreVerticalIcon, {})
6348
6676
  }),
6349
6677
  children: collapsedActions.map((action, index) => /* @__PURE__ */ jsxs(MenuItem, {
6678
+ disabled: !isActionEnabled(action),
6350
6679
  onClick: (e) => {
6351
6680
  e.stopPropagation();
6352
- action.onClick({
6353
- view: "collection",
6354
- entity,
6355
- path,
6356
- collection,
6357
- context,
6358
- sidePanelController: sidePanelCtrl,
6359
- selectionController,
6360
- highlightEntity,
6361
- unhighlightEntity,
6362
- onCollectionChange,
6363
- openEntityMode: openEntityMode ?? collection?.openEntityMode
6364
- });
6681
+ action.onClick(clickProps);
6365
6682
  },
6366
6683
  children: [getIcon(action.icon, void 0, void 0, "smallest"), action.name]
6367
6684
  }, index))
@@ -6623,6 +6940,7 @@ function StringNumberFilterField({ name, value, setValue, type, isArray, enumVal
6623
6940
  const [operation, setOperation] = useState(fieldOperation === "==" && fieldValue === null ? "is-null" : fieldOperation === "!=" && fieldValue === null ? "is-not-null" : fieldOperation);
6624
6941
  const [internalValue, setInternalValue] = useState(containsOperations.includes(fieldOperation) ? unwrapContains(fieldValue) : fieldValue);
6625
6942
  const isNullOperation = operation === "is-null" || operation === "is-not-null";
6943
+ const hasSingleValue = internalValue !== void 0 && internalValue !== null && !Array.isArray(internalValue);
6626
6944
  if (possibleOperations.length === 0) return null;
6627
6945
  function updateFilter(op, val) {
6628
6946
  if (op === "is-null" || op === "is-not-null") {
@@ -6696,11 +7014,11 @@ function StringNumberFilterField({ name, value, setValue, type, isArray, enumVal
6696
7014
  position: "item-aligned",
6697
7015
  fullWidth: true,
6698
7016
  disabled: isNullOperation,
6699
- value: typeof internalValue === "string" ? internalValue : "",
7017
+ value: hasSingleValue ? String(internalValue) : "",
6700
7018
  onValueChange: (value) => {
6701
7019
  if (value !== "") updateFilter(operation, type === "number" ? parseInt(value) : value);
6702
7020
  },
6703
- endAdornment: internalValue && /* @__PURE__ */ jsx(IconButton, {
7021
+ endAdornment: hasSingleValue && /* @__PURE__ */ jsx(IconButton, {
6704
7022
  onClick: (e) => updateFilter(operation, void 0),
6705
7023
  children: /* @__PURE__ */ jsx(XIcon, {})
6706
7024
  }),
@@ -7303,7 +7621,8 @@ function DefaultFilterField({ propertyKey, property, isArray, operators, value,
7303
7621
  value,
7304
7622
  setValue,
7305
7623
  name: propertyKey,
7306
- mode: property.mode,
7624
+ mode: getDatePropertyMode(property),
7625
+ timezone: getDatePropertyTimezone(property),
7307
7626
  isArray,
7308
7627
  operators,
7309
7628
  title
@@ -7376,7 +7695,10 @@ var SelectableTable = function SelectableTable({ onValueChange, cellRenderer, on
7376
7695
  }, Boolean(localSelectedCell));
7377
7696
  useEffect(() => {
7378
7697
  const escFunction = (event) => {
7379
- if (event.key === "Escape") unselect();
7698
+ if (event.key !== "Escape") return;
7699
+ const target = event.target;
7700
+ if (target instanceof Node && target !== document.body && ref.current && !ref.current.contains(target)) return;
7701
+ unselect();
7380
7702
  };
7381
7703
  document.addEventListener("keydown", escFunction, false);
7382
7704
  return () => {
@@ -8008,7 +8330,9 @@ function useCollectionInlineEditor({ path, collection, dataClient, context }) {
8008
8330
  }, [path, dataClient]);
8009
8331
  const onValueChange = useCallback(({ value, propertyKey, onValueUpdated, setError, data: entity }) => {
8010
8332
  if (!entity) return;
8011
- const updatedValues = setIn({}, propertyKey, value);
8333
+ const column = propertyKey.split(/[.[]/)[0];
8334
+ const updatedRow = setIn(entity.values ?? {}, propertyKey, value);
8335
+ const updatedValues = { [column]: getIn(updatedRow, column) };
8012
8336
  return saveEntityWithCallbacks({
8013
8337
  path: entity.path ?? path,
8014
8338
  entityId: entity.id,
@@ -8084,40 +8408,25 @@ function getViewModeFromSearch(search, customKeys) {
8084
8408
  return isViewMode(value, customKeys) ? value : null;
8085
8409
  }
8086
8410
  /**
8087
- * Append the active view mode to a URL so the target route keeps rendering the
8088
- * view the user was last looking at.
8089
- *
8090
- * Every entity/collection navigation must go through this — a missed call site
8091
- * silently resets the user's view mode mid-session.
8092
- *
8093
- * @param url the URL to decorate. May already contain a query string and/or hash.
8094
- * @param search query string to read the view mode from. Defaults to the
8095
- * current browser location.
8096
- */
8097
- function withViewMode(url, search) {
8098
- const source = search ?? (typeof window !== "undefined" ? window.location.search : "");
8099
- const viewMode = new URLSearchParams(source).get(VIEW_MODE_PARAM);
8100
- if (!viewMode) return url;
8101
- const hashIndex = url.indexOf("#");
8102
- const hash = hashIndex >= 0 ? url.substring(hashIndex) : "";
8103
- const base = hashIndex >= 0 ? url.substring(0, hashIndex) : url;
8104
- if (new URLSearchParams(base.split("?")[1] ?? "").has("__view")) return url;
8105
- return `${base}${base.includes("?") ? "&" : "?"}${VIEW_MODE_PARAM}=${viewMode}${hash}`;
8106
- }
8107
- /**
8108
8411
  * Carry the whole of the collection list's URL state onto a navigation target.
8109
8412
  *
8110
- * `withViewMode` exists because losing the view mode mid-session is jarring.
8111
- * Everything else the list writes to the URL — the search string, the filters,
8112
- * the sort — is state the user built up deliberately, and losing *that* is
8113
- * worse: opening a record from a search and pressing back returned an
8114
- * unfiltered list with an empty search box, so the search had to be retyped
8115
- * for every record they wanted to look at.
8413
+ * Every navigation to the collection or one of its records goes through this —
8414
+ * a missed call site silently resets the list mid-session. The list reads its
8415
+ * state back from the URL on each navigation, so the view mode, the search
8416
+ * string, the filters and the sort all have to travel: a URL without them
8417
+ * returned an unfiltered list with an empty search box, and the search had to
8418
+ * be retyped for every record the user wanted to look at.
8116
8419
  *
8117
8420
  * Copies every param the list currently has rather than an allowlist, because
8118
8421
  * filters are encoded under the field's own name — there is no fixed set to
8119
8422
  * list. Params already on the target win: the caller is more specific.
8120
8423
  *
8424
+ * Carries whatever `__view` holds rather than validating it. These call sites
8425
+ * are navigations with no collection in hand, so they cannot know whether
8426
+ * `map` is one of *this* collection's custom views; checking against the
8427
+ * built-ins here would drop every custom view on the first record click. The
8428
+ * binding validates on read, which is where the answer is knowable.
8429
+ *
8121
8430
  * @param url the URL to decorate. May carry a query string and/or a hash.
8122
8431
  * @param search query string to read from. Defaults to the browser location.
8123
8432
  */
@@ -8170,7 +8479,7 @@ function resolveOpenEntityMode({ collection, viewMode, customView }) {
8170
8479
  * Navigate to a entity using either a side panel or full-screen mode.
8171
8480
  * This is an admin-specific UI concern and lives here (not in common).
8172
8481
  */
8173
- function navigateToEntity({ openEntityMode, collection, entityId, copy, path, selectedTab, defaultValues, sidePanelController, onClose, navigation, replace }) {
8482
+ function navigateToEntity({ openEntityMode, collection, entityId, copy, path, selectedTab, defaultValues, sidePanelController, onClose, onUpdate, navigation, replace }) {
8174
8483
  if (openEntityMode === "side_panel" || openEntityMode === "dialog") sidePanelController.open({
8175
8484
  entityId,
8176
8485
  path,
@@ -8179,6 +8488,7 @@ function navigateToEntity({ openEntityMode, collection, entityId, copy, path, se
8179
8488
  collection,
8180
8489
  updateUrl: openEntityMode !== "dialog",
8181
8490
  onClose,
8491
+ onUpdate,
8182
8492
  defaultValues
8183
8493
  });
8184
8494
  else {
@@ -8194,6 +8504,14 @@ function navigateToEntity({ openEntityMode, collection, entityId, copy, path, se
8194
8504
  navigation.navigate(to, hasOptions ? navigateOptions : void 0);
8195
8505
  }
8196
8506
  }
8507
+ /**
8508
+ * Whether `pathname` is the record at `entityPath` or somewhere below it — one
8509
+ * of its tabs, its `/edit`. A path segment, not a string prefix:
8510
+ * `/c/products/12` is another record, not a place inside `/c/products/1`.
8511
+ */
8512
+ function isPathWithinRecord(pathname, entityPath) {
8513
+ return pathname === entityPath || pathname.startsWith(`${entityPath}/`);
8514
+ }
8197
8515
  //#endregion
8198
8516
  //#region src/util/entity_actions.ts
8199
8517
  var reservedKeys = [
@@ -8703,60 +9021,273 @@ function getWorksheetHeaders(headerRow) {
8703
9021
  order
8704
9022
  };
8705
9023
  }
8706
- //#endregion
8707
- //#region src/data_import/utils/transforms.ts
9024
+ //#endregion
9025
+ //#region src/data_import/utils/transforms.ts
9026
+ /**
9027
+ * Take an object with keys of type `address.street`, `address.city` and
9028
+ * convert it to an object with nested objects like `{ address: { street: ..., city: ... } }`
9029
+ *
9030
+ * Keys here are the **header row of an uploaded file**, so they are attacker
9031
+ * data. `res["__proto__"] = …` is the prototype setter rather than an own
9032
+ * property, so a column named `__proto__.polluted` walked out of the accumulator
9033
+ * and wrote onto `Object.prototype` for the life of the tab — and
9034
+ * `constructor.prototype.x` threw a `TypeError` that surfaced as an unreadable
9035
+ * file. Such a column is refused, not sanitised, the way `setIn` settled it
9036
+ * (`docs/bug-classes.md` class 22).
9037
+ *
9038
+ * @param flatObj
9039
+ */
9040
+ function unflattenObject(flatObj) {
9041
+ return Object.keys(flatObj).reduce((nestedObj, key) => {
9042
+ if (pathTraversesPrototype(key)) {
9043
+ console.warn(`Skipping column "${key}": a header may not reach the prototype chain`);
9044
+ return nestedObj;
9045
+ }
9046
+ let currentObj = nestedObj;
9047
+ const keyParts = key.split(".");
9048
+ keyParts.forEach((keyPart, i) => {
9049
+ if (/^[\w]+\[\d+\]$/.test(keyPart)) {
9050
+ const mainPropertyName = keyPart.slice(0, keyPart.indexOf("["));
9051
+ const index = parseInt(keyPart.slice(keyPart.indexOf("[") + 1, keyPart.indexOf("]")));
9052
+ if (!currentObj[mainPropertyName]) currentObj[mainPropertyName] = [];
9053
+ if (i !== keyParts.length - 1) {
9054
+ currentObj[mainPropertyName][index] = currentObj[mainPropertyName][index] || {};
9055
+ currentObj = currentObj[mainPropertyName][index];
9056
+ } else currentObj[mainPropertyName][index] = flatObj[key];
9057
+ } else if (i !== keyParts.length - 1) {
9058
+ currentObj[keyPart] = currentObj[keyPart] || {};
9059
+ currentObj = currentObj[keyPart];
9060
+ } else currentObj[keyPart] = flatObj[key];
9061
+ });
9062
+ return nestedObj;
9063
+ }, {});
9064
+ }
9065
+ /**
9066
+ * Read a text cell as the number, boolean or array it spells, when it spells
9067
+ * one exactly — so the type inference sees `12`, `true` and the export's
9068
+ * `["a","b"]` for what they are.
9069
+ *
9070
+ * Only a cell that is the canonical JSON of its value is read: `JSON.stringify`
9071
+ * of the result must give back the cell's text. That makes the read lossless,
9072
+ * and the import coerces each value against the property it lands in only
9073
+ * later — a string property turns it back into exactly the text in the file.
9074
+ * Anything else stays the text it was: `1.10`, `00123`, `1E3`, a twenty-digit
9075
+ * SKU that a double cannot hold, the word `null`, and a cell with quotes in it.
9076
+ * So does an object: the import flattens objects into `a.b` columns, which
9077
+ * would scatter a string property's JSON text into columns that do not exist.
9078
+ */
9079
+ function parseCanonicalJsonCell(cell) {
9080
+ if (typeof cell !== "string") return cell;
9081
+ let parsed;
9082
+ try {
9083
+ parsed = JSON.parse(cell);
9084
+ } catch (e) {
9085
+ return cell;
9086
+ }
9087
+ return (typeof parsed === "number" || typeof parsed === "boolean" || Array.isArray(parsed)) && JSON.stringify(parsed) === cell ? parsed : cell;
9088
+ }
9089
+ function mapJsonParse(obj) {
9090
+ return Object.keys(obj).reduce((acc, key) => {
9091
+ if (isPrototypePollutingKey(key)) {
9092
+ console.warn(`Skipping column "${key}": a header may not reach the prototype chain`);
9093
+ return acc;
9094
+ }
9095
+ acc[key] = parseCanonicalJsonCell(obj[key]);
9096
+ return acc;
9097
+ }, {});
9098
+ }
9099
+ //#endregion
9100
+ //#region src/data_export/export/export.ts
9101
+ function downloadEntitiesExport({ data, additionalData, properties, propertiesOrder, name, flattenArrays, additionalHeaders, exportType, dateExportType }) {
9102
+ console.debug("Downloading export", {
9103
+ dataLength: data.length,
9104
+ properties,
9105
+ exportType,
9106
+ dateExportType
9107
+ });
9108
+ if (exportType === "csv") {
9109
+ const headers = getExportHeaders(properties, propertiesOrder, additionalHeaders, flattenArrays ? getArrayValuesCount(data.map((d) => d.values)) : {});
9110
+ const exportableData = getEntityCSVExportableData(data, additionalData, properties, headers, dateExportType);
9111
+ downloadBlob([entryToCSVRow(headers.map((h) => h.label)), ...exportableData.map((entry) => entryToCSVRow(entry))], `${name}.csv`, "text/csv");
9112
+ } else {
9113
+ const exportableData = getEntityJsonExportableData(data, additionalData, properties, dateExportType);
9114
+ downloadBlob([JSON.stringify(exportableData, null, 2)], `${name}.json`, "application/json");
9115
+ }
9116
+ }
9117
+ function getEntityCSVExportableData(data, additionalData, properties, headers, dateExportType) {
9118
+ const mergedData = data.map((e) => ({
9119
+ id: e.id,
9120
+ ...processValuesForExport(e.values, properties, "csv", dateExportType)
9121
+ }));
9122
+ if (additionalData) additionalData.forEach((additional, index) => {
9123
+ mergedData[index] = {
9124
+ ...mergedData[index],
9125
+ ...additional
9126
+ };
9127
+ });
9128
+ return mergedData && mergedData.map((entry) => {
9129
+ return headers.map((header) => getValueInPath(entry, header.key));
9130
+ });
9131
+ }
9132
+ function getEntityJsonExportableData(data, additionalData, properties, dateExportType) {
9133
+ const mergedData = data.map((e) => ({
9134
+ id: e.id,
9135
+ ...processValuesForExport(e.values, properties, "json", dateExportType)
9136
+ }));
9137
+ if (additionalData) additionalData.forEach((additional, index) => {
9138
+ mergedData[index] = {
9139
+ ...mergedData[index],
9140
+ ...additional
9141
+ };
9142
+ });
9143
+ return mergedData;
9144
+ }
9145
+ function getExportHeaders(properties, propertiesOrder, additionalHeaders, arrayValuesCount) {
9146
+ const headers = [{
9147
+ label: "id",
9148
+ key: "id"
9149
+ }, ...(propertiesOrder ?? Object.keys(properties)).flatMap((childKey) => {
9150
+ const property = properties[childKey];
9151
+ if (!property) {
9152
+ console.warn("Property not found", childKey, properties);
9153
+ return [];
9154
+ }
9155
+ if (arrayValuesCount && arrayValuesCount[childKey] > 1) return Array.from({ length: arrayValuesCount[childKey] }, (_, i) => getHeaders(property, `${childKey}[${i}]`, "")).flat();
9156
+ else return getHeaders(property, childKey, "");
9157
+ })];
9158
+ if (additionalHeaders) headers.push(...additionalHeaders.map((h) => ({
9159
+ label: h,
9160
+ key: h
9161
+ })));
9162
+ return headers;
9163
+ }
9164
+ /**
9165
+ * Get headers for property. There could be more than one header per property
9166
+ * @param property
9167
+ * @param propertyKey
9168
+ * @param prefix
9169
+ */
9170
+ function getHeaders(property, propertyKey, prefix = "") {
9171
+ const currentKey = prefix ? `${prefix}.${propertyKey}` : propertyKey;
9172
+ if (property.type === "map" && property.properties) return Object.entries(property.properties).map(([childKey, p]) => getHeaders(p, childKey, currentKey)).flat();
9173
+ else return [{
9174
+ label: currentKey,
9175
+ key: currentKey
9176
+ }];
9177
+ }
9178
+ /**
9179
+ * Read whatever a `date` column actually arrived as into a `Date`.
9180
+ *
9181
+ * The driver/WebSocket path revives `{ __type: "date" }` into a `Date`; the REST
9182
+ * path — which is the one the admin is on — leaves an ISO string, and an epoch
9183
+ * number is what a re-imported export carries. Returns `undefined` for anything
9184
+ * that is not a date, so the caller can pass the raw value through.
9185
+ */
9186
+ function toExportableDate(inputValue) {
9187
+ if (inputValue instanceof Date) return Number.isNaN(inputValue.getTime()) ? void 0 : inputValue;
9188
+ if (typeof inputValue === "number" && Number.isFinite(inputValue)) return new Date(inputValue);
9189
+ if (typeof inputValue === "string" && inputValue.trim() !== "") {
9190
+ const parsed = new Date(inputValue);
9191
+ return Number.isNaN(parsed.getTime()) ? void 0 : parsed;
9192
+ }
9193
+ }
9194
+ /**
9195
+ * The id a relation value points at, for a relation property's value — an
9196
+ * `EntityRelation`, or the same thing still in its `{ __type: "relation" }`
9197
+ * wire form. `undefined` for anything else, so a bare foreign key passes
9198
+ * through as the id it already is.
9199
+ */
9200
+ function relationIdForExport(value) {
9201
+ if (value instanceof EntityRelation) return value.id;
9202
+ if (typeof value === "object" && value !== null && "__type" in value && value.__type === "relation" && "id" in value && (typeof value.id === "string" || typeof value.id === "number")) return value.id;
9203
+ }
9204
+ function processValueForExport(inputValue, property, exportType, dateExportType) {
9205
+ let value;
9206
+ if (property.type === "relation") {
9207
+ const toId = (item) => relationIdForExport(item) ?? item;
9208
+ value = Array.isArray(inputValue) ? inputValue.map(toId) : toId(inputValue);
9209
+ } else if (property.type === "map" && property.properties) value = processValuesForExport(inputValue, property.properties, exportType, dateExportType);
9210
+ else if (property.type === "array") if (property.of && Array.isArray(inputValue)) if (Array.isArray(property.of)) value = property.of.map((p, i) => processValueForExport(inputValue[i], p, exportType, dateExportType));
9211
+ else if (property.of.type === "map") value = exportType === "csv" ? inputValue.map((e) => JSON.stringify(e)) : inputValue.map((e) => processValueForExport(e, property.of, exportType, dateExportType));
9212
+ else value = inputValue.map((e) => processValueForExport(e, property.of, exportType, dateExportType));
9213
+ else value = inputValue;
9214
+ else if (property.type === "reference" && inputValue && typeof inputValue === "object" && "isEntityReference" in inputValue && typeof inputValue.isEntityReference === "function" && inputValue.isEntityReference()) {
9215
+ const ref = inputValue ? inputValue : void 0;
9216
+ value = ref ? ref.fullPath : null;
9217
+ } else if (property.type === "date") {
9218
+ const date = toExportableDate(inputValue);
9219
+ value = date ? dateExportType === "timestamp" ? date.getTime() : date.toISOString() : inputValue;
9220
+ } else value = inputValue;
9221
+ return value;
9222
+ }
9223
+ function processValuesForExport(inputValues, properties, exportType, dateExportType) {
9224
+ const updatedValues = Object.entries(properties).map(([key, property]) => {
9225
+ const updatedValue = processValueForExport(inputValues && inputValues[key], property, exportType, dateExportType);
9226
+ if (updatedValue === void 0) return {};
9227
+ return { [key]: updatedValue };
9228
+ }).reduce((a, b) => ({
9229
+ ...a,
9230
+ ...b
9231
+ }), {});
9232
+ return {
9233
+ ...inputValues,
9234
+ ...updatedValues
9235
+ };
9236
+ }
9237
+ /**
9238
+ * Characters that make a spreadsheet read a cell as a formula rather than text.
9239
+ *
9240
+ * Quoting is CSV *escaping* and is not enough: Excel, LibreOffice Calc and
9241
+ * Sheets strip the CSV quoting at parse time and then evaluate a cell whose
9242
+ * first character is one of these — `=HYPERLINK(…)` exfiltrates neighbouring
9243
+ * cells on click, `=cmd|'…'!A0` is worse.
9244
+ */
9245
+ var FORMULA_TRIGGERS = [
9246
+ "=",
9247
+ "+",
9248
+ "-",
9249
+ "@",
9250
+ " ",
9251
+ "\r"
9252
+ ];
9253
+ /**
9254
+ * Neutralise a cell that a spreadsheet would otherwise evaluate, by prefixing a
9255
+ * single quote — the standard OWASP mitigation. The apostrophe is consumed by
9256
+ * the spreadsheet as "treat the rest as text", so the value reads back intact.
9257
+ */
9258
+ function escapeCsvFormula(value) {
9259
+ const first = value.charAt(0);
9260
+ if (!FORMULA_TRIGGERS.includes(first)) return value;
9261
+ if ((first === "-" || first === "+") && value.trim() !== "" && Number.isFinite(Number(value))) return value;
9262
+ return "'" + value;
9263
+ }
8708
9264
  /**
8709
- * Take an object with keys of type `address.street`, `address.city` and
8710
- * convert it to an object with nested objects like `{ address: { street: ..., city: ... } }`
8711
- *
8712
- * Keys here are the **header row of an uploaded file**, so they are attacker
8713
- * data. `res["__proto__"] = …` is the prototype setter rather than an own
8714
- * property, so a column named `__proto__.polluted` walked out of the accumulator
8715
- * and wrote onto `Object.prototype` for the life of the tab — and
8716
- * `constructor.prototype.x` threw a `TypeError` that surfaced as an unreadable
8717
- * file. Such a column is refused, not sanitised, the way `setIn` settled it
8718
- * (`docs/bug-classes.md` class 22).
8719
- *
8720
- * @param flatObj
9265
+ * The inverse of {@link escapeCsvFormula}, for a file read back in: a cell that
9266
+ * is an apostrophe followed by a formula trigger loses the apostrophe, as it
9267
+ * does when a spreadsheet reads it. Any other apostrophe is text.
8721
9268
  */
8722
- function unflattenObject(flatObj) {
8723
- return Object.keys(flatObj).reduce((nestedObj, key) => {
8724
- if (pathTraversesPrototype(key)) {
8725
- console.warn(`Skipping column "${key}": a header may not reach the prototype chain`);
8726
- return nestedObj;
8727
- }
8728
- let currentObj = nestedObj;
8729
- const keyParts = key.split(".");
8730
- keyParts.forEach((keyPart, i) => {
8731
- if (/^[\w]+\[\d+\]$/.test(keyPart)) {
8732
- const mainPropertyName = keyPart.slice(0, keyPart.indexOf("["));
8733
- const index = parseInt(keyPart.slice(keyPart.indexOf("[") + 1, keyPart.indexOf("]")));
8734
- if (!currentObj[mainPropertyName]) currentObj[mainPropertyName] = [];
8735
- if (i !== keyParts.length - 1) {
8736
- currentObj[mainPropertyName][index] = currentObj[mainPropertyName][index] || {};
8737
- currentObj = currentObj[mainPropertyName][index];
8738
- } else currentObj[mainPropertyName][index] = flatObj[key];
8739
- } else if (i !== keyParts.length - 1) {
8740
- currentObj[keyPart] = currentObj[keyPart] || {};
8741
- currentObj = currentObj[keyPart];
8742
- } else currentObj[keyPart] = flatObj[key];
8743
- });
8744
- return nestedObj;
8745
- }, {});
9269
+ function unescapeCsvFormula(value) {
9270
+ return value.charAt(0) === "'" && FORMULA_TRIGGERS.includes(value.charAt(1)) ? value.slice(1) : value;
8746
9271
  }
8747
- function mapJsonParse(obj) {
8748
- return Object.keys(obj).reduce((acc, key) => {
8749
- if (isPrototypePollutingKey(key)) {
8750
- console.warn(`Skipping column "${key}": a header may not reach the prototype chain`);
8751
- return acc;
8752
- }
8753
- try {
8754
- acc[key] = JSON.parse(obj[key]);
8755
- } catch (e) {
8756
- acc[key] = obj[key];
8757
- }
8758
- return acc;
8759
- }, {});
9272
+ function toCSVCell(v) {
9273
+ if (v === null || v === void 0) return "";
9274
+ return "\"" + escapeCsvFormula(Array.isArray(v) || typeof v === "object" && !(v instanceof Date) ? JSON.stringify(v) : String(v)).replaceAll("\"", "\"\"") + "\"";
9275
+ }
9276
+ function entryToCSVRow(entry) {
9277
+ return entry.map(toCSVCell).join(",") + "\r\n";
9278
+ }
9279
+ function downloadBlob(content, filename, contentType) {
9280
+ const blob = new Blob(content, { type: contentType });
9281
+ const url = URL.createObjectURL(blob);
9282
+ const pom = document.createElement("a");
9283
+ pom.href = url;
9284
+ pom.setAttribute("download", filename);
9285
+ pom.click();
9286
+ }
9287
+ function downloadDataAsCsv(data, name) {
9288
+ if (data.length === 0) return;
9289
+ const headers = Object.keys(data[0]);
9290
+ downloadBlob([[headers.join(","), ...data.map((row) => headers.map((header) => toCSVCell(row[header])).join(","))].join("\r\n")], `${name}.csv`, "text/csv");
8760
9291
  }
8761
9292
  //#endregion
8762
9293
  //#region src/data_import/utils/csv.ts
@@ -8776,8 +9307,9 @@ var CANDIDATE_DELIMITERS = [
8776
9307
  * Split CSV text into rows of raw cells, RFC 4180 style.
8777
9308
  *
8778
9309
  * Quoted fields may contain the delimiter, newlines and doubled quotes; this is
8779
- * the exact inverse of what the export's `entryToCSVRow` writes, so a file this
8780
- * admin produced reads back into the cells it was built from.
9310
+ * the exact inverse of the quoting the export's `entryToCSVRow` writes, and
9311
+ * `parseCsvToObjects` undoes its formula escape, so a file this admin produced
9312
+ * reads back into the cells it was built from.
8781
9313
  */
8782
9314
  function parseCsvRows(text, delimiter = ",") {
8783
9315
  const rows = [];
@@ -8875,7 +9407,7 @@ function parseCsvToObjects(text) {
8875
9407
  headers: [],
8876
9408
  data: []
8877
9409
  };
8878
- const headers = rows[0].map((header, index) => header.trim() || `Column${index + 1}`);
9410
+ const headers = rows[0].map((header, index) => unescapeCsvFormula(header).trim() || `Column${index + 1}`);
8879
9411
  return {
8880
9412
  headers,
8881
9413
  data: rows.slice(1).map((cells) => {
@@ -8884,7 +9416,7 @@ function parseCsvToObjects(text) {
8884
9416
  if (isPrototypePollutingKey(header)) return;
8885
9417
  const cell = cells[index];
8886
9418
  if (cell === void 0) return;
8887
- obj[header] = cell;
9419
+ obj[header] = unescapeCsvFormula(cell);
8888
9420
  });
8889
9421
  return obj;
8890
9422
  })
@@ -9287,9 +9819,11 @@ function convertDataToEntity(authController, navigation, data, idColumn, headers
9287
9819
  const flatObject = flattenEntry(data);
9288
9820
  if (idColumn) delete flatObject[idColumn];
9289
9821
  const mappedKeysObject = Object.entries(flatObject).map(([key, value]) => {
9290
- const mappedKey = getIn(headersMapping, key) ?? key;
9822
+ const mappedKey = Object.prototype.hasOwnProperty.call(headersMapping, key) ? headersMapping[key] : key;
9823
+ if (mappedKey === null) return {};
9824
+ if (value === "") return {};
9291
9825
  const mappedProperty = getPropertyInPath$1(properties, mappedKey);
9292
- if (!mappedProperty) return {};
9826
+ if (!mappedProperty || typeof mappedProperty !== "object") return {};
9293
9827
  const processedValue = processValueMapping(authController, value, navigation, mappedProperty);
9294
9828
  return { [mappedKey]: processedValue };
9295
9829
  }).reduce((acc, curr) => ({
@@ -9342,19 +9876,19 @@ function processValueMapping(authController, value, navigation, property) {
9342
9876
  }
9343
9877
  return value;
9344
9878
  }
9879
+ if (from === "string" && to === "array" && typeof value === "string") return processValueMapping(authController, splitListCell(value), navigation, usedProperty);
9880
+ else if (from === "string" && to === "boolean" && typeof value === "string") return readBooleanCell(value);
9881
+ else if (from === "array" && to === "string") return JSON.stringify(value);
9345
9882
  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));
9346
9883
  else if (from === "string" && to === "number" && typeof value === "string") {
9347
9884
  const trimmed = value.trim();
9348
9885
  if (trimmed === "") return null;
9349
9886
  const num = Number(trimmed);
9350
9887
  return Number.isNaN(num) ? null : num;
9351
- } 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));
9352
- else if (from === "string" && to === "boolean") return value === "true";
9353
- else if (from === "number" && to === "boolean") return value === 1;
9888
+ } else if (from === "number" && to === "boolean") return value === 1;
9354
9889
  else if (from === "boolean" && to === "number") return value ? 1 : 0;
9355
9890
  else if (from === "boolean" && to === "string") return value ? "true" : "false";
9356
9891
  else if (from === "number" && to === "string" && typeof value === "number") return value.toString();
9357
- else if (from === "string" && to === "array" && typeof value === "string") return value.split(",").map((v) => v.trim());
9358
9892
  else if (from === "string" && to === "date" && typeof value === "string") try {
9359
9893
  return new Date(value);
9360
9894
  } catch (e) {
@@ -9383,9 +9917,66 @@ function processValueMapping(authController, value, navigation, property) {
9383
9917
  databaseId
9384
9918
  });
9385
9919
  } else if (from === to) return value;
9386
- else if (from === "array" && to === "string" && Array.isArray(value)) return value.join(",");
9387
9920
  return value;
9388
9921
  }
9922
+ /**
9923
+ * The items of a list cell: a JSON array as written (the export writes lists
9924
+ * that way), otherwise the text split on commas. Each item is trimmed and an
9925
+ * empty one dropped, so `news, sports` is two tags rather than one with a
9926
+ * leading space, and a blank cell is no tags rather than one empty tag.
9927
+ */
9928
+ function splitListCell(cell) {
9929
+ const trimmed = cell.trim();
9930
+ if (trimmed.startsWith("[")) try {
9931
+ const parsed = JSON.parse(trimmed);
9932
+ if (Array.isArray(parsed)) return parsed;
9933
+ } catch (e) {}
9934
+ return trimmed.split(",").map((item) => item.trim()).filter((item) => item !== "");
9935
+ }
9936
+ /**
9937
+ * A boolean cell as spreadsheets and people write it: Excel and Sheets export
9938
+ * `TRUE`/`FALSE`, and `yes`/`no` and `1`/`0` are common by hand. Anything
9939
+ * unreadable is absent — `null`, as a number cell is — rather than a confident
9940
+ * `false`.
9941
+ */
9942
+ function readBooleanCell(cell) {
9943
+ const normalised = cell.trim().toLowerCase();
9944
+ if ([
9945
+ "true",
9946
+ "1",
9947
+ "yes"
9948
+ ].includes(normalised)) return true;
9949
+ if ([
9950
+ "false",
9951
+ "0",
9952
+ "no"
9953
+ ].includes(normalised)) return false;
9954
+ return null;
9955
+ }
9956
+ //#endregion
9957
+ //#region src/data_import/utils/id_column.ts
9958
+ /**
9959
+ * The column an import uses as the row id before the user touches the mapping.
9960
+ *
9961
+ * The guess decides something destructive: the chosen column is taken out of
9962
+ * the values and sent as the id of an upsert, so every row overwrites the record
9963
+ * that already has that id. A header that merely *contained* "id" or "key" used
9964
+ * to qualify, so a file starting with `width`, `video` or `provider` wrote its
9965
+ * widths over the records whose ids they happened to equal.
9966
+ *
9967
+ * Only the first column is a candidate — the one the export writes the id to —
9968
+ * and only when its name says it is the id: the header is `id` (in any case),
9969
+ * or it maps onto a property the collection declares with `isId`. Anything else
9970
+ * is left for the user to choose in the mapping step.
9971
+ */
9972
+ function guessIdColumn(headers, headersMapping, properties) {
9973
+ const firstKey = headers[0];
9974
+ if (firstKey === void 0) return void 0;
9975
+ if (firstKey.trim().toLowerCase() === "id") return firstKey;
9976
+ const mappedKey = headersMapping[firstKey];
9977
+ const mappedProperty = mappedKey ? properties?.[mappedKey] : void 0;
9978
+ if (mappedProperty && "isId" in mappedProperty && mappedProperty.isId) return firstKey;
9979
+ }
9389
9980
  //#endregion
9390
9981
  //#region src/data_import/components/DataNewPropertiesMapping.tsx
9391
9982
  function DataNewPropertiesMapping({ importConfig, destinationProperties, buildPropertyView }) {
@@ -9911,8 +10502,8 @@ function EditorCollectionAction({ path, parentCollectionSlugs, parentEntityIds,
9911
10502
  }
9912
10503
  //#endregion
9913
10504
  //#region src/components/CollectionViewBinding/CollectionViewActions.tsx
9914
- var ImportCollectionAction = lazyChunk(() => import("./import-ByaJMZuT.js").then((n) => n.t).then((m) => ({ default: m.ImportCollectionAction })));
9915
- var ExportCollectionAction = lazyChunk(() => import("./export-DIyk1720.js").then((n) => n.t).then((m) => ({ default: m.ExportCollectionAction })));
10505
+ var ImportCollectionAction = lazyChunk(() => import("./import-TCgkTVlS.js").then((n) => n.t).then((m) => ({ default: m.ImportCollectionAction })));
10506
+ var ExportCollectionAction = lazyChunk(() => import("./export-4bJWdlDK.js").then((n) => n.t).then((m) => ({ default: m.ExportCollectionAction })));
9916
10507
  function CollectionViewActions({ collection, relativePath, parentCollectionSlugs, parentEntityIds, onNewClick, onAddExistingClick, onMultipleDeleteClick, selectionEnabled, path, selectionController, tableController, collectionEntitiesCount, compact, children, openNewDocument }) {
9917
10508
  const context = useAdminContext();
9918
10509
  const { canCreate, canDelete } = usePermissions();
@@ -11452,27 +12043,32 @@ var ListRow = React.memo(function ListRow({ entity, collection, onClick, selecte
11452
12043
  className: "flex items-center justify-end gap-0.5 flex-shrink-0 ml-auto",
11453
12044
  style: { minWidth: actionsWidth },
11454
12045
  onClick: (e) => e.stopPropagation(),
11455
- children: listViewActions.map((action, index) => /* @__PURE__ */ jsx(Tooltip, {
11456
- title: action.name,
11457
- asChild: true,
11458
- children: /* @__PURE__ */ jsx(IconButton, {
11459
- size: "small",
11460
- onClick: (e) => {
11461
- e.stopPropagation();
11462
- action.onClick({
11463
- view: "collection",
11464
- entity,
11465
- path,
11466
- collection,
11467
- context,
11468
- sidePanelController: context?.sidePanelController,
11469
- selectionController,
11470
- openEntityMode: openEntityMode ?? collection?.openEntityMode ?? "full_screen"
11471
- });
11472
- },
11473
- children: getIcon(action.icon, void 0, void 0, "smallest")
11474
- })
11475
- }, action.key ?? index))
12046
+ children: listViewActions.map((action, index) => {
12047
+ const clickProps = {
12048
+ view: "collection",
12049
+ entity,
12050
+ path,
12051
+ collection,
12052
+ context,
12053
+ sidePanelController: context?.sidePanelController,
12054
+ selectionController,
12055
+ openEntityMode: openEntityMode ?? collection?.openEntityMode ?? "full_screen"
12056
+ };
12057
+ const enabled = !action.isEnabled || action.isEnabled(clickProps);
12058
+ return /* @__PURE__ */ jsx(Tooltip, {
12059
+ title: action.name,
12060
+ asChild: true,
12061
+ children: /* @__PURE__ */ jsx(IconButton, {
12062
+ size: "small",
12063
+ disabled: !enabled,
12064
+ onClick: (e) => {
12065
+ e.stopPropagation();
12066
+ action.onClick(clickProps);
12067
+ },
12068
+ children: getIcon(action.icon, void 0, void 0, "smallest")
12069
+ })
12070
+ }, action.key ?? index);
12071
+ })
11476
12072
  })
11477
12073
  ]
11478
12074
  });
@@ -12273,7 +12869,7 @@ function SplitListShowButton({ onClick }) {
12273
12869
  }
12274
12870
  //#endregion
12275
12871
  //#region src/components/EntityInspector.tsx
12276
- var EntityHistoryView = lazyChunk(() => import("./history-CrZEzQ5b.js").then((m) => ({ default: m.EntityHistoryView })));
12872
+ var EntityHistoryView = lazyChunk(() => import("./history-BElrZ5Rw.js").then((m) => ({ default: m.EntityHistoryView })));
12277
12873
  /**
12278
12874
  * The JSON tab pulls `prism-react-renderer` — 85 kB, and it was EAGER.
12279
12875
  *
@@ -12676,7 +13272,7 @@ function DetailViewBindingInner({ path, entityId, selectedTab: selectedTabProp,
12676
13272
  })
12677
13273
  }, `entity_detail_collection_tab_${view.name}`);
12678
13274
  });
12679
- const collectionUrl = withViewMode(urlController.buildUrlCollectionPath(path));
13275
+ const collectionUrl = withListState(urlController.buildUrlCollectionPath(path));
12680
13276
  const backToCollection = layout === "full_screen" && !onShowList ? () => navigate(collectionUrl) : void 0;
12681
13277
  const fullScreenButton = !barActions && (layout === "side_panel" || layout === "dialog") && entityId ? /* @__PURE__ */ jsx(Tooltip, {
12682
13278
  title: "Open full screen",
@@ -12985,7 +13581,7 @@ function SplitListView({ collection, tableController, onEntityClick: externalOnE
12985
13581
  };
12986
13582
  }, [selectedEntityId]);
12987
13583
  const buildUrl = useCallback((subPath) => {
12988
- return withViewMode(urlController.buildUrlCollectionPath(subPath));
13584
+ return withListState(urlController.buildUrlCollectionPath(subPath));
12989
13585
  }, [urlController]);
12990
13586
  const handleCloseDetail = useCallback(() => {
12991
13587
  navigate(buildUrl(path));
@@ -13435,6 +14031,8 @@ function useBoardDataController({ fullPath, collection, columnProperty, columns,
13435
14031
  [currentColumnProperty]: ["==", column]
13436
14032
  };
13437
14033
  const orderByParam = currentOrderProperty ? [currentOrderProperty, "asc"] : void 0;
14034
+ let active = true;
14035
+ const isCurrent = () => active && !isCleaningUpRef.current;
13438
14036
  setColumnData((prev) => ({
13439
14037
  ...prev,
13440
14038
  [column]: {
@@ -13444,7 +14042,7 @@ function useBoardDataController({ fullPath, collection, columnProperty, columns,
13444
14042
  }
13445
14043
  }));
13446
14044
  const onUpdate = async (entities) => {
13447
- if (isCleaningUpRef.current) return;
14045
+ if (!isCurrent()) return;
13448
14046
  const pendingMap = pendingItemsRef.current;
13449
14047
  const mergedEntities = entities.map((e) => {
13450
14048
  const pending = pendingMap[String(e.id)];
@@ -13503,6 +14101,7 @@ function useBoardDataController({ fullPath, collection, columnProperty, columns,
13503
14101
  } catch (e) {
13504
14102
  console.error("Error in afterRead callback:", e);
13505
14103
  }
14104
+ if (!isCurrent()) return;
13506
14105
  const newHasMore = entities.length >= itemCount;
13507
14106
  setColumnData((prev) => {
13508
14107
  const existing = prev[column];
@@ -13544,10 +14143,10 @@ function useBoardDataController({ fullPath, collection, columnProperty, columns,
13544
14143
  searchString: currentSearchString
13545
14144
  });
13546
14145
  const onError = (error) => {
13547
- if (isCleaningUpRef.current) return;
14146
+ if (!isCurrent()) return;
13548
14147
  console.error(`Error loading column ${column}:`, error);
13549
14148
  fetchOnce().then((res) => onUpdate(res.data)).catch(() => {
13550
- if (isCleaningUpRef.current) return;
14149
+ if (!isCurrent()) return;
13551
14150
  setColumnData((prev) => ({
13552
14151
  ...prev,
13553
14152
  [column]: {
@@ -13573,29 +14172,35 @@ function useBoardDataController({ fullPath, collection, columnProperty, columns,
13573
14172
  onUpdate(res.data);
13574
14173
  }, onError);
13575
14174
  const firstPaintFallback = setTimeout(() => {
13576
- if (liveDataReceived || isCleaningUpRef.current) return;
14175
+ if (liveDataReceived || !isCurrent()) return;
13577
14176
  fetchOnce().then((res) => {
13578
- if (liveDataReceived || isCleaningUpRef.current) return;
14177
+ if (liveDataReceived || !isCurrent()) return;
13579
14178
  onUpdate(res.data);
13580
14179
  }).catch(() => void 0);
13581
14180
  }, LIVE_FIRST_PAINT_TIMEOUT_MS);
13582
14181
  unsubscribersRef.current[column] = () => {
14182
+ active = false;
13583
14183
  clearTimeout(firstPaintFallback);
13584
14184
  unsubscribe?.();
13585
14185
  };
13586
- } else fetchOnce().then((res) => onUpdate(res.data)).catch((error) => {
13587
- if (isCleaningUpRef.current) return;
13588
- setColumnData((prev) => ({
13589
- ...prev,
13590
- [column]: {
13591
- ...prev[column],
13592
- entities: prev[column]?.entities ?? [],
13593
- loading: false,
13594
- hasMore: false,
13595
- error
13596
- }
13597
- }));
13598
- });
14186
+ } else {
14187
+ unsubscribersRef.current[column] = () => {
14188
+ active = false;
14189
+ };
14190
+ fetchOnce().then((res) => onUpdate(res.data)).catch((error) => {
14191
+ if (!isCurrent()) return;
14192
+ setColumnData((prev) => ({
14193
+ ...prev,
14194
+ [column]: {
14195
+ ...prev[column],
14196
+ entities: prev[column]?.entities ?? [],
14197
+ loading: false,
14198
+ hasMore: false,
14199
+ error
14200
+ }
14201
+ }));
14202
+ });
14203
+ }
13599
14204
  }, []);
13600
14205
  useEffect(() => {
13601
14206
  isCleaningUpRef.current = false;
@@ -13614,6 +14219,7 @@ function useBoardDataController({ fullPath, collection, columnProperty, columns,
13614
14219
  const currentResolvedPath = resolvedPathRef.current;
13615
14220
  const currentColumns = columns;
13616
14221
  const currentColumnItemCounts = columnItemCounts;
14222
+ let cancelled = false;
13617
14223
  const timeoutId = setTimeout(() => {
13618
14224
  if (isCleaningUpRef.current) return;
13619
14225
  currentColumns.forEach((column) => {
@@ -13630,7 +14236,7 @@ function useBoardDataController({ fullPath, collection, columnProperty, columns,
13630
14236
  where: whereFilter,
13631
14237
  searchString: currentSearchString
13632
14238
  }).then((count) => {
13633
- if (isCleaningUpRef.current) return;
14239
+ if (cancelled || isCleaningUpRef.current) return;
13634
14240
  setColumnData((prev) => ({
13635
14241
  ...prev,
13636
14242
  [column]: {
@@ -13646,6 +14252,7 @@ function useBoardDataController({ fullPath, collection, columnProperty, columns,
13646
14252
  prevColumnItemCountsRef.current = { ...currentColumnItemCounts };
13647
14253
  }, 0);
13648
14254
  return () => {
14255
+ cancelled = true;
13649
14256
  clearTimeout(timeoutId);
13650
14257
  isCleaningUpRef.current = true;
13651
14258
  const unsubscribers = { ...unsubscribersRef.current };
@@ -13698,19 +14305,54 @@ function useBoardDataController({ fullPath, collection, columnProperty, columns,
13698
14305
  [column]: (prev[column] ?? pageSize) + pageSize
13699
14306
  }));
13700
14307
  }, [pageSize]);
14308
+ /**
14309
+ * Drop the optimistic overlay for the moves matching `predicate`.
14310
+ *
14311
+ * An entry is otherwise released only when the database "catches up" to
14312
+ * it, which a refused write never does: the card would stay where it was
14313
+ * dropped, on top of whatever the database re-delivers. A refresh is a
14314
+ * request for what the database says, so it lets go of them.
14315
+ */
14316
+ const dropPendingMoves = useCallback((predicate) => {
14317
+ const pendingMap = pendingItemsRef.current;
14318
+ for (const [id, pending] of Object.entries(pendingMap)) if (predicate(pending.expectedValues)) delete pendingMap[id];
14319
+ }, []);
13701
14320
  const refreshColumn = useCallback((column) => {
14321
+ dropPendingMoves((expected) => expected[columnPropertyRef.current] === column);
13702
14322
  setColumnItemCounts((prev) => ({
13703
14323
  ...prev,
13704
14324
  [column]: pageSize
13705
14325
  }));
13706
- }, [pageSize]);
14326
+ prevColumnItemCountsRef.current = {
14327
+ ...prevColumnItemCountsRef.current,
14328
+ [column]: pageSize
14329
+ };
14330
+ const unsubscribe = unsubscribersRef.current[column];
14331
+ delete unsubscribersRef.current[column];
14332
+ try {
14333
+ unsubscribe?.();
14334
+ } catch (e) {}
14335
+ setTimeout(() => {
14336
+ if (!isCleaningUpRef.current) subscribeToColumn(column, pageSize);
14337
+ }, 0);
14338
+ }, [
14339
+ pageSize,
14340
+ subscribeToColumn,
14341
+ dropPendingMoves
14342
+ ]);
13707
14343
  const refreshAll = useCallback(() => {
14344
+ dropPendingMoves(() => true);
13708
14345
  const reset = {};
13709
14346
  columns.forEach((col) => {
13710
14347
  reset[col] = pageSize;
13711
14348
  });
13712
14349
  setColumnItemCounts(reset);
13713
- }, [columns, pageSize]);
14350
+ setSubscriptionVersion((v) => v + 1);
14351
+ }, [
14352
+ columns,
14353
+ pageSize,
14354
+ dropPendingMoves
14355
+ ]);
13714
14356
  const moveItemOptimistically = useCallback((itemId, sourceColumn, targetColumn, newValues, newIndex) => {
13715
14357
  setColumnData((prev) => {
13716
14358
  const updated = { ...prev };
@@ -13848,6 +14490,8 @@ function isValidOrderKey(value) {
13848
14490
  }
13849
14491
  }
13850
14492
  function useKanbanDragAndDrop({ collection, fullPath, columnProperty, orderProperty, dataClient, context, boardDataController, analyticsController }) {
14493
+ const snackbarController = useSnackbarController();
14494
+ const { t } = useTranslation();
13851
14495
  const handleItemsReorder = useCallback(async (items, moveInfo) => {
13852
14496
  const entity = items.find((item) => item.id === moveInfo?.itemId)?.data;
13853
14497
  if (!entity || !moveInfo) return;
@@ -13899,16 +14543,16 @@ function useKanbanDragAndDrop({ collection, fullPath, columnProperty, orderPrope
13899
14543
  ...saveProps,
13900
14544
  collection,
13901
14545
  data: dataClient,
13902
- context,
13903
- afterSave: () => {},
13904
- afterSaveError: (e) => {
13905
- console.error("Failed to save entity after reorder:", e);
13906
- if (boardDataController.refreshAll) boardDataController.refreshAll();
13907
- }
14546
+ context
13908
14547
  });
13909
14548
  } catch (e) {
13910
- console.error("Error saving entity:", e);
13911
- if (boardDataController.refreshAll) boardDataController.refreshAll();
14549
+ console.error("Failed to save entity after reorder:", e);
14550
+ snackbarController.open({
14551
+ type: "error",
14552
+ title: t("error_saving_entity"),
14553
+ message: e instanceof Error ? e.message : String(e)
14554
+ });
14555
+ boardDataController.refreshAll();
13912
14556
  }
13913
14557
  }, [
13914
14558
  collection,
@@ -13918,7 +14562,9 @@ function useKanbanDragAndDrop({ collection, fullPath, columnProperty, orderPrope
13918
14562
  dataClient,
13919
14563
  boardDataController,
13920
14564
  analyticsController,
13921
- fullPath
14565
+ fullPath,
14566
+ snackbarController,
14567
+ t
13922
14568
  ]);
13923
14569
  return useMemo(() => ({ handleItemsReorder }), [handleItemsReorder]);
13924
14570
  }
@@ -14734,7 +15380,9 @@ function DeleteEntityDialog({ target, collection, onClose, open, onEntityDelete,
14734
15380
  maxWidth: singleEntity ? "2xl" : "lg",
14735
15381
  "aria-labelledby": "delete-dialog",
14736
15382
  open,
14737
- onOpenChange: (open) => !open ? onClose() : void 0,
15383
+ onOpenChange: (open) => {
15384
+ if (!open) handleCancel();
15385
+ },
14738
15386
  children: [
14739
15387
  /* @__PURE__ */ jsx(DialogTitle, {
14740
15388
  id: "delete-dialog-title",
@@ -14960,7 +15608,7 @@ var copyEntityAction = {
14960
15608
  name: "Copy",
14961
15609
  key: "copy",
14962
15610
  isEnabled: ({ entity }) => Boolean(entity),
14963
- onClick({ entity, collection, context, path, sidePanelController, highlightEntity, unhighlightEntity, openEntityMode }) {
15611
+ onClick({ entity, collection, context, path, sidePanelController, highlightEntity, unhighlightEntity, onCollectionChange, openEntityMode }) {
14964
15612
  if (!entity) throw new Error("INTERNAL: copyEntityAction: Entity is undefined");
14965
15613
  if (!sidePanelController) throw new Error("INTERNAL: copyEntityAction: sidePanelController is undefined");
14966
15614
  highlightEntity?.(entity);
@@ -14978,6 +15626,7 @@ var copyEntityAction = {
14978
15626
  copy: true,
14979
15627
  sidePanelController,
14980
15628
  onClose: () => unhighlightEntity?.(entity),
15629
+ onUpdate: onCollectionChange,
14981
15630
  navigation: context.urlController
14982
15631
  });
14983
15632
  return Promise.resolve(void 0);
@@ -15317,6 +15966,29 @@ function useWindowSize() {
15317
15966
  }
15318
15967
  //#endregion
15319
15968
  //#region src/components/CollectionTableBinding/internal/popup_field/PopupFormField.tsx
15969
+ /**
15970
+ * The properties that judge one cell: the property at `propertyKey`, under the
15971
+ * maps its key goes through, and nothing beside it.
15972
+ *
15973
+ * A spread map's child is `address.street`, a path into `properties` rather
15974
+ * than a key of it; looked up as a key it found nothing, and the popup
15975
+ * validated nothing. The maps on the way carry only the edited child and none
15976
+ * of their own rules: the popup shows one field, and an error anywhere else
15977
+ * would block the save where no one could see it.
15978
+ */
15979
+ function propertiesOfCell(properties, propertyKey) {
15980
+ if (propertyKey in properties) return { [propertyKey]: properties[propertyKey] };
15981
+ const dot = propertyKey.indexOf(".");
15982
+ if (dot === -1) return {};
15983
+ const mapKey = propertyKey.slice(0, dot);
15984
+ const map = properties[mapKey];
15985
+ if (map?.type !== "map" || !map.properties) return {};
15986
+ const children = propertiesOfCell(map.properties, propertyKey.slice(dot + 1));
15987
+ return Object.keys(children).length > 0 ? { [mapKey]: {
15988
+ type: "map",
15989
+ properties: children
15990
+ } } : {};
15991
+ }
15320
15992
  function PopupFormField(props) {
15321
15993
  if (!props.open) return null;
15322
15994
  return /* @__PURE__ */ jsx(PopupFormFieldLoading, { ...props });
@@ -15416,7 +16088,7 @@ function PopupFormFieldInternal({ tableKey, entityId, customFieldValidator, prop
15416
16088
  ]);
15417
16089
  const validationSchema = useMemo(() => {
15418
16090
  if (!collection || !entityId) return;
15419
- return getEntitySchema(entityId, propertyKey && collection.properties[propertyKey] ? { [propertyKey]: collection.properties[propertyKey] } : {}, customFieldValidator);
16091
+ return getEntitySchema(entityId, propertyKey ? propertiesOfCell(collection.properties, propertyKey) : {}, customFieldValidator);
15420
16092
  }, [
15421
16093
  collection,
15422
16094
  entityId,
@@ -15441,7 +16113,7 @@ function PopupFormFieldInternal({ tableKey, entityId, customFieldValidator, prop
15441
16113
  const saveValue = async (values) => {
15442
16114
  setSavingError(null);
15443
16115
  if (collection && entity && onCellValueChange && propertyKey) return onCellValueChange({
15444
- value: values[propertyKey],
16116
+ value: getIn(applyValueTransforms(values, collection.properties), propertyKey),
15445
16117
  propertyKey,
15446
16118
  data: entity,
15447
16119
  setError: setSavingError,
@@ -16164,7 +16836,7 @@ function CollectionViewStartActions({ collection, relativePath, parentCollection
16164
16836
  openNewDocument
16165
16837
  };
16166
16838
  const handleBackClick = useCallback(() => {
16167
- navigate(withViewMode(urlController.buildUrlCollectionPath(path)));
16839
+ navigate(withListState(urlController.buildUrlCollectionPath(path)));
16168
16840
  }, [
16169
16841
  navigate,
16170
16842
  urlController,
@@ -16477,19 +17149,9 @@ var CollectionViewBindingInner = React.memo(function CollectionViewBindingInner(
16477
17149
  urlController,
16478
17150
  analyticsController
16479
17151
  ]);
16480
- const onNewClick = useCallback(() => {
16481
- const collection = collectionRef.current;
16482
- analyticsController.onAnalyticsEvent?.("new_entity_click", { path });
16483
- navigateToEntity({
16484
- openEntityMode,
16485
- collection,
16486
- entityId: void 0,
16487
- path,
16488
- sidePanelController,
16489
- navigation: urlController,
16490
- onClose: unselectNavigatedEntity
16491
- });
16492
- }, [path, sidePanelController]);
17152
+ const onCreatedInPanel = useCallback(() => {
17153
+ setLastDeleteTimestamp(Date.now());
17154
+ }, []);
16493
17155
  /**
16494
17156
  * Attach rows that already exist to this parent.
16495
17157
  *
@@ -16529,7 +17191,7 @@ var CollectionViewBindingInner = React.memo(function CollectionViewBindingInner(
16529
17191
  const selectionDialog = useSelectionDialog({
16530
17192
  path: linkedTargetSlug ?? false,
16531
17193
  multiselect: true,
16532
- onMultipleEntitiesSelected: linkExistingEntities
17194
+ onSelectionDone: linkExistingEntities
16533
17195
  });
16534
17196
  const openNewDocument = useCallback((defaultValues) => {
16535
17197
  const collection = collectionRef.current;
@@ -16542,15 +17204,18 @@ var CollectionViewBindingInner = React.memo(function CollectionViewBindingInner(
16542
17204
  path,
16543
17205
  sidePanelController,
16544
17206
  navigation: urlController,
16545
- onClose: unselectNavigatedEntity
17207
+ onClose: unselectNavigatedEntity,
17208
+ onUpdate: onCreatedInPanel
16546
17209
  });
16547
17210
  }, [
16548
17211
  path,
16549
17212
  sidePanelController,
16550
17213
  openEntityMode,
16551
17214
  urlController,
16552
- unselectNavigatedEntity
17215
+ unselectNavigatedEntity,
17216
+ onCreatedInPanel
16553
17217
  ]);
17218
+ const onNewClick = useCallback(() => openNewDocument(), [openNewDocument]);
16554
17219
  const onMultipleDeleteClick = () => {
16555
17220
  analyticsController.onAnalyticsEvent?.("multiple_delete_dialog_open", { path });
16556
17221
  setDeleteEntityClicked(selection);
@@ -16759,7 +17424,9 @@ var CollectionViewBindingInner = React.memo(function CollectionViewBindingInner(
16759
17424
  collection,
16760
17425
  collectionWithLocalOrder,
16761
17426
  path,
16762
- sidePanelController
17427
+ sidePanelController,
17428
+ openEntityMode,
17429
+ urlController
16763
17430
  ]);
16764
17431
  const updateLastDeleteTimestamp = useCallback(() => {
16765
17432
  setLastDeleteTimestamp(Date.now());
@@ -16821,7 +17488,8 @@ var CollectionViewBindingInner = React.memo(function CollectionViewBindingInner(
16821
17488
  filter: tableController.filterValues,
16822
17489
  sortBy: tableController.sortBy,
16823
17490
  searchString: tableController.searchString,
16824
- onCountChange: setDocsCount
17491
+ onCountChange: setDocsCount,
17492
+ refreshKey: lastDeleteTimestamp
16825
17493
  });
16826
17494
  const liveSelectionQuery = useMemo(() => ({
16827
17495
  path,
@@ -17200,7 +17868,7 @@ function DefaultCollectionEmptyState({ canCreate, onNewClick, isSearching, searc
17200
17868
  * callers (e.g. React StrictMode double-mount) share the same promise.
17201
17869
  */
17202
17870
  var inflightCountRequests = /* @__PURE__ */ new Map();
17203
- function EntitiesCount({ path, collection, filter, sortBy, searchString, onCountChange }) {
17871
+ function EntitiesCount({ path, collection, filter, sortBy, searchString, onCountChange, refreshKey }) {
17204
17872
  const dataClient = useData();
17205
17873
  const sortKey = React.useMemo(() => sortBy ? serializeOrderBy(sortBy) ?? "" : "", [sortBy]);
17206
17874
  const dataClientRef = React.useRef(dataClient);
@@ -17217,7 +17885,7 @@ function EntitiesCount({ path, collection, filter, sortBy, searchString, onCount
17217
17885
  let cancelled = false;
17218
17886
  const whereParams = filter && Object.keys(filter).length > 0 ? filter : void 0;
17219
17887
  const orderByParams = sortBy && sortBy.length > 0 ? sortBy.map(([field, direction]) => [String(field), direction]) : void 0;
17220
- const cacheKey = `${path}|${filterKey}|${sortKey}|${searchString ?? ""}`;
17888
+ const cacheKey = `${path}|${filterKey}|${sortKey}|${searchString ?? ""}|${refreshKey ?? ""}`;
17221
17889
  let countPromise = inflightCountRequests.get(cacheKey);
17222
17890
  if (!countPromise) {
17223
17891
  countPromise = accessor.count({
@@ -17241,7 +17909,8 @@ function EntitiesCount({ path, collection, filter, sortBy, searchString, onCount
17241
17909
  path,
17242
17910
  filterKey,
17243
17911
  sortKey,
17244
- searchString
17912
+ searchString,
17913
+ refreshKey
17245
17914
  ]);
17246
17915
  return null;
17247
17916
  }
@@ -17393,7 +18062,7 @@ function getChanges(source, comparison) {
17393
18062
  continue;
17394
18063
  }
17395
18064
  if (sourceValue.some((item, index) => !deepEqual(item, comparisonArray[index]))) changes[key] = sourceValue;
17396
- } else if (isObject(sourceValue) && sourceValue && isObject(comparisonValue) && comparisonValue) {
18065
+ } else if (isPlainObject(sourceValue) && isPlainObject(comparisonValue)) {
17397
18066
  const nestedChanges = getChanges(sourceValue, comparisonValue);
17398
18067
  if (Object.keys(nestedChanges).length > 0) changes[key] = nestedChanges;
17399
18068
  } else changes[key] = sourceValue;
@@ -17401,6 +18070,24 @@ function getChanges(source, comparison) {
17401
18070
  return changes;
17402
18071
  }
17403
18072
  /**
18073
+ * What an update of a stored record sends: every top-level property whose value
18074
+ * differs from the stored one — whole.
18075
+ *
18076
+ * Whole, because that is the unit an update replaces. It sets each property it
18077
+ * carries, so the value sent *is* the property's new value: a map sent as the
18078
+ * one key that changed would erase every key left out, a key-value map with a
18079
+ * key removed would arrive empty, and a geopoint with one coordinate moved is
18080
+ * refused for lacking the other. {@link getChanges} walks into plain objects to
18081
+ * find what changed inside them, which is right for comparing a draft with the
18082
+ * form and wrong for a write.
18083
+ */
18084
+ function getChangedProperties(values, storedValues) {
18085
+ const changes = {};
18086
+ const keys = /* @__PURE__ */ new Set([...Object.keys(values), ...Object.keys(storedValues)]);
18087
+ for (const key of keys) if (!deepEqual(values[key], storedValues[key])) changes[key] = values[key];
18088
+ return changes;
18089
+ }
18090
+ /**
17404
18091
  * What the local-changes backup still has to offer, given what the form is
17405
18092
  * already showing — the answer the "unsaved local changes" banner is asking for.
17406
18093
  * `undefined` means there is nothing left to apply, and no banner.
@@ -18036,6 +18723,18 @@ function EditViewBindingInner({ path, entityId, selectedTab: selectedTabProp, co
18036
18723
  path,
18037
18724
  entity: usedEntity
18038
18725
  });
18726
+ const leaveRecord = useCallback(() => {
18727
+ if (layout === "side_panel" || layout === "dialog") sideDialogContext.close(true);
18728
+ else if (onCloseRequest) onCloseRequest();
18729
+ else navigate(withListState(urlController.buildUrlCollectionPath(path)), { replace: true });
18730
+ }, [
18731
+ layout,
18732
+ sideDialogContext,
18733
+ onCloseRequest,
18734
+ navigate,
18735
+ urlController,
18736
+ path
18737
+ ]);
18039
18738
  const recordActionItems = usedEntity && recordActions.length ? recordActions.map((action, index) => {
18040
18739
  const clickProps = {
18041
18740
  view: "form",
@@ -18045,7 +18744,7 @@ function EditViewBindingInner({ path, entityId, selectedTab: selectedTabProp, co
18045
18744
  context,
18046
18745
  sidePanelController: adminContext.sidePanelController,
18047
18746
  openEntityMode: layout,
18048
- navigateBack: navigateBack ?? (() => void 0),
18747
+ navigateBack: leaveRecord,
18049
18748
  formContext
18050
18749
  };
18051
18750
  return /* @__PURE__ */ jsxs(MenuItem, {
@@ -18780,8 +19479,8 @@ var useBuildSidePanel = (collectionRegistryController, urlController, navigation
18780
19479
  const panelsFromUrl = buildSidePanelsFromUrl(urlController.urlPathToDataPath(location.pathname), collectionRegistryController.collections ?? [], newFlag);
18781
19480
  for (let i = 0; i < panelsFromUrl.length; i++) {
18782
19481
  const props = panelsFromUrl[i];
18783
- if (i === 0) sideDialogsController.replace(propsToSidePanel(props, urlController.buildUrlCollectionPath, urlController.resolveDatabasePathsFrom, smallLayout, customizationController, authController, location.search));
18784
- else sideDialogsController.open(propsToSidePanel(props, urlController.buildUrlCollectionPath, urlController.resolveDatabasePathsFrom, smallLayout, customizationController, authController, location.search));
19482
+ if (i === 0) sideDialogsController.replace(propsToSidePanel(props, urlController.buildUrlCollectionPath, urlController.resolveDatabasePathsFrom, smallLayout, customizationController, authController));
19483
+ else sideDialogsController.open(propsToSidePanel(props, urlController.buildUrlCollectionPath, urlController.resolveDatabasePathsFrom, smallLayout, customizationController, authController));
18785
19484
  }
18786
19485
  }
18787
19486
  initialised.current = true;
@@ -18795,7 +19494,7 @@ var useBuildSidePanel = (collectionRegistryController, urlController, navigation
18795
19494
  const panelsFromUrl = buildSidePanelsFromUrl(urlController.urlPathToDataPath(location.pathname), collectionRegistryController.collections ?? [], false);
18796
19495
  if (panelsFromUrl.length <= currentKeys.length) return;
18797
19496
  const lastPanel = panelsFromUrl[panelsFromUrl.length - 1];
18798
- const panelProps = propsToSidePanel(lastPanel, urlController.buildUrlCollectionPath, urlController.resolveDatabasePathsFrom, smallLayout, customizationController, authController, location.search);
19497
+ const panelProps = propsToSidePanel(lastPanel, urlController.buildUrlCollectionPath, urlController.resolveDatabasePathsFrom, smallLayout, customizationController, authController);
18799
19498
  const lastCurrentPanel = currentKeys.length > 0 ? currentKeys[currentKeys.length - 1] : void 0;
18800
19499
  if (!lastCurrentPanel || lastCurrentPanel !== panelProps.key) sideDialogsController.replace(panelProps);
18801
19500
  }
@@ -18827,25 +19526,23 @@ var useBuildSidePanel = (collectionRegistryController, urlController, navigation
18827
19526
  sideDialogsController.open(propsToSidePanel({
18828
19527
  ...props,
18829
19528
  selectedTab: props.selectedTab ?? defaultSelectedView
18830
- }, urlController.buildUrlCollectionPath, urlController.resolveDatabasePathsFrom, smallLayout, customizationController, authController, location.search));
19529
+ }, urlController.buildUrlCollectionPath, urlController.resolveDatabasePathsFrom, smallLayout, customizationController, authController));
18831
19530
  }, [
18832
19531
  sideDialogsController,
18833
19532
  urlController.buildUrlCollectionPath,
18834
19533
  urlController.resolveDatabasePathsFrom,
18835
19534
  smallLayout,
18836
- authController.user,
18837
- location.search
19535
+ authController.user
18838
19536
  ]);
18839
19537
  const replace = useCallback((props) => {
18840
19538
  if (props.copy && !props.entityId) throw Error("If you want to copy a entity you need to provide a entityId");
18841
- sideDialogsController.replace(propsToSidePanel(props, urlController.buildUrlCollectionPath, urlController.resolveDatabasePathsFrom, smallLayout, customizationController, authController, location.search));
19539
+ sideDialogsController.replace(propsToSidePanel(props, urlController.buildUrlCollectionPath, urlController.resolveDatabasePathsFrom, smallLayout, customizationController, authController));
18842
19540
  }, [
18843
19541
  urlController.buildUrlCollectionPath,
18844
19542
  urlController.resolveDatabasePathsFrom,
18845
19543
  sideDialogsController,
18846
19544
  smallLayout,
18847
- authController.user,
18848
- location.search
19545
+ authController.user
18849
19546
  ]);
18850
19547
  return useMemo(() => ({
18851
19548
  close,
@@ -18902,8 +19599,9 @@ function buildSidePanelsFromUrl(path, collections, newFlag) {
18902
19599
  };
18903
19600
  return sidePanel ? [sidePanel] : [];
18904
19601
  }
18905
- var propsToSidePanel = (props, buildUrlCollectionPath, resolveIdsFrom, smallLayout, customizationController, authController, locationSearch) => {
19602
+ var propsToSidePanel = (props, buildUrlCollectionPath, resolveIdsFrom, smallLayout, customizationController, authController) => {
18906
19603
  const collectionPath = removeInitialAndTrailingSlashes(props.path);
19604
+ const locationSearch = typeof window !== "undefined" ? window.location.search : "";
18907
19605
  const shouldUpdateUrl = props.updateUrl !== false;
18908
19606
  const urlPath = shouldUpdateUrl ? props.entityId ? buildUrlCollectionPath(`${collectionPath}/${props.entityId}${props.selectedTab ? "/" + props.selectedTab : ""}${locationSearch}#${SIDE_URL_HASH}`) : buildUrlCollectionPath(`${collectionPath}${locationSearch}#${NEW_URL_HASH}`) : void 0;
18909
19607
  const parentUrlPath = shouldUpdateUrl ? buildUrlCollectionPath(collectionPath) : void 0;
@@ -19081,12 +19779,11 @@ function useHistory(params) {
19081
19779
  refreshTrigger
19082
19780
  ]);
19083
19781
  const loadMore = useCallback(() => {
19084
- if (!isLoading && hasMore && offset + entries.length < total) setOffset((prev) => prev + pageSize);
19782
+ if (!isLoading && hasMore && entries.length < total) setOffset((prev) => prev + pageSize);
19085
19783
  }, [
19086
19784
  isLoading,
19087
19785
  hasMore,
19088
19786
  pageSize,
19089
- offset,
19090
19787
  entries.length,
19091
19788
  total
19092
19789
  ]);
@@ -20169,7 +20866,7 @@ function SelectionTableBinding(props) {
20169
20866
  collection: props.collection
20170
20867
  });
20171
20868
  }
20172
- function SelectionTableBindingInternal({ onSingleEntitySelected, onMultipleEntitiesSelected, multiselect, collection, path: pathInput, selectedEntityIds: selectedEntityIdsProp, description, fixedFilter, maxSelection }) {
20869
+ function SelectionTableBindingInternal({ onSingleEntitySelected, onMultipleEntitiesSelected, onSelectionDone, multiselect, collection, path: pathInput, selectedEntityIds: selectedEntityIdsProp, description, fixedFilter, maxSelection }) {
20173
20870
  const sideDialogContext = useSideDialogContext();
20174
20871
  const sidePanelController = useSidePanel();
20175
20872
  const urlController = useUrlController();
@@ -20276,8 +20973,13 @@ function SelectionTableBindingInternal({ onSingleEntitySelected, onMultipleEntit
20276
20973
  };
20277
20974
  const onDone = useCallback((event) => {
20278
20975
  event.stopPropagation();
20976
+ onSelectionDone?.(selectedEntities);
20279
20977
  sideDialogContext.close(false);
20280
- }, [sideDialogContext]);
20978
+ }, [
20979
+ sideDialogContext,
20980
+ onSelectionDone,
20981
+ selectedEntities
20982
+ ]);
20281
20983
  const displayedColumnIds = useColumnIds(collection, false);
20282
20984
  const tableController = useDataTableController({
20283
20985
  path,
@@ -20442,6 +21144,7 @@ function ArrayOfReferencesFieldBinding({ propertyKey, value, error, showError, d
20442
21144
  });
20443
21145
  const onEntryClick = (e) => {
20444
21146
  e.preventDefault();
21147
+ if (disabled || isSubmitting) return;
20445
21148
  referenceDialogController.open();
20446
21149
  };
20447
21150
  const buildEntry = useCallback(({ index, internalId, storedProps, storeProps }) => {
@@ -20460,7 +21163,9 @@ function ArrayOfReferencesFieldBinding({ propertyKey, value, error, showError, d
20460
21163
  }, [
20461
21164
  ofProperty.path,
20462
21165
  ofProperty.admin?.previewProperties,
20463
- value
21166
+ value,
21167
+ disabled,
21168
+ isSubmitting
20464
21169
  ]);
20465
21170
  const title = /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(LabelWithIcon, {
20466
21171
  propertyKey,
@@ -20482,7 +21187,7 @@ function ArrayOfReferencesFieldBinding({ propertyKey, value, error, showError, d
20482
21187
  children: [/* @__PURE__ */ jsx(ArrayContainer, {
20483
21188
  droppableId: propertyKey,
20484
21189
  value,
20485
- disabled: isSubmitting,
21190
+ disabled: disabled || isSubmitting,
20486
21191
  buildEntry,
20487
21192
  canAddElements: false,
20488
21193
  addLabel: property.name ? t("add_reference_to", { name: property.name }) : t("add_reference"),
@@ -20491,14 +21196,9 @@ function ArrayOfReferencesFieldBinding({ propertyKey, value, error, showError, d
20491
21196
  }), /* @__PURE__ */ jsxs(Button, {
20492
21197
  className: "ml-3.5 my-4 justify-center text-left",
20493
21198
  variant: "text",
20494
- disabled: isSubmitting,
21199
+ disabled: disabled || isSubmitting,
20495
21200
  onClick: onEntryClick,
20496
- children: [
20497
- /* @__PURE__ */ jsx(PencilIcon, { size: 16 }),
20498
- t("edit"),
20499
- " ",
20500
- property.name
20501
- ]
21201
+ children: [/* @__PURE__ */ jsx(PencilIcon, { size: 16 }), t("edit_name", { name: property.name ?? propertyKey })]
20502
21202
  })]
20503
21203
  })] });
20504
21204
  return /* @__PURE__ */ jsxs(Fragment, { children: [
@@ -20696,7 +21396,8 @@ function DateTimeFieldBinding({ propertyKey, value, setValue, autoFocus, error,
20696
21396
  size,
20697
21397
  value: internalValue,
20698
21398
  onChange: (dateValue) => setValue(dateValue),
20699
- mode: property.mode,
21399
+ mode: getDatePropertyMode(property),
21400
+ timezone: getDatePropertyTimezone(property),
20700
21401
  clearable: property.admin?.clearable,
20701
21402
  locale,
20702
21403
  error: showError,
@@ -21279,13 +21980,20 @@ function MarkdownEditorFieldBinding({ property, propertyKey, value, setValue, in
21279
21980
  const internalValue = useRef(value);
21280
21981
  useEffect(() => {
21281
21982
  getMarkdownUtils().then(() => {
21282
- canonicalRef.current = canonicalizeMarkdown(value);
21983
+ canonicalRef.current = canonicalizeMarkdown(canonicalSourceRef.current);
21283
21984
  });
21284
21985
  }, []);
21285
21986
  const canonicalRef = useRef(useMemo(() => canonicalizeMarkdown(value), []));
21987
+ const canonicalSourceRef = useRef(value);
21286
21988
  const onContentChange = useCallback((content) => {
21287
21989
  const normalizedContent = normalizeMarkdown(content);
21288
- if (normalizedContent === canonicalRef.current) return;
21990
+ if (normalizedContent === canonicalRef.current) {
21991
+ if (value !== canonicalSourceRef.current) {
21992
+ internalValue.current = canonicalSourceRef.current;
21993
+ setValue(canonicalSourceRef.current);
21994
+ }
21995
+ return;
21996
+ }
21289
21997
  if (normalizedContent === normalizeMarkdown(value)) return;
21290
21998
  internalValue.current = content;
21291
21999
  setValue(content);
@@ -21293,6 +22001,7 @@ function MarkdownEditorFieldBinding({ property, propertyKey, value, setValue, in
21293
22001
  useEffect(() => {
21294
22002
  if (internalValue.current !== value) {
21295
22003
  internalValue.current = value;
22004
+ canonicalSourceRef.current = value;
21296
22005
  canonicalRef.current = canonicalizeMarkdown(value);
21297
22006
  setFieldVersion((v) => v + 1);
21298
22007
  }
@@ -21495,6 +22204,7 @@ function ReferenceFieldBinding(props) {
21495
22204
  }
21496
22205
  function ReferenceFieldBindingInternal({ propertyKey, value, setValue, error, showError, isSubmitting, disabled, minimalistView, touched, autoFocus, property, includeDescription, hideLabel, size = "medium" }) {
21497
22206
  if (!property.path) throw new Error("Property path is required for ReferenceFieldBinding");
22207
+ const { t } = useTranslation();
21498
22208
  const refValue = value;
21499
22209
  useClearRestoreValue({
21500
22210
  property,
@@ -21520,6 +22230,7 @@ function ReferenceFieldBindingInternal({ propertyKey, value, setValue, error, sh
21520
22230
  });
21521
22231
  const onEntryClick = (e) => {
21522
22232
  e.preventDefault();
22233
+ if (disabled || isSubmitting) return;
21523
22234
  referenceDialogController.open();
21524
22235
  };
21525
22236
  return /* @__PURE__ */ jsxs(Fragment, { children: [
@@ -21544,12 +22255,12 @@ function ReferenceFieldBindingInternal({ propertyKey, value, setValue, error, sh
21544
22255
  className: "justify-center text-left",
21545
22256
  children: /* @__PURE__ */ jsxs(EntityPreviewContainer, {
21546
22257
  className: cls("px-6 h-12 text-sm font-medium flex items-center gap-6", disabled || isSubmitting ? "text-surface-accent-500" : "cursor-pointer text-surface-accent-700 dark:text-surface-accent-300 hover:bg-surface-hover group-hover:bg-surface-hover"),
21547
- onClick: onEntryClick,
22258
+ onClick: disabled || isSubmitting ? void 0 : onEntryClick,
21548
22259
  size: "medium",
21549
22260
  children: [/* @__PURE__ */ jsx(IconForView, {
21550
22261
  collectionOrView: collection,
21551
22262
  className: "text-surface-300 dark:text-surface-600"
21552
- }), `Edit ${property.name}`.toUpperCase()]
22263
+ }), t("edit_name", { name: property.name ?? propertyKey }).toUpperCase()]
21553
22264
  })
21554
22265
  })] }),
21555
22266
  /* @__PURE__ */ jsx(FieldHelperText, {
@@ -22605,6 +23316,7 @@ function MultipleRelationFieldBinding({ propertyKey, value: valueProp, error, sh
22605
23316
  });
22606
23317
  const onEntryClick = (e) => {
22607
23318
  e.preventDefault();
23319
+ if (disabled || isSubmitting) return;
22608
23320
  referenceDialogController.open();
22609
23321
  };
22610
23322
  const buildEntry = useCallback(({ index, internalId, storedProps, storeProps }) => {
@@ -22622,7 +23334,9 @@ function MultipleRelationFieldBinding({ propertyKey, value: valueProp, error, sh
22622
23334
  }, [
22623
23335
  relation,
22624
23336
  property.admin?.previewProperties,
22625
- value
23337
+ value,
23338
+ disabled,
23339
+ isSubmitting
22626
23340
  ]);
22627
23341
  const title = /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(LabelWithIcon, {
22628
23342
  propertyKey,
@@ -22644,7 +23358,7 @@ function MultipleRelationFieldBinding({ propertyKey, value: valueProp, error, sh
22644
23358
  children: [/* @__PURE__ */ jsx(ArrayContainer, {
22645
23359
  droppableId: propertyKey,
22646
23360
  value,
22647
- disabled: isSubmitting,
23361
+ disabled: disabled || isSubmitting,
22648
23362
  buildEntry,
22649
23363
  canAddElements: false,
22650
23364
  addLabel: property.name ? t("add_reference_to", { name: property.name }) : t("add_reference"),
@@ -22654,14 +23368,9 @@ function MultipleRelationFieldBinding({ propertyKey, value: valueProp, error, sh
22654
23368
  className: "ml-3.5 my-4 justify-center text-left",
22655
23369
  variant: "text",
22656
23370
  color: "primary",
22657
- disabled: isSubmitting,
23371
+ disabled: disabled || isSubmitting,
22658
23372
  onClick: onEntryClick,
22659
- children: [
22660
- /* @__PURE__ */ jsx(PencilIcon, { size: 16 }),
22661
- t("edit"),
22662
- " ",
22663
- property.name
22664
- ]
23373
+ children: [/* @__PURE__ */ jsx(PencilIcon, { size: 16 }), t("edit_name", { name: property.name ?? propertyKey })]
22665
23374
  })]
22666
23375
  })] });
22667
23376
  return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(ExpandablePanel, {
@@ -22734,6 +23443,7 @@ function RelationSelectorBinding({ propertyKey, value, size, error, showError, d
22734
23443
  });
22735
23444
  }
22736
23445
  function SingleRelationFieldBinding({ propertyKey, value, size, error, showError, disabled, isSubmitting, property, includeDescription, hideLabel, setValue, relation }) {
23446
+ const { t } = useTranslation();
22737
23447
  const normalizedValue = value && !Array.isArray(value) ? normalizeToEntityRelation(value) : null;
22738
23448
  const validValue = !!normalizedValue;
22739
23449
  const collection = relation?.target();
@@ -22751,6 +23461,7 @@ function SingleRelationFieldBinding({ propertyKey, value, size, error, showError
22751
23461
  });
22752
23462
  const onEntryClick = (e) => {
22753
23463
  e.preventDefault();
23464
+ if (disabled || isSubmitting) return;
22754
23465
  referenceDialogController.open();
22755
23466
  };
22756
23467
  const usedRelation = Array.isArray(value) ? void 0 : normalizedValue ?? void 0;
@@ -22776,12 +23487,12 @@ function SingleRelationFieldBinding({ propertyKey, value, size, error, showError
22776
23487
  className: "justify-center text-left",
22777
23488
  children: /* @__PURE__ */ jsxs(EntityPreviewContainer, {
22778
23489
  className: cls("px-6 h-12 text-sm font-medium flex items-center gap-6", disabled || isSubmitting ? "text-surface-accent-500" : "cursor-pointer text-surface-accent-700 dark:text-surface-accent-300 hover:bg-surface-hover group-hover:bg-surface-hover"),
22779
- onClick: onEntryClick,
23490
+ onClick: disabled || isSubmitting ? void 0 : onEntryClick,
22780
23491
  size: "medium",
22781
23492
  children: [/* @__PURE__ */ jsx(IconForView, {
22782
23493
  collectionOrView: collection,
22783
23494
  className: "text-surface-300 dark:text-surface-600"
22784
- }), `Edit ${property.name}`.toUpperCase()]
23495
+ }), t("edit_name", { name: property.name ?? propertyKey }).toUpperCase()]
22785
23496
  })
22786
23497
  })] }),
22787
23498
  /* @__PURE__ */ jsx(FieldHelperText, {
@@ -23387,6 +24098,25 @@ function PropertyFieldBindingInternal({ propertyKey, property, context, includeD
23387
24098
  }
23388
24099
  function FieldInternal({ Component, componentProps: { propertyKey, property, includeDescription, hideLabel, underlyingValueHasChanged, partOfArray, partOfBlock, minimalistView, autoFocus, context, disabled, size, onPropertyChange }, formexFieldProps }) {
23389
24100
  const { plugins } = useCustomizationController();
24101
+ const { t } = useTranslation();
24102
+ const rebaseContext = useRebaseContext();
24103
+ const fieldSlotProps = React.useMemo(() => ({
24104
+ propertyKey,
24105
+ property,
24106
+ path: context.path ?? "",
24107
+ entityId: context.entityId,
24108
+ collection: context.collection,
24109
+ context: rebaseContext
24110
+ }), [
24111
+ propertyKey,
24112
+ property,
24113
+ context.path,
24114
+ context.entityId,
24115
+ context.collection,
24116
+ rebaseContext
24117
+ ]);
24118
+ const beforeField = useSlot("entity.field.before", fieldSlotProps);
24119
+ const afterField = useSlot("entity.field.after", fieldSlotProps);
23390
24120
  const customFieldProps = property.admin?.customProps;
23391
24121
  const value = formexFieldProps.field.value;
23392
24122
  const error = getIn(formexFieldProps.form.errors, propertyKey);
@@ -23401,42 +24131,47 @@ function FieldInternal({ Component, componentProps: { propertyKey, property, inc
23401
24131
  plugins
23402
24132
  }) ?? Component;
23403
24133
  const isSubmitting = formexFieldProps.form.isSubmitting;
23404
- return /* @__PURE__ */ jsxs(ErrorBoundary, { children: [/* @__PURE__ */ jsx(Suspense, {
23405
- fallback: null,
23406
- children: /* @__PURE__ */ jsx(UsedComponent, {
23407
- propertyKey,
23408
- value,
23409
- setValue: useCallback((value, shouldValidate) => {
23410
- formexFieldProps.form.setFieldTouched(propertyKey, true, false);
23411
- formexFieldProps.form.setFieldValue(propertyKey, value, shouldValidate);
23412
- }, []),
23413
- setFieldValue: useCallback((otherPropertyKey, value, shouldValidate) => {
23414
- formexFieldProps.form.setFieldTouched(propertyKey, true, false);
23415
- formexFieldProps.form.setFieldValue(otherPropertyKey, value, shouldValidate);
23416
- }, []),
23417
- error,
23418
- touched,
23419
- showError,
23420
- isSubmitting,
23421
- includeDescription: includeDescription ?? true,
23422
- hideLabel: hideLabel ?? false,
23423
- property,
23424
- disabled: disabled ?? false,
23425
- underlyingValueHasChanged: underlyingValueHasChanged ?? false,
23426
- partOfArray: partOfArray ?? false,
23427
- partOfBlock: partOfBlock ?? false,
23428
- minimalistView: minimalistView ?? false,
23429
- autoFocus: autoFocus ?? false,
23430
- customProps: customFieldProps,
23431
- context,
23432
- size,
23433
- onPropertyChange
24134
+ return /* @__PURE__ */ jsxs(ErrorBoundary, { children: [
24135
+ beforeField,
24136
+ /* @__PURE__ */ jsx(Suspense, {
24137
+ fallback: null,
24138
+ children: /* @__PURE__ */ jsx(UsedComponent, {
24139
+ propertyKey,
24140
+ value,
24141
+ setValue: useCallback((value, shouldValidate) => {
24142
+ formexFieldProps.form.setFieldTouched(propertyKey, true, false);
24143
+ formexFieldProps.form.setFieldValue(propertyKey, value, shouldValidate);
24144
+ }, []),
24145
+ setFieldValue: useCallback((otherPropertyKey, value, shouldValidate) => {
24146
+ formexFieldProps.form.setFieldTouched(propertyKey, true, false);
24147
+ formexFieldProps.form.setFieldValue(otherPropertyKey, value, shouldValidate);
24148
+ }, []),
24149
+ error,
24150
+ touched,
24151
+ showError,
24152
+ isSubmitting,
24153
+ includeDescription: includeDescription ?? true,
24154
+ hideLabel: hideLabel ?? false,
24155
+ property,
24156
+ disabled: disabled ?? false,
24157
+ underlyingValueHasChanged: underlyingValueHasChanged ?? false,
24158
+ partOfArray: partOfArray ?? false,
24159
+ partOfBlock: partOfBlock ?? false,
24160
+ minimalistView: minimalistView ?? false,
24161
+ autoFocus: autoFocus ?? false,
24162
+ customProps: customFieldProps,
24163
+ context,
24164
+ size,
24165
+ onPropertyChange
24166
+ })
24167
+ }),
24168
+ afterField,
24169
+ underlyingValueHasChanged && !isSubmitting && /* @__PURE__ */ jsx(Typography, {
24170
+ variant: "caption",
24171
+ className: "ml-3.5",
24172
+ children: t("value_updated_elsewhere")
23434
24173
  })
23435
- }), underlyingValueHasChanged && !isSubmitting && /* @__PURE__ */ jsx(Typography, {
23436
- variant: "caption",
23437
- className: "ml-3.5",
23438
- children: "This value has been updated elsewhere"
23439
- })] });
24174
+ ] });
23440
24175
  }
23441
24176
  var shouldPropertyReRender = (property, plugins) => {
23442
24177
  if (plugins?.some((plugin) => plugin.fieldBuilder)) return true;
@@ -23940,7 +24675,13 @@ function EntityForm({ path, entityId: entityIdProp, collection, onValuesModified
23940
24675
  Promise.resolve(onSubmitProp(valuesToBeSaved, formex)).finally(() => setIsSavingAutoSave(false));
23941
24676
  }
23942
24677
  }, false, 2e3);
23943
- const [underlyingChanges] = useState({});
24678
+ /**
24679
+ * The properties the record changed underneath an edit of the user's own,
24680
+ * keyed to the value they changed to — each one shown as "updated
24681
+ * elsewhere" while the form still holds something else. Filled by the
24682
+ * effect that follows the baseline, below.
24683
+ */
24684
+ const [underlyingChanges, setUnderlyingChanges] = useState({});
23944
24685
  const initialEntityId = useMemo(() => {
23945
24686
  if (status === "new" || status === "copy") return;
23946
24687
  else return entityIdProp;
@@ -24009,6 +24750,7 @@ function EntityForm({ path, entityId: entityIdProp, collection, onValuesModified
24009
24750
  }), {}) : {},
24010
24751
  onSubmit,
24011
24752
  onReset: () => {
24753
+ setUnderlyingChanges({});
24012
24754
  onResetProp?.();
24013
24755
  onValuesModified?.(false, baseInitialValues);
24014
24756
  },
@@ -24055,12 +24797,13 @@ function EntityForm({ path, entityId: entityIdProp, collection, onValuesModified
24055
24797
  };
24056
24798
  const lastSavedValues = useRef(entity?.values);
24057
24799
  const save = async (values) => {
24058
- const valuesToSave = status === "existing" ? getChanges(values, entity?.values || {}) : values;
24800
+ const writtenValues = applyValueTransforms(values, collection.properties);
24801
+ const valuesToSave = status === "existing" ? getChangedProperties(writtenValues, entity?.values || {}) : writtenValues;
24059
24802
  if (status === "existing" && Object.keys(valuesToSave).length === 0 && entity) return Promise.resolve(entity);
24060
24803
  lastSavedValues.current = values;
24061
24804
  if (!onSubmitProp) return;
24062
24805
  if (autoSave ?? false) {
24063
- setValuesToBeSaved(values);
24806
+ setValuesToBeSaved(valuesToSave);
24064
24807
  return Promise.resolve();
24065
24808
  }
24066
24809
  return Promise.resolve(onSubmitProp(valuesToSave, formex)).then((result) => {
@@ -24117,23 +24860,45 @@ function EntityForm({ path, entityId: entityIdProp, collection, onValuesModified
24117
24860
  uniqueFieldValidator
24118
24861
  ]);
24119
24862
  useOnAutoSave(autoSave, formex, lastSavedValues, save);
24863
+ /**
24864
+ * The record changed while the form was open: someone else saved it, and
24865
+ * the listener delivered the new version as a new baseline.
24866
+ *
24867
+ * An unmodified form follows it whole — formex does that. A modified one
24868
+ * keeps the edit, and formex keeps *everything* it holds: a property the
24869
+ * user never changed would still show the old value, differ from the new
24870
+ * stored one, and be written back by the next save, reverting the other
24871
+ * change without a word.
24872
+ *
24873
+ * So each top-level property that changed underneath is judged on its own,
24874
+ * against the baseline the form last followed. One the user has not changed
24875
+ * follows the record. One the user has changed keeps the edit — that is
24876
+ * what a save writes — and is marked as updated elsewhere, so the overwrite
24877
+ * is not a surprise.
24878
+ */
24879
+ const followedBaseline = useRef(baseInitialValues);
24120
24880
  useEffect(() => {
24121
- if (!autoSave && !formex.isSubmitting && underlyingChanges && entity) Object.entries(underlyingChanges).forEach(([key, value]) => {
24122
- const formValue = formex.values[key];
24123
- if (!deepEqual(value, formValue) && !formex.touched[key]) {
24124
- console.debug("Updated value from the driver:", key, value);
24125
- formex.setFieldValue(key, value !== void 0 ? value : null);
24126
- }
24881
+ const previous = followedBaseline.current ?? {};
24882
+ const next = baseInitialValues ?? {};
24883
+ followedBaseline.current = baseInitialValues;
24884
+ if (previous === next) return;
24885
+ const current = formex.values;
24886
+ if (deepEqual(previous, current)) return;
24887
+ const conflicts = {};
24888
+ const changedUnderneath = /* @__PURE__ */ new Set([...Object.keys(previous), ...Object.keys(next)]);
24889
+ for (const key of changedUnderneath) if (deepEqual(previous[key], next[key])) changedUnderneath.delete(key);
24890
+ else if (deepEqual(current[key], previous[key])) formex.setFieldValue(key, next[key]);
24891
+ else if (!deepEqual(current[key], next[key])) conflicts[key] = next[key];
24892
+ if (changedUnderneath.size === 0) return;
24893
+ setUnderlyingChanges((prev) => {
24894
+ const updated = { ...prev };
24895
+ changedUnderneath.forEach((key) => {
24896
+ if (key in conflicts) updated[key] = conflicts[key];
24897
+ else delete updated[key];
24898
+ });
24899
+ return updated;
24127
24900
  });
24128
- }, [
24129
- formex.isSubmitting,
24130
- autoSave,
24131
- underlyingChanges,
24132
- entity,
24133
- formex.values,
24134
- formex.touched,
24135
- formex.setFieldValue
24136
- ]);
24901
+ }, [baseInitialValues]);
24137
24902
  const formFieldKeys = getFormFieldKeys(collection);
24138
24903
  const layout = useMemo(() => resolveFormLayout({
24139
24904
  collection,
@@ -24210,7 +24975,7 @@ function EntityForm({ path, entityId: entityIdProp, collection, onValuesModified
24210
24975
  console.warn(`Property ${field.key} not found in collection ${collection.name} in properties or additional fields. Skipping.`);
24211
24976
  return null;
24212
24977
  }
24213
- const underlyingValueHasChanged = !!underlyingChanges && Object.keys(underlyingChanges).includes(field.key) && formex.touched[field.key];
24978
+ const underlyingValueHasChanged = field.key in underlyingChanges && !deepEqual(formex.values[field.key], underlyingChanges[field.key]);
24214
24979
  const disabled = isFieldDisabled(property);
24215
24980
  const selfLabelling = isSelfLabellingProperty(property);
24216
24981
  const cmsFormFieldProps = {
@@ -25620,6 +26385,6 @@ function getFullIdPath(propertyKey, propertyNamespace) {
25620
26385
  return idToPropertiesPath(propertyNamespace ? `${propertyNamespace}.${propertyKey}` : propertyKey);
25621
26386
  }
25622
26387
  //#endregion
25623
- export { NAVIGATION_DEFAULT_GROUP_NAME as $, CollectionRegistryContext as $n, getCollectionBySlugWithin as $t, getFieldId as A, ReadOnlyFieldBinding as An, getIconForWidget as Ar, convertDataToEntity as At, MapFieldBinding as B, MapPropertyPreview as Bn, detectCsvDelimiter as Bt, EntityFormBinding as C, NavigationStateContext as Cn, LABEL_ICON_SIZE as Cr, EntityCardBinding as Ct, getDefaultFieldConfig as D, SelectableTableContext as Dn, getBracketNotation as Dr, useCollectionEditorDialogsState as Dt, DEFAULT_FIELD_CONFIGS as E, SideDialogsControllerContext as En, PropertyKeyHint as Er, ConfigControllerProvider as Et, SelectFieldBinding as F, UserPreview as Fn, isRelationProperty as Fr, ImportSaveInProgress as Ft, useSelectionDialog as G, ArrayOfStorageComponentsPreview as Gn, SearchIconsView as Gt, DateTimeFieldBinding as H, ArrayOfStringsPreview as Hn, parseCsvToObjects as Ht, RepeatFieldBinding as I, NumberPropertyPreview as In, IMPORT_BATCH_SIZE as It, useBuildUrlController as J, InlineEntityListPreview as Jn, BreadcrumbsProvider as Jt, SelectionTableBinding as K, RelationPreview as Kn, FieldCaption as Kt, ReferenceFieldBinding as L, BooleanPreview as Ln, saveImportedEntities as Lt, TextFieldBinding as M, FieldHelperText as Mn, getPropertyInPath$1 as Mr, processValueMapping as Mt, SwitchFieldBinding as N, ArrayOfMapsPreview as Nn, getResolvedPropertyInPath as Nr, getInferenceType as Nt, getDefaultFieldId as O, ArrayCustomShapedFieldBinding as On, getDefaultPropertiesOrder as Or, ImportNewPropertyFieldPreview as Ot, StorageUploadFieldBinding as P, PropertyPreview as Pn, isReferenceProperty as Pr, useImportConfig as Pt, useResolvedCollections as Q, useSidePanel as Qn, addInitialSlash as Qt, MultiSelectFieldBinding as R, DatePreview as Rn, ImportFileUpload as Rt, isSchemaChangeCancelled as S, useUrlController as Sn, FieldBlock as Sr, CollectionCardViewBinding as St, PropertyFieldBinding as T, useSideDialogsController as Tn, spanClass as Tr, useCollectionEditorController as Tt, BlockFieldBinding as U, ArrayPropertyEnumPreview as Un, ArrayContainer as Ut, KeyValueFieldBinding as V, ArrayOneOfPreview as Vn, parseCsvRows as Vt, ArrayOfReferencesFieldBinding as W, ArrayEnumPreview as Wn, PropertyConfigBadge as Wt, useTopLevelNavigation as X, EntityPreviewBinding as Xn, resolveEntityView as Xt, useBuildNavigationStateController as Y, ReferencePreview as Yn, resolveEntityAction as Yt, useResolvedViews as Z, SidePanelControllerContext as Zn, mergeEntityActions as Zt, useCollectionsConfigController as _, CollectionTableBinding as _n, ImagePreview as _r, editEntityAction as _t, namespaceToPropertiesPath as a, resolveCollectionPathIds$1 as an, getEntityTitlePropertyKeyForEntity as ar, getEntityViewWidth as at, asUnavailable as b, useAdminContext as bn, FormRail as br, DetailViewBinding as bt, buildCollectionGenerationCallback as c, SelectionMenu as cn, EnumValuesChip as cr, useSafeSnackbarController as ct, fromSerializableCollectionConfigs as d, resolveSelection as dn, SkeletonPropertyComponent as dr, getInitialEntityValues as dt, getCollectionPathsCombinations as en, useCollectionRegistryController as er, useBuildCollectionRegistryController as et, fromSerializableProperties as f, selectionQueryToFindParams as fn, renderSkeletonCaptionText as fr, removeEmptyContainers as ft, toSerializableProperty as g, VirtualTableInput$1 as gn, UrlComponentPreview as gr, deleteEntityAction as gt, toSerializableProperties as h, useSelectionController as hn, renderSkeletonText as hr, copyEntityAction as ht, namespaceToPropertiesOrderPath as i, removeTrailingSlash$1 as in, getEntityTitlePropertyKey as ir, buildSidePanelsFromUrl as it, VectorFieldBinding as j, LabelWithIcon as jn, getPropertiesWithPropertiesOrder as jr, flattenEntry as jt, getFieldConfig as k, useClearRestoreValue as kn, getIconForProperty as kr, DataNewPropertiesMapping as kt, validateCollectionJson as l, MAX_SELECTION_ROWS as ln, StorageThumbnail as lr, extractTouchedValues as lt, toSerializableCollectionConfig as m, walkEntityPages as mn, renderSkeletonImageThumbnail as mr, CollectionViewBinding as mt, getFullIdPath as n, removeInitialAndTrailingSlashes$1 as nn, useResolvedUser as nr, resolveNavigationFrom as nt, CollectionGenerationApiError as o, resolveOpenEntityMode as on, ArrayPropertyPreview as or, useBuildSidePanel as ot, fromSerializableProperty as p, serializeSelectionQuery as pn, renderSkeletonIcon as pr, zodToFormErrors as pt, SideDialogs as q, ArrayOfReferencesPreview as qn, useBreadcrumbsController as qt, idToPropertiesPath as r, removeInitialSlash as rn, getEntityPreviewKeys as rr, useResolvedNavigationFrom as rt, DEFAULT_COLLECTION_GENERATION_ENDPOINT as s, resolveViewMode as sn, StringPropertyPreview as sr, EditViewBinding as st, getFullId as t, getLastSegment$1 as tn, getUserLabel as tr, useHistory as tt, fromSerializableCollectionConfig as u, SELECTION_PAGE_SIZE as un, StorageThumbnailInternal as ur, getChanges as ut, LiveSchemaError as v, SelectableTable as vn, EmptyValue as vr, resetPasswordAction as vt, EntityForm as w, useNavigationStateController as wn, isSelfLabellingProperty as wr, CollectionViewActions as wt, createLiveSchemaClient as x, UrlContext as xn, RecordMeta as xr, EntityViewBinding as xt, SchemaChangeCancelled as y, CollectionRowActions as yn, FormSections as yr, CreationResultDialog as yt, MarkdownEditorFieldBinding as z, KeyValuePreview as zn, convertFileToJson as zt };
26388
+ export { NAVIGATION_DEFAULT_GROUP_NAME as $, ArrayPropertyEnumPreview as $n, PropertyConfigBadge as $t, getFieldId as A, useAdminContext as An, FormRail as Ar, guessIdColumn as At, MapFieldBinding as B, ReadOnlyFieldBinding as Bn, getIconForWidget as Br, convertFileToJson as Bt, EntityFormBinding as C, serializeSelectionQuery as Cn, renderSkeletonIcon as Cr, EntityCardBinding as Ct, getDefaultFieldConfig as D, CollectionTableBinding as Dn, ImagePreview as Dr, useCollectionEditorDialogsState as Dt, DEFAULT_FIELD_CONFIGS as E, VirtualTableInput$1 as En, UrlComponentPreview as Er, ConfigControllerProvider as Et, SelectFieldBinding as F, useSideDialogsController as Fn, spanClass as Fr, useImportConfig as Ft, useSelectionDialog as G, UserPreview as Gn, isRelationProperty as Gr, downloadDataAsCsv as Gt, DateTimeFieldBinding as H, FieldHelperText as Hn, getPropertyInPath$1 as Hr, parseCsvRows as Ht, RepeatFieldBinding as I, SideDialogsControllerContext as In, PropertyKeyHint as Ir, ImportSaveInProgress as It, useBuildUrlController as J, DatePreview as Jn, escapeCsvFormula as Jt, SelectionTableBinding as K, NumberPropertyPreview as Kn, downloadEntitiesExport as Kt, ReferenceFieldBinding as L, SelectableTableContext as Ln, getBracketNotation as Lr, IMPORT_BATCH_SIZE as Lt, TextFieldBinding as M, useUrlController as Mn, FieldBlock as Mr, flattenEntry as Mt, SwitchFieldBinding as N, NavigationStateContext as Nn, LABEL_ICON_SIZE as Nr, processValueMapping as Nt, getDefaultFieldId as O, SelectableTable as On, EmptyValue as Or, ImportNewPropertyFieldPreview as Ot, StorageUploadFieldBinding as P, useNavigationStateController as Pn, isSelfLabellingProperty as Pr, getInferenceType as Pt, useResolvedCollections as Q, ArrayOfStringsPreview as Qn, ArrayContainer as Qt, MultiSelectFieldBinding as R, ArrayCustomShapedFieldBinding as Rn, getDefaultPropertiesOrder as Rr, saveImportedEntities as Rt, isSchemaChangeCancelled as S, selectionQueryToFindParams as Sn, renderSkeletonCaptionText as Sr, CollectionCardViewBinding as St, PropertyFieldBinding as T, useSelectionController as Tn, renderSkeletonText as Tr, useCollectionEditorController as Tt, BlockFieldBinding as U, ArrayOfMapsPreview as Un, getResolvedPropertyInPath as Ur, parseCsvToObjects as Ut, KeyValueFieldBinding as V, LabelWithIcon as Vn, getPropertiesWithPropertiesOrder as Vr, detectCsvDelimiter as Vt, ArrayOfReferencesFieldBinding as W, PropertyPreview as Wn, isReferenceProperty as Wr, downloadBlob as Wt, useTopLevelNavigation as X, MapPropertyPreview as Xn, getEntityJsonExportableData as Xt, useBuildNavigationStateController as Y, KeyValuePreview as Yn, getEntityCSVExportableData as Yt, useResolvedViews as Z, ArrayOneOfPreview as Zn, unescapeCsvFormula as Zt, useCollectionsConfigController as _, resolveViewMode as _n, StringPropertyPreview as _r, editEntityAction as _t, namespaceToPropertiesPath as a, resolveEntityView as an, ReferencePreview as ar, getEntityViewWidth as at, asUnavailable as b, SELECTION_PAGE_SIZE as bn, StorageThumbnailInternal as br, DetailViewBinding as bt, buildCollectionGenerationCallback as c, getCollectionBySlugWithin as cn, useSidePanel as cr, useSafeSnackbarController as ct, fromSerializableCollectionConfigs as d, isPathWithinRecord as dn, getUserLabel as dr, getInitialEntityValues as dt, SearchIconsView as en, ArrayEnumPreview as er, useBuildCollectionRegistryController as et, fromSerializableProperties as f, removeInitialAndTrailingSlashes$1 as fn, useResolvedUser as fr, removeEmptyContainers as ft, toSerializableProperty as g, resolveOpenEntityMode as gn, ArrayPropertyPreview as gr, deleteEntityAction as gt, toSerializableProperties as h, resolveCollectionPathIds$1 as hn, getEntityTitlePropertyKeyForEntity as hr, copyEntityAction as ht, namespaceToPropertiesOrderPath as i, resolveEntityAction as in, InlineEntityListPreview as ir, buildSidePanelsFromUrl as it, VectorFieldBinding as j, UrlContext as jn, RecordMeta as jr, convertDataToEntity as jt, getFieldConfig as k, CollectionRowActions as kn, FormSections as kr, DataNewPropertiesMapping as kt, validateCollectionJson as l, getCollectionPathsCombinations as ln, CollectionRegistryContext as lr, extractTouchedValues as lt, toSerializableCollectionConfig as m, removeTrailingSlash$1 as mn, getEntityTitlePropertyKey as mr, CollectionViewBinding as mt, getFullIdPath as n, useBreadcrumbsController as nn, RelationPreview as nr, resolveNavigationFrom as nt, CollectionGenerationApiError as o, mergeEntityActions as on, EntityPreviewBinding as or, useBuildSidePanel as ot, fromSerializableProperty as p, removeInitialSlash as pn, getEntityPreviewKeys as pr, zodToFormErrors as pt, SideDialogs as q, BooleanPreview as qn, entryToCSVRow as qt, idToPropertiesPath as r, BreadcrumbsProvider as rn, ArrayOfReferencesPreview as rr, useResolvedNavigationFrom as rt, DEFAULT_COLLECTION_GENERATION_ENDPOINT as s, addInitialSlash as sn, SidePanelControllerContext as sr, EditViewBinding as st, getFullId as t, FieldCaption as tn, ArrayOfStorageComponentsPreview as tr, useHistory as tt, fromSerializableCollectionConfig as u, getLastSegment$1 as un, useCollectionRegistryController as ur, getChanges as ut, LiveSchemaError as v, SelectionMenu as vn, EnumValuesChip as vr, resetPasswordAction as vt, EntityForm as w, walkEntityPages as wn, renderSkeletonImageThumbnail as wr, CollectionViewActions as wt, createLiveSchemaClient as x, resolveSelection as xn, SkeletonPropertyComponent as xr, EntityViewBinding as xt, SchemaChangeCancelled as y, MAX_SELECTION_ROWS as yn, StorageThumbnail as yr, CreationResultDialog as yt, MarkdownEditorFieldBinding as z, useClearRestoreValue as zn, getIconForProperty as zr, ImportFileUpload as zt };
25624
26389
 
25625
- //# sourceMappingURL=util-qfCPxJzB.js.map
26390
+ //# sourceMappingURL=util-DybQBkug.js.map