@rebasepro/admin 0.6.0 → 0.7.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 (149) hide show
  1. package/README.md +7 -3
  2. package/dist/{CollectionEditorDialog-jA-PLh6N.js → CollectionEditorDialog-iGgS4rLX.js} +233 -152
  3. package/dist/CollectionEditorDialog-iGgS4rLX.js.map +1 -0
  4. package/dist/{PropertyEditView-DEUddmg_.js → PropertyEditView-C9yhLyja.js} +129 -15
  5. package/dist/PropertyEditView-C9yhLyja.js.map +1 -0
  6. package/dist/{CollectionsStudioView-UKouBPOm.js → RouterCollectionsStudioView-Dqx27GD8.js} +61 -39
  7. package/dist/RouterCollectionsStudioView-Dqx27GD8.js.map +1 -0
  8. package/dist/collection_editor/extensibility_types.d.ts +135 -0
  9. package/dist/collection_editor/index.d.ts +6 -0
  10. package/dist/collection_editor/serializable_types.d.ts +374 -0
  11. package/dist/collection_editor/serializable_utils.d.ts +50 -0
  12. package/dist/collection_editor/ui/collection_editor/CollectionEditorDialog.d.ts +2 -1
  13. package/dist/collection_editor/ui/collection_editor/CollectionPropertiesEditorForm.d.ts +12 -1
  14. package/dist/collection_editor/ui/collection_editor/CollectionStudioView.d.ts +13 -1
  15. package/dist/collection_editor/ui/collection_editor/CollectionsStudioView.d.ts +24 -2
  16. package/dist/collection_editor/ui/collection_editor/DisplaySettingsForm.d.ts +2 -1
  17. package/dist/collection_editor/ui/collection_editor/GeneralSettingsForm.d.ts +4 -1
  18. package/dist/collection_editor/ui/collection_editor/PropertyEditView.d.ts +12 -1
  19. package/dist/collection_editor/ui/collection_editor/RouterCollectionStudioView.d.ts +15 -0
  20. package/dist/collection_editor/ui/collection_editor/RouterCollectionsStudioView.d.ts +14 -0
  21. package/dist/collection_editor/useJsonCollectionsConfigController.d.ts +37 -0
  22. package/dist/collection_editor/useSafeSnackbarController.d.ts +2 -0
  23. package/dist/collection_editor_ui.d.ts +4 -0
  24. package/dist/collection_editor_ui.js +33 -4
  25. package/dist/collection_editor_ui.js.map +1 -0
  26. package/dist/components/EntityCollectionView/EntityBoardCard.d.ts +3 -3
  27. package/dist/components/EntityCollectionView/hooks/useKanbanDragAndDrop.d.ts +3 -3
  28. package/dist/components/EntityEditView.d.ts +3 -2
  29. package/dist/components/RebaseCMS.d.ts +1 -1
  30. package/dist/editor/editor.d.ts +10 -4
  31. package/dist/editor.js +6 -4
  32. package/dist/editor.js.map +1 -1
  33. package/dist/form/EntityForm.d.ts +10 -9
  34. package/dist/form/EntityFormBinding.d.ts +27 -0
  35. package/dist/form/field_bindings/MarkdownEditorFieldBinding.d.ts +2 -2
  36. package/dist/form/form_utils.d.ts +8 -0
  37. package/dist/form/index.d.ts +5 -1
  38. package/dist/{history-BL49TIDb.js → history-ChHgyyvM.js} +2 -2
  39. package/dist/{history-BL49TIDb.js.map → history-ChHgyyvM.js.map} +1 -1
  40. package/dist/hooks/navigation/contexts/CollectionRegistryContext.d.ts +1 -1
  41. package/dist/hooks/navigation/useAsyncResolver.d.ts +26 -0
  42. package/dist/hooks/navigation/useBuildCollectionRegistryController.d.ts +2 -0
  43. package/dist/hooks/navigation/useTopLevelNavigation.d.ts +1 -1
  44. package/dist/hooks/navigation/utils.d.ts +1 -1
  45. package/dist/index.d.ts +3 -1
  46. package/dist/index.js +2 -2
  47. package/dist/routes/CustomViewRoute.d.ts +2 -1
  48. package/dist/{src-BJ8fkM8y.js → src-DOX2nOyh.js} +1634 -1330
  49. package/dist/src-DOX2nOyh.js.map +1 -0
  50. package/dist/types/components/EntityFormActionsProps.d.ts +1 -1
  51. package/dist/types/components/EntityFormProps.d.ts +59 -0
  52. package/dist/types/fields.d.ts +6 -1
  53. package/dist/util/dataControllerAdapter.d.ts +30 -0
  54. package/dist/util/navigation_utils.d.ts +1 -1
  55. package/dist/util/propertyConfigMapper.d.ts +17 -0
  56. package/package.json +18 -18
  57. package/src/collection_editor/extensibility_types.ts +168 -0
  58. package/src/collection_editor/index.ts +46 -0
  59. package/src/collection_editor/serializable_types.ts +487 -0
  60. package/src/collection_editor/serializable_utils.ts +588 -0
  61. package/src/collection_editor/ui/AddKanbanColumnAction.tsx +2 -0
  62. package/src/collection_editor/ui/collection_editor/AICollectionGeneratorPopover.tsx +4 -4
  63. package/src/collection_editor/ui/collection_editor/CollectionEditorDialog.tsx +134 -70
  64. package/src/collection_editor/ui/collection_editor/CollectionPropertiesEditorForm.tsx +95 -78
  65. package/src/collection_editor/ui/collection_editor/CollectionStudioView.tsx +23 -20
  66. package/src/collection_editor/ui/collection_editor/CollectionsStudioView.tsx +79 -27
  67. package/src/collection_editor/ui/collection_editor/DisplaySettingsForm.tsx +9 -7
  68. package/src/collection_editor/ui/collection_editor/GeneralSettingsForm.tsx +22 -5
  69. package/src/collection_editor/ui/collection_editor/GetCodeDialog.tsx +3 -3
  70. package/src/collection_editor/ui/collection_editor/PropertyEditView.tsx +155 -10
  71. package/src/collection_editor/ui/collection_editor/PropertyFieldPreview.tsx +6 -6
  72. package/src/collection_editor/ui/collection_editor/PropertyTree.tsx +1 -0
  73. package/src/collection_editor/ui/collection_editor/RouterCollectionStudioView.tsx +44 -0
  74. package/src/collection_editor/ui/collection_editor/RouterCollectionsStudioView.tsx +41 -0
  75. package/src/collection_editor/ui/collection_editor/properties/EnumPropertyField.tsx +4 -3
  76. package/src/collection_editor/useJsonCollectionsConfigController.tsx +286 -0
  77. package/src/collection_editor/useSafeSnackbarController.ts +9 -0
  78. package/src/collection_editor_ui.ts +6 -0
  79. package/src/components/ArrayContainer.tsx +5 -5
  80. package/src/components/DefaultDrawer.tsx +13 -3
  81. package/src/components/DrawerNavigationGroup.tsx +2 -2
  82. package/src/components/DrawerNavigationItem.tsx +4 -4
  83. package/src/components/EntityCollectionView/EntityBoardCard.tsx +6 -6
  84. package/src/components/EntityCollectionView/EntityCollectionBoardView.tsx +14 -11
  85. package/src/components/EntityCollectionView/EntityCollectionCardView.tsx +40 -190
  86. package/src/components/EntityCollectionView/EntityCollectionListView.tsx +54 -183
  87. package/src/components/EntityCollectionView/hooks/useKanbanDragAndDrop.ts +6 -6
  88. package/src/components/EntityEditView.tsx +5 -4
  89. package/src/components/EntityEditViewFormActions.tsx +32 -26
  90. package/src/components/HomePage/ContentHomePage.tsx +13 -4
  91. package/src/components/HomePage/NavigationCard.tsx +8 -14
  92. package/src/components/HomePage/NavigationGroup.tsx +1 -1
  93. package/src/components/HomePage/SmallNavigationCard.tsx +1 -5
  94. package/src/components/RebaseCMS.tsx +3 -2
  95. package/src/components/RebaseNavigation.tsx +71 -7
  96. package/src/components/SelectableTable/SelectableTable.tsx +2 -2
  97. package/src/components/SelectableTable/filters/StringNumberFilterField.tsx +28 -21
  98. package/src/components/SideDialogs.tsx +0 -7
  99. package/src/components/common/default_entity_actions.tsx +18 -10
  100. package/src/contexts/BreacrumbsContext.tsx +15 -6
  101. package/src/editor/editor.tsx +14 -8
  102. package/src/editor/selectors/ai-selector.tsx +0 -1
  103. package/src/form/EntityForm.tsx +191 -428
  104. package/src/form/EntityFormActions.tsx +33 -47
  105. package/src/form/EntityFormBinding.tsx +323 -0
  106. package/src/form/components/FormLayout.tsx +1 -1
  107. package/src/form/field_bindings/MarkdownEditorFieldBinding.tsx +4 -4
  108. package/src/form/field_bindings/ReadOnlyFieldBinding.tsx +7 -2
  109. package/src/form/field_bindings/StorageUploadFieldBinding.tsx +1 -1
  110. package/src/form/form_utils.ts +163 -0
  111. package/src/form/index.tsx +14 -5
  112. package/src/hooks/navigation/contexts/CollectionRegistryContext.tsx +2 -2
  113. package/src/hooks/navigation/contexts/NavigationStateContext.tsx +2 -2
  114. package/src/hooks/navigation/contexts/UrlContext.tsx +1 -2
  115. package/src/hooks/navigation/useAsyncResolver.ts +100 -0
  116. package/src/hooks/navigation/useBuildCollectionRegistryController.tsx +15 -8
  117. package/src/hooks/navigation/useBuildNavigationStateController.tsx +12 -8
  118. package/src/hooks/navigation/useNavigationResolution.ts +33 -0
  119. package/src/hooks/navigation/useResolvedCollections.ts +29 -71
  120. package/src/hooks/navigation/useResolvedViews.tsx +40 -70
  121. package/src/hooks/navigation/useTopLevelNavigation.ts +20 -16
  122. package/src/hooks/navigation/utils.ts +7 -4
  123. package/src/index.ts +7 -2
  124. package/src/routes/CustomViewRoute.tsx +14 -6
  125. package/src/types/components/EntityFormActionsProps.tsx +1 -1
  126. package/src/types/components/EntityFormProps.tsx +68 -0
  127. package/src/types/fields.tsx +7 -1
  128. package/src/util/dataControllerAdapter.ts +93 -0
  129. package/src/util/navigation_utils.ts +1 -1
  130. package/src/util/propertyConfigMapper.ts +134 -0
  131. package/LICENSE +0 -21
  132. package/dist/CollectionEditorDialog-jA-PLh6N.js.map +0 -1
  133. package/dist/CollectionsStudioView-UKouBPOm.js.map +0 -1
  134. package/dist/PropertyEditView-DEUddmg_.js.map +0 -1
  135. package/dist/components/EntityCollectionView/Board.d.ts +0 -3
  136. package/dist/components/EntityCollectionView/BoardColumn.d.ts +0 -42
  137. package/dist/components/EntityCollectionView/BoardColumnTitle.d.ts +0 -9
  138. package/dist/components/EntityCollectionView/BoardSortableList.d.ts +0 -14
  139. package/dist/components/EntityCollectionView/board_types.d.ts +0 -105
  140. package/dist/hooks/navigation/useNavigationRegistry.d.ts +0 -12
  141. package/dist/hooks/navigation/useNavigationURLs.d.ts +0 -11
  142. package/dist/src-BJ8fkM8y.js.map +0 -1
  143. package/src/components/EntityCollectionView/Board.tsx +0 -475
  144. package/src/components/EntityCollectionView/BoardColumn.tsx +0 -165
  145. package/src/components/EntityCollectionView/BoardColumnTitle.tsx +0 -46
  146. package/src/components/EntityCollectionView/BoardSortableList.tsx +0 -177
  147. package/src/components/EntityCollectionView/board_types.ts +0 -113
  148. package/src/hooks/navigation/useNavigationRegistry.ts +0 -164
  149. package/src/hooks/navigation/useNavigationURLs.ts +0 -56
@@ -2,9 +2,9 @@ import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.js";
2
2
  import * as React$1 from "react";
3
3
  import React, { Suspense, createContext, createElement, forwardRef, lazy, memo, useCallback, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
4
4
  import { deepEqual } from "fast-equals";
5
- import { Alert, AlertCircleIcon, AlertTriangleIcon, AlignLeftIcon, ArrowLeftIcon, ArrowRightIcon, ArrowRightToLineIcon, Avatar, Badge, BooleanSwitch, BooleanSwitchWithLabel, Button, CHIP_COLORS, CalendarIcon, Card, CenteredView, CheckCircleIcon, CheckIcon, Checkbox, ChevronDownIcon, ChevronRightIcon, ChevronUpIcon, ChevronsLeftIcon, ChevronsRightIcon, ChevronsUpDownIcon, Chip, CircleIcon, CircularProgress, CircularProgressCenter, CodeIcon, Collapse, ColumnsIcon, Container, CopyIcon, DateTimeField, Dialog, DialogActions, DialogContent, DialogTitle, DownloadIcon, ErrorBoundary, ExpandablePanel, ExternalLinkIcon, EyeIcon, EyeOffIcon, FileTextIcon, FileUpload, FilterChip, FilterIcon, FilterXIcon, FlagIcon, FolderUpIcon, FunctionSquareIcon, GlobeIcon, HandleIcon, HashIcon, HistoryIcon, IconButton, ImageOffIcon, KanbanIcon, KeyRoundIcon, Label, LayoutGridIcon, LinkIcon, ListIcon, ListOrderedIcon, LoadingButton, LogOutIcon, MailIcon, Markdown, Maximize2Icon, Menu, MenuIcon, MenuItem, MinusIcon, MoonIcon, MoreVerticalIcon, MultiSelect, MultiSelectItem, PencilIcon, PlusIcon, Popover, PopoverPrimitive, Portal, QuoteIcon, RadioGroup, RadioGroupItem, RefreshCwIcon, RepeatIcon, ResizablePanels, Rows3Icon, SearchBar, SearchIcon, Select, SelectItem, Separator, SettingsIcon, Sheet, Skeleton, StarIcon, SunIcon, SunMoonIcon, Tab, Table, TableBody, TableCell, TableHeader, TableIcon, TableRow, Tabs, TextField, TextIcon, TextareaAutosize, ToggleButtonGroup, Tooltip, Trash2Icon, Typography, UploadIcon, UserIcon, VirtualTable, VoteIcon, XCircleIcon, XIcon, cls, coolIconKeys, debounce, defaultBorderMixin, fieldBackgroundDisabledMixin, fieldBackgroundHoverMixin, fieldBackgroundInvisibleMixin, fieldBackgroundMixin, focusedDisabled, getColorSchemeForKey, getColorSchemeForSeed, iconSize, lucideIcons, paperMixin, useDebouncedCallback, useInjectStyles, useOutsideAlerter } from "@rebasepro/ui";
6
- import { CollectionRegistry, DEFAULT_ONE_OF_TYPE, DEFAULT_ONE_OF_VALUE, addInitialSlash, canReadCollection, enumToObjectEntries, getArrayResolvedProperties, getCollectionBySlugWithin, getCollectionPathsCombinations, getDefaultValueFor, getDefaultValueFortype, getDefaultValuesFor, getEntityImagePreviewPropertyKey, getLabelOrConfigFrom, getLastSegment, getLastSegment as getLastSegment$1, getLocalChangesBackup, getNavigationEntriesFromPath, getParentReferencesFromPath, getReferenceFrom, getRelationFrom, getSubcollections, getTableName, isHidden, isPropertyBuilder, isReadOnly, normalizeToEntityRelation, removeInitialAndTrailingSlashes, removeInitialAndTrailingSlashes as removeInitialAndTrailingSlashes$1, removeInitialSlash, removeTrailingSlash, resolveArrayProperties, resolveCollectionPathIds, resolveCollectionPathIds as resolveCollectionPathIds$1, resolveDefaultSelectedView, resolveEnumValues, resolveProperty, resolveRelationProperty, resolveStorageFilenameString, resolveStoragePathString } from "@rebasepro/common";
7
- import { ADDITIONAL_TAB_WIDTH, CONTAINER_FULL_WIDTH, CollectionComponentOverrideProvider, ConfirmationDialog, CustomizationControllerContext, DEFAULT_PAGE_SIZE, ErrorTooltip, ErrorView, FORM_CONTAINER_WIDTH, IconForView, LanguageToggle, LoginView, NotFoundPage, PluginProviderStack, RebaseLogo, RebaseRoutes, SchemaDriftBanner, StudioBridgeRegistryProvider, UIReferenceView, UnsavedChangesDialog, UserDisplay, UserSettingsView, buildCollapsedDefaults, buildEnumLabel, createFormexStub, deleteEntityWithCallbacks, flattenKeys, getColorScheme, getColumnKeysForProperty, getEntityFromCache, getEntityFromMemoryCache, getFormFieldKeys, getIcon, getRowHeight, iconsSearch, removeEntityFromCache, removeEntityFromMemoryCache, resolveComponentRef, saveEntityToCache, saveEntityToMemoryCache, saveEntityWithCallbacks, useAdminModeController, useAnalyticsController, useApiConfig, useAuthController, useBridgeRegistration, useBuildLocalConfigurationPersistence, useCollapsedGroups, useColumnIds, useComponentOverride, useCustomizationController, useData, useDataTableController, useEntityFetch, useLargeLayout, useModeController, usePermissions, useRebaseClient, useRebaseContext, useRebaseRegistry, useRebaseRegistryDispatch, useRelationSelector, useRestoreScroll, useScrollRestoration, useSlot, useSnackbarController, useStorageSource, useStorageUploadController, useTranslation, useUnsavedChangesDialog, useUserConfigurationPersistence } from "@rebasepro/core";
5
+ import { Alert, AlertCircleIcon, AlertTriangleIcon, AlignLeftIcon, ArrowLeftIcon, ArrowRightToLineIcon, Avatar, Badge, BooleanSwitch, BooleanSwitchWithLabel, Button, CHIP_COLORS, CalendarIcon, Card, CardView, CenteredView, CheckCircleIcon, CheckIcon, Checkbox, ChevronDownIcon, ChevronRightIcon, ChevronUpIcon, ChevronsLeftIcon, ChevronsRightIcon, ChevronsUpDownIcon, Chip, CircleIcon, CircularProgress, CircularProgressCenter, CodeIcon, Collapse, ColumnsIcon, Container, CopyIcon, DateTimeField, Dialog, DialogActions, DialogContent, DialogTitle, DownloadIcon, ErrorBoundary, ExpandablePanel, ExternalLinkIcon, EyeIcon, EyeOffIcon, FileTextIcon, FileUpload, FilterChip, FilterIcon, FilterXIcon, FlagIcon, FolderUpIcon, FunctionSquareIcon, GlobeIcon, HandleIcon, HashIcon, HistoryIcon, IconButton, ImageOffIcon, KanbanIcon, KanbanView, KeyRoundIcon, Label, LayoutGridIcon, LinkIcon, ListIcon, ListOrderedIcon, ListView, LoaderIcon, LoadingButton, LogOutIcon, MailIcon, Markdown, Maximize2Icon, Menu, MenuIcon, MenuItem, MinusIcon, MoonIcon, MoreVerticalIcon, MultiSelect, MultiSelectItem, PencilIcon, PlusIcon, Popover, PopoverPrimitive, Portal, QuoteIcon, RadioGroup, RadioGroupItem, RefreshCwIcon, RepeatIcon, ResizablePanels, Rows3Icon, SearchBar, SearchIcon, Select, SelectItem, Separator, SettingsIcon, Sheet, Skeleton, StarIcon, SunIcon, SunMoonIcon, Tab, Table, TableBody, TableCell, TableHeader, TableIcon, TableRow, TableView, Tabs, TextField, TextIcon, TextareaAutosize, ToggleButtonGroup, Tooltip, Trash2Icon, Typography, UploadIcon, UserIcon, VoteIcon, XCircleIcon, XIcon, cls, coolIconKeys, debounce, defaultBorderMixin, fieldBackgroundDisabledMixin, fieldBackgroundHoverMixin, fieldBackgroundInvisibleMixin, fieldBackgroundMixin, focusedDisabled, getColorSchemeForSeed, iconSize, lucideIcons, paperMixin, useDebouncedCallback, useInjectStyles, useOutsideAlerter } from "@rebasepro/ui";
6
+ import { CollectionRegistry, DEFAULT_ONE_OF_TYPE, DEFAULT_ONE_OF_VALUE, addInitialSlash, buildRoutedRebaseData, canReadCollection, enumToObjectEntries, getArrayResolvedProperties, getCollectionBySlugWithin, getCollectionPathsCombinations, getDefaultValueFor, getDefaultValueFortype, getDefaultValuesFor, getEntityImagePreviewPropertyKey, getLabelOrConfigFrom, getLastSegment, getLastSegment as getLastSegment$1, getLocalChangesBackup, getNavigationEntriesFromPath, getParentReferencesFromPath, getReferenceFrom, getRelationFrom, getSubcollections, getTableName, isHidden, isPropertyBuilder, isReadOnly, normalizeToEntityRelation, removeInitialAndTrailingSlashes, removeInitialAndTrailingSlashes as removeInitialAndTrailingSlashes$1, removeInitialSlash, removeTrailingSlash, resolveArrayProperties, resolveCollectionPathIds, resolveCollectionPathIds as resolveCollectionPathIds$1, resolveDataSource, resolveDefaultSelectedView, resolveEnumValues, resolveProperty, resolveRelationProperty, resolveStorageFilenameString, resolveStoragePathString } from "@rebasepro/common";
7
+ import { ADDITIONAL_TAB_WIDTH, CONTAINER_FULL_WIDTH, CollectionComponentOverrideProvider, ConfirmationDialog, CustomizationControllerContext, DEFAULT_PAGE_SIZE, ErrorTooltip, ErrorView, FORM_CONTAINER_WIDTH, IconForView, LanguageToggle, LoginView, NotFoundPage, PluginProviderStack, RebaseDataContext, RebaseLogo, RebaseRoutes, SchemaDriftBanner, StudioBridgeRegistryProvider, UIReferenceView, UnsavedChangesDialog, UserDisplay, UserSettingsView, buildCollapsedDefaults, buildEnumLabel, createFormexStub, deleteEntityWithCallbacks, flattenKeys, getColorScheme, getColumnKeysForProperty, getEntityFromCache, getEntityFromMemoryCache, getFormFieldKeys, getIcon, getRowHeight, iconsSearch, removeEntityFromCache, removeEntityFromMemoryCache, resolveComponentRef, saveEntityToCache, saveEntityToMemoryCache, saveEntityWithCallbacks, useAdminModeController, useAnalyticsController, useApiConfig, useAuthController, useBridgeRegistration, useBuildLocalConfigurationPersistence, useCollapsedGroups, useColumnIds, useComponentOverride, useCustomizationController, useData, useDataSources, useDataTableController, useEntityFetch, useLargeLayout, useModeController, usePermissions, useRebaseClient, useRebaseContext, useRebaseRegistry, useRebaseRegistryDispatch, useRelationSelector, useRestoreScroll, useScrollRestoration, useSlot, useSnackbarController, useStorageSource, useStorageUploadController, useTranslation, useUnsavedChangesDialog, useUserConfigurationPersistence } from "@rebasepro/core";
8
8
  import { Field, Formex, getIn, setIn, useCreateFormex, useFormex } from "@rebasepro/formex";
9
9
  import { z } from "zod";
10
10
  import { defaultDateFormat, getArrayValuesCount, getHashValue, getValueInPath, hydrateRegExp, isObject, mergeDeep, prettifyIdentifier, randomString, slugify, toArray } from "@rebasepro/utils";
@@ -21,7 +21,6 @@ import { SortableContext, arrayMove, defaultAnimateLayoutChanges, horizontalList
21
21
  import { CSS } from "@dnd-kit/utilities";
22
22
  import ExcelJS from "exceljs";
23
23
  import { buildEntityPropertiesFromData, inferTypeFromValue } from "@rebasepro/schema-inference";
24
- import { createPortal } from "react-dom";
25
24
  import { generateKeyBetween, generateNKeysBetween } from "fractional-indexing";
26
25
  import Fuse from "fuse.js";
27
26
  import { format } from "date-fns";
@@ -293,16 +292,11 @@ function getZodBinarySchema({ property }) {
293
292
  //#endregion
294
293
  //#region src/form/EntityFormActions.tsx
295
294
  function EntityFormActions({ path, collection, entity, layout, savingError, formex, disabled, status, pluginActions, openEntityMode, navigateBack, formContext }) {
296
- const context = useCMSContext();
297
- const sideEntityController = context.sideEntityController;
298
- const { t } = useTranslation();
299
295
  const bottomActionsProps = {
300
296
  path,
301
297
  savingError,
302
298
  entity,
303
299
  collection,
304
- context,
305
- sideEntityController,
306
300
  disabled,
307
301
  status,
308
302
  pluginActions,
@@ -310,7 +304,6 @@ function EntityFormActions({ path, collection, entity, layout, savingError, form
310
304
  navigateBack,
311
305
  formContext,
312
306
  formex,
313
- t,
314
307
  className: layout === "responsive" ? "@6xl:hidden" : void 0
315
308
  };
316
309
  const sideActionsProps = {
@@ -318,8 +311,6 @@ function EntityFormActions({ path, collection, entity, layout, savingError, form
318
311
  savingError,
319
312
  entity,
320
313
  collection,
321
- context,
322
- sideEntityController,
323
314
  disabled,
324
315
  status,
325
316
  pluginActions,
@@ -327,7 +318,6 @@ function EntityFormActions({ path, collection, entity, layout, savingError, form
327
318
  navigateBack,
328
319
  formContext,
329
320
  formex,
330
- t,
331
321
  className: layout === "responsive" ? "hidden @6xl:flex" : void 0
332
322
  };
333
323
  const bottomActions = buildBottomActions$1(bottomActionsProps);
@@ -335,7 +325,7 @@ function EntityFormActions({ path, collection, entity, layout, savingError, form
335
325
  if (layout === "responsive") return /* @__PURE__ */ jsxs(Fragment, { children: [bottomActions, sideActions] });
336
326
  return layout === "bottom" ? bottomActions : sideActions;
337
327
  }
338
- function buildBottomActions$1({ savingError, entity, path, formActions, collection, context, sideEntityController, disabled, status, pluginActions, openEntityMode, navigateBack, formContext, formex, t, className }) {
328
+ function buildBottomActions$1({ savingError, entity, path, formActions, collection, disabled, status, pluginActions, openEntityMode, navigateBack, formContext, formex, className }) {
339
329
  const hasErrors = Object.keys(formex.errors).length > 0 && formex.submitCount > 0;
340
330
  return /* @__PURE__ */ jsxs(DialogActions, {
341
331
  position: "absolute",
@@ -359,8 +349,8 @@ function buildBottomActions$1({ savingError, entity, path, formActions, collecti
359
349
  entity,
360
350
  path: path ?? collection.slug,
361
351
  collection,
362
- context,
363
- sideEntityController,
352
+ context: void 0,
353
+ sideEntityController: void 0,
364
354
  openEntityMode,
365
355
  navigateBack,
366
356
  formContext
@@ -375,47 +365,55 @@ function buildBottomActions$1({ savingError, entity, path, formActions, collecti
375
365
  disabled: disabled || formex.isSubmitting,
376
366
  color: "primary",
377
367
  type: "reset",
378
- children: status === "existing" ? t("discard") : t("clear")
368
+ children: status === "existing" ? "Discard" : "Clear"
379
369
  }),
380
- /* @__PURE__ */ jsxs(Button, {
381
- variant: "filled",
382
- color: "primary",
383
- type: "submit",
384
- disabled: disabled || formex.isSubmitting,
385
- startIcon: hasErrors ? /* @__PURE__ */ jsx(AlertCircleIcon, {}) : void 0,
386
- children: [
387
- status === "existing" && t("save"),
388
- status === "copy" && t("create_copy"),
389
- status === "new" && t("create")
390
- ]
370
+ /* @__PURE__ */ jsx(Tooltip, {
371
+ title: hasErrors ? "Fix highlighted errors before saving" : void 0,
372
+ children: /* @__PURE__ */ jsxs(LoadingButton, {
373
+ variant: "filled",
374
+ color: "primary",
375
+ type: "submit",
376
+ loading: formex.isSubmitting,
377
+ disabled: disabled || formex.isSubmitting,
378
+ startIcon: hasErrors ? /* @__PURE__ */ jsx(AlertCircleIcon, {}) : void 0,
379
+ children: [
380
+ status === "existing" && "Save",
381
+ status === "copy" && "Create copy",
382
+ status === "new" && "Create"
383
+ ]
384
+ })
391
385
  })
392
386
  ]
393
387
  });
394
388
  }
395
- function buildSideActions$1({ savingError, entity, formActions, path, openEntityMode, collection, context, sideEntityController, disabled, status, pluginActions, formex, t, className }) {
389
+ function buildSideActions$1({ savingError, entity, formActions, path, openEntityMode, collection, disabled, status, pluginActions, formex, className }) {
396
390
  const hasErrors = Object.keys(formex.errors).length > 0 && formex.submitCount > 0;
397
391
  return /* @__PURE__ */ jsxs("div", {
398
392
  className: cls("overflow-auto h-full flex flex-col gap-2 w-80 2xl:w-96 px-4 py-16 sticky top-0 border-l", defaultBorderMixin, className),
399
393
  children: [
400
- /* @__PURE__ */ jsxs(LoadingButton, {
401
- fullWidth: true,
402
- variant: "filled",
403
- color: "primary",
404
- type: "submit",
405
- startIcon: hasErrors ? /* @__PURE__ */ jsx(AlertCircleIcon, {}) : void 0,
406
- disabled: disabled || formex.isSubmitting,
407
- children: [
408
- status === "existing" && t("save"),
409
- status === "copy" && t("create_copy"),
410
- status === "new" && t("create")
411
- ]
394
+ /* @__PURE__ */ jsx(Tooltip, {
395
+ title: hasErrors ? "Fix highlighted errors before saving" : void 0,
396
+ children: /* @__PURE__ */ jsxs(LoadingButton, {
397
+ fullWidth: true,
398
+ variant: "filled",
399
+ color: "primary",
400
+ type: "submit",
401
+ loading: formex.isSubmitting,
402
+ startIcon: hasErrors ? /* @__PURE__ */ jsx(AlertCircleIcon, {}) : void 0,
403
+ disabled: disabled || formex.isSubmitting,
404
+ children: [
405
+ status === "existing" && "Save",
406
+ status === "copy" && "Create copy",
407
+ status === "new" && "Create"
408
+ ]
409
+ })
412
410
  }),
413
411
  /* @__PURE__ */ jsx(Button, {
414
412
  fullWidth: true,
415
413
  variant: "text",
416
414
  disabled: disabled || formex.isSubmitting,
417
415
  type: "reset",
418
- children: status === "existing" ? t("discard") : t("clear")
416
+ children: status === "existing" ? "Discard" : "Clear"
419
417
  }),
420
418
  pluginActions,
421
419
  savingError && /* @__PURE__ */ jsx("div", {
@@ -1578,7 +1576,7 @@ var MAIN_TAB_VALUE = "__main_##Q$SC^#S6";
1578
1576
  var JSON_TAB_VALUE = "__json";
1579
1577
  //#endregion
1580
1578
  //#region src/components/EntityDetailView.tsx
1581
- var EntityHistoryView$1 = lazy(() => import("./history-BL49TIDb.js").then((m) => ({ default: m.EntityHistoryView })));
1579
+ var EntityHistoryView$1 = lazy(() => import("./history-ChHgyyvM.js").then((m) => ({ default: m.EntityHistoryView })));
1582
1580
  function EntityDetailView({ entityId, ...props }) {
1583
1581
  const { entity, dataLoading, dataLoadingError } = useEntityFetch({
1584
1582
  path: props.path,
@@ -4889,7 +4887,7 @@ function StringNumberFilterField({ name, value, setValue, type, isArray, enumVal
4889
4887
  value: Array.isArray(internalValue) ? internalValue.map((e) => String(e)) : [],
4890
4888
  disabled: isNullOperation,
4891
4889
  onValueChange: (value) => {
4892
- updateFilter(operation, type === "number" ? value.map((v) => parseInt(v)) : value);
4890
+ updateFilter(operation, type === "number" ? (value ?? []).map((v) => parseInt(v)) : value);
4893
4891
  },
4894
4892
  multiple,
4895
4893
  endAdornment: internalValue && /* @__PURE__ */ jsx(IconButton, {
@@ -5206,7 +5204,7 @@ var SelectableTable = function SelectableTable({ onValueChange, cellRenderer, on
5206
5204
  children: /* @__PURE__ */ jsx("div", {
5207
5205
  className: "h-full w-full flex flex-col bg-white dark:bg-surface-900",
5208
5206
  ref,
5209
- children: /* @__PURE__ */ jsx(VirtualTable, {
5207
+ children: /* @__PURE__ */ jsx(TableView, {
5210
5208
  data,
5211
5209
  columns,
5212
5210
  cellRenderer: (props) => cellRenderer(props),
@@ -5734,19 +5732,22 @@ function buildBottomActions({ savingError, entity, formActions, collection, cont
5734
5732
  onClick: navigateBack,
5735
5733
  children: t("back_to_detail") ?? "Back to details"
5736
5734
  }),
5737
- /* @__PURE__ */ jsxs(Button, {
5738
- variant: canClose ? "text" : "filled",
5739
- color: "primary",
5740
- type: "submit",
5741
- disabled: disabled || formex.isSubmitting,
5742
- onClick: () => {
5743
- sideDialogContext.setPendingClose(false);
5744
- },
5745
- children: [
5746
- status === "existing" && t("save"),
5747
- status === "copy" && t("create_copy"),
5748
- status === "new" && t("create")
5749
- ]
5735
+ /* @__PURE__ */ jsx(Tooltip, {
5736
+ title: hasErrors ? t("fix_errors_before_saving") ?? "Fix highlighted errors before saving" : void 0,
5737
+ children: /* @__PURE__ */ jsxs(Button, {
5738
+ variant: canClose ? "text" : "filled",
5739
+ color: "primary",
5740
+ type: "submit",
5741
+ disabled: disabled || formex.isSubmitting,
5742
+ onClick: () => {
5743
+ sideDialogContext.setPendingClose(false);
5744
+ },
5745
+ children: [
5746
+ status === "existing" && t("save"),
5747
+ status === "copy" && t("create_copy"),
5748
+ status === "new" && t("create")
5749
+ ]
5750
+ })
5750
5751
  }),
5751
5752
  canClose && /* @__PURE__ */ jsxs(LoadingButton, {
5752
5753
  variant: "filled",
@@ -5771,21 +5772,25 @@ function buildSideActions({ savingError, entity, formActions, collection, contex
5771
5772
  return /* @__PURE__ */ jsxs("div", {
5772
5773
  className: cls("overflow-auto h-full flex flex-col gap-2 w-80 2xl:w-96 px-4 py-16 sticky top-0 border-l", defaultBorderMixin, className),
5773
5774
  children: [
5774
- /* @__PURE__ */ jsxs(LoadingButton, {
5775
- fullWidth: true,
5776
- variant: "filled",
5777
- color: "primary",
5778
- type: "submit",
5779
- startIcon: hasErrors ? /* @__PURE__ */ jsx(AlertCircleIcon, {}) : void 0,
5780
- disabled: disabled || formex.isSubmitting,
5781
- onClick: () => {
5782
- sideDialogContext.setPendingClose?.(false);
5783
- },
5784
- children: [
5785
- status === "existing" && t("save"),
5786
- status === "copy" && t("create_copy"),
5787
- status === "new" && t("create")
5788
- ]
5775
+ /* @__PURE__ */ jsx(Tooltip, {
5776
+ title: hasErrors ? t("fix_errors_before_saving") ?? "Fix highlighted errors before saving" : void 0,
5777
+ children: /* @__PURE__ */ jsxs(LoadingButton, {
5778
+ fullWidth: true,
5779
+ variant: "filled",
5780
+ color: "primary",
5781
+ type: "submit",
5782
+ loading: formex.isSubmitting,
5783
+ startIcon: hasErrors ? /* @__PURE__ */ jsx(AlertCircleIcon, {}) : void 0,
5784
+ disabled: disabled || formex.isSubmitting,
5785
+ onClick: () => {
5786
+ sideDialogContext.setPendingClose?.(false);
5787
+ },
5788
+ children: [
5789
+ status === "existing" && t("save"),
5790
+ status === "copy" && t("create_copy"),
5791
+ status === "new" && t("create")
5792
+ ]
5793
+ })
5789
5794
  }),
5790
5795
  /* @__PURE__ */ jsx(Button, {
5791
5796
  fullWidth: true,
@@ -5875,7 +5880,7 @@ function EntityActionButton({ action, enabled, props }) {
5875
5880
  }
5876
5881
  //#endregion
5877
5882
  //#region src/components/EntityEditView.tsx
5878
- var EntityHistoryView = lazy(() => import("./history-BL49TIDb.js").then((m) => ({ default: m.EntityHistoryView })));
5883
+ var EntityHistoryView = lazy(() => import("./history-ChHgyyvM.js").then((m) => ({ default: m.EntityHistoryView })));
5879
5884
  /**
5880
5885
  * This is the default view that is used as the content of a side panel when
5881
5886
  * an entity is opened.
@@ -5927,7 +5932,7 @@ function EntityEditView({ entityId, ...props }) {
5927
5932
  }
5928
5933
  function EntityEditViewInner({ path, entityId, selectedTab: selectedTabProp, collection, parentCollectionSlugs, parentEntityIds, onValuesModified, onSaved, onTabChange, navigateBack, entity, initialDirtyValues, dataLoading, layout = "side_panel", barActions, status, setStatus, formProps, canEdit }) {
5929
5934
  const ResolvedFormActions = useComponentOverride("Entity.FormActions", EntityEditViewFormActions);
5930
- const ResolvedEntityForm = useComponentOverride("Entity.Form", EntityForm);
5935
+ const ResolvedEntityForm = useComponentOverride("Entity.Form", EntityFormBinding);
5931
5936
  const ResolvedCollectionView = useComponentOverride("Collection.View", EntityCollectionView);
5932
5937
  const context = useRebaseContext();
5933
5938
  const urlController = useUrlController();
@@ -7070,8 +7075,9 @@ function useEntityHistory(params) {
7070
7075
  //#endregion
7071
7076
  //#region src/hooks/navigation/useBuildCollectionRegistryController.tsx
7072
7077
  function useBuildCollectionRegistryController(props) {
7073
- const { userConfigPersistence } = props;
7074
- const collectionRegistryRef = useRef(new CollectionRegistry());
7078
+ const { userConfigPersistence, dataSources } = props;
7079
+ const collectionRegistryRef = useRef(new CollectionRegistry(void 0, dataSources));
7080
+ if (dataSources) collectionRegistryRef.current.setDataSources(dataSources);
7075
7081
  const [initialised, setInitialised] = useState(false);
7076
7082
  const getCollection = useCallback((slugOrPath, includeUserOverride = false) => {
7077
7083
  const registry = collectionRegistryRef.current;
@@ -7163,7 +7169,9 @@ function useBuildCollectionRegistryController(props) {
7163
7169
  return paths;
7164
7170
  }, []);
7165
7171
  const collections = collectionRegistryRef.current.getCollections();
7166
- if (!initialised && collections.length > 0) setInitialised(true);
7172
+ useEffect(() => {
7173
+ if (!initialised && collections.length > 0) setInitialised(true);
7174
+ }, [initialised, collections.length]);
7167
7175
  return useMemo(() => ({
7168
7176
  collections,
7169
7177
  initialised,
@@ -7491,7 +7499,7 @@ var editEntityAction = {
7491
7499
  if (!entity) throw new Error("INTERNAL: editEntityAction: Entity is undefined");
7492
7500
  if (!sideEntityController) throw new Error("INTERNAL: editEntityAction: sideEntityController is undefined");
7493
7501
  highlightEntity?.(entity);
7494
- context.analyticsController?.onAnalyticsEvent?.("entity_click", {
7502
+ context?.analyticsController?.onAnalyticsEvent?.("entity_click", {
7495
7503
  path: entity.path,
7496
7504
  entityId: entity.id
7497
7505
  });
@@ -7508,7 +7516,7 @@ var editEntityAction = {
7508
7516
  path: newFullIdPath,
7509
7517
  sideEntityController,
7510
7518
  onClose: () => unhighlightEntity?.(entity),
7511
- navigation: context.urlController,
7519
+ navigation: context?.urlController,
7512
7520
  selectedTab: defaultSelectedView
7513
7521
  });
7514
7522
  return Promise.resolve(void 0);
@@ -7523,7 +7531,7 @@ var copyEntityAction = {
7523
7531
  if (!entity) throw new Error("INTERNAL: copyEntityAction: Entity is undefined");
7524
7532
  if (!sideEntityController) throw new Error("INTERNAL: copyEntityAction: sideEntityController is undefined");
7525
7533
  highlightEntity?.(entity);
7526
- context.analyticsController?.onAnalyticsEvent?.("copy_entity_click", {
7534
+ context?.analyticsController?.onAnalyticsEvent?.("copy_entity_click", {
7527
7535
  path: entity.path,
7528
7536
  entityId: entity.id
7529
7537
  });
@@ -7536,7 +7544,7 @@ var copyEntityAction = {
7536
7544
  copy: true,
7537
7545
  sideEntityController,
7538
7546
  onClose: () => unhighlightEntity?.(entity),
7539
- navigation: context.urlController
7547
+ navigation: context?.urlController
7540
7548
  });
7541
7549
  return Promise.resolve(void 0);
7542
7550
  }
@@ -7546,9 +7554,9 @@ var deleteEntityAction = {
7546
7554
  name: "Delete",
7547
7555
  key: "delete",
7548
7556
  isEnabled: ({ entity }) => Boolean(entity),
7549
- onClick({ entity, path, collection, context, selectionController, onCollectionChange, navigateBack }) {
7557
+ onClick({ entity, path, collection, context, selectionController, onCollectionChange, navigateBack, openEntityMode }) {
7550
7558
  if (!entity) throw new Error("INTERNAL: deleteEntityAction: Entity is undefined");
7551
- if (!context.dialogsController) throw new Error("INTERNAL: deleteEntityAction: context.dialogsController is undefined");
7559
+ if (!context?.dialogsController) throw new Error("INTERNAL: deleteEntityAction: context.dialogsController is undefined");
7552
7560
  const { closeDialog } = context.dialogsController.open({
7553
7561
  key: "delete_entity_dialog_" + entity.id,
7554
7562
  Component: ({ open }) => {
@@ -7560,10 +7568,13 @@ var deleteEntityAction = {
7560
7568
  callbacks: collection.callbacks,
7561
7569
  open,
7562
7570
  onEntityDelete: () => {
7563
- context.analyticsController?.onAnalyticsEvent?.("single_entity_deleted", { path });
7571
+ context?.analyticsController?.onAnalyticsEvent?.("single_entity_deleted", { path });
7564
7572
  selectionController?.setSelectedEntities(selectionController.selectedEntities.filter((e) => e.id !== entity.id));
7565
7573
  onCollectionChange?.();
7566
- navigateBack?.();
7574
+ if (openEntityMode === "full_screen" && context?.urlController) {
7575
+ const collectionUrl = context.urlController.buildUrlCollectionPath(path);
7576
+ context.urlController.navigate(collectionUrl, { replace: true });
7577
+ } else navigateBack?.();
7567
7578
  },
7568
7579
  onClose: closeDialog
7569
7580
  });
@@ -7640,7 +7651,7 @@ var resetPasswordAction = {
7640
7651
  isEnabled: ({ entity }) => Boolean(entity),
7641
7652
  onClick({ entity, context }) {
7642
7653
  if (!entity) throw new Error("INTERNAL: resetPasswordAction: Entity is undefined");
7643
- if (!context.dialogsController) throw new Error("INTERNAL: resetPasswordAction: context.dialogsController is undefined");
7654
+ if (!context?.dialogsController) throw new Error("INTERNAL: resetPasswordAction: context.dialogsController is undefined");
7644
7655
  const user = {
7645
7656
  uid: entity.id,
7646
7657
  email: entity.values?.email,
@@ -7784,12 +7795,34 @@ async function resolveAppViews(baseViews, authController, data, plugins) {
7784
7795
  if (plugins) {
7785
7796
  for (const plugin of plugins) if (plugin.views && plugin.views.length > 0) resolvedViews = [...resolvedViews, ...plugin.views];
7786
7797
  }
7798
+ if (process.env.NODE_ENV !== "production") {
7799
+ const slugCounts = /* @__PURE__ */ new Map();
7800
+ resolvedViews.forEach((v) => {
7801
+ slugCounts.set(v.slug, (slugCounts.get(v.slug) ?? 0) + 1);
7802
+ });
7803
+ slugCounts.forEach((count, slug) => {
7804
+ if (count > 1) console.warn(`[Rebase] Duplicate view slug "${slug}" detected (${count} views). Last-write-wins. Ensure unique slugs across CMS views and plugins.`);
7805
+ });
7806
+ }
7807
+ resolvedViews = filterViewsByRole(resolvedViews, authController);
7787
7808
  return resolvedViews;
7788
7809
  }
7810
+ /**
7811
+ * Filter views by the `roles` field on AppView.
7812
+ * When `roles` is set, the view is only included if the current user
7813
+ * has at least one of the listed roles. Views without `roles` (or with
7814
+ * an empty array) are always included.
7815
+ */
7816
+ function filterViewsByRole(views, authController) {
7817
+ const userRoles = authController.user?.roles ?? [];
7818
+ return views.filter((view) => {
7819
+ if (!view.roles || view.roles.length === 0) return true;
7820
+ return view.roles.some((role) => userRoles.includes(role));
7821
+ });
7822
+ }
7789
7823
  //#endregion
7790
7824
  //#region src/hooks/navigation/utils.ts
7791
7825
  var NAVIGATION_DEFAULT_GROUP_NAME = "Views";
7792
- var NAVIGATION_ADMIN_GROUP_NAME = "Admin";
7793
7826
  function getGroup(collectionOrView) {
7794
7827
  const trimmed = collectionOrView.group?.trim();
7795
7828
  if (!trimmed || trimmed === "") return NAVIGATION_DEFAULT_GROUP_NAME;
@@ -7798,7 +7831,8 @@ function getGroup(collectionOrView) {
7798
7831
  function computeNavigationGroups({ navigationGroupMappings, collections, views, plugins }) {
7799
7832
  let result = navigationGroupMappings ? navigationGroupMappings.map((g) => ({
7800
7833
  name: g.name,
7801
- entries: [...g.entries]
7834
+ entries: [...g.entries],
7835
+ ...g.collapsedByDefault !== void 0 && { collapsedByDefault: g.collapsedByDefault }
7802
7836
  })) : navigationGroupMappings;
7803
7837
  if (plugins) result = plugins.reduce((acc, plugin) => {
7804
7838
  if (plugin.hooks?.navigationEntries) plugin.hooks.navigationEntries.forEach((entry) => {
@@ -7888,48 +7922,38 @@ function areCollectionsEqual(a, b, visitedSlugs = []) {
7888
7922
  return deepEqual(Object.fromEntries(Object.entries(a).filter(([k, v]) => typeof v !== "function" && k !== "subcollections")), Object.fromEntries(Object.entries(b).filter(([k, v]) => typeof v !== "function" && k !== "subcollections")));
7889
7923
  }
7890
7924
  //#endregion
7891
- //#region src/hooks/navigation/useResolvedCollections.ts
7925
+ //#region src/hooks/navigation/useAsyncResolver.ts
7892
7926
  /**
7893
- * Hook that resolves collection props (which may be async builders or arrays)
7894
- * into concrete EntityCollection[], and registers them with the CollectionRegistry.
7895
- *
7896
- * When userManagement is provided, the default users collection is always
7897
- * prepended. Developer collections override via generic slug-based dedup
7898
- * (Map keyed by slug, last-write-wins). No hardcoded string checks.
7927
+ * Generic hook that resolves an async value with loading/error/refresh
7928
+ * state management, cancellation on unmount or deps change, and
7929
+ * ref-based change detection to prevent unnecessary state updates.
7899
7930
  *
7900
- * Uses refs for potentially-unstable dependencies (driver, authController,
7901
- * plugins) to avoid re-triggering effects when their object identity changes.
7931
+ * Extracted from the common pattern in useResolvedCollections and
7932
+ * useResolvedViews.
7902
7933
  */
7903
- function useResolvedCollections(props) {
7904
- const { authController, collections: collectionsProp, data, plugins, disabled, collectionRegistryController } = props;
7934
+ function useAsyncResolver({ resolver, initialValue, isEqual, deps, disabled }) {
7935
+ const [data, setData] = useState(initialValue);
7905
7936
  const [loading, setLoading] = useState(true);
7906
7937
  const [error, setError] = useState(void 0);
7907
- const [resolvedCollections, setResolvedCollections] = useState([]);
7908
7938
  const [refreshTrigger, setRefreshTrigger] = useState(0);
7909
7939
  const refresh = useCallback(() => {
7910
7940
  setRefreshTrigger((prev) => prev + 1);
7911
7941
  }, []);
7912
- const dataRef = useRef(data);
7913
- dataRef.current = data;
7914
- const authControllerRef = useRef(authController);
7915
- authControllerRef.current = authController;
7916
- const pluginsRef = useRef(plugins);
7917
- pluginsRef.current = plugins;
7918
- const resolvedCollectionsRef = useRef([]);
7919
- const initialLoading = authController.initialLoading;
7920
- const user = authController.user;
7942
+ const dataRef = useRef(initialValue);
7943
+ const resolverRef = useRef(resolver);
7944
+ resolverRef.current = resolver;
7945
+ const isEqualRef = useRef(isEqual);
7946
+ isEqualRef.current = isEqual;
7921
7947
  useEffect(() => {
7922
- if (disabled || initialLoading) return;
7948
+ if (disabled) return;
7923
7949
  let cancelled = false;
7924
7950
  (async () => {
7925
7951
  try {
7926
- const resolved = await resolveCollections(collectionsProp, authControllerRef.current, dataRef.current, pluginsRef.current);
7952
+ const result = await resolverRef.current();
7927
7953
  if (cancelled) return;
7928
- const deduped = [...resolved];
7929
- if (collectionRegistryController.collectionRegistryRef.current.registerMultiple(deduped)) console.debug("Collections have changed", deduped);
7930
- if (!areCollectionListsEqual(resolvedCollectionsRef.current, deduped)) {
7931
- resolvedCollectionsRef.current = deduped;
7932
- setResolvedCollections(deduped);
7954
+ if (!isEqualRef.current(dataRef.current, result)) {
7955
+ dataRef.current = result;
7956
+ setData(result);
7933
7957
  }
7934
7958
  setError(void 0);
7935
7959
  } catch (e) {
@@ -7945,20 +7969,66 @@ function useResolvedCollections(props) {
7945
7969
  cancelled = true;
7946
7970
  };
7947
7971
  }, [
7948
- collectionsProp,
7949
- disabled,
7950
- collectionRegistryController.collectionRegistryRef,
7972
+ ...deps,
7951
7973
  refreshTrigger,
7952
- initialLoading,
7953
- user
7974
+ disabled
7975
+ ]);
7976
+ return useMemo(() => ({
7977
+ data,
7978
+ loading,
7979
+ error,
7980
+ refresh
7981
+ }), [
7982
+ data,
7983
+ loading,
7984
+ error,
7985
+ refresh
7954
7986
  ]);
7987
+ }
7988
+ //#endregion
7989
+ //#region src/hooks/navigation/useResolvedCollections.ts
7990
+ /**
7991
+ * Hook that resolves collection props (which may be async builders or arrays)
7992
+ * into concrete EntityCollection[], and registers them with the CollectionRegistry.
7993
+ *
7994
+ * When userManagement is provided, the default users collection is always
7995
+ * prepended. Developer collections override via generic slug-based dedup
7996
+ * (Map keyed by slug, last-write-wins). No hardcoded string checks.
7997
+ *
7998
+ * Uses refs for potentially-unstable dependencies (driver, authController,
7999
+ * plugins) to avoid re-triggering effects when their object identity changes.
8000
+ */
8001
+ function useResolvedCollections(props) {
8002
+ const { authController, collections: collectionsProp, data, plugins, disabled, collectionRegistryController } = props;
8003
+ const userIdentity = authController.user ? `${authController.user.uid}:${(authController.user.roles ?? []).sort().join(",")}` : null;
8004
+ const dataRef = useRef(data);
8005
+ dataRef.current = data;
8006
+ const authControllerRef = useRef(authController);
8007
+ authControllerRef.current = authController;
8008
+ const pluginsRef = useRef(plugins);
8009
+ pluginsRef.current = plugins;
8010
+ const { data: collections, loading, error, refresh } = useAsyncResolver({
8011
+ resolver: async () => {
8012
+ const deduped = [...await resolveCollections(collectionsProp, authControllerRef.current, dataRef.current, pluginsRef.current)];
8013
+ if (collectionRegistryController.collectionRegistryRef.current.registerMultiple(deduped)) console.debug("Collections have changed", deduped);
8014
+ return deduped;
8015
+ },
8016
+ initialValue: [],
8017
+ isEqual: areCollectionListsEqual,
8018
+ deps: [
8019
+ collectionsProp,
8020
+ userIdentity,
8021
+ disabled
8022
+ ],
8023
+ disabled: disabled || authController.initialLoading
8024
+ });
7955
8025
  return useMemo(() => ({
7956
- collections: resolvedCollections,
8026
+ collections,
7957
8027
  loading,
7958
8028
  error,
7959
8029
  refresh
7960
8030
  }), [
7961
- resolvedCollections,
8031
+ collections,
7962
8032
  loading,
7963
8033
  error,
7964
8034
  refresh
@@ -7979,6 +8049,13 @@ function viewSlugsEqual(a, b) {
7979
8049
  return true;
7980
8050
  }
7981
8051
  /**
8052
+ * Equality check for the combined views data.
8053
+ * Compares both views and adminViews by slug identity.
8054
+ */
8055
+ function areResolvedViewsEqual(a, b) {
8056
+ return viewSlugsEqual(a.views, b.views) && viewSlugsEqual(a.adminViews, b.adminViews);
8057
+ }
8058
+ /**
7982
8059
  * Hook that resolves view and admin view props (which may be async builders or arrays)
7983
8060
  * into concrete AppView[].
7984
8061
  *
@@ -7987,16 +8064,7 @@ function viewSlugsEqual(a, b) {
7987
8064
  */
7988
8065
  function useResolvedViews(props) {
7989
8066
  const { authController, views: viewsProp, adminViews: adminViewsProp, data, plugins, adminMode = "content", effectiveRoleController } = props;
7990
- const [loading, setLoading] = useState(true);
7991
- const [error, setError] = useState(void 0);
7992
- const [resolvedViews, setResolvedViews] = useState(void 0);
7993
- const [resolvedAdminViews, setResolvedAdminViews] = useState(void 0);
7994
- const [refreshTrigger, setRefreshTrigger] = useState(0);
7995
- const refresh = useCallback(() => {
7996
- setRefreshTrigger((prev) => prev + 1);
7997
- }, []);
7998
- const viewsRef = useRef(void 0);
7999
- const adminViewsRef = useRef(void 0);
8067
+ const userIdentity = authController.user ? `${authController.user.uid}:${(authController.user.roles ?? []).sort().join(",")}` : null;
8000
8068
  const dataRef = useRef(data);
8001
8069
  dataRef.current = data;
8002
8070
  const authControllerRef = useRef(authController);
@@ -8020,51 +8088,35 @@ function useResolvedViews(props) {
8020
8088
  const resolvedAuthControllerRef = useRef(resolvedAuthController);
8021
8089
  resolvedAuthControllerRef.current = resolvedAuthController;
8022
8090
  const initialLoading = resolvedAuthController.initialLoading;
8023
- const user = resolvedAuthController.user;
8024
- useEffect(() => {
8025
- if (initialLoading) return;
8026
- let cancelled = false;
8027
- (async () => {
8028
- try {
8029
- const [newViews, newAdminViews] = await Promise.all([resolveAppViews(viewsProp, resolvedAuthControllerRef.current, dataRef.current, pluginsRef.current), resolveAppViews(adminViewsProp, resolvedAuthControllerRef.current, dataRef.current)]);
8030
- if (!viewSlugsEqual(viewsRef.current, newViews)) {
8031
- viewsRef.current = newViews;
8032
- setResolvedViews(newViews);
8033
- }
8034
- if (!viewSlugsEqual(adminViewsRef.current, newAdminViews)) {
8035
- adminViewsRef.current = newAdminViews;
8036
- setResolvedAdminViews(newAdminViews);
8037
- }
8038
- setError(void 0);
8039
- } catch (e) {
8040
- if (!cancelled) {
8041
- console.error("Error resolving views:", e);
8042
- setError(e);
8043
- }
8044
- } finally {
8045
- if (!cancelled) setLoading(false);
8046
- }
8047
- })();
8048
- return () => {
8049
- cancelled = true;
8050
- };
8051
- }, [
8052
- viewsProp,
8053
- adminViewsProp,
8054
- refreshTrigger,
8055
- adminMode,
8056
- initialLoading,
8057
- user
8058
- ]);
8091
+ const { data: resolvedData, loading, error, refresh } = useAsyncResolver({
8092
+ resolver: async () => {
8093
+ const [newViews, newAdminViews] = await Promise.all([resolveAppViews(viewsProp, resolvedAuthControllerRef.current, dataRef.current, pluginsRef.current), resolveAppViews(adminViewsProp, resolvedAuthControllerRef.current, dataRef.current)]);
8094
+ return {
8095
+ views: newViews,
8096
+ adminViews: newAdminViews
8097
+ };
8098
+ },
8099
+ initialValue: {
8100
+ views: void 0,
8101
+ adminViews: void 0
8102
+ },
8103
+ isEqual: areResolvedViewsEqual,
8104
+ deps: [
8105
+ viewsProp,
8106
+ adminViewsProp,
8107
+ adminMode,
8108
+ userIdentity
8109
+ ],
8110
+ disabled: initialLoading
8111
+ });
8059
8112
  return useMemo(() => ({
8060
- views: resolvedViews,
8061
- adminViews: resolvedAdminViews,
8113
+ views: resolvedData.views,
8114
+ adminViews: resolvedData.adminViews,
8062
8115
  loading,
8063
8116
  error,
8064
8117
  refresh
8065
8118
  }), [
8066
- resolvedViews,
8067
- resolvedAdminViews,
8119
+ resolvedData,
8068
8120
  loading,
8069
8121
  error,
8070
8122
  refresh
@@ -8173,21 +8225,24 @@ function useTopLevelNavigation(props) {
8173
8225
  }, [])
8174
8226
  ];
8175
8227
  const groupOrderValue = (groupName) => {
8176
- if (groupName === "Admin") return 1;
8228
+ if (groupName === "Admin" || groupName === "Settings" || groupName === "Admin") return 1;
8177
8229
  return 0;
8178
8230
  };
8179
- navigationEntries = navigationEntries.sort((a, b) => {
8180
- return groupOrderValue(a.group) - groupOrderValue(b.group);
8181
- });
8182
8231
  const usedViewsOrder = viewsOrder ?? navigationEntriesOrder;
8183
- if (usedViewsOrder) navigationEntries = navigationEntries.sort((a, b) => {
8184
- const getSortPath = (navEntry) => typeof navEntry.slug === "string" ? navEntry.slug : navEntry.slug[0];
8185
- const aIndex = usedViewsOrder.indexOf(getSortPath(a));
8186
- const bIndex = usedViewsOrder.indexOf(getSortPath(b));
8187
- if (aIndex === -1 && bIndex === -1) return 0;
8188
- if (aIndex === -1) return 1;
8189
- if (bIndex === -1) return -1;
8190
- return aIndex - bIndex;
8232
+ navigationEntries = navigationEntries.sort((a, b) => {
8233
+ const groupDiff = groupOrderValue(a.group) - groupOrderValue(b.group);
8234
+ if (groupDiff !== 0) return groupDiff;
8235
+ if (usedViewsOrder) {
8236
+ const getSortPath = (navEntry) => typeof navEntry.slug === "string" ? navEntry.slug : navEntry.slug[0];
8237
+ const aIndex = usedViewsOrder.indexOf(getSortPath(a));
8238
+ const bIndex = usedViewsOrder.indexOf(getSortPath(b));
8239
+ if (aIndex !== -1 || bIndex !== -1) {
8240
+ if (aIndex === -1) return 1;
8241
+ if (bIndex === -1) return -1;
8242
+ return aIndex - bIndex;
8243
+ }
8244
+ }
8245
+ return 0;
8191
8246
  });
8192
8247
  const collectedGroupsFromEntries = navigationEntries.map((e) => e.group).filter(Boolean);
8193
8248
  const groupsFromMappings = finalNavigationGroupMappings.map((g) => g.name);
@@ -8198,8 +8253,8 @@ function useTopLevelNavigation(props) {
8198
8253
  ...additionalGroups
8199
8254
  ];
8200
8255
  const uniqueGroupsArray = [...new Set(allDefinedGroups)];
8201
- const adminGroups = uniqueGroupsArray.filter((g) => g === NAVIGATION_ADMIN_GROUP_NAME);
8202
- const uniqueGroups = [...uniqueGroupsArray.filter((g) => g !== NAVIGATION_ADMIN_GROUP_NAME), ...adminGroups];
8256
+ const lastGroups = uniqueGroupsArray.filter((g) => g === "Admin" || g === "Settings" || g === "Admin");
8257
+ const uniqueGroups = [...uniqueGroupsArray.filter((g) => g !== "Admin" && g !== "Settings" && g !== "Admin"), ...lastGroups];
8203
8258
  const computedTopLevelNav = {
8204
8259
  allowDragAndDrop: plugins?.some((plugin) => plugin.hooks?.allowDragAndDrop) ?? false,
8205
8260
  navigationEntries,
@@ -8266,9 +8321,15 @@ function useBuildNavigationStateController(props) {
8266
8321
  adminMode,
8267
8322
  collectionRegistryController
8268
8323
  });
8324
+ const pendingRefreshRef = useRef(false);
8269
8325
  const refreshNavigation = useCallback(() => {
8270
- refreshCollections();
8271
- refreshViews();
8326
+ if (pendingRefreshRef.current) return;
8327
+ pendingRefreshRef.current = true;
8328
+ queueMicrotask(() => {
8329
+ pendingRefreshRef.current = false;
8330
+ refreshCollections();
8331
+ refreshViews();
8332
+ });
8272
8333
  }, [refreshCollections, refreshViews]);
8273
8334
  return useMemo(() => ({
8274
8335
  views,
@@ -8276,8 +8337,7 @@ function useBuildNavigationStateController(props) {
8276
8337
  topLevelNavigation,
8277
8338
  loading: collectionsLoading || viewsLoading,
8278
8339
  navigationLoadingError: collectionsError ?? viewsError,
8279
- refreshNavigation,
8280
- plugins
8340
+ refreshNavigation
8281
8341
  }), [
8282
8342
  views,
8283
8343
  adminViews,
@@ -8286,8 +8346,7 @@ function useBuildNavigationStateController(props) {
8286
8346
  viewsLoading,
8287
8347
  collectionsError,
8288
8348
  viewsError,
8289
- refreshNavigation,
8290
- plugins
8349
+ refreshNavigation
8291
8350
  ]);
8292
8351
  }
8293
8352
  //#endregion
@@ -8349,14 +8408,18 @@ var BreadcrumbContext = React.createContext({
8349
8408
  var BreadcrumbsProvider = ({ children }) => {
8350
8409
  const [breadcrumbs, setBreadcrumbs] = useState([]);
8351
8410
  const set = useCallback((props) => {
8352
- setBreadcrumbs((prev) => props.breadcrumbs.map((newEntry) => {
8353
- const prevEntry = newEntry.id ? prev.find((p) => p.id === newEntry.id) : void 0;
8354
- if (prevEntry && newEntry.count === null && typeof prevEntry.count === "number") return {
8355
- ...newEntry,
8356
- count: prevEntry.count
8357
- };
8358
- return newEntry;
8359
- }));
8411
+ setBreadcrumbs((prev) => {
8412
+ const next = props.breadcrumbs.map((newEntry) => {
8413
+ const prevEntry = newEntry.id ? prev.find((p) => p.id === newEntry.id) : void 0;
8414
+ if (prevEntry && newEntry.count === null && typeof prevEntry.count === "number") return {
8415
+ ...newEntry,
8416
+ count: prevEntry.count
8417
+ };
8418
+ return newEntry;
8419
+ });
8420
+ if (prev.length === next.length && prev.every((p, i) => p.title === next[i].title && p.url === next[i].url && p.id === next[i].id && p.count === next[i].count)) return prev;
8421
+ return next;
8422
+ });
8360
8423
  }, []);
8361
8424
  const updateCount = useCallback((id, count) => {
8362
8425
  setBreadcrumbs((prev) => prev.map((entry) => entry.id === id ? {
@@ -8499,11 +8562,7 @@ function SideDialogView({ offsetPosition, panel, panelIndex, isTopPanel }) {
8499
8562
  overlayStyle: { zIndex: 40 + panelIndex * 10 },
8500
8563
  style: { zIndex: 45 + panelIndex * 10 },
8501
8564
  onOpenChange: (open) => {
8502
- if (!open) {
8503
- const suggestionMenu = document.querySelector("[data-suggestion-menu=\"true\"]");
8504
- if (suggestionMenu && window.getComputedStyle(suggestionMenu).visibility !== "hidden") return;
8505
- onCloseRequest();
8506
- }
8565
+ if (!open) onCloseRequest();
8507
8566
  },
8508
8567
  onPointerDownOutside: !isTopPanel ? preventDismiss : void 0,
8509
8568
  onInteractOutside: !isTopPanel ? preventDismiss : void 0,
@@ -8966,7 +9025,7 @@ function FormEntry({ propertyKey, widthPercentage = 100, children }) {
8966
9025
  //#region src/form/components/FormLayout.tsx
8967
9026
  function FormLayout({ children, className }) {
8968
9027
  return /* @__PURE__ */ jsx("div", {
8969
- className: cls("flex flex-wrap gap-x-4 w-full space-y-8", className),
9028
+ className: cls("flex flex-wrap gap-x-4 w-full space-y-6", className),
8970
9029
  children
8971
9030
  });
8972
9031
  }
@@ -8991,7 +9050,7 @@ function ReadOnlyFieldBinding({ propertyKey, value, error, showError, minimalist
8991
9050
  className: "h-8 text-text-secondary dark:text-text-secondary-dark ml-3.5"
8992
9051
  }),
8993
9052
  /* @__PURE__ */ jsx("div", {
8994
- className: cls(!skipCardWrapper && paperMixin, "w-full min-h-14 overflow-x-scroll no-scrollbar", !skipCardWrapper && "p-4 md:p-6"),
9053
+ className: cls("w-full overflow-x-scroll no-scrollbar flex items-center", skipCardWrapper ? "" : "rounded-lg border border-surface-200 dark:border-surface-700 px-4 md:px-6 min-h-10 opacity-80"),
8995
9054
  children: /* @__PURE__ */ jsx(ErrorBoundary, { children: /* @__PURE__ */ jsx(PropertyPreview, {
8996
9055
  propertyKey,
8997
9056
  value,
@@ -9278,7 +9337,7 @@ var buildIdsMap = (value) => value && Array.isArray(value) && value.length > 0 ?
9278
9337
  ...a,
9279
9338
  ...b
9280
9339
  }), {}) : {};
9281
- function SortableItem$1({ id, index, size, disabled, buildEntry, remove, copy, addInIndex, canAddElements, sortable, storedProps, updateItemCustomProps }) {
9340
+ function SortableItem({ id, index, size, disabled, buildEntry, remove, copy, addInIndex, canAddElements, sortable, storedProps, updateItemCustomProps }) {
9282
9341
  const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id });
9283
9342
  return /* @__PURE__ */ jsx(ArrayContainerItem, {
9284
9343
  nodeRef: setNodeRef,
@@ -9452,12 +9511,12 @@ function ArrayContainer({ droppableId, addLabel, value, disabled = false, buildE
9452
9511
  const newIds = [...internalIds];
9453
9512
  newIds.splice(index, 1);
9454
9513
  setInternalIds(newIds);
9455
- onValueChange(value.filter((_, i) => i !== index));
9514
+ onValueChange((value ?? []).filter((_, i) => i !== index));
9456
9515
  };
9457
9516
  const copy = (index) => {
9458
9517
  if ((value ?? []).length >= max) return;
9459
9518
  const id = getRandomId$1();
9460
- const copyingItem = value[index];
9519
+ const copyingItem = (value ?? [])[index];
9461
9520
  const newIds = [
9462
9521
  ...internalIds.slice(0, index + 1),
9463
9522
  id,
@@ -9466,9 +9525,9 @@ function ArrayContainer({ droppableId, addLabel, value, disabled = false, buildE
9466
9525
  if (onInternalIdAdded) onInternalIdAdded(id);
9467
9526
  setInternalIds(newIds);
9468
9527
  onValueChange([
9469
- ...value.slice(0, index + 1),
9528
+ ...(value ?? []).slice(0, index + 1),
9470
9529
  copyingItem,
9471
- ...value.slice(index + 1)
9530
+ ...(value ?? []).slice(index + 1)
9472
9531
  ]);
9473
9532
  };
9474
9533
  const addInIndex = (index) => {
@@ -9482,9 +9541,9 @@ function ArrayContainer({ droppableId, addLabel, value, disabled = false, buildE
9482
9541
  if (onInternalIdAdded) onInternalIdAdded(id);
9483
9542
  setInternalIds(newIds);
9484
9543
  onValueChange([
9485
- ...value.slice(0, index),
9544
+ ...(value ?? []).slice(0, index),
9486
9545
  newDefaultEntry,
9487
- ...value.slice(index)
9546
+ ...(value ?? []).slice(index)
9488
9547
  ]);
9489
9548
  };
9490
9549
  const onDragEnd = (event) => {
@@ -9494,7 +9553,7 @@ function ArrayContainer({ droppableId, addLabel, value, disabled = false, buildE
9494
9553
  const newIndex = internalIds.indexOf(over.id);
9495
9554
  if (oldIndex === -1 || newIndex === -1) return;
9496
9555
  setInternalIds(arrayMove$1(internalIds, oldIndex, newIndex));
9497
- onValueChange(arrayMove$1(value, oldIndex, newIndex));
9556
+ onValueChange(arrayMove$1(value ?? [], oldIndex, newIndex));
9498
9557
  };
9499
9558
  return sortable ? /* @__PURE__ */ jsx(DndContext, {
9500
9559
  sensors,
@@ -9507,7 +9566,7 @@ function ArrayContainer({ droppableId, addLabel, value, disabled = false, buildE
9507
9566
  children: /* @__PURE__ */ jsxs("div", {
9508
9567
  className: cls("space-y-1", className),
9509
9568
  id: droppableId,
9510
- children: [hasValue && internalIds.map((internalId, index) => /* @__PURE__ */ jsx(SortableItem$1, {
9569
+ children: [hasValue && internalIds.map((internalId, index) => /* @__PURE__ */ jsx(SortableItem, {
9511
9570
  id: internalId,
9512
9571
  index,
9513
9572
  size,
@@ -10391,7 +10450,7 @@ function MapFieldBinding({ propertyKey, value, showError, error, disabled, prope
10391
10450
  }
10392
10451
  //#endregion
10393
10452
  //#region src/form/field_bindings/MarkdownEditorFieldBinding.tsx
10394
- var RebaseEditor = lazy(() => import("./editor.js").then((m) => ({ default: m.RebaseEditor })));
10453
+ var RichTextEditor = lazy(() => import("./editor.js").then((m) => ({ default: m.RichTextEditor })));
10395
10454
  var loadMarkdownUtils = () => import("./markdown-CklalUUk.js").then((n) => n.t);
10396
10455
  var _markdownUtils = null;
10397
10456
  var getMarkdownUtils = async () => {
@@ -10506,7 +10565,7 @@ function MarkdownEditorFieldBinding({ property, propertyKey, value, setValue, in
10506
10565
  height: 200,
10507
10566
  className: "w-full rounded-md"
10508
10567
  }),
10509
- children: /* @__PURE__ */ jsx(RebaseEditor, {
10568
+ children: /* @__PURE__ */ jsx(RichTextEditor, {
10510
10569
  content: value,
10511
10570
  onMarkdownContentChange: onContentChange,
10512
10571
  version: context.formex.version + fieldVersion,
@@ -11249,7 +11308,7 @@ function StorageUpload({ property, name, value, setInternalValue, onChange, mult
11249
11308
  onDragStart: handleDragStart,
11250
11309
  onDragEnd: handleDragEnd,
11251
11310
  children: /* @__PURE__ */ jsx(SortableContext, {
11252
- items: value.map((v) => v.id),
11311
+ items: (value ?? []).map((v) => v.id),
11253
11312
  strategy: horizontalListSortingStrategy,
11254
11313
  children: /* @__PURE__ */ jsx(FileDropComponent, { ...fileDropProps })
11255
11314
  })
@@ -13589,189 +13648,57 @@ function EntityCard({ entity, collection, onClick, selected, highlighted, onSele
13589
13648
  //#endregion
13590
13649
  //#region src/components/EntityCollectionView/EntityCollectionCardView.tsx
13591
13650
  /**
13592
- * Get grid column classes based on the size.
13593
- * Smaller size = more columns (smaller cards)
13594
- * Larger size = fewer columns (larger cards)
13595
- */
13596
- function getGridColumnsClass(size) {
13597
- switch (size) {
13598
- case "xs": return "grid-cols-4 sm:grid-cols-5 md:grid-cols-6 lg:grid-cols-8 xl:grid-cols-10";
13599
- case "s": return "grid-cols-3 sm:grid-cols-4 md:grid-cols-5 lg:grid-cols-6 xl:grid-cols-8";
13600
- case "m": return "grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6";
13601
- case "l": return "grid-cols-2 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-4";
13602
- case "xl": return "grid-cols-1 sm:grid-cols-2 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-3";
13603
- default: return "grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6";
13604
- }
13605
- }
13606
- function getScrollParent$1(element) {
13607
- if (!element) return null;
13608
- let parent = element.parentElement;
13609
- while (parent) {
13610
- const overflowY = window.getComputedStyle(parent).overflowY;
13611
- if (overflowY === "auto" || overflowY === "scroll") return parent;
13612
- parent = parent.parentElement;
13613
- }
13614
- return document.documentElement;
13615
- }
13616
- /**
13617
13651
  * Card grid view for displaying entities with infinite scroll.
13618
13652
  * Alternative to the EntityCollectionTable for visual browsing.
13619
13653
  */
13620
13654
  function EntityCollectionCardView({ collection, tableController, onEntityClick, selectionController, selectionEnabled = true, highlightedEntities, emptyComponent, onScroll, initialScroll, size = "m" }) {
13621
13655
  const ResolvedEntityCard = useComponentOverride("Collection.Card", EntityCard);
13622
- const containerRef = useRef(null);
13623
- const hasRestoredScroll = useRef(false);
13624
13656
  const { data, dataLoading, noMoreToLoad, dataLoadingError, itemCount, setItemCount, pageSize = 50, paginationEnabled } = tableController;
13625
- const isLoadingMore = useRef(false);
13626
- const paginationStateRef = useRef({
13627
- paginationEnabled,
13628
- noMoreToLoad,
13629
- dataLoading,
13630
- itemCount,
13631
- pageSize
13632
- });
13633
- useEffect(() => {
13634
- paginationStateRef.current = {
13635
- paginationEnabled,
13636
- noMoreToLoad,
13637
- dataLoading,
13638
- itemCount,
13639
- pageSize
13640
- };
13641
- }, [
13642
- paginationEnabled,
13643
- noMoreToLoad,
13644
- dataLoading,
13645
- itemCount,
13646
- pageSize
13647
- ]);
13648
- useEffect(() => {
13649
- if (!dataLoading) isLoadingMore.current = false;
13650
- }, [dataLoading]);
13651
- useEffect(() => {
13652
- const el = containerRef.current;
13653
- if (!el) return;
13654
- const scrollEl = getScrollParent$1(el);
13655
- if (!scrollEl) return;
13656
- let rafId = null;
13657
- const update = () => {
13658
- rafId = null;
13659
- const { paginationEnabled: pe, noMoreToLoad: nm, itemCount: ic, pageSize: ps } = paginationStateRef.current;
13660
- if (pe && !nm && !isLoadingMore.current && scrollEl.scrollHeight - scrollEl.scrollTop - scrollEl.clientHeight < 400) {
13661
- isLoadingMore.current = true;
13662
- setItemCount?.((ic ?? ps) + ps);
13663
- }
13664
- };
13665
- const onScrollEvent = () => {
13666
- if (rafId === null) rafId = requestAnimationFrame(update);
13667
- };
13668
- scrollEl.addEventListener("scroll", onScrollEvent, { passive: true });
13669
- const ro = new ResizeObserver(() => update());
13670
- ro.observe(scrollEl);
13671
- update();
13672
- return () => {
13673
- scrollEl.removeEventListener("scroll", onScrollEvent);
13674
- ro.disconnect();
13675
- if (rafId !== null) cancelAnimationFrame(rafId);
13676
- };
13677
- }, [setItemCount]);
13678
- useEffect(() => {
13679
- if (!containerRef.current || !initialScroll || hasRestoredScroll.current || data.length === 0) return;
13680
- const scrollEl = getScrollParent$1(containerRef.current);
13681
- if (!scrollEl) return;
13682
- let attempts = 0;
13683
- const maxAttempts = 5;
13684
- const tryRestore = () => {
13685
- if (scrollEl.scrollHeight >= initialScroll || attempts >= maxAttempts) {
13686
- scrollEl.scrollTop = initialScroll;
13687
- hasRestoredScroll.current = true;
13688
- } else {
13689
- attempts++;
13690
- requestAnimationFrame(tryRestore);
13691
- }
13692
- };
13693
- requestAnimationFrame(tryRestore);
13694
- }, [initialScroll, data.length]);
13695
- const lastScrollOffset = useRef(0);
13696
- useEffect(() => {
13697
- const el = containerRef.current;
13698
- if (!el || !onScroll) return;
13699
- const scrollEl = getScrollParent$1(el);
13700
- if (!scrollEl) return;
13701
- const handleScroll = () => {
13702
- const currentOffset = scrollEl.scrollTop;
13703
- const direction = currentOffset > lastScrollOffset.current ? "forward" : "backward";
13704
- lastScrollOffset.current = currentOffset;
13705
- onScroll({
13706
- scrollDirection: direction,
13707
- scrollOffset: currentOffset,
13708
- scrollUpdateWasRequested: false
13709
- });
13710
- };
13711
- scrollEl.addEventListener("scroll", handleScroll, { passive: true });
13712
- return () => scrollEl.removeEventListener("scroll", handleScroll);
13713
- }, [onScroll]);
13714
13657
  const handleEntityClick = useCallback((entity) => {
13715
13658
  onEntityClick?.(entity);
13716
13659
  }, [onEntityClick]);
13717
13660
  const handleSelectionChange = useCallback((entity, selected) => {
13718
13661
  selectionController?.toggleEntitySelection(entity, selected);
13719
13662
  }, [selectionController]);
13720
- const isEntitySelected = useCallback((entity) => {
13721
- return selectionController?.isEntitySelected(entity) ?? false;
13722
- }, [selectionController]);
13723
- const isEntityHighlighted = useCallback((entity) => {
13724
- return highlightedEntities?.some((e) => e.id === entity.id && e.path === entity.path) ?? false;
13725
- }, [highlightedEntities]);
13726
- const gridColumnsClass = getGridColumnsClass(size);
13727
- const isInitialLoading = dataLoading && data.length === 0 && !dataLoadingError;
13728
- const isEmpty = !dataLoading && data.length === 0 && !dataLoadingError;
13729
- return /* @__PURE__ */ jsx("div", {
13730
- ref: containerRef,
13731
- className: "w-full p-4",
13732
- children: dataLoadingError && data.length === 0 ? /* @__PURE__ */ jsx("div", {
13733
- className: "h-full flex items-center justify-center p-8",
13734
- children: /* @__PURE__ */ jsxs(Typography, {
13735
- className: "text-red-500",
13736
- children: ["Error loading data: ", dataLoadingError.message]
13737
- })
13738
- }) : isInitialLoading ? /* @__PURE__ */ jsx("div", {
13739
- className: "flex items-center justify-center py-12 px-8",
13740
- children: /* @__PURE__ */ jsx(CircularProgress, { size: "small" })
13741
- }) : isEmpty ? /* @__PURE__ */ jsx("div", {
13742
- className: "w-full flex items-center justify-center py-12 px-8",
13743
- children: emptyComponent ?? /* @__PURE__ */ jsx(Typography, {
13744
- variant: "label",
13745
- color: "secondary",
13746
- children: "No entries found"
13747
- })
13748
- }) : /* @__PURE__ */ jsx(Fragment, { children: /* @__PURE__ */ jsxs("div", {
13749
- className: "max-w-7xl mx-auto",
13750
- children: [/* @__PURE__ */ jsx("div", {
13751
- className: cls("grid gap-4", gridColumnsClass),
13752
- children: data.map((entity) => /* @__PURE__ */ jsx(ResolvedEntityCard, {
13753
- entity,
13754
- collection,
13755
- onClick: handleEntityClick,
13756
- selected: isEntitySelected(entity),
13757
- highlighted: isEntityHighlighted(entity),
13758
- onSelectionChange: handleSelectionChange,
13759
- selectionEnabled,
13760
- size
13761
- }, `${entity.path}_${entity.id}`))
13762
- }), /* @__PURE__ */ jsxs("div", {
13763
- className: "flex items-center justify-center py-8",
13764
- children: [dataLoading && /* @__PURE__ */ jsx(CircularProgress, { size: "small" }), !dataLoading && noMoreToLoad && data.length > 0 && /* @__PURE__ */ jsxs(Typography, {
13765
- variant: "caption",
13766
- color: "secondary",
13767
- children: [
13768
- "All ",
13769
- data.length,
13770
- " entries loaded"
13771
- ]
13772
- })]
13773
- })]
13774
- }) })
13663
+ const selectedIds = useMemo(() => new Set(selectionController?.selectedEntities.map((e) => e.id)), [selectionController?.selectedEntities]);
13664
+ const highlightedIds = useMemo(() => new Set(highlightedEntities?.map((e) => e.id)), [highlightedEntities]);
13665
+ const handleRowSelectionChange = useCallback((entity, selected) => {
13666
+ handleSelectionChange(entity, selected);
13667
+ }, [handleSelectionChange]);
13668
+ return /* @__PURE__ */ jsx(CardView, {
13669
+ data,
13670
+ dataLoading,
13671
+ noMoreToLoad,
13672
+ dataLoadingError,
13673
+ itemCount,
13674
+ setItemCount,
13675
+ pageSize,
13676
+ paginationEnabled,
13677
+ onItemClick: handleEntityClick,
13678
+ selectedIds,
13679
+ highlightedIds,
13680
+ selectionEnabled,
13681
+ onSelectionChange: handleRowSelectionChange,
13682
+ onScroll,
13683
+ initialScroll,
13684
+ size,
13685
+ emptyComponent,
13686
+ renderCard: useCallback((entity, { selected, highlighted, onClick }) => /* @__PURE__ */ jsx(ResolvedEntityCard, {
13687
+ entity,
13688
+ collection,
13689
+ onClick,
13690
+ selected,
13691
+ highlighted,
13692
+ onSelectionChange: handleRowSelectionChange,
13693
+ selectionEnabled,
13694
+ size
13695
+ }, `${entity.path}_${entity.id}`), [
13696
+ collection,
13697
+ selectionEnabled,
13698
+ size,
13699
+ handleRowSelectionChange,
13700
+ ResolvedEntityCard
13701
+ ])
13775
13702
  });
13776
13703
  }
13777
13704
  //#endregion
@@ -13789,38 +13716,9 @@ function getRowClasses(size) {
13789
13716
  default: return "py-3 px-5";
13790
13717
  }
13791
13718
  }
13792
- /**
13793
- * Estimated row height in pixels for virtualization, based on size.
13794
- */
13795
- function getEstimatedRowHeight(size) {
13796
- switch (size) {
13797
- case "xs": return 44;
13798
- case "s": return 52;
13799
- case "m": return 64;
13800
- case "l": return 76;
13801
- case "xl": return 88;
13802
- default: return 64;
13803
- }
13804
- }
13805
- /** Number of extra rows rendered above/below the viewport. */
13806
- var OVERSCAN_COUNT = 8;
13807
- /** Threshold in pixels from the bottom of the scroll area to trigger loading more. */
13808
- var LOAD_MORE_THRESHOLD = 400;
13809
13719
  /** Stable empty array for when no list-view actions are available. */
13810
13720
  var EMPTY_LIST_VIEW_ACTIONS = [];
13811
13721
  /**
13812
- * Walk up the DOM from `element` to find the nearest scrollable ancestor.
13813
- */
13814
- function getScrollParent(element) {
13815
- let parent = element?.parentElement ?? null;
13816
- while (parent) {
13817
- const style = getComputedStyle(parent);
13818
- if (style.overflowY === "auto" || style.overflowY === "scroll" || style.overflow === "auto" || style.overflow === "scroll") return parent;
13819
- parent = parent.parentElement;
13820
- }
13821
- return document.documentElement;
13822
- }
13823
- /**
13824
13722
  * Returns true if a property type should NOT be rendered via
13825
13723
  * PropertyPreview in list row columns (because it would blow up height).
13826
13724
  */
@@ -14074,10 +13972,10 @@ function EntityCollectionListView({ collection, tableController, onEntityClick,
14074
13972
  const handleSelectionChange = useCallback((entity, selected) => {
14075
13973
  selectionController?.toggleEntitySelection(entity, selected);
14076
13974
  }, [selectionController]);
14077
- const isEntitySelected = useCallback((entity) => {
13975
+ useCallback((entity) => {
14078
13976
  return selectionController?.isEntitySelected(entity) ?? false;
14079
13977
  }, [selectionController]);
14080
- const isEntityHighlighted = useCallback((entity) => {
13978
+ useCallback((entity) => {
14081
13979
  return highlightedEntities?.some((e) => e.id === entity.id && e.path === entity.path) ?? false;
14082
13980
  }, [highlightedEntities]);
14083
13981
  const getListViewActions = useCallback((entity) => {
@@ -14092,158 +13990,70 @@ function EntityCollectionListView({ collection, tableController, onEntityClick,
14092
13990
  customizationController.entityActions
14093
13991
  ]);
14094
13992
  const rowClasses = getRowClasses(size);
14095
- const isInitialLoading = dataLoading && data.length === 0;
14096
- const isEmpty = !dataLoading && data.length === 0 && !dataLoadingError;
14097
- const estimatedRowHeight = getEstimatedRowHeight(size);
14098
- const [effectiveScrollTop, setEffectiveScrollTop] = useState(0);
14099
- const [viewportHeight, setViewportHeight] = useState(800);
14100
- const paginationStateRef = useRef({
14101
- paginationEnabled,
13993
+ const selectedIds = useMemo(() => new Set(selectionController?.selectedEntities.map((e) => e.id)), [selectionController?.selectedEntities]);
13994
+ const highlightedIds = useMemo(() => new Set(highlightedEntities?.map((e) => e.id)), [highlightedEntities]);
13995
+ const handleRowSelectionChange = useCallback((entity, selected) => {
13996
+ handleSelectionChange(entity, selected);
13997
+ }, [handleSelectionChange]);
13998
+ return /* @__PURE__ */ jsx(ListView, {
13999
+ data,
14000
+ dataLoading,
14102
14001
  noMoreToLoad,
14002
+ dataLoadingError,
14103
14003
  itemCount,
14104
- pageSize
14105
- });
14106
- useEffect(() => {
14107
- paginationStateRef.current = {
14108
- paginationEnabled,
14109
- noMoreToLoad,
14110
- itemCount,
14111
- pageSize
14112
- };
14113
- }, [
14004
+ setItemCount,
14005
+ pageSize,
14114
14006
  paginationEnabled,
14115
- noMoreToLoad,
14116
- itemCount,
14117
- pageSize
14118
- ]);
14119
- useEffect(() => {
14120
- const el = containerRef.current;
14121
- if (!el) return;
14122
- const scrollEl = getScrollParent(el);
14123
- if (!scrollEl) return;
14124
- let rafId = null;
14125
- const update = () => {
14126
- rafId = null;
14127
- const scrollRect = scrollEl.getBoundingClientRect();
14128
- const listTopRelative = el.getBoundingClientRect().top - scrollRect.top;
14129
- setEffectiveScrollTop(Math.max(0, -listTopRelative));
14130
- setViewportHeight(scrollRect.height);
14131
- const { paginationEnabled: pe, noMoreToLoad: nm, itemCount: ic, pageSize: ps } = paginationStateRef.current;
14132
- if (pe && !nm && !isLoadingMore.current && scrollEl.scrollHeight - scrollEl.scrollTop - scrollEl.clientHeight < LOAD_MORE_THRESHOLD) {
14133
- isLoadingMore.current = true;
14134
- setItemCount?.((ic ?? ps) + ps);
14135
- }
14136
- };
14137
- const onScroll = () => {
14138
- if (rafId === null) rafId = requestAnimationFrame(update);
14139
- };
14140
- scrollEl.addEventListener("scroll", onScroll, { passive: true });
14141
- const ro = new ResizeObserver(() => update());
14142
- ro.observe(scrollEl);
14143
- update();
14144
- return () => {
14145
- scrollEl.removeEventListener("scroll", onScroll);
14146
- ro.disconnect();
14147
- if (rafId !== null) cancelAnimationFrame(rafId);
14148
- };
14149
- }, [setItemCount]);
14150
- const totalHeight = data.length * estimatedRowHeight;
14151
- const startIndex = Math.max(0, Math.floor(effectiveScrollTop / estimatedRowHeight) - OVERSCAN_COUNT);
14152
- const endIndex = Math.min(data.length, Math.ceil((effectiveScrollTop + viewportHeight) / estimatedRowHeight) + OVERSCAN_COUNT);
14153
- const visibleData = data.slice(startIndex, endIndex);
14154
- const offsetY = startIndex * estimatedRowHeight;
14155
- const footerHeight = dataLoading ? 48 : !dataLoading && noMoreToLoad && data.length > 0 ? 32 : 0;
14156
- return /* @__PURE__ */ jsx("div", {
14157
- ref: containerRef,
14158
- className: cls("w-full", selectedEntityId === void 0 && "rounded-lg overflow-hidden border " + defaultBorderMixin),
14159
- children: dataLoadingError && data.length === 0 ? /* @__PURE__ */ jsx("div", {
14160
- className: "flex items-center justify-center p-8",
14161
- children: /* @__PURE__ */ jsxs(Typography, {
14162
- className: "text-red-500",
14163
- children: ["Error loading data: ", dataLoadingError.message]
14164
- })
14165
- }) : isInitialLoading ? /* @__PURE__ */ jsx("div", {
14166
- className: "flex items-center justify-center py-12 px-8",
14167
- children: /* @__PURE__ */ jsx(CircularProgress, { size: "small" })
14168
- }) : isEmpty ? /* @__PURE__ */ jsx("div", {
14169
- className: "w-full flex items-center justify-center py-12 px-8",
14170
- children: emptyComponent ?? /* @__PURE__ */ jsx(Typography, {
14171
- variant: "label",
14172
- color: "secondary",
14173
- children: "No entries found"
14174
- })
14175
- }) : /* @__PURE__ */ jsxs("div", {
14176
- style: {
14177
- height: totalHeight + footerHeight,
14178
- position: "relative"
14179
- },
14180
- children: [
14181
- /* @__PURE__ */ jsx("div", {
14182
- style: {
14183
- position: "absolute",
14184
- top: offsetY,
14185
- left: 0,
14186
- right: 0
14187
- },
14188
- children: visibleData.map((entity, i) => {
14189
- const isLast = startIndex + i === data.length - 1;
14190
- return /* @__PURE__ */ jsx("div", {
14191
- style: { height: estimatedRowHeight },
14192
- className: cls(!isLast && "border-b", !isLast && defaultBorderMixin),
14193
- children: /* @__PURE__ */ jsx(ListRow, {
14194
- entity,
14195
- collection: resolvedCollection,
14196
- onClick: handleEntityClick,
14197
- selected: isEntitySelected(entity),
14198
- highlighted: isEntityHighlighted(entity),
14199
- onSelectionChange: handleSelectionChange,
14200
- selectionEnabled,
14201
- columns: visibleColumns,
14202
- slotKeys,
14203
- rowClasses,
14204
- showImage,
14205
- size,
14206
- isLast,
14207
- isActive: selectedEntityId !== void 0 && entity.id === selectedEntityId,
14208
- listViewActions: getListViewActions(entity),
14209
- context,
14210
- path,
14211
- selectionController,
14212
- openEntityMode
14213
- })
14214
- }, entity.id);
14215
- })
14216
- }),
14217
- dataLoading && /* @__PURE__ */ jsx("div", {
14218
- className: "flex items-center justify-center py-3",
14219
- style: {
14220
- position: "absolute",
14221
- top: totalHeight,
14222
- left: 0,
14223
- right: 0
14224
- },
14225
- children: /* @__PURE__ */ jsx(CircularProgress, { size: "small" })
14226
- }),
14227
- !dataLoading && noMoreToLoad && data.length > 0 && /* @__PURE__ */ jsx("div", {
14228
- className: "flex items-center justify-center py-2 dark:bg-surface-900",
14229
- style: {
14230
- position: "absolute",
14231
- top: totalHeight,
14232
- left: 0,
14233
- right: 0
14234
- },
14235
- children: /* @__PURE__ */ jsxs(Typography, {
14236
- variant: "caption",
14237
- color: "secondary",
14238
- children: [
14239
- "All ",
14240
- data.length,
14241
- " entries loaded"
14242
- ]
14243
- })
14007
+ onItemClick: handleEntityClick,
14008
+ selectedIds,
14009
+ highlightedIds,
14010
+ selectionEnabled,
14011
+ emptyComponent,
14012
+ size,
14013
+ selectedEntityId,
14014
+ renderRow: useCallback(({ item: entity, style, className, selected, highlighted, isLast, onClick, onSelectionChange }) => {
14015
+ return /* @__PURE__ */ jsx("div", {
14016
+ style,
14017
+ className,
14018
+ children: /* @__PURE__ */ jsx(ListRow, {
14019
+ entity,
14020
+ collection: resolvedCollection,
14021
+ onClick: handleEntityClick,
14022
+ selected,
14023
+ highlighted,
14024
+ onSelectionChange: handleRowSelectionChange,
14025
+ selectionEnabled,
14026
+ columns: visibleColumns,
14027
+ slotKeys,
14028
+ rowClasses,
14029
+ showImage,
14030
+ size,
14031
+ isLast,
14032
+ isActive: selectedEntityId !== void 0 && entity.id === selectedEntityId,
14033
+ listViewActions: getListViewActions(entity),
14034
+ context,
14035
+ path,
14036
+ selectionController,
14037
+ openEntityMode
14244
14038
  })
14245
- ]
14246
- })
14039
+ }, entity.id);
14040
+ }, [
14041
+ resolvedCollection,
14042
+ selectionEnabled,
14043
+ visibleColumns,
14044
+ slotKeys,
14045
+ rowClasses,
14046
+ showImage,
14047
+ size,
14048
+ selectedEntityId,
14049
+ getListViewActions,
14050
+ context,
14051
+ path,
14052
+ selectionController,
14053
+ openEntityMode,
14054
+ handleRowSelectionChange,
14055
+ handleEntityClick
14056
+ ])
14247
14057
  });
14248
14058
  }
14249
14059
  /**
@@ -14679,457 +14489,6 @@ function SplitListView({ collection, tableController, onEntityClick: externalOnE
14679
14489
  });
14680
14490
  }
14681
14491
  //#endregion
14682
- //#region src/components/EntityCollectionView/BoardSortableList.tsx
14683
- function BoardSortableList({ columnId, items, ItemComponent, isDragging, isDragOverColumn, loading = false, hasMore = false, onLoadMore }) {
14684
- const { setNodeRef } = useDroppable({
14685
- id: columnId,
14686
- data: { type: "ITEM-LIST" }
14687
- });
14688
- const sentinelRef = useRef(null);
14689
- const isLoadingRef = useRef(false);
14690
- isLoadingRef.current = loading;
14691
- const lastLoadTimeRef = useRef(0);
14692
- useEffect(() => {
14693
- if (!sentinelRef.current || !hasMore || !onLoadMore) return;
14694
- const sentinel = sentinelRef.current;
14695
- const observer = new IntersectionObserver((entries) => {
14696
- const now = Date.now();
14697
- if (entries[0].isIntersecting && hasMore && !isLoadingRef.current && now - lastLoadTimeRef.current > 500) {
14698
- lastLoadTimeRef.current = now;
14699
- onLoadMore();
14700
- }
14701
- }, { threshold: .1 });
14702
- observer.observe(sentinel);
14703
- const rect = sentinel.getBoundingClientRect();
14704
- const containerRect = sentinel.parentElement?.getBoundingClientRect();
14705
- if (containerRect && rect.top < containerRect.bottom && rect.bottom > containerRect.top) {
14706
- const now = Date.now();
14707
- if (hasMore && !isLoadingRef.current && now - lastLoadTimeRef.current > 500) {
14708
- lastLoadTimeRef.current = now;
14709
- onLoadMore();
14710
- }
14711
- }
14712
- return () => observer.disconnect();
14713
- }, [hasMore, onLoadMore]);
14714
- return /* @__PURE__ */ jsx("div", {
14715
- ref: setNodeRef,
14716
- className: useMemo(() => cls("flex flex-col p-2 transition-opacity duration-100 transition-bg ease-linear w-full overflow-y-auto no-scrollbar flex-1 rounded-md", isDragging && isDragOverColumn ? "bg-surface-accent-200 dark:bg-surface-900" : isDragging ? "bg-surface-50 dark:bg-surface-900 hover:bg-surface-accent-100 dark:hover:bg-surface-800" : "bg-surface-50 dark:bg-surface-900"), [isDragging, isDragOverColumn]),
14717
- style: { minHeight: 80 },
14718
- children: /* @__PURE__ */ jsx(SortableContext, {
14719
- items: items.map((i) => i.id),
14720
- strategy: verticalListSortingStrategy,
14721
- children: items.length === 0 && !loading ? /* @__PURE__ */ jsx("div", {
14722
- className: "flex-1 flex items-center justify-center",
14723
- children: /* @__PURE__ */ jsx("span", {
14724
- className: "text-xs text-surface-400 dark:text-surface-500",
14725
- children: "No items"
14726
- })
14727
- }) : /* @__PURE__ */ jsxs(Fragment, { children: [items.map((item, index) => /* @__PURE__ */ jsx(SortableItem, {
14728
- item,
14729
- index,
14730
- columnId,
14731
- ItemComponent
14732
- }, item.id)), (loading || hasMore) && /* @__PURE__ */ jsx("div", {
14733
- ref: sentinelRef,
14734
- className: "flex items-center justify-center py-2 min-h-6",
14735
- children: loading && /* @__PURE__ */ jsx(CircularProgress, { size: "smallest" })
14736
- })] })
14737
- })
14738
- });
14739
- }
14740
- var SortableItem = memo(function SortableItem({ item, index, columnId, ItemComponent }) {
14741
- const { setNodeRef, attributes, listeners, isDragging: isItemBeingDragged, transform, transition } = useSortable({
14742
- id: item.id,
14743
- data: {
14744
- type: "ITEM",
14745
- columnId
14746
- }
14747
- });
14748
- return /* @__PURE__ */ jsx("div", {
14749
- ref: setNodeRef,
14750
- style: useMemo(() => ({
14751
- transform: CSS.Transform.toString(transform),
14752
- transition,
14753
- zIndex: isItemBeingDragged ? 2 : 1,
14754
- opacity: isItemBeingDragged ? 0 : 1
14755
- }), [
14756
- transform,
14757
- transition,
14758
- isItemBeingDragged
14759
- ]),
14760
- ...attributes,
14761
- ...listeners,
14762
- children: /* @__PURE__ */ jsx(ItemComponent, {
14763
- item,
14764
- isDragging: isItemBeingDragged,
14765
- index
14766
- })
14767
- });
14768
- });
14769
- //#endregion
14770
- //#region src/components/EntityCollectionView/BoardColumnTitle.tsx
14771
- function BoardColumnTitle({ children, className, color, ...props }) {
14772
- const colorScheme = useMemo(() => {
14773
- if (!color) return void 0;
14774
- if (typeof color === "string") return getColorSchemeForKey(color);
14775
- return color;
14776
- }, [color]);
14777
- return /* @__PURE__ */ jsxs(Typography, {
14778
- variant: "subtitle2",
14779
- component: "h4",
14780
- className: cls("py-3 px-3 transition-colors duration-200 flex-grow select-none relative outline-none focus:outline focus:outline-2 focus:outline-offset-2 flex items-center gap-3", className),
14781
- ...props,
14782
- children: [colorScheme && /* @__PURE__ */ jsx("div", {
14783
- className: "w-3 h-3 rounded-full flex-shrink-0",
14784
- style: { backgroundColor: colorScheme.darkColor ?? colorScheme.color }
14785
- }), children]
14786
- });
14787
- }
14788
- //#endregion
14789
- //#region src/components/EntityCollectionView/BoardColumn.tsx
14790
- var BoardColumn = memo(function BoardColumn({ id, title, items, ItemComponent, isDragging, isDragOverColumn, allowReorder = false, loading = false, hasMore = false, onLoadMore, onAddItem, totalCount, color, style }) {
14791
- const { setNodeRef, attributes, listeners, isDragging: isColumnBeingDragged, transform, transition } = useSortable({
14792
- id,
14793
- data: { type: "COLUMN" },
14794
- disabled: !allowReorder
14795
- });
14796
- const combinedStyle = useMemo(() => ({
14797
- ...style,
14798
- transform: CSS.Translate.toString(transform),
14799
- transition,
14800
- zIndex: isColumnBeingDragged ? 2 : 1
14801
- }), [
14802
- style,
14803
- transform,
14804
- transition,
14805
- isColumnBeingDragged
14806
- ]);
14807
- const dragListeners = allowReorder ? listeners : {};
14808
- const columnClassName = useMemo(() => cls("border h-full w-80 min-w-80 mx-2 flex flex-col rounded-md", defaultBorderMixin, isColumnBeingDragged ? "ring-2 ring-primary" : ""), [isColumnBeingDragged]);
14809
- const headerClassName = useMemo(() => cls("flex items-center justify-between px-2 rounded-t-md transition-colors duration-200 ease-in-out", isColumnBeingDragged ? "bg-surface-100 dark:bg-surface-700" : "bg-surface-50 hover:bg-surface-100 dark:bg-surface-800 dark:hover:bg-surface-700", allowReorder ? "cursor-grab" : ""), [isColumnBeingDragged, allowReorder]);
14810
- const itemIds = useMemo(() => items.map((i) => i.id), [items]);
14811
- return /* @__PURE__ */ jsxs("div", {
14812
- ref: setNodeRef,
14813
- style: combinedStyle,
14814
- ...attributes,
14815
- className: columnClassName,
14816
- children: [/* @__PURE__ */ jsxs("div", {
14817
- ...dragListeners,
14818
- className: headerClassName,
14819
- children: [/* @__PURE__ */ jsxs("div", {
14820
- className: "flex items-center gap-2",
14821
- children: [/* @__PURE__ */ jsx(BoardColumnTitle, {
14822
- "aria-label": `${title} item list`,
14823
- color,
14824
- children: title
14825
- }), totalCount !== void 0 && /* @__PURE__ */ jsx("span", {
14826
- className: "text-xs text-surface-500 dark:text-surface-400",
14827
- children: totalCount
14828
- })]
14829
- }), onAddItem && /* @__PURE__ */ jsx(IconButton, {
14830
- size: "small",
14831
- onClick: (e) => {
14832
- e.stopPropagation();
14833
- onAddItem();
14834
- },
14835
- className: "opacity-60 hover:opacity-100",
14836
- children: /* @__PURE__ */ jsx(PlusIcon, { size: iconSize.small })
14837
- })]
14838
- }), /* @__PURE__ */ jsx(SortableContext, {
14839
- items: itemIds,
14840
- strategy: verticalListSortingStrategy,
14841
- children: /* @__PURE__ */ jsx(BoardSortableList, {
14842
- columnId: id,
14843
- items,
14844
- ItemComponent,
14845
- isDragging,
14846
- isDragOverColumn,
14847
- loading,
14848
- hasMore,
14849
- onLoadMore
14850
- })
14851
- })]
14852
- });
14853
- });
14854
- //#endregion
14855
- //#region src/components/EntityCollectionView/Board.tsx
14856
- function Board({ data, columns: columnsProp, columnLabels, columnColors, className, assignColumn, allowColumnReorder = false, onColumnReorder, onItemsReorder, ItemComponent, columnLoadingState, onLoadMoreColumn, onAddItemToColumn, AddColumnComponent }) {
14857
- const [activeItem, setActiveItem] = useState(null);
14858
- const [activeColumn, setActiveColumn] = useState(null);
14859
- const [isDragging, setIsDragging] = useState(false);
14860
- const [dragOverColumnId, setDragOverColumnId] = useState(null);
14861
- const grabOffsetRef = useRef({
14862
- x: 0,
14863
- y: 0
14864
- });
14865
- const [overlayPos, setOverlayPos] = useState(null);
14866
- const handleMouseMove = useCallback((e) => {
14867
- setOverlayPos({
14868
- x: e.clientX - grabOffsetRef.current.x,
14869
- y: e.clientY - grabOffsetRef.current.y
14870
- });
14871
- }, []);
14872
- useEffect(() => {
14873
- return () => {
14874
- window.removeEventListener("mousemove", handleMouseMove);
14875
- window.removeEventListener("pointermove", handleMouseMove);
14876
- };
14877
- }, [handleMouseMove]);
14878
- const [itemMapState, setItemMapState] = useState(() => {
14879
- const dataColumnMap = data.reduce((prev, item) => ({
14880
- ...prev,
14881
- [item.id]: assignColumn(item)
14882
- }), {});
14883
- return columnsProp.reduce((previous, column) => ({
14884
- ...previous,
14885
- [column]: data.filter((item) => dataColumnMap[item.id] === column)
14886
- }), {});
14887
- });
14888
- const sensors = useSensors(useSensor(PointerSensor, { activationConstraint: { distance: 5 } }));
14889
- useEffect(() => {
14890
- if (isDragging) return;
14891
- const dataColumnMap = data.reduce((prev, item) => ({
14892
- ...prev,
14893
- [item.id]: assignColumn(item)
14894
- }), {});
14895
- const newItemMap = columnsProp.reduce((previous, column) => ({
14896
- ...previous,
14897
- [column]: data.filter((item) => dataColumnMap[item.id] === column)
14898
- }), {});
14899
- setItemMapState((prevMap) => {
14900
- let changed = false;
14901
- for (const col of columnsProp) {
14902
- const prevItems = prevMap[col] ?? [];
14903
- const newItems = newItemMap[col] ?? [];
14904
- if (prevItems.length !== newItems.length) {
14905
- changed = true;
14906
- continue;
14907
- }
14908
- for (let i = 0; i < prevItems.length; i++) if (prevItems[i].id !== newItems[i].id || prevItems[i].entity !== newItems[i].entity) {
14909
- changed = true;
14910
- break;
14911
- }
14912
- }
14913
- if (!changed) return prevMap;
14914
- const updated = {};
14915
- for (const col of columnsProp) {
14916
- const prevItems = prevMap[col] ?? [];
14917
- const newItems = newItemMap[col] ?? [];
14918
- const prevById = /* @__PURE__ */ new Map();
14919
- for (const item of prevItems) prevById.set(item.id, item);
14920
- updated[col] = newItems.map((newItem) => {
14921
- const prev = prevById.get(newItem.id);
14922
- if (prev && prev.entity === newItem.entity) return prev;
14923
- return newItem;
14924
- });
14925
- }
14926
- return updated;
14927
- });
14928
- }, [
14929
- data,
14930
- columnsProp,
14931
- assignColumn
14932
- ]);
14933
- const findColumnByItemId = (id) => {
14934
- return Object.keys(itemMapState).find((col) => itemMapState[col]?.some((i) => i.id === id));
14935
- };
14936
- const handleDragStart = (event) => {
14937
- setIsDragging(true);
14938
- setDragOverColumnId(null);
14939
- const { active } = event;
14940
- const activatorEvt = event.activatorEvent;
14941
- if (activatorEvt) {
14942
- const target = activatorEvt.target;
14943
- const rect = (target.closest("[role='button']") ?? target).getBoundingClientRect();
14944
- grabOffsetRef.current = {
14945
- x: activatorEvt.clientX - rect.left,
14946
- y: activatorEvt.clientY - rect.top
14947
- };
14948
- setOverlayPos({
14949
- x: rect.left,
14950
- y: rect.top
14951
- });
14952
- }
14953
- window.addEventListener("mousemove", handleMouseMove);
14954
- window.addEventListener("pointermove", handleMouseMove);
14955
- if (active.data.current?.type === "COLUMN") {
14956
- const columnId = active.id;
14957
- const column = columnsProp.find((col) => String(col) === columnId);
14958
- if (column) setActiveColumn(column);
14959
- } else if (active.data.current?.type === "ITEM") {
14960
- const columnId = findColumnByItemId(active.id);
14961
- if (columnId) {
14962
- const item = itemMapState[columnId]?.find((i) => i.id === active.id);
14963
- setActiveItem(item || null);
14964
- }
14965
- }
14966
- };
14967
- const handleDragOver = (event) => {
14968
- const { active, over } = event;
14969
- if (!over) {
14970
- setDragOverColumnId(null);
14971
- return;
14972
- }
14973
- let currentHoveredColumnId = null;
14974
- const overId = over.id;
14975
- const overDataType = over.data.current?.type;
14976
- if (overDataType === "ITEM-LIST" || overDataType === "COLUMN") currentHoveredColumnId = overId;
14977
- else if (overDataType === "ITEM") currentHoveredColumnId = findColumnByItemId(overId) || null;
14978
- else if (columnsProp.includes(overId)) currentHoveredColumnId = overId;
14979
- setDragOverColumnId(currentHoveredColumnId);
14980
- if (active.data.current?.type !== "ITEM") return;
14981
- const activeId = active.id;
14982
- const activeColumn = findColumnByItemId(activeId);
14983
- let overColumnForMove = findColumnByItemId(overId);
14984
- if (!overColumnForMove && overDataType === "ITEM-LIST") overColumnForMove = overId;
14985
- if (!overColumnForMove && columnsProp.includes(overId)) overColumnForMove = overId;
14986
- if (!activeColumn || !overColumnForMove) return;
14987
- if (activeColumn === overColumnForMove) return;
14988
- if (itemMapState[overColumnForMove]?.some((i) => i.id === activeId)) return;
14989
- setItemMapState((currentMap) => {
14990
- const activeItems = [...currentMap[activeColumn] || []];
14991
- const overItems = [...currentMap[overColumnForMove] || []];
14992
- const activeIndex = activeItems.findIndex((i) => i.id === activeId);
14993
- if (activeIndex === -1) return currentMap;
14994
- let overIndex;
14995
- if (overDataType === "ITEM-LIST" || columnsProp.includes(overId) && !findColumnByItemId(overId)) overIndex = overItems.length;
14996
- else {
14997
- overIndex = overItems.findIndex((i) => i.id === overId);
14998
- if (overIndex !== -1) {
14999
- const modifier = (active.rect.current.translated?.top ?? 0) + (active.rect.current.translated?.height ?? 0) / 2 > (over?.rect.top ?? 0) + (over?.rect.height ?? 0) / 2 ? 1 : 0;
15000
- overIndex = overIndex >= 0 ? overIndex + modifier : overItems.length;
15001
- } else overIndex = overItems.length;
15002
- }
15003
- const newItemMap = { ...currentMap };
15004
- const [moved] = activeItems.splice(activeIndex, 1);
15005
- overItems.splice(overIndex, 0, moved);
15006
- newItemMap[activeColumn] = activeItems;
15007
- newItemMap[overColumnForMove] = overItems;
15008
- return newItemMap;
15009
- });
15010
- };
15011
- const handleDragEnd = (event) => {
15012
- const { active, over } = event;
15013
- window.removeEventListener("mousemove", handleMouseMove);
15014
- window.removeEventListener("pointermove", handleMouseMove);
15015
- setOverlayPos(null);
15016
- setIsDragging(false);
15017
- setActiveItem(null);
15018
- setActiveColumn(null);
15019
- setDragOverColumnId(null);
15020
- if (!over) return;
15021
- const activeId = active.id;
15022
- const overId = over.id;
15023
- if (active.data.current?.type === "COLUMN" && over.data.current?.type === "COLUMN" && activeId !== overId) {
15024
- const oldIndex = columnsProp.findIndex((col) => String(col) === activeId);
15025
- const newIndex = columnsProp.findIndex((col) => String(col) === overId);
15026
- if (oldIndex !== -1 && newIndex !== -1 && onColumnReorder && allowColumnReorder) onColumnReorder(arrayMove([...columnsProp], oldIndex, newIndex));
15027
- } else if (active.data.current?.type === "ITEM" && onItemsReorder) {
15028
- const originalColumn = data.find((item) => item.id === activeId) ? assignColumn(data.find((item) => item.id === activeId)) : void 0;
15029
- const currentColumn = findColumnByItemId(activeId);
15030
- const overColumn = findColumnByItemId(overId) || currentColumn;
15031
- const finalItemMapState = { ...itemMapState };
15032
- if (currentColumn && currentColumn === overColumn) {
15033
- const activeIndex = finalItemMapState[currentColumn].findIndex((i) => i.id === activeId);
15034
- let overIndex = finalItemMapState[overColumn].findIndex((i) => i.id === overId);
15035
- const overType = over.data.current?.type;
15036
- if (overIndex === -1 && (overType === "COLUMN" || overType === "ITEM-LIST")) if (originalColumn === currentColumn) overIndex = finalItemMapState[overColumn].length;
15037
- else overIndex = activeIndex;
15038
- else if (overIndex === -1 && typeof over.data.current?.sortable?.index === "number") overIndex = over.data.current.sortable.index;
15039
- if (activeIndex !== -1 && overIndex !== -1 && activeIndex !== overIndex) {
15040
- finalItemMapState[currentColumn] = arrayMove(finalItemMapState[currentColumn], activeIndex, overIndex);
15041
- setItemMapState(finalItemMapState);
15042
- }
15043
- }
15044
- if (originalColumn !== currentColumn && originalColumn && currentColumn) onItemsReorder(finalItemMapState[currentColumn], {
15045
- itemId: activeId,
15046
- sourceColumn: originalColumn,
15047
- targetColumn: currentColumn
15048
- });
15049
- else if (currentColumn) onItemsReorder(finalItemMapState[currentColumn], {
15050
- itemId: activeId,
15051
- sourceColumn: currentColumn,
15052
- targetColumn: currentColumn
15053
- });
15054
- }
15055
- };
15056
- const customCollisionDetection = (args) => {
15057
- const pointerCollisions = pointerWithin(args);
15058
- if (pointerCollisions.length > 0) return pointerCollisions;
15059
- return rectIntersection(args);
15060
- };
15061
- return /* @__PURE__ */ jsxs(DndContext, {
15062
- sensors,
15063
- collisionDetection: customCollisionDetection,
15064
- onDragStart: handleDragStart,
15065
- onDragOver: handleDragOver,
15066
- onDragEnd: handleDragEnd,
15067
- children: [
15068
- /* @__PURE__ */ jsx(DragOverlay, {
15069
- dropAnimation: null,
15070
- style: { opacity: 0 },
15071
- children: activeItem || activeColumn ? /* @__PURE__ */ jsx("div", { style: {
15072
- width: 1,
15073
- height: 1
15074
- } }) : null
15075
- }),
15076
- overlayPos && (activeItem || activeColumn) && createPortal(/* @__PURE__ */ jsx("div", {
15077
- className: document.documentElement.classList.contains("dark") ? "dark" : "",
15078
- style: {
15079
- position: "fixed",
15080
- top: overlayPos.y,
15081
- left: overlayPos.x,
15082
- zIndex: 9999,
15083
- pointerEvents: "none",
15084
- touchAction: "none"
15085
- },
15086
- children: activeItem ? /* @__PURE__ */ jsx(ItemComponent, {
15087
- item: activeItem,
15088
- isDragging: true,
15089
- index: -1,
15090
- style: {
15091
- boxShadow: "0 4px 16px rgba(0,0,0,0.15)",
15092
- opacity: .9,
15093
- width: "304px"
15094
- }
15095
- }) : activeColumn ? /* @__PURE__ */ jsx(BoardColumn, {
15096
- index: -1,
15097
- id: String(activeColumn),
15098
- title: columnLabels?.[activeColumn] ?? String(activeColumn),
15099
- items: itemMapState[String(activeColumn)] || [],
15100
- ItemComponent,
15101
- isDragging: true,
15102
- isDragOverColumn: false
15103
- }, String(activeColumn)) : null
15104
- }), document.body),
15105
- /* @__PURE__ */ jsx(SortableContext, {
15106
- items: columnsProp.map(String),
15107
- strategy: horizontalListSortingStrategy,
15108
- children: /* @__PURE__ */ jsxs("div", {
15109
- className: cls("p-2 md:p-3 lg:p-4 h-full min-w-full inline-flex", className),
15110
- children: [columnsProp.map((key, index) => /* @__PURE__ */ jsx(BoardColumn, {
15111
- index,
15112
- id: String(key),
15113
- title: columnLabels?.[key] ?? String(key),
15114
- color: columnColors?.[key],
15115
- items: itemMapState[String(key)] || [],
15116
- ItemComponent,
15117
- isDragging,
15118
- isDragOverColumn: String(key) === dragOverColumnId,
15119
- allowReorder: allowColumnReorder,
15120
- loading: columnLoadingState?.[String(key)]?.loading,
15121
- hasMore: columnLoadingState?.[String(key)]?.hasMore,
15122
- totalCount: columnLoadingState?.[String(key)]?.totalCount,
15123
- onLoadMore: onLoadMoreColumn ? () => onLoadMoreColumn(key) : void 0,
15124
- onAddItem: onAddItemToColumn ? () => onAddItemToColumn(key) : void 0,
15125
- style: { opacity: activeColumn === key ? 0 : 1 }
15126
- }, String(key))), AddColumnComponent]
15127
- })
15128
- })
15129
- ]
15130
- });
15131
- }
15132
- //#endregion
15133
14492
  //#region src/components/EntityCollectionView/EntityBoardCard.tsx
15134
14493
  /**
15135
14494
  * Compact card component for displaying an entity in a Kanban board.
@@ -15140,7 +14499,7 @@ function Board({ data, columns: columnsProp, columnLabels, columnColors, classNa
15140
14499
  * layout clean while providing intuitive selection affordance.
15141
14500
  */
15142
14501
  function EntityBoardCardInner({ item, isDragging, isGroupedOver, style, collection, onClick, selected, onSelectionChange, selectionEnabled = false }) {
15143
- const entity = item.entity;
14502
+ const entity = item.data;
15144
14503
  const slotKeys = useCollectionSlotKeys(collection, useAuthController(), useCustomizationController().propertyConfigs);
15145
14504
  const slots = useMemo(() => resolveEntitySlots(entity, collection, slotKeys), [
15146
14505
  entity,
@@ -15755,7 +15114,7 @@ function useBoardDataController({ fullPath, collection, columnProperty, columns,
15755
15114
  //#region src/components/EntityCollectionView/hooks/useKanbanDragAndDrop.ts
15756
15115
  function useKanbanDragAndDrop({ collection, fullPath, columnProperty, orderProperty, dataClient, context, boardDataController, analyticsController }) {
15757
15116
  const handleItemsReorder = useCallback(async (items, moveInfo) => {
15758
- const entity = items.find((item) => item.id === moveInfo?.itemId)?.entity;
15117
+ const entity = items.find((item) => item.id === moveInfo?.itemId)?.data;
15759
15118
  if (!entity || !moveInfo) return;
15760
15119
  analyticsController.onAnalyticsEvent?.("kanban_card_moved", {
15761
15120
  path: fullPath,
@@ -15769,8 +15128,8 @@ function useKanbanDragAndDrop({ collection, fullPath, columnProperty, orderPrope
15769
15128
  if (orderProperty) {
15770
15129
  const targetColumnItems = items;
15771
15130
  const movedIndex = targetColumnItems.findIndex((item) => item.id === moveInfo.itemId);
15772
- const prevKey = movedIndex > 0 ? targetColumnItems[movedIndex - 1].entity.values?.[orderProperty] ?? null : null;
15773
- const nextKey = movedIndex < targetColumnItems.length - 1 ? targetColumnItems[movedIndex + 1].entity.values?.[orderProperty] ?? null : null;
15131
+ const prevKey = movedIndex > 0 ? targetColumnItems[movedIndex - 1].data.values?.[orderProperty] ?? null : null;
15132
+ const nextKey = movedIndex < targetColumnItems.length - 1 ? targetColumnItems[movedIndex + 1].data.values?.[orderProperty] ?? null : null;
15774
15133
  try {
15775
15134
  const a = prevKey;
15776
15135
  let b = nextKey;
@@ -16009,7 +15368,7 @@ function EntityCollectionBoardView({ collection, tableController, fullPath, pare
16009
15368
  const boardItems = useMemo(() => {
16010
15369
  return allEntities.map((entity) => ({
16011
15370
  id: String(entity.id),
16012
- entity
15371
+ data: entity
16013
15372
  }));
16014
15373
  }, [allEntities]);
16015
15374
  const columnLoadingState = useMemo(() => {
@@ -16028,7 +15387,7 @@ function EntityCollectionBoardView({ collection, tableController, fullPath, pare
16028
15387
  const assignColumn = useCallback((item) => {
16029
15388
  const column = entityColumnMap[item.id];
16030
15389
  if (column) return column;
16031
- const value = item.entity.values?.[columnProperty];
15390
+ const value = item.data.values?.[columnProperty];
16032
15391
  if (value && columns.includes(String(value))) return String(value);
16033
15392
  return columns[0] || "";
16034
15393
  }, [
@@ -16122,7 +15481,7 @@ function EntityCollectionBoardView({ collection, tableController, fullPath, pare
16122
15481
  ...props,
16123
15482
  collection: collectionRef.current,
16124
15483
  onClick: stableOnClick,
16125
- selected: isEntitySelectedRef.current(props.item.entity),
15484
+ selected: isEntitySelectedRef.current(props.item.data),
16126
15485
  onSelectionChange: stableOnSelectionChange,
16127
15486
  selectionEnabled: selectionEnabledRef.current
16128
15487
  });
@@ -16224,7 +15583,7 @@ function EntityCollectionBoardView({ collection, tableController, fullPath, pare
16224
15583
  }),
16225
15584
  /* @__PURE__ */ jsx("div", {
16226
15585
  className: "flex-1 overflow-auto no-scrollbar",
16227
- children: /* @__PURE__ */ jsx(Board, {
15586
+ children: /* @__PURE__ */ jsx(KanbanView, {
16228
15587
  data: boardItems,
16229
15588
  columns,
16230
15589
  columnLabels,
@@ -16255,23 +15614,30 @@ function EntityCollectionBoardView({ collection, tableController, fullPath, pare
16255
15614
  /* @__PURE__ */ jsxs(Dialog, {
16256
15615
  open: showBackfillDialog,
16257
15616
  onOpenChange: setShowBackfillDialog,
16258
- children: [/* @__PURE__ */ jsxs(DialogContent, { children: [/* @__PURE__ */ jsx(Typography, {
16259
- variant: "h6",
16260
- className: "mb-4",
16261
- children: t("initialize_kanban_order")
16262
- }), /* @__PURE__ */ jsx(Typography, {
16263
- variant: "body2",
16264
- children: t("initialize_kanban_order_desc")
16265
- })] }), /* @__PURE__ */ jsxs(DialogActions, { children: [/* @__PURE__ */ jsx(Button, {
16266
- variant: "text",
16267
- onClick: () => setShowBackfillDialog(false),
16268
- disabled: backfillLoading,
16269
- children: t("cancel")
16270
- }), /* @__PURE__ */ jsx(Button, {
16271
- onClick: handleBackfill,
16272
- disabled: backfillLoading,
16273
- children: backfillLoading ? /* @__PURE__ */ jsx(CircularProgress, { size: "smallest" }) : t("initialize")
16274
- })] })]
15617
+ children: [
15618
+ /* @__PURE__ */ jsx(DialogTitle, {
15619
+ hidden: true,
15620
+ children: t("initialize_kanban_order")
15621
+ }),
15622
+ /* @__PURE__ */ jsxs(DialogContent, { children: [/* @__PURE__ */ jsx(Typography, {
15623
+ variant: "h6",
15624
+ className: "mb-4",
15625
+ children: t("initialize_kanban_order")
15626
+ }), /* @__PURE__ */ jsx(Typography, {
15627
+ variant: "body2",
15628
+ children: t("initialize_kanban_order_desc")
15629
+ })] }),
15630
+ /* @__PURE__ */ jsxs(DialogActions, { children: [/* @__PURE__ */ jsx(Button, {
15631
+ variant: "text",
15632
+ onClick: () => setShowBackfillDialog(false),
15633
+ disabled: backfillLoading,
15634
+ children: t("cancel")
15635
+ }), /* @__PURE__ */ jsx(Button, {
15636
+ onClick: handleBackfill,
15637
+ disabled: backfillLoading,
15638
+ children: backfillLoading ? /* @__PURE__ */ jsx(CircularProgress, { size: "smallest" }) : t("initialize")
15639
+ })] })
15640
+ ]
16275
15641
  })
16276
15642
  ]
16277
15643
  });
@@ -18675,10 +18041,10 @@ function DrawerNavigationItem({ name, icon, drawerOpen, adminMenuOpen, tooltipsO
18675
18041
  width: "100%",
18676
18042
  transition: drawerOpen ? "width 150ms ease-in" : void 0
18677
18043
  },
18678
- className: ({ isActive }) => cls("rounded-md truncate", "hover:bg-surface-100 dark:hover:bg-surface-800/60 text-surface-700 dark:text-surface-300 hover:text-surface-900 dark:hover:text-white", "flex flex-row items-center", drawerOpen ? "pr-4 h-[30px]" : "h-[30px]", "font-medium text-[13px]", isActive ? "bg-surface-900/[0.06] dark:bg-surface-800/50 text-surface-900 dark:text-white" : ""),
18044
+ className: ({ isActive }) => cls("rounded-lg truncate group/nav", "hover:bg-primary/5 dark:hover:bg-primary/5 text-surface-700 dark:text-surface-300 hover:text-surface-900 dark:hover:text-white", "flex flex-row items-center", drawerOpen ? "pr-4 h-[30px]" : "h-[30px]", "font-medium text-[13px]", isActive ? "bg-primary/8 dark:bg-primary/10 text-primary dark:text-primary [&_div]:text-primary" : ""),
18679
18045
  to: url,
18680
18046
  children: [/* @__PURE__ */ jsx("div", {
18681
- className: "shrink-0 flex items-center justify-center w-[44px] h-[30px] text-surface-500 dark:text-text-secondary-dark [&>svg]:size-4",
18047
+ className: "shrink-0 flex items-center justify-center w-[44px] h-[30px] text-surface-500 dark:text-text-secondary-dark [&>svg]:size-4 group-hover/nav:text-primary transition-colors duration-150",
18682
18048
  children: icon
18683
18049
  }), /* @__PURE__ */ jsx("div", {
18684
18050
  className: cls("text-text-primary dark:text-surface-200", drawerOpen ? "opacity-100" : "opacity-0 hidden", "font-inherit truncate space-x-2"),
@@ -18723,7 +18089,7 @@ function DrawerNavigationGroup({ group, entries, collapsed, onToggleCollapsed, d
18723
18089
  })
18724
18090
  ]
18725
18091
  }), /* @__PURE__ */ jsx("div", {
18726
- className: cls("transition-all duration-200 ease-in-out", "overflow-hidden", !hideHeader && "dark:bg-transparent", !hideHeader ? drawerOpen ? "rounded-lg" : "rounded-lg" : "rounded-lg", !hideHeader && collapsed ? "max-h-0 opacity-0" : "max-h-[2000px] opacity-100"),
18092
+ className: cls("transition-all duration-200 ease-in-out", "overflow-hidden", !hideHeader && "dark:bg-transparent", "rounded-lg", !hideHeader && collapsed ? "max-h-0 opacity-0" : "max-h-[2000px] opacity-100"),
18727
18093
  children: entries.map((entry) => /* @__PURE__ */ jsx(ResolvedDrawerNavigationItem, {
18728
18094
  icon: /* @__PURE__ */ jsx(IconForView, {
18729
18095
  collectionOrView: entry.collection ?? entry.view,
@@ -18813,7 +18179,13 @@ function CMSNavigationContent() {
18813
18179
  const registry = useRebaseRegistry();
18814
18180
  const ResolvedDrawerNavigationGroup = useComponentOverride("Shell.DrawerNavigationGroup", DrawerNavigationGroup);
18815
18181
  const allNavigationEntries = navigationState.topLevelNavigation?.navigationEntries ?? [];
18816
- const filteredEntries = adminModeController.mode === "studio" ? allNavigationEntries.filter((e) => e.type === "view" || e.type === "admin") : allNavigationEntries.filter((e) => e.type !== "view");
18182
+ const studioViewSlugs = useMemo(() => {
18183
+ const slugs = /* @__PURE__ */ new Set();
18184
+ (registry.studioConfig?.devViews ?? []).forEach((v) => slugs.add(v.slug));
18185
+ if (registry.studioConfig && registry.cmsConfig?.collectionEditor) slugs.add("schema");
18186
+ return slugs;
18187
+ }, [registry.studioConfig, registry.cmsConfig?.collectionEditor]);
18188
+ const filteredEntries = adminModeController.mode === "studio" ? allNavigationEntries.filter((e) => e.type === "view" || e.type === "admin") : allNavigationEntries.filter((e) => e.type !== "view" || !studioViewSlugs.has(e.slug));
18817
18189
  const entryGroups = new Set(filteredEntries.map((e) => e.group).filter(Boolean));
18818
18190
  const orderedGroups = navigationState.topLevelNavigation?.groups ?? [];
18819
18191
  const groupsToRender = [...orderedGroups.filter((g) => entryGroups.has(g)), ...[...entryGroups].filter((g) => !orderedGroups.includes(g))];
@@ -19251,11 +18623,12 @@ function DrawerWrapper(props) {
19251
18623
  * is auto-wired as a native feature (slots, provider, Studio view) without
19252
18624
  * needing any external plugin.
19253
18625
  */
19254
- function RebaseCMS({ collections, homePage, entityViews, entityActions, plugins, collectionEditor, navigationGroupMappings }) {
18626
+ function RebaseCMS({ collections, views, homePage, entityViews, entityActions, plugins, collectionEditor, navigationGroupMappings }) {
19255
18627
  const dispatch = useRebaseRegistryDispatch();
19256
18628
  useLayoutEffect(() => {
19257
18629
  dispatch.registerCMS({
19258
18630
  collections,
18631
+ views,
19259
18632
  homePage,
19260
18633
  entityViews,
19261
18634
  entityActions,
@@ -19267,6 +18640,7 @@ function RebaseCMS({ collections, homePage, entityViews, entityActions, plugins,
19267
18640
  }, [
19268
18641
  dispatch,
19269
18642
  collections,
18643
+ views,
19270
18644
  homePage,
19271
18645
  entityViews,
19272
18646
  entityActions,
@@ -19406,7 +18780,7 @@ function useLocalCollectionsConfigController(clientOrUrl, baseCollections = [],
19406
18780
  //#region src/components/RebaseNavigation.tsx
19407
18781
  var EMPTY_PLUGINS = [];
19408
18782
  var EMPTY_COLLECTIONS = [];
19409
- var CollectionsStudioView = lazy(() => import("./CollectionsStudioView-UKouBPOm.js").then((n) => n.n).then((m) => ({ default: m.CollectionsStudioView })));
18783
+ var CollectionsStudioView = lazy(() => import("./RouterCollectionsStudioView-Dqx27GD8.js").then((n) => n.n).then((m) => ({ default: m.RouterCollectionsStudioView })));
19410
18784
  /**
19411
18785
  * Navigation layer — builds and provides all CMS navigation controllers:
19412
18786
  * collection registry, URL controller, navigation state, side entity,
@@ -19439,7 +18813,19 @@ function RebaseNavigation({ children }) {
19439
18813
  }, [collectionEditorConfig]);
19440
18814
  const cmsCollections = registry.cmsConfig?.collections ?? EMPTY_COLLECTIONS;
19441
18815
  const collectionsBuilder = useMemo(() => Array.isArray(cmsCollections) ? () => [...cmsCollections] : cmsCollections, [cmsCollections]);
19442
- const collectionRegistryController = useBuildCollectionRegistryController({ userConfigPersistence });
18816
+ const dataSources = useDataSources();
18817
+ const collectionRegistryController = useBuildCollectionRegistryController({
18818
+ userConfigPersistence,
18819
+ dataSources: dataSources.registry
18820
+ });
18821
+ const defaultData = useData();
18822
+ const getCollectionRef = useRef(collectionRegistryController.getCollection);
18823
+ getCollectionRef.current = collectionRegistryController.getCollection;
18824
+ const routedData = useMemo(() => buildRoutedRebaseData({
18825
+ defaultData,
18826
+ sources: dataSources.sources,
18827
+ resolveKey: (slugOrPath) => resolveDataSource(getCollectionRef.current(slugOrPath), dataSources.registry).key
18828
+ }), [defaultData, dataSources]);
19443
18829
  const urlController = useBuildUrlController({
19444
18830
  basePath: "/",
19445
18831
  baseCollectionPath: "/c",
@@ -19447,7 +18833,14 @@ function RebaseNavigation({ children }) {
19447
18833
  });
19448
18834
  const rebaseClient = useRebaseClient();
19449
18835
  const authController = useAuthController();
19450
- const internalConfigController = useLocalCollectionsConfigController(rebaseClient, useMemo(() => Array.isArray(cmsCollections) ? cmsCollections : [], [cmsCollections]), collectionEditorEnabled ? {
18836
+ const resolvedCollections = useMemo(() => Array.isArray(cmsCollections) ? cmsCollections : [], [cmsCollections]);
18837
+ useEffect(() => {
18838
+ if (process.env.NODE_ENV === "production") return;
18839
+ const { registry, sources } = dataSources;
18840
+ const missing = Object.values(registry).filter((d) => (d.transport === "direct" || d.transport === "custom") && !sources[d.key]).map((d) => d.key);
18841
+ if (missing.length > 0) console.warn(`[Rebase] These data source(s) declare a direct/custom transport but have no client-side driver and will fall back to the default data source: ${missing.map((k) => `"${k}"`).join(", ")}. Provide a \`driver\` for them in \`dataSources\` on <Rebase>.`);
18842
+ }, [dataSources]);
18843
+ const internalConfigController = useLocalCollectionsConfigController(rebaseClient, resolvedCollections, collectionEditorEnabled ? {
19451
18844
  readOnly: collectionEditorOptions?.readOnly ?? process.env.NODE_ENV === "production",
19452
18845
  getAuthToken: collectionEditorOptions?.getAuthToken ?? authController?.getAuthToken
19453
18846
  } : { readOnly: true });
@@ -19464,6 +18857,7 @@ function RebaseNavigation({ children }) {
19464
18857
  return {
19465
18858
  slug: "schema",
19466
18859
  name: "Edit collections",
18860
+ group: "Database",
19467
18861
  icon: "LayoutList",
19468
18862
  nestedRoutes: true,
19469
18863
  view: /* @__PURE__ */ jsx(Suspense, {
@@ -19481,13 +18875,22 @@ function RebaseNavigation({ children }) {
19481
18875
  if (schemaView) return [...base, schemaView];
19482
18876
  return base;
19483
18877
  }, [registry.studioConfig?.devViews, schemaView]);
18878
+ const cmsViews = registry.cmsConfig?.views;
18879
+ const mergedViews = useMemo(() => {
18880
+ if (!cmsViews) return devViews;
18881
+ if (Array.isArray(cmsViews) && cmsViews.length === 0) return devViews;
18882
+ if (typeof cmsViews === "function") return async (params) => {
18883
+ return [...await cmsViews(params) ?? [], ...devViews];
18884
+ };
18885
+ return [...cmsViews, ...devViews];
18886
+ }, [cmsViews, devViews]);
19484
18887
  const navigationStateController = useBuildNavigationStateController({
19485
18888
  plugins: registry.cmsConfig?.plugins ?? EMPTY_PLUGINS,
19486
18889
  collections: collectionsBuilder,
19487
- views: devViews,
18890
+ views: mergedViews,
19488
18891
  navigationGroupMappings: registry.cmsConfig?.navigationGroupMappings,
19489
18892
  authController: context.authController,
19490
- data: context.data,
18893
+ data: routedData,
19491
18894
  collectionRegistryController,
19492
18895
  urlController,
19493
18896
  adminMode: adminModeController?.mode
@@ -19507,22 +18910,25 @@ function RebaseNavigation({ children }) {
19507
18910
  registry.cmsConfig?.entityViews,
19508
18911
  registry.cmsConfig?.entityActions
19509
18912
  ]);
19510
- const navigationContent = /* @__PURE__ */ jsx(CustomizationControllerContext.Provider, {
19511
- value: enrichedCustomizationController,
19512
- children: /* @__PURE__ */ jsx(StudioBridgeRegistryProvider, { children: /* @__PURE__ */ jsx(CollectionRegistryContext.Provider, {
19513
- value: collectionRegistryController,
19514
- children: /* @__PURE__ */ jsx(UrlContext.Provider, {
19515
- value: urlController,
19516
- children: /* @__PURE__ */ jsx(NavigationStateContext.Provider, {
19517
- value: navigationStateController,
19518
- children: /* @__PURE__ */ jsxs(SideEntityProvider, { children: [/* @__PURE__ */ jsx(BridgeAutoRegistrar, {
19519
- collectionRegistryController,
19520
- urlController,
19521
- navigationStateController
19522
- }), children] })
18913
+ const navigationContent = /* @__PURE__ */ jsx(RebaseDataContext.Provider, {
18914
+ value: routedData,
18915
+ children: /* @__PURE__ */ jsx(CustomizationControllerContext.Provider, {
18916
+ value: enrichedCustomizationController,
18917
+ children: /* @__PURE__ */ jsx(StudioBridgeRegistryProvider, { children: /* @__PURE__ */ jsx(CollectionRegistryContext.Provider, {
18918
+ value: collectionRegistryController,
18919
+ children: /* @__PURE__ */ jsx(UrlContext.Provider, {
18920
+ value: urlController,
18921
+ children: /* @__PURE__ */ jsx(NavigationStateContext.Provider, {
18922
+ value: navigationStateController,
18923
+ children: /* @__PURE__ */ jsxs(SideEntityProvider, { children: [/* @__PURE__ */ jsx(BridgeAutoRegistrar, {
18924
+ collectionRegistryController,
18925
+ urlController,
18926
+ navigationStateController
18927
+ }), children] })
18928
+ })
19523
18929
  })
19524
- })
19525
- }) })
18930
+ }) })
18931
+ })
19526
18932
  });
19527
18933
  if (collectionEditorEnabled) return /* @__PURE__ */ jsx(ConfigControllerProvider, {
19528
18934
  collectionConfigController: internalConfigController,
@@ -19635,7 +19041,7 @@ function NavigationGroup({ children, group, minimised, isPreview, isPotentialCar
19635
19041
  variant: isPreview ? "body2" : "caption",
19636
19042
  component: "h2",
19637
19043
  color: "secondary",
19638
- className: cls("px-4 py-1 rounded", "font-semibold text-[11px] uppercase tracking-wider text-surface-400 dark:text-surface-400"),
19044
+ className: cls("px-4 py-1 rounded", "font-medium text-[10px] uppercase tracking-[0.08em] text-primary/50 dark:text-primary/70"),
19639
19045
  children: currentGroupName
19640
19046
  }), !isPreview && onEditGroup && !dndDisabled && /* @__PURE__ */ jsx(IconButton, {
19641
19047
  size: "smallest",
@@ -19698,7 +19104,7 @@ function NavigationGroup({ children, group, minimised, isPreview, isPotentialCar
19698
19104
  //#region src/components/HomePage/NavigationCard.tsx
19699
19105
  var NavigationCard = React.memo(function NavigationCard({ name, description, icon, actions, additionalContent, onClick, shrink }) {
19700
19106
  return /* @__PURE__ */ jsx(Card, {
19701
- className: cls("group h-full p-4 cursor-pointer transition-all duration-150 ease-in-out", "border-surface-200 dark:border-surface-700/40", "hover:shadow-md hover:shadow-black/[0.04]", "hover:border-surface-300 dark:hover:border-primary/20", shrink && "w-full max-w-full min-h-0 scale-75"),
19107
+ className: cls("group h-full p-4 cursor-pointer transition-colors duration-150 ease-in-out", "hover:bg-primary/5 dark:hover:bg-primary/5", shrink && "w-full max-w-full min-h-0 scale-75"),
19702
19108
  onClick: () => {
19703
19109
  onClick?.();
19704
19110
  },
@@ -19708,9 +19114,9 @@ var NavigationCard = React.memo(function NavigationCard({ name, description, ico
19708
19114
  /* @__PURE__ */ jsxs("div", {
19709
19115
  className: "flex items-center w-full justify-between mb-1",
19710
19116
  children: [/* @__PURE__ */ jsxs("div", {
19711
- className: "flex items-center gap-4",
19117
+ className: "flex items-center gap-3",
19712
19118
  children: [/* @__PURE__ */ jsx("div", {
19713
- className: "flex items-center justify-center w-6 h-6 rounded-lg bg-primary/8 dark:bg-primary/10 text-primary/70 dark:text-primary/60 transition-colors duration-200 group-hover:bg-primary/12 dark:group-hover:bg-primary/15 group-hover:text-primary dark:group-hover:text-primary/80",
19119
+ className: "flex items-center justify-center w-5 h-5 text-surface-400 dark:text-surface-500 transition-colors duration-150 group-hover:text-primary dark:group-hover:text-primary",
19714
19120
  children: icon
19715
19121
  }), /* @__PURE__ */ jsx(Typography, {
19716
19122
  variant: "subtitle1",
@@ -19727,7 +19133,7 @@ var NavigationCard = React.memo(function NavigationCard({ name, description, ico
19727
19133
  })]
19728
19134
  }),
19729
19135
  /* @__PURE__ */ jsx("div", {
19730
- className: "grow pl-[40px]",
19136
+ className: "pl-8",
19731
19137
  children: description && /* @__PURE__ */ jsx(Typography, {
19732
19138
  variant: "caption",
19733
19139
  color: "secondary",
@@ -19739,19 +19145,10 @@ var NavigationCard = React.memo(function NavigationCard({ name, description, ico
19739
19145
  })
19740
19146
  }),
19741
19147
  additionalContent && /* @__PURE__ */ jsx("div", {
19742
- className: "pl-[40px] pointer-events-none",
19148
+ className: "pl-8 pointer-events-none",
19743
19149
  children: additionalContent
19744
19150
  }),
19745
- /* @__PURE__ */ jsx("div", {
19746
- className: "self-end mt-1",
19747
- children: /* @__PURE__ */ jsx("div", {
19748
- className: "transition-transform duration-200 group-hover:translate-x-0.5",
19749
- children: /* @__PURE__ */ jsx(ArrowRightIcon, {
19750
- className: "text-primary",
19751
- size: iconSize.small
19752
- })
19753
- })
19754
- })
19151
+ /* @__PURE__ */ jsx("div", { className: "grow" })
19755
19152
  ]
19756
19153
  })
19757
19154
  });
@@ -20316,10 +19713,20 @@ function ContentHomePage({ additionalActions, additionalChildrenStart, additiona
20316
19713
  breadcrumbs.set({ breadcrumbs: [] });
20317
19714
  }, [breadcrumbs.set]);
20318
19715
  const { allowDragAndDrop = false, navigationEntries: unFilteredNavigationEntries = [], groups: unFilteredGroupOrder = [], onNavigationEntriesUpdate = () => {} } = navigationStateController.topLevelNavigation || {};
19716
+ const studioViewSlugs = useMemo(() => {
19717
+ const slugs = /* @__PURE__ */ new Set();
19718
+ (registry.studioConfig?.devViews ?? []).forEach((v) => slugs.add(v.slug));
19719
+ if (registry.studioConfig && registry.cmsConfig?.collectionEditor) slugs.add("schema");
19720
+ return slugs;
19721
+ }, [registry.studioConfig, registry.cmsConfig?.collectionEditor]);
20319
19722
  const rawNavigationEntries = useMemo(() => {
20320
19723
  if (adminModeController.mode === "studio") return unFilteredNavigationEntries.filter((e) => e.type === "view" || e.type === "admin");
20321
- return unFilteredNavigationEntries.filter((e) => e.type !== "view");
20322
- }, [unFilteredNavigationEntries, adminModeController.mode]);
19724
+ return unFilteredNavigationEntries.filter((e) => e.type !== "view" || !studioViewSlugs.has(e.slug));
19725
+ }, [
19726
+ unFilteredNavigationEntries,
19727
+ adminModeController.mode,
19728
+ studioViewSlugs
19729
+ ]);
20323
19730
  const groupOrderFromNavController = useMemo(() => {
20324
19731
  const entryGroups = new Set(rawNavigationEntries.map((e) => e.group).filter(Boolean));
20325
19732
  return unFilteredGroupOrder.filter((g) => entryGroups.has(g));
@@ -20849,18 +20256,24 @@ function EntityFullScreenRoute({ pathname, navigationEntries, isNew, isCopy }) {
20849
20256
  function CustomViewRoute({ view }) {
20850
20257
  const breadcrumbs = useBreadcrumbsController();
20851
20258
  const urlController = useUrlController();
20259
+ const breadcrumbsRef = useRef(breadcrumbs);
20260
+ breadcrumbsRef.current = breadcrumbs;
20852
20261
  useEffect(() => {
20853
- breadcrumbs.set({ breadcrumbs: [{
20262
+ breadcrumbsRef.current.set({ breadcrumbs: [{
20854
20263
  title: view.name,
20855
20264
  url: urlController.buildAppUrlPath(view.slug)
20856
20265
  }] });
20857
20266
  }, [view.slug, urlController]);
20858
- return view.view;
20267
+ if (typeof view.view === "function") {
20268
+ const ViewComponent = view.view;
20269
+ return /* @__PURE__ */ jsx(ViewComponent, {});
20270
+ }
20271
+ return /* @__PURE__ */ jsx(Fragment, { children: view.view });
20859
20272
  }
20860
20273
  //#endregion
20861
20274
  //#region src/components/CollectionEditorDialogs.tsx
20862
- var CollectionEditorDialog = lazy(() => import("./CollectionEditorDialog-jA-PLh6N.js").then((n) => n.r).then((m) => ({ default: m.CollectionEditorDialog })));
20863
- var PropertyFormDialog = lazy(() => import("./PropertyEditView-DEUddmg_.js").then((n) => n.t).then((m) => ({ default: m.PropertyFormDialog })));
20275
+ var CollectionEditorDialog = lazy(() => import("./CollectionEditorDialog-iGgS4rLX.js").then((n) => n.r).then((m) => ({ default: m.CollectionEditorDialog })));
20276
+ var PropertyFormDialog = lazy(() => import("./PropertyEditView-C9yhLyja.js").then((n) => n.t).then((m) => ({ default: m.PropertyFormDialog })));
20864
20277
  /**
20865
20278
  * Renders the CollectionEditorDialog and PropertyFormDialog inside
20866
20279
  * the RebaseShell tree where CMS-internal contexts
@@ -22184,7 +21597,7 @@ function LocalChangesMenu({ cachedData, formex, onClearLocalChanges, cacheKey, p
22184
21597
  })] });
22185
21598
  }
22186
21599
  //#endregion
22187
- //#region src/form/EntityForm.tsx
21600
+ //#region src/form/form_utils.ts
22188
21601
  function extractTouchedValues(values, touched) {
22189
21602
  let acc = {};
22190
21603
  if (!touched || typeof touched !== "object") return acc;
@@ -22200,7 +21613,7 @@ function extractTouchedValues(values, touched) {
22200
21613
  * from falsely triggering the unsaved local changes indicator.
22201
21614
  */
22202
21615
  /**
22203
- * CheckIcon if a value is semantically empty (null, undefined, or empty string).
21616
+ * Check if a value is semantically empty (null, undefined, or empty string).
22204
21617
  */
22205
21618
  function isSemanticEmpty(v) {
22206
21619
  return v === null || v === void 0 || v === "";
@@ -22244,23 +21657,51 @@ function getChanges(source, comparison) {
22244
21657
  }
22245
21658
  return changes;
22246
21659
  }
22247
- function EntityForm({ path, entityId: entityIdProp, collection, onValuesModified, onIdChange, onSaved, entity, initialDirtyValues, onFormContextReady, forceActionsAtTheBottom, initialStatus, className, onStatusChange, onEntityChange, openEntityMode = "full_screen", formex: formexProp, disabled: disabledProp, Builder, EntityFormActionsComponent = EntityFormActions, showDefaultActions = true, showEntityPath = true, navigateBack: navigateBackProp, children }) {
22248
- const { t } = useTranslation();
22249
- const sideEntityController = useSideEntityController();
22250
- const collectionRegistryController = useCollectionRegistryController();
22251
- const navigateBack = useCallback(() => {
22252
- if (navigateBackProp) {
22253
- navigateBackProp();
22254
- return;
21660
+ function getInitialEntityValues(authController, collection, path, status, entity, propertyConfigs) {
21661
+ const properties = collection.properties;
21662
+ if ((status === "existing" || status === "copy") && entity) {
21663
+ let values;
21664
+ if (!collection.alwaysApplyDefaultValues) values = entity.values ?? getDefaultValuesFor(properties);
21665
+ else values = mergeDeep(getDefaultValuesFor(properties), entity.values ?? {});
21666
+ if (status === "copy") {
21667
+ const result = { ...values };
21668
+ for (const [key, property] of Object.entries(properties)) if (property && "isId" in property && property.isId) delete result[key];
21669
+ return result;
22255
21670
  }
22256
- if (openEntityMode === "side_panel" || openEntityMode === "dialog") sideEntityController.close();
22257
- else window.history.back();
22258
- }, [
22259
- navigateBackProp,
22260
- openEntityMode,
22261
- sideEntityController
22262
- ]);
22263
- const authController = useAuthController();
21671
+ return values;
21672
+ } else if (status === "new") return getDefaultValuesFor(properties);
21673
+ else {
21674
+ console.error({
21675
+ status,
21676
+ entity
21677
+ });
21678
+ throw new Error("Form has not been initialised with the correct parameters");
21679
+ }
21680
+ }
21681
+ function zodToFormErrors(zodError) {
21682
+ let errors = {};
21683
+ for (const issue of zodError.issues) {
21684
+ const path = issue.path.join(".");
21685
+ if (path && !getIn(errors, path)) errors = setIn(errors, path, issue.message);
21686
+ }
21687
+ return errors;
21688
+ }
21689
+ //#endregion
21690
+ //#region src/form/EntityForm.tsx
21691
+ /**
21692
+ * Headless entity form component.
21693
+ *
21694
+ * Renders a form for an entity collection without any CMS or backend dependencies.
21695
+ * All backend concerns (save, caching, analytics, plugin slots) are provided via
21696
+ * callback props. For CMS-connected usage, use {@link EntityFormBinding} instead.
21697
+ *
21698
+ * @group Components
21699
+ */
21700
+ function EntityForm({ path, entityId: entityIdProp, collection, onValuesModified, onIdChange, onSaved, entity, initialDirtyValues, onFormContextReady, forceActionsAtTheBottom, initialStatus, className, onStatusChange, onEntityChange, openEntityMode = "full_screen", formex: formexProp, disabled: disabledProp, Builder, EntityFormActionsComponent = EntityFormActions, showDefaultActions = true, showEntityPath = true, navigateBack: navigateBackProp, children, onSubmit: onSubmitProp, onValuesChangeDeferred: onValuesChangeDeferredProp, onReset: onResetProp, uniqueFieldValidator: uniqueFieldValidatorProp, beforeFields, afterFields, pluginActions: pluginActionsProp, computedInitialValues, hasLocalChanges: hasLocalChangesProp, localChangesData, manualApplyLocalChanges, localChangesCacheKey, onClearLocalChanges }) {
21701
+ const customizationController = useCustomizationController();
21702
+ const navigateBack = useCallback(() => {
21703
+ if (navigateBackProp) navigateBackProp();
21704
+ }, [navigateBackProp]);
22264
21705
  const [status, setStatus] = useState(initialStatus);
22265
21706
  const updateStatus = (status) => {
22266
21707
  setStatus(status);
@@ -22268,18 +21709,11 @@ function EntityForm({ path, entityId: entityIdProp, collection, onValuesModified
22268
21709
  };
22269
21710
  const [valuesToBeSaved, setValuesToBeSaved] = useState(void 0);
22270
21711
  useDebouncedCallback(valuesToBeSaved, () => {
22271
- if (valuesToBeSaved) saveEntity({
22272
- entityId: entityIdProp,
22273
- collection,
22274
- path,
22275
- values: valuesToBeSaved
22276
- });
21712
+ if (valuesToBeSaved && onSubmitProp) {
21713
+ setIsSavingAutoSave(true);
21714
+ Promise.resolve(onSubmitProp(valuesToBeSaved, formex)).finally(() => setIsSavingAutoSave(false));
21715
+ }
22277
21716
  }, false, 2e3);
22278
- const dataClient = useData();
22279
- const snackbarController = useSnackbarController();
22280
- const customizationController = useCustomizationController();
22281
- const context = useCMSContext();
22282
- const analyticsController = useAnalyticsController();
22283
21717
  const [underlyingChanges] = useState({});
22284
21718
  const initialEntityId = useMemo(() => {
22285
21719
  if (status === "new" || status === "copy") return;
@@ -22288,58 +21722,45 @@ function EntityForm({ path, entityId: entityIdProp, collection, onValuesModified
22288
21722
  const [entityId, setEntityId] = useState(initialEntityId);
22289
21723
  const [entityIdError, setEntityIdError] = useState(false);
22290
21724
  const [savingError, setSavingError] = useState();
21725
+ const [isSavingAutoSave, setIsSavingAutoSave] = useState(false);
21726
+ const [discardDialogOpen, setDiscardDialogOpen] = useState(false);
22291
21727
  const autoSave = collection.formAutoSave;
22292
- const baseInitialValues = useMemo(() => getInitialEntityValues(authController, collection, path, status, entity, customizationController.propertyConfigs), [
22293
- authController,
22294
- collection,
22295
- path,
21728
+ const baseInitialValues = useMemo(() => {
21729
+ if (computedInitialValues !== void 0) return computedInitialValues;
21730
+ if ((status === "existing" || status === "copy") && entity) return entity.values ?? getDefaultValuesFor(collection.properties);
21731
+ return getDefaultValuesFor(collection.properties);
21732
+ }, [
21733
+ computedInitialValues,
21734
+ collection.properties,
22296
21735
  status,
22297
- entity,
22298
- customizationController.propertyConfigs
21736
+ entity
22299
21737
  ]);
22300
- const localChangesDataRaw = useMemo(() => entityId ? getEntityFromCache(path + "/" + entityId) : getEntityFromCache(path + "#new"), [entityId, path]);
22301
21738
  const [localChangesCleared, setLocalChangesCleared] = useState(false);
22302
- const localChangesBackup = getLocalChangesBackup(collection);
22303
- const autoApplyLocalChanges = localChangesBackup === "auto_apply";
22304
- const manualApplyLocalChanges = localChangesBackup === "manual_apply";
21739
+ const hasLocalChanges = hasLocalChangesProp !== void 0 ? hasLocalChangesProp && !localChangesCleared : false;
22305
21740
  const onSubmit = (values, formexController) => {
22306
21741
  setSavingError(void 0);
22307
21742
  setEntityIdError(false);
22308
21743
  if (status === "existing") {
22309
21744
  if (!entity?.id) throw Error("Form misconfiguration when saving, no id for existing entity");
22310
21745
  } else if (status !== "new" && status !== "copy") throw Error("New FormType added, check EntityForm");
22311
- return save(values)?.then((savedEntity) => {
22312
- if (savedEntity) {
22313
- formexController.resetForm({
22314
- values: savedEntity.values || values,
22315
- submitCount: 0,
22316
- touched: {}
22317
- });
22318
- if (!autoSave) snackbarController.open({
22319
- type: "success",
22320
- message: `${collection.singularName ?? collection.name}: ${t("saved_correctly")}`
22321
- });
22322
- }
21746
+ if (!onSubmitProp) {
21747
+ console.warn("EntityForm: no onSubmit callback provided. Form submission has no effect.");
21748
+ formexController.setSubmitting(false);
21749
+ return;
21750
+ }
21751
+ return Promise.resolve(save(values))?.then((savedEntity) => {
21752
+ if (savedEntity) formexController.resetForm({
21753
+ values: savedEntity.values || values,
21754
+ submitCount: 0,
21755
+ touched: {}
21756
+ });
22323
21757
  }).finally(() => {
22324
21758
  formexController.setSubmitting(false);
22325
21759
  });
22326
21760
  };
22327
21761
  const [initialValues, initialDirty] = useMemo(() => {
22328
- const initialValuesWithLocalChanges = autoApplyLocalChanges && localChangesDataRaw ? mergeDeep(baseInitialValues, localChangesDataRaw) : baseInitialValues;
22329
- return [initialDirtyValues ? mergeDeep(initialValuesWithLocalChanges, initialDirtyValues) : initialValuesWithLocalChanges, Boolean(initialDirtyValues) && initialDirtyValues && Object.keys(initialDirtyValues).length > 0];
22330
- }, [
22331
- autoApplyLocalChanges,
22332
- localChangesDataRaw,
22333
- baseInitialValues,
22334
- initialDirtyValues
22335
- ]);
22336
- const localChangesData = useMemo(() => {
22337
- if (!localChangesDataRaw) return;
22338
- const cleaned = removeEmptyContainers(getChanges(localChangesDataRaw, initialValues));
22339
- if (cleaned && typeof cleaned === "object" && Object.keys(cleaned).length === 0) return;
22340
- return cleaned;
22341
- }, [localChangesDataRaw, initialValues]);
22342
- const hasLocalChanges = !localChangesCleared && !!localChangesData && Object.keys(localChangesData).length > 0;
21762
+ return [initialDirtyValues ? mergeDeep(baseInitialValues, initialDirtyValues) : baseInitialValues, Boolean(initialDirtyValues) && initialDirtyValues && Object.keys(initialDirtyValues).length > 0];
21763
+ }, [baseInitialValues, initialDirtyValues]);
22343
21764
  const internalFormex = useCreateFormex({
22344
21765
  initialValues,
22345
21766
  initialDirty,
@@ -22350,17 +21771,10 @@ function EntityForm({ path, entityId: entityIdProp, collection, onValuesModified
22350
21771
  }), {}) : {},
22351
21772
  onSubmit,
22352
21773
  onReset: () => {
22353
- clearDirtyCache();
21774
+ onResetProp?.();
22354
21775
  onValuesModified?.(false, initialValues);
22355
21776
  },
22356
- onValuesChangeDeferred: (values, controller) => {
22357
- const key = status === "new" || status === "copy" ? path + "#new" : path + "/" + entityId;
22358
- if (controller.dirty) {
22359
- const touchedValues = removeEmptyContainers(extractTouchedValues(values, controller.touched));
22360
- if (touchedValues && Object.keys(touchedValues).length > 0) saveEntityToCache(key, touchedValues);
22361
- else removeEntityFromCache(key);
22362
- }
22363
- },
21777
+ onValuesChangeDeferred: onValuesChangeDeferredProp,
22364
21778
  validation: async (values) => {
22365
21779
  if (!validationSchema) return {};
22366
21780
  const result = await validationSchema.safeParseAsync(values);
@@ -22373,7 +21787,10 @@ function EntityForm({ path, entityId: entityIdProp, collection, onValuesModified
22373
21787
  const handleKeyDown = (e) => {
22374
21788
  const isUndo = (e.metaKey || e.ctrlKey) && !e.shiftKey && e.key.toLowerCase() === "z";
22375
21789
  const isRedo = (e.metaKey || e.ctrlKey) && e.shiftKey && e.key.toLowerCase() === "z" || (e.metaKey || e.ctrlKey) && !e.shiftKey && e.key.toLowerCase() === "y";
22376
- if (isUndo && formex.canUndo) {
21790
+ if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === "s" && !disabled && formex.dirty) {
21791
+ e.preventDefault();
21792
+ formex.handleSubmit();
21793
+ } else if (isUndo && formex.canUndo) {
22377
21794
  e.preventDefault();
22378
21795
  formex.undo();
22379
21796
  } else if (isRedo && formex.canRedo) {
@@ -22384,31 +21801,8 @@ function EntityForm({ path, entityId: entityIdProp, collection, onValuesModified
22384
21801
  window.addEventListener("keydown", handleKeyDown);
22385
21802
  return () => window.removeEventListener("keydown", handleKeyDown);
22386
21803
  }, [formex]);
22387
- useCallback((e) => {
22388
- snackbarController.open({
22389
- type: "error",
22390
- message: `${t("error_before_saving")}: ${e?.message}`
22391
- });
22392
- console.error(e);
22393
- }, [snackbarController]);
22394
- useCallback((e) => {
22395
- snackbarController.open({
22396
- type: "error",
22397
- message: `${t("error_after_saving")}: ${e?.message}`
22398
- });
22399
- console.error(e);
22400
- }, [snackbarController]);
22401
- function clearDirtyCache() {
22402
- if (status === "new" || status === "copy") {
22403
- removeEntityFromMemoryCache(path + "#new");
22404
- removeEntityFromCache(path + "#new");
22405
- } else {
22406
- removeEntityFromMemoryCache(path + "/" + entityId);
22407
- removeEntityFromCache(path + "/" + entityId);
22408
- }
22409
- }
22410
21804
  const afterSave = (updatedEntity) => {
22411
- clearDirtyCache();
21805
+ onResetProp?.();
22412
21806
  onValuesModified?.(false, updatedEntity.values);
22413
21807
  onEntityChange?.(updatedEntity);
22414
21808
  updateStatus("existing");
@@ -22421,62 +21815,19 @@ function EntityForm({ path, entityId: entityIdProp, collection, onValuesModified
22421
21815
  collection
22422
21816
  });
22423
21817
  };
22424
- const afterSaveError = useCallback((e) => {
22425
- snackbarController.open({
22426
- type: "error",
22427
- title: t("error_saving_entity"),
22428
- message: e?.message
22429
- });
22430
- console.error("Error saving entity", path, entityId, e);
22431
- console.error(e);
22432
- }, [
22433
- entityId,
22434
- path,
22435
- snackbarController
22436
- ]);
22437
- const saveEntity = ({ values, previousValues, entityId, collection, path }) => {
22438
- return saveEntityWithCallbacks({
22439
- path,
22440
- entityId,
22441
- values,
22442
- previousValues,
22443
- collection,
22444
- status,
22445
- data: dataClient,
22446
- context,
22447
- afterSave,
22448
- afterSaveError
22449
- });
22450
- };
22451
- const onSaveEntityRequest = async ({ collection, path, entityId, values, previousValues, autoSave }) => {
22452
- if (!status) return;
22453
- if (autoSave) {
22454
- setValuesToBeSaved(values);
22455
- return Promise.resolve();
22456
- } else return saveEntity({
22457
- collection,
22458
- path,
22459
- entityId,
22460
- values,
22461
- previousValues
22462
- });
22463
- };
22464
21818
  const lastSavedValues = useRef(entity?.values);
22465
21819
  const save = async (values) => {
22466
21820
  const valuesToSave = status === "existing" ? getChanges(values, entity?.values || {}) : values;
22467
21821
  if (status === "existing" && Object.keys(valuesToSave).length === 0 && entity) return Promise.resolve(entity);
22468
21822
  lastSavedValues.current = values;
22469
- return onSaveEntityRequest({
22470
- collection,
22471
- path,
22472
- entityId,
22473
- values: valuesToSave,
22474
- previousValues: entity?.values,
22475
- autoSave: autoSave ?? false
22476
- }).then((savedEntity) => {
22477
- const eventName = status === "new" ? "new_entity_saved" : status === "copy" ? "entity_copied" : status === "existing" ? "entity_edited" : "unmapped_event";
22478
- analyticsController.onAnalyticsEvent?.(eventName, { path });
22479
- return savedEntity;
21823
+ if (!onSubmitProp) return;
21824
+ if (autoSave ?? false) {
21825
+ setValuesToBeSaved(values);
21826
+ return Promise.resolve();
21827
+ }
21828
+ return Promise.resolve(onSubmitProp(valuesToSave, formex)).then((result) => {
21829
+ if (result) afterSave(result);
21830
+ return result;
22480
21831
  }).catch((e) => {
22481
21832
  console.error(e);
22482
21833
  setSavingError(e);
@@ -22507,44 +21858,18 @@ function EntityForm({ path, entityId: entityIdProp, collection, onValuesModified
22507
21858
  path
22508
21859
  ]);
22509
21860
  const actionsDisabled = disabled || formex.isSubmitting || status === "existing" && !formex.dirty || Boolean(disabledProp);
22510
- const formActionProps = {
22511
- entityId,
22512
- parentCollectionSlugs: collectionRegistryController.getParentCollectionSlugs(path),
22513
- parentEntityIds: collectionRegistryController.getParentEntityIds(path),
22514
- path,
22515
- status,
22516
- collection,
22517
- context,
22518
- formContext,
22519
- openEntityMode,
22520
- disabled: actionsDisabled
22521
- };
22522
- const pluginFormActions = useSlot("form.actions", formActionProps);
22523
- const pluginFormBefore = useSlot("form.before", formActionProps);
22524
- const pluginFormAfter = useSlot("form.after", formActionProps);
22525
21861
  const titlePropertyKey = getEntityTitlePropertyKey(collection, customizationController.propertyConfigs);
22526
21862
  const rawTitle = formex.values && titlePropertyKey ? getValueInPath(formex.values, titlePropertyKey) : void 0;
22527
21863
  const title = rawTitle !== void 0 && rawTitle !== null ? resolveTitleToString(rawTitle) : collection.singularName ?? collection.name;
21864
+ const modified = formex.dirty;
22528
21865
  useEffect(() => {
22529
21866
  if (!autoSave) onValuesModified?.(modified, formex.values);
22530
21867
  }, [formex.dirty]);
22531
- const modified = formex.dirty;
22532
- const uniqueFieldValidator = useCallback(async ({ name, value }) => {
22533
- try {
22534
- const { data } = await dataClient.collection(path).find({
22535
- where: { [name]: `eq.${value}` },
22536
- limit: 2
22537
- });
22538
- return (entityId ? data.filter((e) => e.id !== entityId) : data).length === 0;
22539
- } catch (e) {
22540
- console.error("Error checking unique field", e);
22541
- return true;
22542
- }
22543
- }, [
22544
- dataClient,
22545
- path,
22546
- entityId
22547
- ]);
21868
+ const defaultUniqueFieldValidator = useCallback(async () => true, []);
21869
+ const uniqueFieldValidator = useMemo(() => {
21870
+ if (uniqueFieldValidatorProp) return uniqueFieldValidatorProp;
21871
+ return defaultUniqueFieldValidator;
21872
+ }, [uniqueFieldValidatorProp, defaultUniqueFieldValidator]);
22548
21873
  const validationSchema = useMemo(() => getEntitySchema(entityId, collection.properties, uniqueFieldValidator), [
22549
21874
  entityId,
22550
21875
  collection.properties,
@@ -22576,6 +21901,8 @@ function EntityForm({ path, entityId: entityIdProp, collection, onValuesModified
22576
21901
  modifiedValues: formex.values,
22577
21902
  formContext
22578
21903
  });
21904
+ const isNewEntity = status === "new" || status === "copy";
21905
+ let firstFocusableIndex = -1;
22579
21906
  return /* @__PURE__ */ jsx(FormLayout, { children: formFieldKeys.map((key) => {
22580
21907
  const property = collection.properties?.[key];
22581
21908
  if (property) {
@@ -22584,9 +21911,12 @@ function EntityForm({ path, entityId: entityIdProp, collection, onValuesModified
22584
21911
  const isIdAndAuto = (property.type === "string" || property.type === "number") && "isId" in property && typeof property.isId === "string" && property.isId !== "manual";
22585
21912
  const disabled = disabledProp || !autoSave && formex.isSubmitting || isReadOnly(property) || Boolean(property.ui?.disabled) || !isNew && "isId" in property && Boolean(property.isId) || isNew && isIdAndAuto;
22586
21913
  if (isHidden(property)) return null;
21914
+ const widthPercentage = property.ui?.widthPercentage ?? 100;
21915
+ const shouldAutoFocus = isNewEntity && !disabled && firstFocusableIndex === -1;
21916
+ if (shouldAutoFocus) firstFocusableIndex = 0;
22587
21917
  return /* @__PURE__ */ jsx(FormEntry, {
22588
21918
  propertyKey: key,
22589
- widthPercentage: property.ui?.widthPercentage ?? 100,
21919
+ widthPercentage,
22590
21920
  children: /* @__PURE__ */ jsx(PropertyFieldBinding, {
22591
21921
  propertyKey: key,
22592
21922
  disabled,
@@ -22596,22 +21926,23 @@ function EntityForm({ path, entityId: entityIdProp, collection, onValuesModified
22596
21926
  context: formContext,
22597
21927
  partOfArray: false,
22598
21928
  minimalistView: false,
22599
- autoFocus: false
21929
+ autoFocus: shouldAutoFocus
22600
21930
  })
22601
21931
  }, `field_${key}`);
22602
21932
  }
22603
21933
  const additionalField = collection.additionalFields?.find((f) => f.key === key);
22604
21934
  if (additionalField && entity) {
22605
- const Builder = additionalField.Builder;
22606
- if (!Builder && !additionalField.value) throw new Error("When using additional fields you need to provide a Builder or a value");
22607
- const child = Builder ? /* @__PURE__ */ jsx(Builder, {
21935
+ const AdditionalFieldBuilder = additionalField.Builder;
21936
+ if (!AdditionalFieldBuilder && !additionalField.value) throw new Error("When using additional fields you need to provide a Builder or a value");
21937
+ const additionalFieldContext = formContext;
21938
+ const child = AdditionalFieldBuilder ? /* @__PURE__ */ jsx(AdditionalFieldBuilder, {
22608
21939
  entity,
22609
- context
21940
+ context: additionalFieldContext
22610
21941
  }) : /* @__PURE__ */ jsx("div", {
22611
21942
  className: "w-full",
22612
21943
  children: additionalField.value?.({
22613
21944
  entity,
22614
- context
21945
+ context: additionalFieldContext
22615
21946
  })?.toString()
22616
21947
  });
22617
21948
  return /* @__PURE__ */ jsxs("div", {
@@ -22632,8 +21963,9 @@ function EntityForm({ path, entityId: entityIdProp, collection, onValuesModified
22632
21963
  }).filter(Boolean) });
22633
21964
  };
22634
21965
  const formRef = useRef(null);
21966
+ const hasFormErrors = Object.keys(formex.errors).length > 0 && formex.submitCount > 0;
22635
21967
  const formView = /* @__PURE__ */ jsx(ErrorBoundary, { children: /* @__PURE__ */ jsxs(Fragment, { children: [
22636
- pluginFormBefore,
21968
+ beforeFields,
22637
21969
  !Builder && /* @__PURE__ */ jsxs("div", {
22638
21970
  className: "w-full flex flex-col items-start my-4 lg:my-6",
22639
21971
  children: [
@@ -22646,7 +21978,7 @@ function EntityForm({ path, entityId: entityIdProp, collection, onValuesModified
22646
21978
  color: "warning",
22647
21979
  size: "small",
22648
21980
  outerClassName: "w-full mb-4 text-xs",
22649
- children: t("entity_does_not_exist")
21981
+ children: "This entity does not exist yet"
22650
21982
  }),
22651
21983
  showEntityPath && /* @__PURE__ */ jsx(Alert, {
22652
21984
  color: "base",
@@ -22667,15 +21999,43 @@ function EntityForm({ path, entityId: entityIdProp, collection, onValuesModified
22667
21999
  initialEntityId && !entity && initialStatus !== "new" && /* @__PURE__ */ jsx(Alert, {
22668
22000
  color: "info",
22669
22001
  size: "small",
22670
- children: t("entity_does_not_exist")
22002
+ children: "This entity does not exist yet"
22003
+ }),
22004
+ hasFormErrors && /* @__PURE__ */ jsx(Alert, {
22005
+ color: "error",
22006
+ size: "small",
22007
+ outerClassName: "w-full mt-2",
22008
+ children: "Please fix the highlighted errors before saving."
22671
22009
  }),
22672
22010
  formContext && /* @__PURE__ */ jsx(Fragment, { children: /* @__PURE__ */ jsxs("div", {
22673
22011
  className: "mt-12 flex flex-col gap-8",
22674
22012
  ref: formRef,
22675
22013
  children: [formFields(), /* @__PURE__ */ jsx(ErrorFocus, { containerRef: formRef })]
22676
22014
  }) }),
22677
- pluginFormAfter,
22678
- forceActionsAtTheBottom && /* @__PURE__ */ jsx("div", { className: "h-16" })
22015
+ afterFields,
22016
+ forceActionsAtTheBottom && /* @__PURE__ */ jsx("div", { className: "h-16" }),
22017
+ /* @__PURE__ */ jsxs(Dialog, {
22018
+ open: discardDialogOpen,
22019
+ onOpenChange: setDiscardDialogOpen,
22020
+ maxWidth: "sm",
22021
+ children: [
22022
+ /* @__PURE__ */ jsx(DialogTitle, { children: status === "existing" ? "Discard changes?" : "Clear form?" }),
22023
+ /* @__PURE__ */ jsx(DialogContent, { children: /* @__PURE__ */ jsx(Typography, { children: status === "existing" ? "All unsaved changes will be lost. This cannot be undone." : "All entered values will be cleared. This cannot be undone." }) }),
22024
+ /* @__PURE__ */ jsxs(DialogActions, { children: [/* @__PURE__ */ jsx(Button, {
22025
+ variant: "text",
22026
+ onClick: () => setDiscardDialogOpen(false),
22027
+ children: "Cancel"
22028
+ }), /* @__PURE__ */ jsx(Button, {
22029
+ variant: "filled",
22030
+ color: "error",
22031
+ onClick: () => {
22032
+ setDiscardDialogOpen(false);
22033
+ formex.resetForm({ values: baseInitialValues });
22034
+ },
22035
+ children: status === "existing" ? "Discard" : "Clear"
22036
+ })] })
22037
+ ]
22038
+ })
22679
22039
  ] }) });
22680
22040
  useEffect(() => {
22681
22041
  if (entityId && onIdChange) onIdChange(entityId);
@@ -22690,7 +22050,7 @@ function EntityForm({ path, entityId: entityIdProp, collection, onValuesModified
22690
22050
  formex,
22691
22051
  disabled: actionsDisabled,
22692
22052
  status,
22693
- pluginActions: pluginFormActions ?? [],
22053
+ pluginActions: pluginActionsProp ?? [],
22694
22054
  openEntityMode,
22695
22055
  showDefaultActions,
22696
22056
  navigateBack,
@@ -22700,7 +22060,11 @@ function EntityForm({ path, entityId: entityIdProp, collection, onValuesModified
22700
22060
  value: formex,
22701
22061
  children: /* @__PURE__ */ jsxs("form", {
22702
22062
  onSubmit: formex.handleSubmit,
22703
- onReset: () => formex.resetForm({ values: baseInitialValues }),
22063
+ onReset: (e) => {
22064
+ e.preventDefault();
22065
+ if (formex.dirty) setDiscardDialogOpen(true);
22066
+ else formex.resetForm({ values: baseInitialValues });
22067
+ },
22704
22068
  noValidate: true,
22705
22069
  className: cls("@container flex-1 flex flex-row w-full overflow-y-auto justify-center", className),
22706
22070
  children: [/* @__PURE__ */ jsx("div", {
@@ -22709,15 +22073,29 @@ function EntityForm({ path, entityId: entityIdProp, collection, onValuesModified
22709
22073
  children: /* @__PURE__ */ jsxs("div", {
22710
22074
  className: cls("flex flex-col w-full", openEntityMode === "dialog" ? "pt-4 pb-12 px-6 sm:px-8" : "pt-12 pb-16 px-4 sm:px-8 md:px-10"),
22711
22075
  children: [/* @__PURE__ */ jsxs("div", {
22712
- className: "flex flex-row gap-4 self-end sticky top-4 z-10",
22713
- children: [manualApplyLocalChanges && hasLocalChanges && /* @__PURE__ */ jsx(LocalChangesMenu, {
22714
- cacheKey: status === "new" || status === "copy" ? path + "#new" : path + "/" + entityId,
22076
+ className: "flex flex-row gap-4 justify-end h-0 overflow-visible sticky top-4 z-10",
22077
+ children: [manualApplyLocalChanges && hasLocalChanges && localChangesCacheKey && /* @__PURE__ */ jsx(LocalChangesMenu, {
22078
+ cacheKey: localChangesCacheKey,
22715
22079
  properties: collection.properties,
22716
22080
  cachedData: localChangesData,
22717
22081
  formex,
22718
- onClearLocalChanges: () => setLocalChangesCleared(true)
22719
- }), formex.dirty ? /* @__PURE__ */ jsx(Tooltip, {
22720
- title: t("form_modified"),
22082
+ onClearLocalChanges: () => {
22083
+ setLocalChangesCleared(true);
22084
+ onClearLocalChanges?.();
22085
+ }
22086
+ }), isSavingAutoSave ? /* @__PURE__ */ jsx(Tooltip, {
22087
+ title: "Saving…",
22088
+ children: /* @__PURE__ */ jsx(Chip, {
22089
+ size: "small",
22090
+ className: "py-1",
22091
+ colorScheme: "blueDarker",
22092
+ children: /* @__PURE__ */ jsx(LoaderIcon, {
22093
+ size: iconSize.smallest,
22094
+ className: "animate-spin"
22095
+ })
22096
+ })
22097
+ }) : formex.dirty ? /* @__PURE__ */ jsx(Tooltip, {
22098
+ title: "Form has been modified",
22721
22099
  children: /* @__PURE__ */ jsx(Chip, {
22722
22100
  size: "small",
22723
22101
  className: "py-1",
@@ -22725,7 +22103,7 @@ function EntityForm({ path, entityId: entityIdProp, collection, onValuesModified
22725
22103
  children: /* @__PURE__ */ jsx(PencilIcon, { size: iconSize.smallest })
22726
22104
  })
22727
22105
  }) : /* @__PURE__ */ jsx(Tooltip, {
22728
- title: t("form_in_sync"),
22106
+ title: "Form is in sync",
22729
22107
  children: /* @__PURE__ */ jsx(Chip, {
22730
22108
  size: "small",
22731
22109
  className: "py-1",
@@ -22738,40 +22116,401 @@ function EntityForm({ path, entityId: entityIdProp, collection, onValuesModified
22738
22116
  })
22739
22117
  });
22740
22118
  }
22741
- function getInitialEntityValues(authController, collection, path, status, entity, propertyConfigs) {
22742
- const properties = collection.properties;
22743
- if ((status === "existing" || status === "copy") && entity) {
22744
- let values;
22745
- if (!collection.alwaysApplyDefaultValues) values = entity.values ?? getDefaultValuesFor(properties);
22746
- else values = mergeDeep(getDefaultValuesFor(properties), entity.values ?? {});
22747
- if (status === "copy") {
22748
- const result = { ...values };
22749
- for (const [key, property] of Object.entries(properties)) if (property && "isId" in property && property.isId) delete result[key];
22750
- return result;
22751
- }
22752
- return values;
22753
- } else if (status === "new") return getDefaultValuesFor(properties);
22754
- else {
22755
- console.error({
22756
- status,
22757
- entity
22758
- });
22759
- throw new Error("Form has not been initialised with the correct parameters");
22119
+ function useOnAutoSave(autoSave, formex, lastSavedValues, save) {
22120
+ useEffect(() => {
22121
+ if (!autoSave) return;
22122
+ if (formex.values && !deepEqual(formex.values, lastSavedValues.current)) save(formex.values);
22123
+ }, [autoSave, formex.values]);
22124
+ }
22125
+ //#endregion
22126
+ //#region src/form/EntityFormBinding.tsx
22127
+ /**
22128
+ * CMS-connected wrapper around EntityForm.
22129
+ *
22130
+ * Provides all backend concerns that the headless EntityForm delegates via callbacks:
22131
+ * - Save via the data layer (`saveEntityWithCallbacks`)
22132
+ * - Unique field validation via the data layer
22133
+ * - Entity caching (local changes backup/restore)
22134
+ * - Plugin slots (form.before, form.after, form.actions)
22135
+ * - Snackbar notifications on save success/error
22136
+ * - Analytics events
22137
+ * - Navigation via side entity controller
22138
+ *
22139
+ * Use this component inside the CMS (`<RebaseShell>`).
22140
+ * For headless usage outside the CMS, use `EntityForm` directly.
22141
+ *
22142
+ * @group Components
22143
+ */
22144
+ function EntityFormBinding({ path, entityId: entityIdProp, collection, entity, initialStatus, onSaved, onValuesModified, onEntityChange, onStatusChange, openEntityMode = "full_screen", navigateBack: navigateBackProp, initialDirtyValues, ...restProps }) {
22145
+ const { t } = useTranslation();
22146
+ const sideEntityController = useSideEntityController();
22147
+ const collectionRegistryController = useCollectionRegistryController();
22148
+ const authController = useAuthController();
22149
+ const dataClient = useData();
22150
+ const snackbarController = useSnackbarController();
22151
+ const customizationController = useCustomizationController();
22152
+ const context = useCMSContext();
22153
+ const analyticsController = useAnalyticsController();
22154
+ const [status, setStatus] = useState(initialStatus);
22155
+ const [entityId, setEntityId] = useState(() => {
22156
+ if (status === "new" || status === "copy") return void 0;
22157
+ return entityIdProp;
22158
+ });
22159
+ const baseInitialValues = useMemo(() => getInitialEntityValues(authController, collection, path, status, entity, customizationController.propertyConfigs), [
22160
+ authController,
22161
+ collection,
22162
+ path,
22163
+ status,
22164
+ entity,
22165
+ customizationController.propertyConfigs
22166
+ ]);
22167
+ const localChangesBackup = getLocalChangesBackup(collection);
22168
+ const autoApplyLocalChanges = localChangesBackup === "auto_apply";
22169
+ const manualApplyLocalChanges = localChangesBackup === "manual_apply";
22170
+ const localChangesDataRaw = useMemo(() => entityId ? getEntityFromCache(path + "/" + entityId) : getEntityFromCache(path + "#new"), [entityId, path]);
22171
+ const [localChangesCleared, setLocalChangesCleared] = useState(false);
22172
+ const computedInitialValues = useMemo(() => {
22173
+ const withLocalChanges = autoApplyLocalChanges && localChangesDataRaw ? mergeDeep(baseInitialValues, localChangesDataRaw) : baseInitialValues;
22174
+ return initialDirtyValues ? mergeDeep(withLocalChanges, initialDirtyValues) : withLocalChanges;
22175
+ }, [
22176
+ autoApplyLocalChanges,
22177
+ localChangesDataRaw,
22178
+ baseInitialValues,
22179
+ initialDirtyValues
22180
+ ]);
22181
+ const localChangesData = useMemo(() => {
22182
+ if (!localChangesDataRaw) return void 0;
22183
+ const cleaned = removeEmptyContainers(getChanges(localChangesDataRaw, computedInitialValues));
22184
+ if (cleaned && typeof cleaned === "object" && Object.keys(cleaned).length === 0) return;
22185
+ return cleaned;
22186
+ }, [localChangesDataRaw, computedInitialValues]);
22187
+ const hasLocalChanges = !localChangesCleared && !!localChangesData && Object.keys(localChangesData).length > 0;
22188
+ const localChangesCacheKey = useMemo(() => {
22189
+ return status === "new" || status === "copy" ? path + "#new" : path + "/" + entityId;
22190
+ }, [
22191
+ status,
22192
+ path,
22193
+ entityId
22194
+ ]);
22195
+ const navigateBack = useCallback(() => {
22196
+ if (navigateBackProp) {
22197
+ navigateBackProp();
22198
+ return;
22199
+ }
22200
+ if (openEntityMode === "side_panel" || openEntityMode === "dialog") sideEntityController.close();
22201
+ else window.history.back();
22202
+ }, [
22203
+ navigateBackProp,
22204
+ openEntityMode,
22205
+ sideEntityController
22206
+ ]);
22207
+ const clearDirtyCache = useCallback(() => {
22208
+ if (status === "new" || status === "copy") {
22209
+ removeEntityFromMemoryCache(path + "#new");
22210
+ removeEntityFromCache(path + "#new");
22211
+ } else {
22212
+ removeEntityFromMemoryCache(path + "/" + entityId);
22213
+ removeEntityFromCache(path + "/" + entityId);
22214
+ }
22215
+ }, [
22216
+ status,
22217
+ path,
22218
+ entityId
22219
+ ]);
22220
+ const onReset = useCallback(() => {
22221
+ clearDirtyCache();
22222
+ }, [clearDirtyCache]);
22223
+ const onValuesChangeDeferred = useCallback((values, controller) => {
22224
+ const key = status === "new" || status === "copy" ? path + "#new" : path + "/" + entityId;
22225
+ if (controller.dirty) {
22226
+ const touchedValues = removeEmptyContainers(extractTouchedValues(values, controller.touched));
22227
+ if (touchedValues && Object.keys(touchedValues).length > 0) saveEntityToCache(key, touchedValues);
22228
+ else removeEntityFromCache(key);
22229
+ }
22230
+ }, [
22231
+ status,
22232
+ path,
22233
+ entityId
22234
+ ]);
22235
+ const onSubmit = useCallback(async (values, formex) => {
22236
+ try {
22237
+ const savedEntity = await saveEntityWithCallbacks({
22238
+ path,
22239
+ entityId,
22240
+ values,
22241
+ previousValues: entity?.values,
22242
+ collection,
22243
+ status,
22244
+ data: dataClient,
22245
+ context,
22246
+ afterSave: (updatedEntity) => {
22247
+ clearDirtyCache();
22248
+ onValuesModified?.(false, updatedEntity.values);
22249
+ onEntityChange?.(updatedEntity);
22250
+ setStatus("existing");
22251
+ setEntityId(updatedEntity.id);
22252
+ onStatusChange?.("existing");
22253
+ if (onSaved) onSaved({
22254
+ entity: updatedEntity,
22255
+ status,
22256
+ path,
22257
+ entityId: updatedEntity.id,
22258
+ collection
22259
+ });
22260
+ },
22261
+ afterSaveError: (e) => {
22262
+ snackbarController.open({
22263
+ type: "error",
22264
+ title: t("error_saving_entity"),
22265
+ message: e?.message
22266
+ });
22267
+ console.error("Error saving entity", path, entityId, e);
22268
+ }
22269
+ });
22270
+ if (!collection.formAutoSave) snackbarController.open({
22271
+ type: "success",
22272
+ message: `${collection.singularName ?? collection.name}: ${t("saved_correctly")}`
22273
+ });
22274
+ const eventName = status === "new" ? "new_entity_saved" : status === "copy" ? "entity_copied" : status === "existing" ? "entity_edited" : "unmapped_event";
22275
+ analyticsController.onAnalyticsEvent?.(eventName, { path });
22276
+ return savedEntity;
22277
+ } catch (e) {
22278
+ console.error(e);
22279
+ throw e;
22280
+ }
22281
+ }, [
22282
+ path,
22283
+ entityId,
22284
+ entity,
22285
+ collection,
22286
+ status,
22287
+ dataClient,
22288
+ context,
22289
+ clearDirtyCache,
22290
+ onValuesModified,
22291
+ onEntityChange,
22292
+ onSaved,
22293
+ onStatusChange,
22294
+ snackbarController,
22295
+ analyticsController,
22296
+ t
22297
+ ]);
22298
+ const uniqueFieldValidator = useCallback(async ({ name, value }) => {
22299
+ try {
22300
+ const { data } = await dataClient.collection(path).find({
22301
+ where: { [name]: `eq.${value}` },
22302
+ limit: 2
22303
+ });
22304
+ return (entityId ? data.filter((e) => e.id !== entityId) : data).length === 0;
22305
+ } catch (e) {
22306
+ console.error("Error checking unique field", e);
22307
+ return true;
22308
+ }
22309
+ }, [
22310
+ dataClient,
22311
+ path,
22312
+ entityId
22313
+ ]);
22314
+ const parentCollectionSlugs = collectionRegistryController.getParentCollectionSlugs(path);
22315
+ const parentEntityIds = collectionRegistryController.getParentEntityIds(path);
22316
+ const formActionProps = useMemo(() => ({
22317
+ entityId,
22318
+ parentCollectionSlugs,
22319
+ parentEntityIds,
22320
+ path,
22321
+ status,
22322
+ collection,
22323
+ context,
22324
+ formContext: void 0,
22325
+ openEntityMode,
22326
+ disabled: false
22327
+ }), [
22328
+ entityId,
22329
+ parentCollectionSlugs,
22330
+ parentEntityIds,
22331
+ path,
22332
+ status,
22333
+ collection,
22334
+ context,
22335
+ openEntityMode
22336
+ ]);
22337
+ const pluginFormActions = useSlot("form.actions", formActionProps);
22338
+ const pluginFormBefore = useSlot("form.before", formActionProps);
22339
+ const pluginFormAfter = useSlot("form.after", formActionProps);
22340
+ return /* @__PURE__ */ jsx(EntityForm, {
22341
+ ...restProps,
22342
+ path,
22343
+ entityId: entityIdProp,
22344
+ collection,
22345
+ entity,
22346
+ initialStatus,
22347
+ initialDirtyValues,
22348
+ onSaved,
22349
+ onValuesModified,
22350
+ onEntityChange,
22351
+ onStatusChange,
22352
+ openEntityMode,
22353
+ navigateBack,
22354
+ onSubmit,
22355
+ onValuesChangeDeferred,
22356
+ onReset,
22357
+ uniqueFieldValidator,
22358
+ beforeFields: pluginFormBefore,
22359
+ afterFields: pluginFormAfter,
22360
+ pluginActions: pluginFormActions ?? [],
22361
+ computedInitialValues,
22362
+ hasLocalChanges,
22363
+ localChangesData,
22364
+ manualApplyLocalChanges,
22365
+ localChangesCacheKey,
22366
+ onClearLocalChanges: () => setLocalChangesCleared(true)
22367
+ });
22368
+ }
22369
+ //#endregion
22370
+ //#region src/util/propertyConfigMapper.ts
22371
+ /**
22372
+ * Map Rebase property types to headless-supported types.
22373
+ * `vector` and `binary` are not directly renderable, so we fall back to `string`.
22374
+ */
22375
+ var TYPE_MAP = {
22376
+ string: "string",
22377
+ number: "number",
22378
+ boolean: "boolean",
22379
+ date: "date",
22380
+ geopoint: "geopoint",
22381
+ reference: "reference",
22382
+ relation: "relation",
22383
+ array: "array",
22384
+ map: "map",
22385
+ vector: "string",
22386
+ binary: "string"
22387
+ };
22388
+ /**
22389
+ * Convert a single Property to a CollectionPropertyConfig.
22390
+ */
22391
+ function mapPropertyToConfig(property) {
22392
+ const base = {
22393
+ type: TYPE_MAP[property.type] ?? "string",
22394
+ name: property.name,
22395
+ description: property.description,
22396
+ columnWidth: property.ui?.columnWidth,
22397
+ hideFromCollection: property.ui?.hideFromCollection
22398
+ };
22399
+ if (property.type === "string") {
22400
+ if (property.enum) base.enum = mapEnumValues(property.enum);
22401
+ if (property.multiline) base.multiline = true;
22402
+ if (property.ui?.previewAsTag) base.previewAsTag = true;
22403
+ if (property.url || property.ui?.url) base.url = property.url ?? property.ui?.url;
22404
+ if (property.ui?.markdown || property.markdown) base.markdown = true;
22405
+ if (property.storage) base.storage = true;
22406
+ if (property.email) base.email = true;
22407
+ }
22408
+ if (property.type === "number" && property.enum) base.enum = mapEnumValues(property.enum);
22409
+ if (property.type === "array" && property.of && !Array.isArray(property.of)) base.of = mapPropertyToConfig(property.of);
22410
+ if (property.type === "map" && property.properties) {
22411
+ base.properties = mapPropertiesToConfigs(property.properties);
22412
+ if (property.propertiesOrder) base.propertiesOrder = property.propertiesOrder;
22760
22413
  }
22414
+ if (property.type === "date" && property.mode) base.mode = property.mode;
22415
+ if (property.ui?.Preview) {}
22416
+ return base;
22761
22417
  }
22762
- function zodToFormErrors(zodError) {
22763
- let errors = {};
22764
- for (const issue of zodError.issues) {
22765
- const path = issue.path.join(".");
22766
- if (path && !getIn(errors, path)) errors = setIn(errors, path, issue.message);
22418
+ /**
22419
+ * Convert a Properties record to a CollectionPropertyConfig record.
22420
+ */
22421
+ function mapPropertiesToConfigs(properties) {
22422
+ const result = {};
22423
+ for (const [key, property] of Object.entries(properties)) result[key] = mapPropertyToConfig(property);
22424
+ return result;
22425
+ }
22426
+ /**
22427
+ * Convert Rebase EnumValues to CollectionEnumValueConfig format.
22428
+ * Handles both array form (EnumValueConfig[]) and record form.
22429
+ */
22430
+ function mapEnumValues(enumValues) {
22431
+ if (!enumValues) return void 0;
22432
+ if (Array.isArray(enumValues)) {
22433
+ const result = {};
22434
+ for (const item of enumValues) result[String(item.id)] = normalizeEnumValueConfig(item);
22435
+ return result;
22767
22436
  }
22768
- return errors;
22437
+ const result = {};
22438
+ for (const [key, value] of Object.entries(enumValues)) result[key] = typeof value === "string" ? value : normalizeEnumValueConfig(value);
22439
+ return result;
22769
22440
  }
22770
- function useOnAutoSave(autoSave, formex, lastSavedValues, save) {
22771
- useEffect(() => {
22772
- if (!autoSave) return;
22773
- if (formex.values && !deepEqual(formex.values, lastSavedValues.current)) save(formex.values);
22774
- }, [autoSave, formex.values]);
22441
+ function normalizeEnumValueConfig(config) {
22442
+ return {
22443
+ label: config.label,
22444
+ color: typeof config.color === "string" ? config.color : void 0,
22445
+ disabled: config.disabled
22446
+ };
22447
+ }
22448
+ //#endregion
22449
+ //#region src/util/dataControllerAdapter.ts
22450
+ /**
22451
+ * Adapts an `EntityTableController<M>` (from @rebasepro/types) to a
22452
+ * `CollectionDataController<T>` (from @rebasepro/ui), bridging the
22453
+ * entity-aware data layer to the headless collection view.
22454
+ */
22455
+ /**
22456
+ * Flatten Entity<M> objects to plain row objects.
22457
+ * The headless view works with flat `Record<string, unknown>` rows,
22458
+ * while the entity system uses `{ id, path, values: M }`.
22459
+ *
22460
+ * The flattened row includes `id` and `path` at the top level,
22461
+ * plus all values spread.
22462
+ */
22463
+ function flattenEntities(entities) {
22464
+ return entities.map((entity) => ({
22465
+ id: entity.id,
22466
+ path: entity.path,
22467
+ ...entity.values
22468
+ }));
22469
+ }
22470
+ /**
22471
+ * React hook that adapts an EntityTableController to a CollectionDataController.
22472
+ *
22473
+ * Usage:
22474
+ * ```tsx
22475
+ * const tableController = useDataTableController({ path, collection });
22476
+ * const dataController = useCollectionDataController(tableController);
22477
+ *
22478
+ * <CollectionView dataController={dataController} ... />
22479
+ * ```
22480
+ */
22481
+ function useCollectionDataController(tableController) {
22482
+ const flatData = useMemo(() => flattenEntities(tableController.data), [tableController.data]);
22483
+ return useMemo(() => ({
22484
+ data: flatData,
22485
+ loading: tableController.dataLoading,
22486
+ noMoreToLoad: tableController.noMoreToLoad,
22487
+ error: tableController.dataLoadingError,
22488
+ filterValues: tableController.filterValues,
22489
+ setFilterValues: tableController.setFilterValues,
22490
+ sortBy: tableController.sortBy,
22491
+ setSortBy: tableController.setSortBy,
22492
+ searchString: tableController.searchString,
22493
+ setSearchString: tableController.setSearchString,
22494
+ clearFilter: tableController.clearFilter,
22495
+ initialScroll: tableController.initialScroll,
22496
+ onScroll: tableController.onScroll,
22497
+ paginationEnabled: tableController.paginationEnabled,
22498
+ pageSize: tableController.pageSize,
22499
+ itemCount: tableController.itemCount,
22500
+ setItemCount: tableController.setItemCount
22501
+ }), [flatData, tableController]);
22502
+ }
22503
+ /**
22504
+ * Non-hook version for cases where you already have the data
22505
+ * and just need to build a static controller.
22506
+ */
22507
+ function createStaticDataController(data, options) {
22508
+ return {
22509
+ data,
22510
+ loading: options?.loading ?? false,
22511
+ noMoreToLoad: true,
22512
+ error: options?.error
22513
+ };
22775
22514
  }
22776
22515
  //#endregion
22777
22516
  //#region src/data_import/import/ImportCollectionAction.tsx
@@ -23605,6 +23344,571 @@ var export_exports = /* @__PURE__ */ __exportAll({
23605
23344
  */
23606
23345
  var useCollectionsConfigController = () => useContext(ConfigControllerContext);
23607
23346
  //#endregion
23347
+ //#region src/collection_editor/serializable_utils.ts
23348
+ /**
23349
+ * Strip non-serializable fields from a `BaseUIConfig`.
23350
+ * Removes `Field` and `Preview` (ComponentRef).
23351
+ */
23352
+ function toSerializableUIConfig(ui) {
23353
+ if (!ui) return void 0;
23354
+ const { Field, Preview, ...rest } = ui;
23355
+ const result = {};
23356
+ for (const [key, value] of Object.entries(rest)) if (key !== "Field" && key !== "Preview" && value !== void 0) result[key] = value;
23357
+ return Object.keys(result).length > 0 ? result : void 0;
23358
+ }
23359
+ /**
23360
+ * Convert a `StorageConfig` to its serializable form.
23361
+ * Drops function variants of `storagePath`, `fileName`, and all function fields.
23362
+ */
23363
+ function toSerializableStorageConfig(storage) {
23364
+ if (!storage) return void 0;
23365
+ const storagePath = typeof storage.storagePath === "string" ? storage.storagePath : void 0;
23366
+ if (!storagePath) return void 0;
23367
+ const result = { storagePath };
23368
+ if (storage.acceptedFiles) result.acceptedFiles = storage.acceptedFiles;
23369
+ if (storage.imageResize) result.imageResize = storage.imageResize;
23370
+ if (storage.metadata) result.metadata = storage.metadata;
23371
+ if (typeof storage.fileName === "string") result.fileName = storage.fileName;
23372
+ if (storage.includeBucketUrl !== void 0) result.includeBucketUrl = storage.includeBucketUrl;
23373
+ if (storage.storeUrl !== void 0) result.storeUrl = storage.storeUrl;
23374
+ if (storage.maxSize !== void 0) result.maxSize = storage.maxSize;
23375
+ return result;
23376
+ }
23377
+ /**
23378
+ * Convert a RegExp or string `matches` validation to a plain string.
23379
+ */
23380
+ function serializeMatches(matches) {
23381
+ if (matches === void 0) return void 0;
23382
+ if (typeof matches === "string") return matches;
23383
+ return matches.source;
23384
+ }
23385
+ /**
23386
+ * Resolve a relation target to a string slug.
23387
+ * Functions are called to extract the target; EntityCollection objects
23388
+ * use their slug.
23389
+ */
23390
+ function resolveRelationTarget(target) {
23391
+ if (!target) return void 0;
23392
+ if (typeof target === "string") return target;
23393
+ if (typeof target === "function") try {
23394
+ const resolved = target();
23395
+ if (typeof resolved === "string") return resolved;
23396
+ if (resolved && typeof resolved === "object" && "slug" in resolved) return resolved.slug;
23397
+ } catch {}
23398
+ }
23399
+ /**
23400
+ * Strip non-serializable base property fields.
23401
+ * Returns a clean object with only the common serializable fields from BaseProperty.
23402
+ * Validation is intentionally excluded — each property type handles its own.
23403
+ */
23404
+ function toSerializableBaseFields(property) {
23405
+ const result = { name: property.name };
23406
+ if (property.description) result.description = property.description;
23407
+ if (property.propertyConfig) result.propertyConfig = property.propertyConfig;
23408
+ if (property.columnName) result.columnName = property.columnName;
23409
+ if (property.defaultValue !== void 0) result.defaultValue = property.defaultValue;
23410
+ if (property.conditions) result.conditions = property.conditions;
23411
+ if (property.metadata) result.metadata = property.metadata;
23412
+ const ui = toSerializableUIConfig(property.ui);
23413
+ if (ui) result.ui = ui;
23414
+ return result;
23415
+ }
23416
+ /**
23417
+ * Convert a single `Property` to its JSON-serializable form.
23418
+ * Strips functions, converts RegExp to string, Date to ISO string.
23419
+ */
23420
+ function toSerializableProperty(property) {
23421
+ const base = toSerializableBaseFields(property);
23422
+ switch (property.type) {
23423
+ case "string": {
23424
+ const sp = property;
23425
+ const result = {
23426
+ ...base,
23427
+ type: "string"
23428
+ };
23429
+ if (sp.columnType) result.columnType = sp.columnType;
23430
+ if (sp.isId !== void 0) result.isId = sp.isId;
23431
+ if (sp.enum) result.enum = sp.enum;
23432
+ if (sp.multiline) result.multiline = sp.multiline;
23433
+ if (sp.markdown) result.markdown = sp.markdown;
23434
+ if (sp.userSelect) result.userSelect = sp.userSelect;
23435
+ if (sp.url) result.url = sp.url;
23436
+ if (sp.email) result.email = sp.email;
23437
+ if (sp.previewAsTag) result.previewAsTag = sp.previewAsTag;
23438
+ if (sp.validation) {
23439
+ const { matches, ...restValidation } = sp.validation;
23440
+ const serializedValidation = { ...restValidation };
23441
+ const matchStr = serializeMatches(matches);
23442
+ if (matchStr !== void 0) serializedValidation.matches = matchStr;
23443
+ result.validation = serializedValidation;
23444
+ }
23445
+ const storage = toSerializableStorageConfig(sp.storage);
23446
+ if (storage) result.storage = storage;
23447
+ if (sp.reference) result.reference = toSerializableProperty(sp.reference);
23448
+ return result;
23449
+ }
23450
+ case "number": {
23451
+ const np = property;
23452
+ const result = {
23453
+ ...base,
23454
+ type: "number"
23455
+ };
23456
+ if (np.columnType) result.columnType = np.columnType;
23457
+ if (np.validation) result.validation = np.validation;
23458
+ if (np.isId !== void 0) result.isId = np.isId;
23459
+ if (np.enum) result.enum = np.enum;
23460
+ return result;
23461
+ }
23462
+ case "boolean": {
23463
+ const bp = property;
23464
+ const result = {
23465
+ ...base,
23466
+ type: "boolean"
23467
+ };
23468
+ if (bp.validation) result.validation = bp.validation;
23469
+ return result;
23470
+ }
23471
+ case "date": {
23472
+ const dp = property;
23473
+ const result = {
23474
+ ...base,
23475
+ type: "date"
23476
+ };
23477
+ if (dp.columnType) result.columnType = dp.columnType;
23478
+ if (dp.mode) result.mode = dp.mode;
23479
+ if (dp.timezone) result.timezone = dp.timezone;
23480
+ if (dp.autoValue) result.autoValue = dp.autoValue;
23481
+ if (dp.clearable) result.clearable = dp.clearable;
23482
+ if (dp.validation) {
23483
+ const serializedValidation = {};
23484
+ if (dp.validation.required !== void 0) serializedValidation.required = dp.validation.required;
23485
+ if (dp.validation.requiredMessage) serializedValidation.requiredMessage = dp.validation.requiredMessage;
23486
+ if (dp.validation.unique) serializedValidation.unique = dp.validation.unique;
23487
+ if (dp.validation.uniqueInArray) serializedValidation.uniqueInArray = dp.validation.uniqueInArray;
23488
+ if (dp.validation.min instanceof Date) serializedValidation.min = dp.validation.min.toISOString();
23489
+ else if (typeof dp.validation.min === "string") serializedValidation.min = dp.validation.min;
23490
+ if (dp.validation.max instanceof Date) serializedValidation.max = dp.validation.max.toISOString();
23491
+ else if (typeof dp.validation.max === "string") serializedValidation.max = dp.validation.max;
23492
+ result.validation = serializedValidation;
23493
+ }
23494
+ return result;
23495
+ }
23496
+ case "geopoint": {
23497
+ const gp = property;
23498
+ const result = {
23499
+ ...base,
23500
+ type: "geopoint"
23501
+ };
23502
+ if (gp.validation) result.validation = gp.validation;
23503
+ return result;
23504
+ }
23505
+ case "reference": {
23506
+ const rp = property;
23507
+ const result = {
23508
+ ...base,
23509
+ type: "reference"
23510
+ };
23511
+ if (rp.isId !== void 0) result.isId = rp.isId;
23512
+ if (rp.path) result.path = rp.path;
23513
+ if (rp.fixedFilter) result.fixedFilter = rp.fixedFilter;
23514
+ if (rp.includeId !== void 0) result.includeId = rp.includeId;
23515
+ if (rp.includeEntityLink !== void 0) result.includeEntityLink = rp.includeEntityLink;
23516
+ return result;
23517
+ }
23518
+ case "relation": {
23519
+ const rl = property;
23520
+ const result = {
23521
+ ...base,
23522
+ type: "relation"
23523
+ };
23524
+ if (rl.isId !== void 0) result.isId = rl.isId;
23525
+ const target = resolveRelationTarget(rl.target);
23526
+ if (target) result.target = target;
23527
+ if (rl.cardinality) result.cardinality = rl.cardinality;
23528
+ if (rl.direction) result.direction = rl.direction;
23529
+ if (rl.inverseRelationName) result.inverseRelationName = rl.inverseRelationName;
23530
+ if (rl.localKey) result.localKey = rl.localKey;
23531
+ if (rl.foreignKeyOnTarget) result.foreignKeyOnTarget = rl.foreignKeyOnTarget;
23532
+ if (rl.through) result.through = rl.through;
23533
+ if (rl.joinPath) result.joinPath = rl.joinPath;
23534
+ if (rl.onUpdate) result.onUpdate = rl.onUpdate;
23535
+ if (rl.onDelete) result.onDelete = rl.onDelete;
23536
+ if (rl.relationName) result.relationName = rl.relationName;
23537
+ if (rl.fixedFilter) result.fixedFilter = rl.fixedFilter;
23538
+ if (rl.includeId !== void 0) result.includeId = rl.includeId;
23539
+ if (rl.includeEntityLink !== void 0) result.includeEntityLink = rl.includeEntityLink;
23540
+ if (rl.widget) result.widget = rl.widget;
23541
+ return result;
23542
+ }
23543
+ case "array": {
23544
+ const ap = property;
23545
+ const result = {
23546
+ ...base,
23547
+ type: "array"
23548
+ };
23549
+ if (ap.columnType) result.columnType = ap.columnType;
23550
+ if (ap.validation) result.validation = ap.validation;
23551
+ if (ap.sortable !== void 0) result.sortable = ap.sortable;
23552
+ if (ap.canAddElements !== void 0) result.canAddElements = ap.canAddElements;
23553
+ if (ap.of) if (Array.isArray(ap.of)) result.of = ap.of.map(toSerializableProperty);
23554
+ else result.of = toSerializableProperty(ap.of);
23555
+ if (ap.oneOf) {
23556
+ result.oneOf = { properties: toSerializableProperties(ap.oneOf.properties) };
23557
+ if (ap.oneOf.propertiesOrder) result.oneOf.propertiesOrder = ap.oneOf.propertiesOrder;
23558
+ if (ap.oneOf.typeField) result.oneOf.typeField = ap.oneOf.typeField;
23559
+ if (ap.oneOf.valueField) result.oneOf.valueField = ap.oneOf.valueField;
23560
+ }
23561
+ return result;
23562
+ }
23563
+ case "map": {
23564
+ const mp = property;
23565
+ const result = {
23566
+ ...base,
23567
+ type: "map"
23568
+ };
23569
+ if (mp.columnType) result.columnType = mp.columnType;
23570
+ if (mp.validation) result.validation = mp.validation;
23571
+ if (mp.propertiesOrder) result.propertiesOrder = mp.propertiesOrder;
23572
+ if (mp.previewProperties) result.previewProperties = mp.previewProperties;
23573
+ if (mp.keyValue) result.keyValue = mp.keyValue;
23574
+ if (mp.properties) result.properties = toSerializableProperties(mp.properties);
23575
+ return result;
23576
+ }
23577
+ case "vector": {
23578
+ const vp = property;
23579
+ const result = {
23580
+ ...base,
23581
+ type: "vector",
23582
+ dimensions: vp.dimensions
23583
+ };
23584
+ if (vp.validation) result.validation = vp.validation;
23585
+ return result;
23586
+ }
23587
+ case "binary": {
23588
+ const bp = property;
23589
+ const result = {
23590
+ ...base,
23591
+ type: "binary"
23592
+ };
23593
+ if (bp.validation) result.validation = bp.validation;
23594
+ return result;
23595
+ }
23596
+ default: return base;
23597
+ }
23598
+ }
23599
+ /**
23600
+ * Convert a `Properties` record to its serializable form.
23601
+ */
23602
+ function toSerializableProperties(properties) {
23603
+ const result = {};
23604
+ for (const [key, property] of Object.entries(properties)) result[key] = toSerializableProperty(property);
23605
+ return result;
23606
+ }
23607
+ /**
23608
+ * Convert an `EntityCollection` to its JSON-serializable form.
23609
+ *
23610
+ * Strips all non-serializable fields (functions, React nodes, class instances)
23611
+ * while preserving the structural schema that the collection editor works with.
23612
+ *
23613
+ * The result is safe for `JSON.stringify()` and database storage.
23614
+ */
23615
+ function toSerializableCollection(collection) {
23616
+ const result = {
23617
+ slug: collection.slug,
23618
+ name: collection.name,
23619
+ properties: toSerializableProperties(collection.properties)
23620
+ };
23621
+ if (collection.singularName) result.singularName = collection.singularName;
23622
+ if (collection.description) result.description = collection.description;
23623
+ if (collection.group) result.group = collection.group;
23624
+ if (collection.driver) result.driver = collection.driver;
23625
+ if (collection.databaseId) result.databaseId = collection.databaseId;
23626
+ if (collection.titleProperty) result.titleProperty = collection.titleProperty;
23627
+ if (collection.ownerId) result.ownerId = collection.ownerId;
23628
+ if (collection.metadata) result.metadata = collection.metadata;
23629
+ if (collection.table) result.table = collection.table;
23630
+ if ("schema" in collection && collection.schema) result.schema = collection.schema;
23631
+ if (collection.orderProperty) result.orderProperty = collection.orderProperty;
23632
+ if (typeof collection.icon === "string") result.icon = collection.icon;
23633
+ if (collection.propertiesOrder) result.propertiesOrder = collection.propertiesOrder;
23634
+ if (collection.previewProperties) result.previewProperties = collection.previewProperties;
23635
+ if (collection.listProperties) result.listProperties = collection.listProperties;
23636
+ if (collection.enabledViews) result.enabledViews = collection.enabledViews;
23637
+ if (collection.disableDefaultActions) result.disableDefaultActions = collection.disableDefaultActions;
23638
+ if (collection.securityRules) result.securityRules = collection.securityRules;
23639
+ if (collection.openEntityMode) result.openEntityMode = collection.openEntityMode;
23640
+ if (collection.defaultEntityAction) result.defaultEntityAction = collection.defaultEntityAction;
23641
+ if (collection.defaultViewMode) result.defaultViewMode = collection.defaultViewMode;
23642
+ if (collection.defaultSize) result.defaultSize = collection.defaultSize;
23643
+ if (collection.localChangesBackup !== void 0) result.localChangesBackup = collection.localChangesBackup;
23644
+ if (collection.kanban) result.kanban = collection.kanban;
23645
+ if (collection.fixedFilter) result.fixedFilter = collection.fixedFilter;
23646
+ if (collection.defaultFilter) result.defaultFilter = collection.defaultFilter;
23647
+ if (collection.filterPresets) result.filterPresets = collection.filterPresets;
23648
+ if (collection.sort) result.sort = collection.sort;
23649
+ if (collection.pagination !== void 0) result.pagination = collection.pagination;
23650
+ if (collection.selectionEnabled !== void 0) result.selectionEnabled = collection.selectionEnabled;
23651
+ if (collection.inlineEditing !== void 0) result.inlineEditing = collection.inlineEditing;
23652
+ if (collection.hideFromNavigation !== void 0) result.hideFromNavigation = collection.hideFromNavigation;
23653
+ if (collection.hideIdFromForm !== void 0) result.hideIdFromForm = collection.hideIdFromForm;
23654
+ if (collection.hideIdFromCollection !== void 0) result.hideIdFromCollection = collection.hideIdFromCollection;
23655
+ if (collection.formAutoSave !== void 0) result.formAutoSave = collection.formAutoSave;
23656
+ if (collection.alwaysApplyDefaultValues !== void 0) result.alwaysApplyDefaultValues = collection.alwaysApplyDefaultValues;
23657
+ if (collection.includeJsonView !== void 0) result.includeJsonView = collection.includeJsonView;
23658
+ if (collection.history !== void 0) result.history = collection.history;
23659
+ if (collection.sideDialogWidth !== void 0) result.sideDialogWidth = collection.sideDialogWidth;
23660
+ if (typeof collection.exportable === "boolean") result.exportable = collection.exportable;
23661
+ if (typeof collection.auth === "boolean") result.auth = collection.auth;
23662
+ if (typeof collection.defaultSelectedView === "string") result.defaultSelectedView = collection.defaultSelectedView;
23663
+ return result;
23664
+ }
23665
+ /**
23666
+ * Convert a serializable property back to the original `Property` type.
23667
+ *
23668
+ * This is a mostly pass-through operation since serializable properties
23669
+ * are a subset of the original types. The main conversions are:
23670
+ * - `validation.matches` (string) stays as string (compatible with Property)
23671
+ * - `validation.min/max` on dates: ISO strings → Date objects
23672
+ */
23673
+ function fromSerializableProperty(serialized) {
23674
+ switch (serialized.type) {
23675
+ case "date": {
23676
+ const { validation: dateValidation, ...dateRest } = serialized;
23677
+ const result = {
23678
+ ...dateRest,
23679
+ type: "date"
23680
+ };
23681
+ if (dateValidation) {
23682
+ const convertedValidation = { ...dateValidation };
23683
+ if (typeof dateValidation.min === "string") convertedValidation.min = new Date(dateValidation.min);
23684
+ if (typeof dateValidation.max === "string") convertedValidation.max = new Date(dateValidation.max);
23685
+ result.validation = convertedValidation;
23686
+ }
23687
+ return result;
23688
+ }
23689
+ case "array": {
23690
+ const sp = serialized;
23691
+ const result = { ...sp };
23692
+ if (sp.of) if (Array.isArray(sp.of)) result.of = sp.of.map(fromSerializableProperty);
23693
+ else result.of = fromSerializableProperty(sp.of);
23694
+ if (sp.oneOf) result.oneOf = {
23695
+ ...sp.oneOf,
23696
+ properties: fromSerializableProperties(sp.oneOf.properties)
23697
+ };
23698
+ return result;
23699
+ }
23700
+ case "map": {
23701
+ const sp = serialized;
23702
+ const result = { ...sp };
23703
+ if (sp.properties) result.properties = fromSerializableProperties(sp.properties);
23704
+ return result;
23705
+ }
23706
+ case "string": {
23707
+ const sp = serialized;
23708
+ const result = { ...sp };
23709
+ if (sp.reference) result.reference = fromSerializableProperty(sp.reference);
23710
+ return result;
23711
+ }
23712
+ default: return serialized;
23713
+ }
23714
+ }
23715
+ /**
23716
+ * Convert a `SerializableProperties` record back to `Properties`.
23717
+ */
23718
+ function fromSerializableProperties(serialized) {
23719
+ const result = {};
23720
+ for (const [key, property] of Object.entries(serialized)) result[key] = fromSerializableProperty(property);
23721
+ return result;
23722
+ }
23723
+ /**
23724
+ * Convert a `SerializableCollection` back to an `EntityCollection`.
23725
+ *
23726
+ * The result will NOT contain any of the non-serializable fields
23727
+ * (callbacks, entityActions, etc.) — those must be re-attached by the
23728
+ * consumer if needed.
23729
+ */
23730
+ function fromSerializableCollection(serialized) {
23731
+ const { properties, ...rest } = serialized;
23732
+ return {
23733
+ ...rest,
23734
+ properties: fromSerializableProperties(properties)
23735
+ };
23736
+ }
23737
+ //#endregion
23738
+ //#region src/collection_editor/useJsonCollectionsConfigController.tsx
23739
+ /**
23740
+ * A `CollectionsConfigController` implementation that persists collection
23741
+ * configurations as JSON via a pluggable `JsonCollectionStore` adapter.
23742
+ *
23743
+ * Use this when you want to store collection schemas in a database, API,
23744
+ * localStorage, or any other JSON-compatible backend — as opposed to the
23745
+ * default code-based persistence.
23746
+ *
23747
+ * @example
23748
+ * ```tsx
23749
+ * const store: JsonCollectionStore = {
23750
+ * load: async () => { const res = await fetch('/api/schemas'); return res.json(); },
23751
+ * save: async (slug, data) => { await fetch(`/api/schemas/${slug}`, { method: 'PUT', body: JSON.stringify(data) }); },
23752
+ * delete: async (slug) => { await fetch(`/api/schemas/${slug}`, { method: 'DELETE' }); },
23753
+ * };
23754
+ *
23755
+ * const configController = useJsonCollectionsConfigController({ store });
23756
+ * ```
23757
+ */
23758
+ function useJsonCollectionsConfigController({ store, readOnly = false, autoLoad = true }) {
23759
+ const [collections, setCollections] = useState([]);
23760
+ const [loading, setLoading] = useState(autoLoad);
23761
+ const [navigationEntries, setNavigationEntries] = useState([]);
23762
+ const storeRef = useRef(store);
23763
+ storeRef.current = store;
23764
+ useEffect(() => {
23765
+ if (!autoLoad) return;
23766
+ let cancelled = false;
23767
+ (async () => {
23768
+ try {
23769
+ const [serialized, navEntries] = await Promise.all([storeRef.current.load(), storeRef.current.loadNavigationEntries?.() ?? Promise.resolve([])]);
23770
+ if (cancelled) return;
23771
+ setCollections(serialized.map(fromSerializableCollection));
23772
+ setNavigationEntries(navEntries);
23773
+ } catch (e) {
23774
+ console.error("useJsonCollectionsConfigController: failed to load collections", e);
23775
+ } finally {
23776
+ if (!cancelled) setLoading(false);
23777
+ }
23778
+ })();
23779
+ return () => {
23780
+ cancelled = true;
23781
+ };
23782
+ }, [autoLoad]);
23783
+ /**
23784
+ * Update local state and persist a collection to the store.
23785
+ */
23786
+ const persistCollection = useCallback(async (collection) => {
23787
+ const serializable = toSerializableCollection(collection);
23788
+ await storeRef.current.save(collection.slug, serializable);
23789
+ }, []);
23790
+ /**
23791
+ * Find collection by id (slug).
23792
+ */
23793
+ const getCollection = useCallback((id) => {
23794
+ const found = collections.find((c) => c.slug === id);
23795
+ if (found) return found;
23796
+ throw new Error(`Collection "${id}" not found`);
23797
+ }, [collections]);
23798
+ const saveCollection = useCallback(async ({ id, collectionData }) => {
23799
+ const collection = collectionData;
23800
+ await persistCollection(collection);
23801
+ setCollections((prev) => {
23802
+ const idx = prev.findIndex((c) => c.slug === id);
23803
+ if (idx >= 0) {
23804
+ const next = [...prev];
23805
+ next[idx] = collection;
23806
+ return next;
23807
+ }
23808
+ return [...prev, collection];
23809
+ });
23810
+ }, [persistCollection]);
23811
+ const updateCollection = useCallback(async ({ id, collectionData, previousId }) => {
23812
+ setCollections((prev) => {
23813
+ const lookupId = previousId ?? id;
23814
+ const idx = prev.findIndex((c) => c.slug === lookupId);
23815
+ if (idx < 0) return prev;
23816
+ const merged = {
23817
+ ...prev[idx],
23818
+ ...collectionData
23819
+ };
23820
+ const next = [...prev];
23821
+ next[idx] = merged;
23822
+ persistCollection(merged).catch((e) => console.error("useJsonCollectionsConfigController: failed to update collection", e));
23823
+ if (previousId && previousId !== id) storeRef.current.delete(previousId).catch((e) => console.error("useJsonCollectionsConfigController: failed to delete old slug", e));
23824
+ return next;
23825
+ });
23826
+ }, [persistCollection]);
23827
+ const deleteCollection = useCallback(async ({ id }) => {
23828
+ await storeRef.current.delete(id);
23829
+ setCollections((prev) => prev.filter((c) => c.slug !== id));
23830
+ }, []);
23831
+ const saveProperty = useCallback(async ({ path, propertyKey, property, newPropertiesOrder }) => {
23832
+ setCollections((prev) => {
23833
+ const idx = prev.findIndex((c) => c.slug === path);
23834
+ if (idx < 0) return prev;
23835
+ const collection = { ...prev[idx] };
23836
+ collection.properties = {
23837
+ ...collection.properties,
23838
+ [propertyKey]: property
23839
+ };
23840
+ if (newPropertiesOrder) collection.propertiesOrder = newPropertiesOrder;
23841
+ const next = [...prev];
23842
+ next[idx] = collection;
23843
+ persistCollection(next[idx]).catch((e) => console.error("useJsonCollectionsConfigController: failed to save property", e));
23844
+ return next;
23845
+ });
23846
+ }, [persistCollection]);
23847
+ const deleteProperty = useCallback(async ({ path, propertyKey, newPropertiesOrder }) => {
23848
+ setCollections((prev) => {
23849
+ const idx = prev.findIndex((c) => c.slug === path);
23850
+ if (idx < 0) return prev;
23851
+ const collection = { ...prev[idx] };
23852
+ const { [propertyKey]: _removed, ...remaining } = collection.properties;
23853
+ collection.properties = remaining;
23854
+ if (newPropertiesOrder) collection.propertiesOrder = newPropertiesOrder;
23855
+ const next = [...prev];
23856
+ next[idx] = collection;
23857
+ persistCollection(next[idx]).catch((e) => console.error("useJsonCollectionsConfigController: failed to delete property", e));
23858
+ return next;
23859
+ });
23860
+ }, [persistCollection]);
23861
+ const updatePropertiesOrder = useCallback(async ({ collection, newPropertiesOrder }) => {
23862
+ const updated = {
23863
+ ...collection,
23864
+ propertiesOrder: newPropertiesOrder
23865
+ };
23866
+ setCollections((prev) => {
23867
+ const idx = prev.findIndex((c) => c.slug === collection.slug);
23868
+ if (idx < 0) return prev;
23869
+ const next = [...prev];
23870
+ next[idx] = updated;
23871
+ return next;
23872
+ });
23873
+ await persistCollection(updated);
23874
+ }, [persistCollection]);
23875
+ const updateKanbanColumnsOrder = useCallback(async ({ collection, kanbanColumnProperty, newColumnsOrder }) => {}, []);
23876
+ const saveNavigationEntriesHandler = useCallback(async (entries) => {
23877
+ setNavigationEntries(entries);
23878
+ if (storeRef.current.saveNavigationEntries) await storeRef.current.saveNavigationEntries(entries);
23879
+ }, []);
23880
+ return useMemo(() => ({
23881
+ loading,
23882
+ readOnly,
23883
+ readOnlyReason: readOnly ? "Configuration is read-only." : void 0,
23884
+ collections,
23885
+ getCollection,
23886
+ saveCollection,
23887
+ updateCollection,
23888
+ deleteCollection,
23889
+ saveProperty,
23890
+ deleteProperty,
23891
+ updatePropertiesOrder,
23892
+ updateKanbanColumnsOrder,
23893
+ navigationEntries,
23894
+ saveNavigationEntries: saveNavigationEntriesHandler
23895
+ }), [
23896
+ loading,
23897
+ readOnly,
23898
+ collections,
23899
+ getCollection,
23900
+ saveCollection,
23901
+ updateCollection,
23902
+ deleteCollection,
23903
+ saveProperty,
23904
+ deleteProperty,
23905
+ updatePropertiesOrder,
23906
+ updateKanbanColumnsOrder,
23907
+ navigationEntries,
23908
+ saveNavigationEntriesHandler
23909
+ ]);
23910
+ }
23911
+ //#endregion
23608
23912
  //#region src/collection_editor/validateCollectionJson.ts
23609
23913
  /**
23610
23914
  * Valid dataType values for properties
@@ -24005,6 +24309,6 @@ function getFullIdPath(propertyKey, propertyNamespace) {
24005
24309
  return idToPropertiesPath(propertyNamespace ? `${propertyNamespace}.${propertyKey}` : propertyKey);
24006
24310
  }
24007
24311
  //#endregion
24008
- export { useApp as $, getBracketNotation as $n, SideDialogs as $t, KeyValuePreview as A, useSideDialogsController as An, RepeatFieldBinding as At, RebaseRouteDefs as B, SelectableTableContext as Bn, ArrayContainer as Bt, zodToFormErrors as C, useCMSContext as Cn, getFieldConfig as Ct, NumberPropertyPreview as D, useNavigationStateController as Dn, SwitchFieldBinding as Dt, UserPreview as E, NavigationStateContext as En, TextFieldBinding as Et, ArrayEnumPreview as F, useSelectionController as Fn, MapFieldBinding as Ft, useLocalCollectionsConfigController as G, resolveEntityAction as Gn, FormEntry as Gt, RebaseRoute as H, useSideEntityController as Hn, PropertyFieldBinding as Ht, ArrayOfStorageComponentsPreview as I, EntityCollectionTable as In, KeyValueFieldBinding as It, Scaffold as J, ArrayOfReferencesPreview as Jn, LabelWithIcon as Jt, RebaseAuthGate as K, resolveEntityView as Kn, LabelWithIconAndTooltip as Kt, RelationPreview as L, SelectableTable as Ln, DateTimeFieldBinding as Lt, ArrayOneOfPreview as M, getEntityViewWidth as Mn, ReferenceAsStringFieldBinding as Mt, ArrayOfStringsPreview as N, useBuildSideEntityController as Nn, MultiSelectFieldBinding as Nt, BooleanPreview as O, CollectionRegistryContext as On, StorageUploadFieldBinding as Ot, ArrayPropertyEnumPreview as P, mergeEntityActions as Pn, MarkdownEditorFieldBinding as Pt, AppBar as Q, getEntityTitlePropertyKey as Qn, EntitySelectionTable as Qt, CollectionPanel as R, EntityCollectionRowActions as Rn, BlockFieldBinding as Rt, EntityForm as S, useResolvedNavigationFrom as Sn, getDefaultFieldId as St, PropertyPreview as T, useUrlController as Tn, VectorFieldBinding as Tt, RebaseLayout as U, useEntitySelectionDialog as Un, ReadOnlyFieldBinding as Ut, CustomViewRoute as V, SideEntityControllerContext as Vn, ArrayCustomShapedFieldBinding as Vt, RebaseNavigation as W, VirtualTableInput as Wn, FormLayout as Wt, DefaultDrawer as X, EntityPreview as Xn, SearchIconsView as Xt, Drawer as Y, ReferencePreview as Yn, FieldHelperText as Yt, DrawerFooterActions as Z, getEntityPreviewKeys as Zn, FieldCaption as Zt, getEntityCSVExportableData as _, removeTrailingSlash as _n, renderSkeletonText as _r, convertFileToJson as _t, namespaceToPropertiesPath as a, useResolvedCollections as an, getResolvedPropertyInPath as ar, EntityCard as at, ImportDataPreview as b, useEntityHistory as bn, ImagePreview as br, DEFAULT_FIELD_CONFIGS as bt, DEFAULT_COLLECTION_GENERATION_ENDPOINT as c, editEntityAction as cn, ArrayPropertyPreview as cr, ImportNewPropertyFieldPreview as ct, useCollectionsConfigController as d, addInitialSlash as dn, StorageThumbnail as dr, flattenEntry as dt, useBreadcrumbsController as en, getDefaultPropertiesOrder as er, AdminModeSyncer as et, ExportCollectionAction as f, getCollectionBySlugWithin as fn, StorageThumbnailInternal as fr, processValueMapping as ft, downloadEntitiesExport as g, removeInitialSlash as gn, renderSkeletonImageThumbnail as gr, ImportFileUpload as gt, downloadDataAsCsv as h, removeInitialAndTrailingSlashes$1 as hn, renderSkeletonIcon as hr, ImportSaveInProgress as ht, namespaceToPropertiesOrderPath as i, useResolvedViews as in, getPropertyInPath as ir, EntityCollectionCardView as it, MapPropertyPreview as j, buildSidePanelsFromUrl as jn, ReferenceFieldBinding as jt, DatePreview as k, useCollectionRegistryController as kn, SelectFieldBinding as kt, buildCollectionGenerationCallback as l, resetPasswordAction as ln, StringPropertyPreview as lr, DataNewPropertiesMapping as lt, downloadBlob as m, getLastSegment$1 as mn, renderSkeletonCaptionText as mr, useImportConfig as mt, getFullIdPath as n, useBuildNavigationStateController as nn, getIconForWidget as nr, ReferenceWidget as nt, MissingReferenceWidget as o, copyEntityAction as on, isReferenceProperty as or, EntityCollectionViewActions as ot, BasicExportAction as p, getCollectionPathsCombinations as pn, SkeletonPropertyComponent as pr, getInferenceType as pt, RebaseCMS as q, EntityView as qn, PropertyIdCopyTooltip as qt, idToPropertiesPath as r, useTopLevelNavigation as rn, getPropertiesWithPropertiesOrder as rr, EntityCollectionView as rt, CollectionGenerationApiError as s, deleteEntityAction as sn, isRelationProperty as sr, useCollectionEditorController as st, getFullId as t, useBuildUrlController as tn, getIconForProperty as tr, SideEntityProvider as tt, validateCollectionJson as u, CreationResultDialog as un, EnumValuesChip as ur, convertDataToEntity as ut, getEntityJsonExportableData as v, resolveCollectionPathIds$1 as vn, UrlComponentPreview as vr, unflattenObject as vt, ArrayOfMapsPreview as w, UrlContext as wn, getFieldId as wt, PropertySelectEntry as x, resolveNavigationFrom as xn, EmptyValue as xr, getDefaultFieldConfig as xt, ImportCollectionAction as y, useBuildCollectionRegistryController as yn, sanitizeUrl as yr, PropertyConfigBadge as yt, RebaseShell as z, useClearRestoreValue as zn, ArrayOfReferencesFieldBinding as zt };
24312
+ export { RelationPreview as $, SelectableTable as $n, DateTimeFieldBinding as $t, useCollectionDataController as A, getLastSegment$1 as An, renderSkeletonCaptionText as Ar, useImportConfig as At, ArrayOfMapsPreview as B, UrlContext as Bn, getFieldId as Bt, downloadEntitiesExport as C, deleteEntityAction as Cn, isRelationProperty as Cr, useCollectionEditorController as Ct, ImportDataPreview as D, addInitialSlash as Dn, StorageThumbnail as Dr, flattenEntry as Dt, ImportCollectionAction as E, CreationResultDialog as En, EnumValuesChip as Er, convertDataToEntity as Et, extractTouchedValues as F, useBuildCollectionRegistryController as Fn, sanitizeUrl as Fr, PropertyConfigBadge as Ft, DatePreview as G, useCollectionRegistryController as Gn, SelectFieldBinding as Gt, UserPreview as H, NavigationStateContext as Hn, TextFieldBinding as Ht, getChanges as I, useEntityHistory as In, ImagePreview as Ir, DEFAULT_FIELD_CONFIGS as It, ArrayOneOfPreview as J, getEntityViewWidth as Jn, ReferenceAsStringFieldBinding as Jt, KeyValuePreview as K, useSideDialogsController as Kn, RepeatFieldBinding as Kt, getInitialEntityValues as L, resolveNavigationFrom as Ln, EmptyValue as Lr, getDefaultFieldConfig as Lt, mapPropertyToConfig as M, removeInitialSlash as Mn, renderSkeletonImageThumbnail as Mr, ImportFileUpload as Mt, EntityFormBinding as N, removeTrailingSlash as Nn, renderSkeletonText as Nr, convertFileToJson as Nt, PropertySelectEntry as O, getCollectionBySlugWithin as On, StorageThumbnailInternal as Or, processValueMapping as Ot, EntityForm as P, resolveCollectionPathIds$1 as Pn, UrlComponentPreview as Pr, unflattenObject as Pt, ArrayOfStorageComponentsPreview as Q, EntityCollectionTable as Qn, KeyValueFieldBinding as Qt, removeEmptyContainers as R, useResolvedNavigationFrom as Rn, getDefaultFieldId as Rt, downloadDataAsCsv as S, copyEntityAction as Sn, isReferenceProperty as Sr, EntityCollectionViewActions as St, getEntityJsonExportableData as T, resetPasswordAction as Tn, StringPropertyPreview as Tr, DataNewPropertiesMapping as Tt, NumberPropertyPreview as U, useNavigationStateController as Un, SwitchFieldBinding as Ut, PropertyPreview as V, useUrlController as Vn, VectorFieldBinding as Vt, BooleanPreview as W, CollectionRegistryContext as Wn, StorageUploadFieldBinding as Wt, ArrayPropertyEnumPreview as X, mergeEntityActions as Xn, MarkdownEditorFieldBinding as Xt, ArrayOfStringsPreview as Y, useBuildSideEntityController as Yn, MultiSelectFieldBinding as Yt, ArrayEnumPreview as Z, useSelectionController as Zn, MapFieldBinding as Zt, toSerializableProperty as _, useBuildUrlController as _n, getIconForProperty as _r, SideEntityProvider as _t, namespaceToPropertiesPath as a, ReadOnlyFieldBinding as an, useEntitySelectionDialog as ar, RebaseLayout as at, BasicExportAction as b, useResolvedViews as bn, getPropertyInPath as br, EntityCollectionCardView as bt, DEFAULT_COLLECTION_GENERATION_ENDPOINT as c, LabelWithIconAndTooltip as cn, resolveEntityView as cr, RebaseAuthGate as ct, useJsonCollectionsConfigController as d, FieldHelperText as dn, ReferencePreview as dr, Drawer as dt, BlockFieldBinding as en, EntityCollectionRowActions as er, CollectionPanel as et, fromSerializableCollection as f, SearchIconsView as fn, EntityPreview as fr, DefaultDrawer as ft, toSerializableProperties as g, useBreadcrumbsController as gn, getDefaultPropertiesOrder as gr, AdminModeSyncer as gt, toSerializableCollection as h, SideDialogs as hn, getBracketNotation as hr, useApp as ht, namespaceToPropertiesOrderPath as i, PropertyFieldBinding as in, useSideEntityController as ir, RebaseRoute as it, mapPropertiesToConfigs as j, removeInitialAndTrailingSlashes$1 as jn, renderSkeletonIcon as jr, ImportSaveInProgress as jt, createStaticDataController as k, getCollectionPathsCombinations as kn, SkeletonPropertyComponent as kr, getInferenceType as kt, buildCollectionGenerationCallback as l, PropertyIdCopyTooltip as ln, EntityView as lr, RebaseCMS as lt, fromSerializableProperty as m, EntitySelectionTable as mn, getEntityTitlePropertyKey as mr, AppBar as mt, getFullIdPath as n, ArrayContainer as nn, SelectableTableContext as nr, RebaseRouteDefs as nt, MissingReferenceWidget as o, FormLayout as on, VirtualTableInput as or, RebaseNavigation as ot, fromSerializableProperties as p, FieldCaption as pn, getEntityPreviewKeys as pr, DrawerFooterActions as pt, MapPropertyPreview as q, buildSidePanelsFromUrl as qn, ReferenceFieldBinding as qt, idToPropertiesPath as r, ArrayCustomShapedFieldBinding as rn, SideEntityControllerContext as rr, CustomViewRoute as rt, CollectionGenerationApiError as s, FormEntry as sn, resolveEntityAction as sr, useLocalCollectionsConfigController as st, getFullId as t, ArrayOfReferencesFieldBinding as tn, useClearRestoreValue as tr, RebaseShell as tt, validateCollectionJson as u, LabelWithIcon as un, ArrayOfReferencesPreview as ur, Scaffold as ut, useCollectionsConfigController as v, useBuildNavigationStateController as vn, getIconForWidget as vr, ReferenceWidget as vt, getEntityCSVExportableData as w, editEntityAction as wn, ArrayPropertyPreview as wr, ImportNewPropertyFieldPreview as wt, downloadBlob as x, useResolvedCollections as xn, getResolvedPropertyInPath as xr, EntityCard as xt, ExportCollectionAction as y, useTopLevelNavigation as yn, getPropertiesWithPropertiesOrder as yr, EntityCollectionView as yt, zodToFormErrors as z, useCMSContext as zn, getFieldConfig as zt };
24009
24313
 
24010
- //# sourceMappingURL=src-BJ8fkM8y.js.map
24314
+ //# sourceMappingURL=src-DOX2nOyh.js.map