@rebasepro/cms 0.17.3 → 0.18.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 (449) hide show
  1. package/README.md +4 -0
  2. package/dist/{CollectionEditorDialog-B-akreOU.js → CollectionEditorDialog-BeX4b92r.js} +13 -7
  3. package/dist/{CollectionEditorDialog-B-akreOU.js.map → CollectionEditorDialog-BeX4b92r.js.map} +1 -1
  4. package/dist/{PropertyEditView-CiY84wkR.js → PropertyEditView-C7thfj4L.js} +3 -3
  5. package/dist/{PropertyEditView-CiY84wkR.js.map → PropertyEditView-C7thfj4L.js.map} +1 -1
  6. package/dist/{RouterCollectionsStudioView-DkAWda4N.js → RouterCollectionsStudioView-BtjzvB6E.js} +4 -4
  7. package/dist/{RouterCollectionsStudioView-DkAWda4N.js.map → RouterCollectionsStudioView-BtjzvB6E.js.map} +1 -1
  8. package/dist/{SchemaChangeDialog-DceT8QFX.js → SchemaChangeDialog-CkqK6KOC.js} +4 -2
  9. package/dist/{SchemaChangeDialog-DceT8QFX.js.map → SchemaChangeDialog-CkqK6KOC.js.map} +1 -1
  10. package/dist/collection_editor/api/generateCollectionApi.d.ts +1 -2
  11. package/dist/collection_editor_ui.d.ts +2 -0
  12. package/dist/collection_editor_ui.js +6 -5
  13. package/dist/collection_editor_ui.js.map +1 -1
  14. package/dist/components/DeleteEntityDialog.d.ts +2 -3
  15. package/dist/components/RebaseLayout.d.ts +11 -4
  16. package/dist/components/field_configs.d.ts +1 -1
  17. package/dist/editor/sanitize-html.d.ts +15 -0
  18. package/dist/editor.js +195 -10
  19. package/dist/editor.js.map +1 -1
  20. package/dist/{export-Ben8E355.js → export--arni_VY.js} +2 -2
  21. package/dist/{export-Ben8E355.js.map → export--arni_VY.js.map} +1 -1
  22. package/dist/form/components/LabelWithIcon.d.ts +10 -0
  23. package/dist/{history-DIEVd26f.js → history-C2TpoQwi.js} +2 -2
  24. package/dist/{history-DIEVd26f.js.map → history-C2TpoQwi.js.map} +1 -1
  25. package/dist/hooks/navigation/useBuildNavigationStateController.d.ts +1 -1
  26. package/dist/hooks/navigation/useResolvedViews.d.ts +1 -1
  27. package/dist/hooks/navigation/useTopLevelNavigation.d.ts +1 -1
  28. package/dist/{import-D25x7ko-.js → import-DTkXNoBg.js} +2 -2
  29. package/dist/{import-D25x7ko-.js.map → import-DTkXNoBg.js.map} +1 -1
  30. package/dist/index.js +78 -33
  31. package/dist/index.js.map +1 -1
  32. package/dist/util/propertyConfigMapper.d.ts +1 -1
  33. package/dist/util-CfLFggP7.js +57 -0
  34. package/dist/util-CfLFggP7.js.map +1 -0
  35. package/dist/{util-DVqnqlLn.js → util-DE1WQW6h.js} +134 -112
  36. package/dist/util-DE1WQW6h.js.map +1 -0
  37. package/package.json +42 -34
  38. package/dist/util-DVqnqlLn.js.map +0 -1
  39. package/src/collection_editor/CollectionEditorDialogsContext.tsx +0 -25
  40. package/src/collection_editor/ConfigControllerProvider.tsx +0 -395
  41. package/src/collection_editor/_cms_internals.ts +0 -41
  42. package/src/collection_editor/api/generateCollectionApi.ts +0 -126
  43. package/src/collection_editor/api/index.ts +0 -1
  44. package/src/collection_editor/extensibility_types.ts +0 -168
  45. package/src/collection_editor/index.ts +0 -108
  46. package/src/collection_editor/liveSchemaClient.ts +0 -285
  47. package/src/collection_editor/relation_kinds.ts +0 -44
  48. package/src/collection_editor/serializable_types.ts +0 -535
  49. package/src/collection_editor/serializable_utils.ts +0 -729
  50. package/src/collection_editor/types/collection_editor_controller.tsx +0 -62
  51. package/src/collection_editor/types/collection_inference.ts +0 -18
  52. package/src/collection_editor/types/config_controller.tsx +0 -121
  53. package/src/collection_editor/ui/AddKanbanColumnAction.tsx +0 -199
  54. package/src/collection_editor/ui/CollectionViewHeaderAction.tsx +0 -51
  55. package/src/collection_editor/ui/EditorCollectionAction.tsx +0 -54
  56. package/src/collection_editor/ui/EditorCollectionActionStart.tsx +0 -90
  57. package/src/collection_editor/ui/EditorEntityAction.tsx +0 -50
  58. package/src/collection_editor/ui/HomePageEditorCollectionAction.tsx +0 -114
  59. package/src/collection_editor/ui/KanbanSetupAction.tsx +0 -43
  60. package/src/collection_editor/ui/MissingReferenceWidget.tsx +0 -35
  61. package/src/collection_editor/ui/NewCollectionButton.tsx +0 -27
  62. package/src/collection_editor/ui/NewCollectionCard.tsx +0 -48
  63. package/src/collection_editor/ui/PropertyAddColumnComponent.tsx +0 -49
  64. package/src/collection_editor/ui/collection_editor/AICollectionGeneratorPopover.tsx +0 -260
  65. package/src/collection_editor/ui/collection_editor/AIModifiedPathsContext.tsx +0 -90
  66. package/src/collection_editor/ui/collection_editor/CollectionDetailsForm.tsx +0 -388
  67. package/src/collection_editor/ui/collection_editor/CollectionEditorDialog.tsx +0 -1098
  68. package/src/collection_editor/ui/collection_editor/CollectionEditorWelcomeView.tsx +0 -301
  69. package/src/collection_editor/ui/collection_editor/CollectionJsonImportDialog.tsx +0 -172
  70. package/src/collection_editor/ui/collection_editor/CollectionPropertiesEditorForm.tsx +0 -620
  71. package/src/collection_editor/ui/collection_editor/CollectionRLSTab.tsx +0 -530
  72. package/src/collection_editor/ui/collection_editor/CollectionRelationsTab.tsx +0 -462
  73. package/src/collection_editor/ui/collection_editor/CollectionStudioView.tsx +0 -104
  74. package/src/collection_editor/ui/collection_editor/CollectionYupValidation.tsx +0 -9
  75. package/src/collection_editor/ui/collection_editor/CollectionsStudioView.tsx +0 -199
  76. package/src/collection_editor/ui/collection_editor/DisplaySettingsForm.tsx +0 -315
  77. package/src/collection_editor/ui/collection_editor/EntityActionsEditTab.tsx +0 -164
  78. package/src/collection_editor/ui/collection_editor/EntityActionsSelectDialog.tsx +0 -40
  79. package/src/collection_editor/ui/collection_editor/EntityCustomViewsSelectDialog.tsx +0 -39
  80. package/src/collection_editor/ui/collection_editor/EnumForm.tsx +0 -373
  81. package/src/collection_editor/ui/collection_editor/GeneralSettingsForm.tsx +0 -288
  82. package/src/collection_editor/ui/collection_editor/GetCodeDialog.tsx +0 -141
  83. package/src/collection_editor/ui/collection_editor/KanbanConfigSection.tsx +0 -194
  84. package/src/collection_editor/ui/collection_editor/LayoutModeSwitch.tsx +0 -53
  85. package/src/collection_editor/ui/collection_editor/PropertyEditView.tsx +0 -980
  86. package/src/collection_editor/ui/collection_editor/PropertyFieldPreview.tsx +0 -164
  87. package/src/collection_editor/ui/collection_editor/PropertyTree.tsx +0 -364
  88. package/src/collection_editor/ui/collection_editor/RouterCollectionStudioView.tsx +0 -44
  89. package/src/collection_editor/ui/collection_editor/RouterCollectionsStudioView.tsx +0 -41
  90. package/src/collection_editor/ui/collection_editor/SchemaChangeDialog.tsx +0 -470
  91. package/src/collection_editor/ui/collection_editor/SubcollectionsEditTab.tsx +0 -256
  92. package/src/collection_editor/ui/collection_editor/SwitchControl.tsx +0 -39
  93. package/src/collection_editor/ui/collection_editor/ViewModeSwitch.tsx +0 -68
  94. package/src/collection_editor/ui/collection_editor/import/CollectionEditorImportDataPreview.tsx +0 -67
  95. package/src/collection_editor/ui/collection_editor/import/CollectionEditorImportMapping.tsx +0 -296
  96. package/src/collection_editor/ui/collection_editor/import/clean_import_data.ts +0 -57
  97. package/src/collection_editor/ui/collection_editor/properties/BlockPropertyField.tsx +0 -147
  98. package/src/collection_editor/ui/collection_editor/properties/BooleanPropertyField.tsx +0 -41
  99. package/src/collection_editor/ui/collection_editor/properties/CommonPropertyFields.tsx +0 -113
  100. package/src/collection_editor/ui/collection_editor/properties/DateTimePropertyField.tsx +0 -250
  101. package/src/collection_editor/ui/collection_editor/properties/EnumPropertyField.tsx +0 -126
  102. package/src/collection_editor/ui/collection_editor/properties/KeyValuePropertyField.tsx +0 -20
  103. package/src/collection_editor/ui/collection_editor/properties/MapPropertyField.tsx +0 -153
  104. package/src/collection_editor/ui/collection_editor/properties/MarkdownPropertyField.tsx +0 -217
  105. package/src/collection_editor/ui/collection_editor/properties/NumberPropertyField.tsx +0 -117
  106. package/src/collection_editor/ui/collection_editor/properties/ReferencePropertyField.tsx +0 -129
  107. package/src/collection_editor/ui/collection_editor/properties/RelationPropertyField.tsx +0 -335
  108. package/src/collection_editor/ui/collection_editor/properties/RepeatPropertyField.tsx +0 -107
  109. package/src/collection_editor/ui/collection_editor/properties/StoragePropertyField.tsx +0 -403
  110. package/src/collection_editor/ui/collection_editor/properties/StringPropertyField.tsx +0 -148
  111. package/src/collection_editor/ui/collection_editor/properties/UrlPropertyField.tsx +0 -91
  112. package/src/collection_editor/ui/collection_editor/properties/VectorPropertyField.tsx +0 -34
  113. package/src/collection_editor/ui/collection_editor/properties/advanced/AdvancedPropertyValidation.tsx +0 -49
  114. package/src/collection_editor/ui/collection_editor/properties/conditions/ConditionsEditor.tsx +0 -925
  115. package/src/collection_editor/ui/collection_editor/properties/conditions/ConditionsPanel.tsx +0 -28
  116. package/src/collection_editor/ui/collection_editor/properties/conditions/EnumConditionsEditor.tsx +0 -626
  117. package/src/collection_editor/ui/collection_editor/properties/conditions/index.ts +0 -6
  118. package/src/collection_editor/ui/collection_editor/properties/conditions/property_paths.ts +0 -93
  119. package/src/collection_editor/ui/collection_editor/properties/validation/ArrayPropertyValidation.tsx +0 -52
  120. package/src/collection_editor/ui/collection_editor/properties/validation/GeneralPropertyValidation.tsx +0 -63
  121. package/src/collection_editor/ui/collection_editor/properties/validation/NumberPropertyValidation.tsx +0 -114
  122. package/src/collection_editor/ui/collection_editor/properties/validation/StringPropertyValidation.tsx +0 -154
  123. package/src/collection_editor/ui/collection_editor/properties/validation/ValidationPanel.tsx +0 -28
  124. package/src/collection_editor/ui/collection_editor/templates/blog_template.ts +0 -112
  125. package/src/collection_editor/ui/collection_editor/templates/pages_template.ts +0 -183
  126. package/src/collection_editor/ui/collection_editor/templates/products_template.ts +0 -88
  127. package/src/collection_editor/ui/collection_editor/templates/users_template.ts +0 -42
  128. package/src/collection_editor/ui/collection_editor/util.ts +0 -28
  129. package/src/collection_editor/ui/collection_editor/utils/strings.ts +0 -1
  130. package/src/collection_editor/ui/collection_editor/utils/supported_fields.tsx +0 -39
  131. package/src/collection_editor/ui/collection_editor/utils/update_property_for_widget.ts +0 -276
  132. package/src/collection_editor/useCollectionEditorController.tsx +0 -9
  133. package/src/collection_editor/useCollectionsConfigController.tsx +0 -9
  134. package/src/collection_editor/useJsonCollectionsConfigController.tsx +0 -297
  135. package/src/collection_editor/useLiveSchemaEditing.tsx +0 -315
  136. package/src/collection_editor/useLocalCollectionsConfigController.tsx +0 -341
  137. package/src/collection_editor/useSafeSnackbarController.ts +0 -9
  138. package/src/collection_editor/validateCollectionJson.ts +0 -440
  139. package/src/collection_editor_ui.ts +0 -21
  140. package/src/components/AdditionalFieldValue.tsx +0 -68
  141. package/src/components/AdminModeSyncer.tsx +0 -56
  142. package/src/components/ArrayContainer.tsx +0 -542
  143. package/src/components/ClearFilterSortButton.tsx +0 -56
  144. package/src/components/CollectionEditorDialogs.tsx +0 -69
  145. package/src/components/CollectionPanel.tsx +0 -179
  146. package/src/components/CollectionTableBinding/CollectionRowActions.tsx +0 -244
  147. package/src/components/CollectionTableBinding/CollectionTableBinding.tsx +0 -394
  148. package/src/components/CollectionTableBinding/CollectionTableBindingProps.tsx +0 -172
  149. package/src/components/CollectionTableBinding/PropertyTableCell.tsx +0 -350
  150. package/src/components/CollectionTableBinding/column_utils.tsx +0 -155
  151. package/src/components/CollectionTableBinding/fields/TableMultipleRelationField.tsx +0 -131
  152. package/src/components/CollectionTableBinding/fields/TableReferenceField.tsx +0 -193
  153. package/src/components/CollectionTableBinding/fields/TableRelationField.tsx +0 -192
  154. package/src/components/CollectionTableBinding/fields/TableRelationSelectorField.tsx +0 -42
  155. package/src/components/CollectionTableBinding/fields/TableStorageUpload.tsx +0 -324
  156. package/src/components/CollectionTableBinding/fields/VirtualTableSelect.tsx +0 -132
  157. package/src/components/CollectionTableBinding/fields/VirtualTableUserSelect.tsx +0 -41
  158. package/src/components/CollectionTableBinding/index.tsx +0 -14
  159. package/src/components/CollectionTableBinding/internal/CollectionTableToolbar.tsx +0 -96
  160. package/src/components/CollectionTableBinding/internal/EntityTableCell.tsx +0 -309
  161. package/src/components/CollectionTableBinding/internal/EntityTableCellActions.tsx +0 -80
  162. package/src/components/CollectionTableBinding/internal/common.tsx +0 -74
  163. package/src/components/CollectionTableBinding/internal/popup_field/PopupFormField.tsx +0 -422
  164. package/src/components/CollectionTableBinding/internal/popup_field/useDraggable.tsx +0 -99
  165. package/src/components/CollectionTableBinding/internal/popup_field/useWindowSize.tsx +0 -22
  166. package/src/components/CollectionTableBinding/table_bindings.tsx +0 -350
  167. package/src/components/CollectionViewBinding/BoardCardBinding.tsx +0 -263
  168. package/src/components/CollectionViewBinding/CollectionBoardViewBinding.tsx +0 -662
  169. package/src/components/CollectionViewBinding/CollectionCardViewBinding.tsx +0 -159
  170. package/src/components/CollectionViewBinding/CollectionListViewBinding.tsx +0 -1430
  171. package/src/components/CollectionViewBinding/CollectionViewActions.tsx +0 -227
  172. package/src/components/CollectionViewBinding/CollectionViewBinding.tsx +0 -1603
  173. package/src/components/CollectionViewBinding/CollectionViewStartActions.tsx +0 -240
  174. package/src/components/CollectionViewBinding/EntityCardBinding.tsx +0 -239
  175. package/src/components/CollectionViewBinding/FilterPresetsButton.tsx +0 -298
  176. package/src/components/CollectionViewBinding/FiltersDialog.tsx +0 -218
  177. package/src/components/CollectionViewBinding/SearchExplanation.tsx +0 -106
  178. package/src/components/CollectionViewBinding/SearchHighlight.tsx +0 -311
  179. package/src/components/CollectionViewBinding/SlotValue.tsx +0 -156
  180. package/src/components/CollectionViewBinding/SortButton.tsx +0 -354
  181. package/src/components/CollectionViewBinding/SplitListCloseButton.tsx +0 -49
  182. package/src/components/CollectionViewBinding/SplitListShowButton.tsx +0 -28
  183. package/src/components/CollectionViewBinding/SplitListView.tsx +0 -488
  184. package/src/components/CollectionViewBinding/SplitViewContext.tsx +0 -34
  185. package/src/components/CollectionViewBinding/ViewModeToggle.tsx +0 -250
  186. package/src/components/CollectionViewBinding/hooks/useCollectionInlineEditor.ts +0 -81
  187. package/src/components/CollectionViewBinding/hooks/useKanbanDragAndDrop.ts +0 -181
  188. package/src/components/CollectionViewBinding/useBoardDataController.tsx +0 -802
  189. package/src/components/CollectionViewBinding/usePreviewSlots.ts +0 -703
  190. package/src/components/CollectionViewBinding/useSelectionController.tsx +0 -40
  191. package/src/components/CollectionViewBinding/utils.ts +0 -22
  192. package/src/components/DefaultAppBar.tsx +0 -264
  193. package/src/components/DefaultDrawer.tsx +0 -593
  194. package/src/components/DeleteEntityDialog.tsx +0 -205
  195. package/src/components/DetailViewBinding.tsx +0 -731
  196. package/src/components/DrawerNavigationGroup.tsx +0 -141
  197. package/src/components/DrawerNavigationItem.tsx +0 -115
  198. package/src/components/EditFormActions.tsx +0 -303
  199. package/src/components/EditViewBinding.tsx +0 -1069
  200. package/src/components/EntityDisplayHeader.tsx +0 -170
  201. package/src/components/EntityIdentityBar.tsx +0 -428
  202. package/src/components/EntityInspector.tsx +0 -156
  203. package/src/components/EntityPreviewBinding.tsx +0 -432
  204. package/src/components/EntityPreviewNesting.tsx +0 -50
  205. package/src/components/EntityViewBinding.tsx +0 -374
  206. package/src/components/FieldCaption.tsx +0 -14
  207. package/src/components/HomePage/ContentHomePage.tsx +0 -586
  208. package/src/components/HomePage/FavouritesView.tsx +0 -60
  209. package/src/components/HomePage/HomePageDnD.tsx +0 -704
  210. package/src/components/HomePage/NavigationCard.tsx +0 -81
  211. package/src/components/HomePage/NavigationCardBinding.tsx +0 -107
  212. package/src/components/HomePage/NavigationGroup.tsx +0 -156
  213. package/src/components/HomePage/RenameGroupDialog.tsx +0 -123
  214. package/src/components/HomePage/SmallNavigationCard.tsx +0 -41
  215. package/src/components/HomePage/index.tsx +0 -5
  216. package/src/components/InlineEntityPreview.tsx +0 -149
  217. package/src/components/JsonPreviewBinding.tsx +0 -66
  218. package/src/components/PropertyCollectionView.tsx +0 -336
  219. package/src/components/PropertyConfigBadge.tsx +0 -28
  220. package/src/components/PropertyIdCopyTooltip.tsx +0 -47
  221. package/src/components/RebaseAuthGate.tsx +0 -44
  222. package/src/components/RebaseCMS.tsx +0 -30
  223. package/src/components/RebaseLayout.tsx +0 -80
  224. package/src/components/RebaseNavigation.tsx +0 -369
  225. package/src/components/RebaseRouteDefs.tsx +0 -127
  226. package/src/components/RebaseShell.tsx +0 -83
  227. package/src/components/ReferenceTable/SelectionTableBinding.tsx +0 -389
  228. package/src/components/ReferenceWidget.tsx +0 -156
  229. package/src/components/RelationSelector.tsx +0 -926
  230. package/src/components/SearchIconsView.tsx +0 -78
  231. package/src/components/SelectableTable/SelectableTable.tsx +0 -309
  232. package/src/components/SelectableTable/SelectableTableContext.tsx +0 -7
  233. package/src/components/SelectableTable/SelectionStore.ts +0 -43
  234. package/src/components/SelectableTable/filters/BooleanFilterField.tsx +0 -59
  235. package/src/components/SelectableTable/filters/DateTimeFilterField.tsx +0 -146
  236. package/src/components/SelectableTable/filters/FilterFieldBinding.tsx +0 -190
  237. package/src/components/SelectableTable/filters/ReferenceFilterField.tsx +0 -250
  238. package/src/components/SelectableTable/filters/RelationFilterField.tsx +0 -238
  239. package/src/components/SelectableTable/filters/StringNumberFilterField.tsx +0 -282
  240. package/src/components/SelectableTable/filters/null_filter.ts +0 -63
  241. package/src/components/SideDialogs.tsx +0 -238
  242. package/src/components/SidePanelBinding.tsx +0 -361
  243. package/src/components/SidePanelProvider.tsx +0 -80
  244. package/src/components/UserSelector.tsx +0 -513
  245. package/src/components/admin/CreationResultDialog.tsx +0 -138
  246. package/src/components/admin/index.ts +0 -1
  247. package/src/components/app/AppBar.tsx +0 -17
  248. package/src/components/app/Drawer.tsx +0 -38
  249. package/src/components/app/Scaffold.tsx +0 -366
  250. package/src/components/app/index.ts +0 -4
  251. package/src/components/app/useApp.tsx +0 -36
  252. package/src/components/common/default_entity_actions.tsx +0 -440
  253. package/src/components/common/index.ts +0 -1
  254. package/src/components/field_configs.tsx +0 -506
  255. package/src/components/history/EntityHistoryEntry.tsx +0 -203
  256. package/src/components/history/EntityHistoryView.tsx +0 -206
  257. package/src/components/history/LastEditedByFormAction.tsx +0 -24
  258. package/src/components/history/LastEditedByIndicator.tsx +0 -95
  259. package/src/components/history/UserChip.tsx +0 -15
  260. package/src/components/history/index.tsx +0 -5
  261. package/src/components/index.ts +0 -63
  262. package/src/contexts/BreacrumbsContext.tsx +0 -45
  263. package/src/contexts/SideDialogsControllerContext.tsx +0 -5
  264. package/src/contexts/SidePanelControllerContext.tsx +0 -5
  265. package/src/data_export/export/BasicExportAction.tsx +0 -142
  266. package/src/data_export/export/ExportCollectionAction.tsx +0 -321
  267. package/src/data_export/export/export.ts +0 -295
  268. package/src/data_export/export/fetch_export_data.ts +0 -90
  269. package/src/data_export/export/index.ts +0 -4
  270. package/src/data_export/index.ts +0 -1
  271. package/src/data_import/components/DataNewPropertiesMapping.tsx +0 -261
  272. package/src/data_import/components/ImportFileUpload.tsx +0 -42
  273. package/src/data_import/components/ImportNewPropertyFieldPreview.tsx +0 -58
  274. package/src/data_import/components/ImportSaveInProgress.tsx +0 -121
  275. package/src/data_import/components/index.ts +0 -4
  276. package/src/data_import/hooks/index.ts +0 -1
  277. package/src/data_import/hooks/useImportConfig.tsx +0 -34
  278. package/src/data_import/import/ImportCollectionAction.tsx +0 -461
  279. package/src/data_import/import/index.ts +0 -1
  280. package/src/data_import/index.ts +0 -5
  281. package/src/data_import/types/column_mapping.ts +0 -32
  282. package/src/data_import/types/index.ts +0 -1
  283. package/src/data_import/utils/csv.ts +0 -153
  284. package/src/data_import/utils/data.ts +0 -181
  285. package/src/data_import/utils/file_headers.ts +0 -14
  286. package/src/data_import/utils/file_to_json.ts +0 -149
  287. package/src/data_import/utils/get_import_inference_type.ts +0 -33
  288. package/src/data_import/utils/get_properties_mapping.ts +0 -67
  289. package/src/data_import/utils/index.ts +0 -5
  290. package/src/data_import/utils/save_entities.ts +0 -138
  291. package/src/data_import/utils/transforms.ts +0 -70
  292. package/src/editor/components/SlashCommandMenu.tsx +0 -532
  293. package/src/editor/components/editor-bubble-item.tsx +0 -32
  294. package/src/editor/components/editor-bubble.tsx +0 -143
  295. package/src/editor/components/image-bubble.tsx +0 -183
  296. package/src/editor/components/index.ts +0 -14
  297. package/src/editor/components/table-bubble.tsx +0 -233
  298. package/src/editor/editor.tsx +0 -529
  299. package/src/editor/extensions/HighlightDecorationExtension.ts +0 -115
  300. package/src/editor/extensions/Image.ts +0 -159
  301. package/src/editor/extensions/TextLoadingDecorationExtension.tsx +0 -107
  302. package/src/editor/extensions/clipboard.ts +0 -72
  303. package/src/editor/extensions/custom-keymap.ts +0 -25
  304. package/src/editor/extensions/drag-and-drop.tsx +0 -508
  305. package/src/editor/hooks/useProseMirror.ts +0 -126
  306. package/src/editor/hooks/useProseMirrorContext.ts +0 -15
  307. package/src/editor/index.ts +0 -2
  308. package/src/editor/markdown-it-plugins.d.ts +0 -24
  309. package/src/editor/markdown.ts +0 -204
  310. package/src/editor/nodeViews/ImageComponent.tsx +0 -20
  311. package/src/editor/nodeViews/ReactNodeView.tsx +0 -89
  312. package/src/editor/nodeViews/TaskItemComponent.tsx +0 -30
  313. package/src/editor/nodeViews/index.ts +0 -35
  314. package/src/editor/plugins/index.ts +0 -58
  315. package/src/editor/plugins/inputrules.ts +0 -82
  316. package/src/editor/plugins/placeholderPlugin.ts +0 -55
  317. package/src/editor/plugins/slashCommandPlugin.ts +0 -65
  318. package/src/editor/schema.ts +0 -339
  319. package/src/editor/selectors/ai-selector.tsx +0 -110
  320. package/src/editor/selectors/color-selector.tsx +0 -199
  321. package/src/editor/selectors/link-selector.tsx +0 -122
  322. package/src/editor/selectors/node-selector.tsx +0 -159
  323. package/src/editor/selectors/text-buttons.tsx +0 -79
  324. package/src/editor/types.ts +0 -6
  325. package/src/editor/useProseMirror.ts +0 -125
  326. package/src/editor/utils/prosemirror-utils.ts +0 -113
  327. package/src/editor/utils/remove_classes.ts +0 -18
  328. package/src/editor/utils/useDebouncedCallback.ts +0 -25
  329. package/src/form/EntityForm.tsx +0 -761
  330. package/src/form/EntityFormActions.tsx +0 -160
  331. package/src/form/EntityFormBinding.tsx +0 -342
  332. package/src/form/PropertyFieldBinding.tsx +0 -348
  333. package/src/form/components/ErrorFocus.tsx +0 -44
  334. package/src/form/components/FieldBlock.tsx +0 -189
  335. package/src/form/components/FieldHelperText.tsx +0 -84
  336. package/src/form/components/FormRail.tsx +0 -153
  337. package/src/form/components/FormSections.tsx +0 -145
  338. package/src/form/components/LabelWithIcon.tsx +0 -43
  339. package/src/form/components/LabelWithIconAndTooltip.tsx +0 -28
  340. package/src/form/components/LocalChangesMenu.tsx +0 -147
  341. package/src/form/components/StorageItemPreview.tsx +0 -81
  342. package/src/form/components/StorageUploadProgress.tsx +0 -118
  343. package/src/form/components/index.tsx +0 -6
  344. package/src/form/components/useRailVisible.tsx +0 -47
  345. package/src/form/field_bindings/ArrayCustomShapedFieldBinding.tsx +0 -109
  346. package/src/form/field_bindings/ArrayOfReferencesFieldBinding.tsx +0 -172
  347. package/src/form/field_bindings/BinaryFieldBinding.tsx +0 -177
  348. package/src/form/field_bindings/BlockFieldBinding.tsx +0 -282
  349. package/src/form/field_bindings/DateTimeFieldBinding.tsx +0 -78
  350. package/src/form/field_bindings/GeopointFieldBinding.tsx +0 -200
  351. package/src/form/field_bindings/KeyValueFieldBinding.tsx +0 -574
  352. package/src/form/field_bindings/MapFieldBinding.tsx +0 -120
  353. package/src/form/field_bindings/MarkdownEditorFieldBinding.tsx +0 -259
  354. package/src/form/field_bindings/MultiSelectFieldBinding.tsx +0 -130
  355. package/src/form/field_bindings/MultipleRelationFieldBinding.tsx +0 -168
  356. package/src/form/field_bindings/ReadOnlyFieldBinding.tsx +0 -94
  357. package/src/form/field_bindings/ReferenceFieldBinding.tsx +0 -146
  358. package/src/form/field_bindings/RelationFieldBinding.tsx +0 -202
  359. package/src/form/field_bindings/RepeatFieldBinding.tsx +0 -143
  360. package/src/form/field_bindings/SelectFieldBinding.tsx +0 -111
  361. package/src/form/field_bindings/StorageUploadFieldBinding.tsx +0 -521
  362. package/src/form/field_bindings/SwitchFieldBinding.tsx +0 -70
  363. package/src/form/field_bindings/TextFieldBinding.tsx +0 -189
  364. package/src/form/field_bindings/UserSelectFieldBinding.tsx +0 -71
  365. package/src/form/field_bindings/VectorFieldBinding.tsx +0 -209
  366. package/src/form/form_utils.ts +0 -243
  367. package/src/form/index.tsx +0 -36
  368. package/src/form/useClearRestoreValue.tsx +0 -36
  369. package/src/form/useUndoableDiscard.ts +0 -39
  370. package/src/form/validation.ts +0 -583
  371. package/src/hooks/index.ts +0 -16
  372. package/src/hooks/navigation/contexts/CollectionRegistryContext.tsx +0 -22
  373. package/src/hooks/navigation/contexts/NavigationStateContext.tsx +0 -16
  374. package/src/hooks/navigation/contexts/UrlContext.tsx +0 -24
  375. package/src/hooks/navigation/contexts/index.ts +0 -14
  376. package/src/hooks/navigation/useAsyncResolver.ts +0 -100
  377. package/src/hooks/navigation/useBuildCollectionRegistryController.tsx +0 -218
  378. package/src/hooks/navigation/useBuildNavigationStateController.tsx +0 -134
  379. package/src/hooks/navigation/useBuildUrlController.tsx +0 -66
  380. package/src/hooks/navigation/useNavigationResolution.ts +0 -271
  381. package/src/hooks/navigation/useResolvedCollections.ts +0 -102
  382. package/src/hooks/navigation/useResolvedViews.tsx +0 -139
  383. package/src/hooks/navigation/useTopLevelNavigation.ts +0 -273
  384. package/src/hooks/navigation/utils.ts +0 -200
  385. package/src/hooks/useAdminContext.tsx +0 -83
  386. package/src/hooks/useBreadcrumbsController.tsx +0 -17
  387. package/src/hooks/useBuildSideDialogsController.tsx +0 -145
  388. package/src/hooks/useBuildSidePanel.tsx +0 -315
  389. package/src/hooks/useChildViewSource.ts +0 -46
  390. package/src/hooks/useEntityDisplay.tsx +0 -262
  391. package/src/hooks/useEntityDisplayTitle.tsx +0 -49
  392. package/src/hooks/useHistory.ts +0 -178
  393. package/src/hooks/useRecordActions.tsx +0 -69
  394. package/src/hooks/useResolvedNavigationFrom.tsx +0 -167
  395. package/src/hooks/useResolvedUsers.ts +0 -148
  396. package/src/hooks/useSelectionDialog.tsx +0 -58
  397. package/src/hooks/useSideDialogsController.tsx +0 -25
  398. package/src/hooks/useSidePanel.tsx +0 -13
  399. package/src/index.ts +0 -124
  400. package/src/preview/PropertyPreview.tsx +0 -331
  401. package/src/preview/compact_text.ts +0 -68
  402. package/src/preview/components/ArrayEnumPreview.tsx +0 -39
  403. package/src/preview/components/BooleanPreview.tsx +0 -28
  404. package/src/preview/components/DatePreview.tsx +0 -106
  405. package/src/preview/components/EmptyValue.tsx +0 -10
  406. package/src/preview/components/EnumValuesChip.tsx +0 -39
  407. package/src/preview/components/ImagePreview.tsx +0 -144
  408. package/src/preview/components/InlineEntityListPreview.tsx +0 -42
  409. package/src/preview/components/ReferencePreview.tsx +0 -235
  410. package/src/preview/components/RelationPreview.tsx +0 -271
  411. package/src/preview/components/StorageThumbnail.tsx +0 -147
  412. package/src/preview/components/UrlComponentPreview.tsx +0 -116
  413. package/src/preview/components/UserPreview.tsx +0 -30
  414. package/src/preview/components/date_fns_locales.ts +0 -125
  415. package/src/preview/index.ts +0 -26
  416. package/src/preview/property_previews/ArrayOfMapsPreview.tsx +0 -73
  417. package/src/preview/property_previews/ArrayOfReferencesPreview.tsx +0 -67
  418. package/src/preview/property_previews/ArrayOfRelationsPreview.tsx +0 -77
  419. package/src/preview/property_previews/ArrayOfStorageComponentsPreview.tsx +0 -49
  420. package/src/preview/property_previews/ArrayOfStringsPreview.tsx +0 -70
  421. package/src/preview/property_previews/ArrayOneOfPreview.tsx +0 -79
  422. package/src/preview/property_previews/ArrayPropertyEnumPreview.tsx +0 -35
  423. package/src/preview/property_previews/ArrayPropertyPreview.tsx +0 -79
  424. package/src/preview/property_previews/MapPropertyPreview.tsx +0 -198
  425. package/src/preview/property_previews/NumberPropertyPreview.tsx +0 -39
  426. package/src/preview/property_previews/SkeletonPropertyComponent.tsx +0 -276
  427. package/src/preview/property_previews/StringPropertyPreview.tsx +0 -70
  428. package/src/preview/util.ts +0 -39
  429. package/src/routes/CustomViewRoute.tsx +0 -33
  430. package/src/routes/RebaseRoute.tsx +0 -467
  431. package/src/routes/index.ts +0 -2
  432. package/src/types/components/EntityFormActionsProps.tsx +0 -20
  433. package/src/types/components/EntityFormProps.tsx +0 -119
  434. package/src/types/components/PropertyPreviewProps.tsx +0 -85
  435. package/src/types/components/formex.tsx +0 -2
  436. package/src/types/components/index.ts +0 -3
  437. package/src/types/fields.tsx +0 -340
  438. package/src/types/index.ts +0 -4
  439. package/src/util/dataControllerAdapter.ts +0 -88
  440. package/src/util/entity_actions.ts +0 -28
  441. package/src/util/index.ts +0 -5
  442. package/src/util/navigation_utils.ts +0 -89
  443. package/src/util/number_format.ts +0 -46
  444. package/src/util/previews.ts +0 -131
  445. package/src/util/propertyConfigMapper.ts +0 -134
  446. package/src/util/property_utils.tsx +0 -236
  447. package/src/util/resolutions.ts +0 -96
  448. package/src/util/view_constants.ts +0 -33
  449. package/src/util/view_mode.ts +0 -176
@@ -1,1603 +0,0 @@
1
- import type { OrderByTuple, Property } from "@rebasepro/types";
2
- import { serializeOrderBy } from "@rebasepro/common";
3
- import type { AdditionalFieldDelegate, EntityAction, AdminCollection } from "@rebasepro/cms-types";
4
- import {
5
- Entity,
6
- EntityReference,
7
- FilterValues,
8
- getCollectionDataPath
9
- } from "@rebasepro/types";
10
- import {
11
- CollectionCustomViewParams,
12
- CollectionSize,
13
- EntityTableController,
14
- PartialCollectionConfig,
15
- ViewMode
16
- } from "@rebasepro/cms-types";
17
- import React, { useCallback, useEffect, useMemo, useState } from "react";
18
- import { useSearchParams } from "react-router";
19
-
20
- import { deepEqual as equal } from "fast-equals"
21
- import { CollectionRowActions, CollectionTableBinding } from "../CollectionTableBinding";
22
- import { CollectionTableToolbar } from "../CollectionTableBinding/internal/CollectionTableToolbar";
23
- import { getEntityChildViews } from "@rebasepro/common";
24
- import { useCollectionInlineEditor } from "./hooks/useCollectionInlineEditor";
25
- import { navigateToEntity } from "../../util/navigation_utils";
26
- import { mergeEntityActions } from "../../util/entity_actions";
27
- import { resolveCollectionViews, resolveEntityAction } from "../../util/resolutions";
28
- import { getPropertyInPath } from "../../util/property_utils";
29
- import { ReferencePreview } from "../../preview";
30
- import {
31
- CollectionScopeProvider,
32
- OnColumnResizeParams,
33
- getRedundantChildViewColumnIds,
34
- useAnalyticsController,
35
- useAuthController,
36
- useColumnIds,
37
- useComponentOverride,
38
- useCustomizationController,
39
- useData,
40
- useDataTableController,
41
- useLargeLayout,
42
- usePermissions,
43
- useScrollRestoration,
44
- useSlot,
45
- useTranslation,
46
- useUserConfigurationPersistence
47
- } from "@rebasepro/app";
48
- import { CollectionViewActions } from "./CollectionViewActions";
49
- import { CollectionCardViewBinding } from "./CollectionCardViewBinding";
50
- import { CollectionListViewBinding } from "./CollectionListViewBinding";
51
- import { SplitListView } from "./SplitListView";
52
- import { CollectionBoardViewBinding } from "./CollectionBoardViewBinding";
53
- import { KanbanPropertyOption, ViewModeToggle } from "./ViewModeToggle";
54
- import {
55
- ArrowRightToLineIcon,
56
- Button,
57
- cls,
58
- ErrorBoundary,
59
- focusedDisabled,
60
- IconButton,
61
- iconSize,
62
- PlusIcon,
63
- Popover,
64
- SearchIcon,
65
- TextField,
66
- Tooltip,
67
- Typography,
68
- VirtualTableColumn
69
- } from "@rebasepro/ui";
70
- import { getSubcollectionColumnId } from "../CollectionTableBinding/internal/common";
71
- import { copyEntityAction, deleteEntityAction, editEntityAction, unlinkEntityAction } from "../common/default_entity_actions";
72
- import { PopupFormField } from "../CollectionTableBinding/internal/popup_field/PopupFormField";
73
- import { GetPropertyForProps } from "../CollectionTableBinding/CollectionTableBindingProps";
74
- import { DeleteEntityDialog } from "../DeleteEntityDialog";
75
- import { useSelectionController } from "./useSelectionController";
76
- import { CollectionViewStartActions } from "./CollectionViewStartActions";
77
- import { addRecentId, getRecentIds } from "./utils";
78
- import { isViewMode, OpenEntityMode, resolveOpenEntityMode, VIEW_MODE_PARAM, VIEW_MODES } from "../../util/view_mode";
79
- import { mergeDeep } from "@rebasepro/utils";
80
- import { useAdminContext } from "../../hooks/useAdminContext";
81
- import { useCollectionRegistryController } from "../../hooks/navigation/contexts/CollectionRegistryContext";
82
- import { useSidePanel } from "../../hooks/useSidePanel";
83
- import { useUrlController } from "../../hooks/navigation/contexts/UrlContext";
84
- import { useChildViewSource } from "../../hooks/useChildViewSource";
85
- import { useSelectionDialog } from "../../hooks/useSelectionDialog";
86
- import { resolveComponentRef, saveEntityWithCallbacks } from "@rebasepro/app";
87
-
88
- const EMPTY_ARRAY: never[] = [];
89
-
90
- /**
91
- * @group Components
92
- */
93
- export type CollectionViewBindingProps<M extends Record<string, unknown>> = {
94
- /**
95
- * Complete path where this collection is located.
96
- * It defaults to the collection path if not provided.
97
- */
98
- path?: string;
99
- /**
100
- * Full path using navigation ids.
101
- */
102
- idPath?: string;
103
- /**
104
- * If this is a subcollection, specify the parent collection ids.
105
- */
106
- parentCollectionSlugs?: string[], parentEntityIds?: string[];
107
- /**
108
- * Whether this is a subcollection or not.
109
- */
110
- isSubCollection?: boolean;
111
-
112
- className?: string;
113
-
114
- /**
115
- * If true, this view will store its filter and sorting status in the url params
116
- */
117
- updateUrl?: boolean;
118
-
119
- /**
120
- * When provided, the split view will render this entity's detail panel.
121
- * Used by the router to pass the entity ID from the URL path.
122
- */
123
- selectedEntityId?: string | number;
124
-
125
- /**
126
- * When provided, the split view will open this tab (e.g. a subcollection slug)
127
- * in the entity detail panel. Used by the router to pass the subcollection from the URL.
128
- */
129
- selectedTab?: string;
130
-
131
- } & AdminCollection<M>;
132
-
133
- /**
134
- * This component is in charge of binding a driver path with an {@link AdminCollection}
135
- * where it's configuration is defined. It includes an infinite scrolling table
136
- * and a 'Add' new entities button,
137
- *
138
- * This component is the default one used for displaying entity collections
139
- * and is in charge of generating all the specific actions and customization
140
- * of the lower level {@link CollectionTableBinding}
141
- *
142
- * Please **note** that you only need to use this component if you are building
143
- * a custom view. If you just need to create a default view you can do it
144
- * exclusively with config options.
145
- *
146
- * If you need a lower level implementation with more granular options, you
147
- * can use {@link CollectionTableBinding}.
148
- *
149
- * If you need a generic table that is not bound to the driver or entities and
150
- * properties at all, you can check {@link VirtualTable}
151
- *
152
- * @param path
153
- * @param collection
154
-
155
- * @group Components
156
- */
157
- const CollectionViewBindingInner = React.memo(
158
- function CollectionViewBindingInner<M extends Record<string, unknown>>({
159
- path: pathProp,
160
-
161
- parentCollectionSlugs, parentEntityIds,
162
- isSubCollection,
163
- className,
164
- updateUrl,
165
- selectedEntityId: selectedEntityIdProp,
166
- selectedTab: selectedTabProp,
167
- ...collectionProp
168
- }: CollectionViewBindingProps<M>
169
- ) {
170
-
171
- const { t } = useTranslation();
172
- const context = useAdminContext();
173
- const collectionRegistry = useCollectionRegistryController();
174
- const urlController = useUrlController();
175
- const path = pathProp ?? getCollectionDataPath(collectionProp);
176
- const dataClient = useData();
177
- const sidePanelController = useSidePanel();
178
- const authController = useAuthController();
179
- const userConfigPersistence = useUserConfigurationPersistence();
180
- const analyticsController = useAnalyticsController();
181
- const customizationController = useCustomizationController();
182
- const { canCreate, canEdit, canDelete } = usePermissions();
183
-
184
-
185
- /**
186
- * Rows shared with other parents through a junction. The parent owns
187
- * the link, not the row — so the destructive action is a removal from
188
- * this record, which is what the server performs for such a path.
189
- */
190
- const childViewSource = useChildViewSource(path);
191
- const isLinkedChildView = childViewSource?.kind === "relation" && childViewSource.mode === "linked";
192
- const linkedTargetSlug = isLinkedChildView && childViewSource.kind === "relation"
193
- ? childViewSource.targetSlug
194
- : undefined;
195
-
196
- const containerRef = React.useRef<HTMLDivElement>(null);
197
-
198
- const scrollRestoration = useScrollRestoration();
199
-
200
- const collection = useMemo(() => {
201
- const registryCollection = collectionRegistry.getCollection(path) || collectionProp;
202
- const userOverride = userConfigPersistence?.getCollectionConfig<M>(path);
203
- return (userOverride ? mergeDeep(registryCollection, userOverride) : registryCollection) as AdminCollection<M>;
204
- }, [collectionProp, path, userConfigPersistence, collectionRegistry]);
205
-
206
- const collectionRef = React.useRef(collection);
207
- useEffect(() => {
208
- collectionRef.current = collection;
209
- }, [collection]);
210
-
211
- const canCreateEntities = canCreate(collection, path);
212
- const [highlightedEntity, setHighlightedEntity] = useState<Entity<M> | undefined>(undefined);
213
- const [deleteEntityClicked, setDeleteEntityClicked] = React.useState<Entity<M> | Entity<M>[] | undefined>(undefined);
214
-
215
- const [lastDeleteTimestamp, setLastDeleteTimestamp] = React.useState<number>(0);
216
-
217
- // Track recently deleted entities for optimistic Kanban count updates
218
- const [deletedEntities, setDeletedEntities] = React.useState<Entity<M>[]>([]);
219
-
220
- // number of entities in the collection (undefined = loading)
221
- const [docsCount, setDocsCount] = useState<number | null | undefined>(null);
222
-
223
- // Optimistic state for column order to prevent UI flickering during persistence
224
- const [localPropertiesOrder, setLocalPropertiesOrder] = useState<string[] | undefined>(collection.propertiesOrder);
225
-
226
- // Sync local state with collection's propertiesOrder when it changes from external sources
227
- useEffect(() => {
228
- setLocalPropertiesOrder(collection.propertiesOrder);
229
- }, [collection.propertiesOrder]);
230
-
231
- const unselectNavigatedEntity = useCallback(() => {
232
- const currentSelection = highlightedEntity;
233
- setTimeout(() => {
234
- if (currentSelection === highlightedEntity)
235
- setHighlightedEntity(undefined);
236
- }, 2400);
237
- }, [highlightedEntity]);
238
-
239
- const checkInlineEditing = useCallback((entity?: Entity<M>): boolean => {
240
- const collection = collectionRef.current;
241
- if (!canEdit(collection, path, entity ?? null)) {
242
- return false;
243
- }
244
- return collection.inlineEditing === undefined || collection.inlineEditing;
245
- }, [canEdit, path]);
246
-
247
- const selectionEnabled = collection.selectionEnabled === undefined || collection.selectionEnabled;
248
- const hoverRow = !checkInlineEditing();
249
-
250
- const [popOverOpen, setPopOverOpen] = useState(false);
251
-
252
- // Custom view modes this collection can render: the ones declared
253
- // inline plus the ones it names by key from the app-level registry.
254
- // Anything unresolvable is dropped here, once, so every consumer below
255
- // — the switcher, `enabledViews`, the URL and saved-config checks —
256
- // agrees on what exists.
257
- const resolvedCustomViews = useMemo(
258
- () => resolveCollectionViews(collection.customViews, customizationController.collectionViews),
259
- [collection.customViews, customizationController]
260
- );
261
-
262
- const customViewKeys = useMemo(
263
- () => resolvedCustomViews.map((v) => v.key),
264
- [resolvedCustomViews]
265
- );
266
-
267
- // View mode priority: URL > saved user config > collection.defaultViewMode
268
- const defaultViewMode = isViewMode(collection.defaultViewMode, customViewKeys)
269
- ? collection.defaultViewMode
270
- // Covers a `defaultViewMode` naming a custom view that has since
271
- // been removed from config; without this the collection would open
272
- // on a mode nothing renders.
273
- : "list";
274
- const [searchParams, setSearchParams] = useSearchParams();
275
-
276
- // Read view from React Router's searchParams (reactive on back/forward)
277
- const urlView = useMemo((): ViewMode | null => {
278
- const v = searchParams.get(VIEW_MODE_PARAM);
279
- return isViewMode(v, customViewKeys) ? v : null;
280
- }, [searchParams, customViewKeys]);
281
-
282
- // Get saved view from local persistence.
283
- //
284
- // Validated rather than cast: what is stored is whatever the user last
285
- // picked, and a custom view removed from config since then would come
286
- // back as a mode nothing renders — a blank panel with a working
287
- // toolbar above it. An unrecognised value reads as "nothing saved",
288
- // which falls through to the collection default.
289
- const getSavedView = useCallback((): ViewMode | null => {
290
- const saved = userConfigPersistence?.getCollectionConfig<M>(path)?.defaultViewMode;
291
- return isViewMode(saved, customViewKeys) ? saved : null;
292
- }, [userConfigPersistence, path, customViewKeys]);
293
-
294
- const [viewMode, setViewModeState] = useState<ViewMode>(() => {
295
- // Priority: URL > saved config > collection default
296
- if (urlView) return urlView;
297
- const savedView = getSavedView();
298
- if (savedView) return savedView;
299
- return defaultViewMode;
300
- });
301
-
302
- const activeCustomView = useMemo(
303
- () => resolvedCustomViews.find((v) => v.key === viewMode),
304
- [resolvedCustomViews, viewMode]
305
- );
306
-
307
- const openEntityMode = resolveOpenEntityMode({
308
- collection,
309
- viewMode,
310
- customView: activeCustomView
311
- });
312
-
313
- // Sync URL with current view on init (if view came from saved config)
314
- useEffect(() => {
315
- if (!urlView && viewMode !== defaultViewMode) {
316
- // View came from saved config but URL doesn't have it - update URL without push
317
- // Built from `window.location.search`, not React Router's
318
- // `prev`. The table controller writes the filter, sort and
319
- // search params with `history.replaceState`, which React Router
320
- // never observes — so `prev` is a stale snapshot, and merging
321
- // into it silently dropped whatever it had not seen. Switching
322
- // view mode after typing a search wiped `search=` from the URL,
323
- // and the next reload came back unfiltered.
324
- setSearchParams(() => {
325
- const next = new URLSearchParams(window.location.search);
326
- next.set(VIEW_MODE_PARAM, viewMode);
327
- return next;
328
- }, { replace: true });
329
- }
330
- }, []); // Only on mount
331
-
332
- // Update URL when view mode changes (user action)
333
- const setViewMode = useCallback((newMode: ViewMode) => {
334
- setViewModeState(newMode);
335
-
336
- // Update URL with __view param via React Router
337
- setSearchParams(() => {
338
- // See the note above: the live URL is the only writer both
339
- // this and the table controller agree on.
340
- const next = new URLSearchParams(window.location.search);
341
- if (newMode === defaultViewMode) {
342
- next.delete(VIEW_MODE_PARAM);
343
- } else {
344
- next.set(VIEW_MODE_PARAM, newMode);
345
- }
346
- return next;
347
- });
348
- }, [defaultViewMode, setSearchParams]);
349
-
350
- // Sync viewMode state when URL changes via back/forward navigation
351
- useEffect(() => {
352
- if (urlView) {
353
- setViewModeState(urlView);
354
- } else {
355
- const savedView = getSavedView();
356
- setViewModeState(savedView ?? defaultViewMode);
357
- }
358
- }, [urlView, getSavedView, defaultViewMode]);
359
-
360
- // List view size state - controls row height and info density
361
- const [listSize, setListSize] = useState<CollectionSize>(collection.defaultSize ?? "m");
362
-
363
- // Card view size state - controls the grid column count
364
- const [cardSize, setCardSize] = useState<CollectionSize>(collection.defaultSize ?? "m");
365
-
366
- // Table view size state - controls row height
367
- const [tableSize, setTableSize] = useState<CollectionSize>(collection.defaultSize ?? "m");
368
-
369
- // Size for custom views that opted in with `sizeable`. One piece of
370
- // state for all of them: only the active view reads it, and switching
371
- // between two sizeable custom views keeping the same density is the
372
- // less surprising behaviour.
373
- const [customViewSize, setCustomViewSize] = useState<CollectionSize>(collection.defaultSize ?? "m");
374
-
375
- const selectionController = useSelectionController<M>();
376
- const usedSelectionController = collection.selectionController ?? selectionController;
377
- const {
378
- selectedEntities,
379
- setSelectedEntities
380
- } = usedSelectionController;
381
-
382
- const tableController = useDataTableController<M>({
383
- path,
384
- collection,
385
- lastDeleteTimestamp,
386
- scrollRestoration,
387
- updateUrl
388
- });
389
-
390
- const tableKey = React.useRef<string>(Math.random().toString(36));
391
- const popupCell = tableController.popupCell as {
392
- entityId: string | number;
393
- propertyKey: Extract<keyof M, string>;
394
- cellRect?: DOMRect;
395
- } | undefined;
396
-
397
- const onPopupClose = useCallback(() => {
398
- tableController.setPopupCell?.(undefined);
399
- }, [tableController.setPopupCell]);
400
-
401
- const onEntityClick = useCallback((clickedEntity: Entity<M>) => {
402
- const collection = collectionRef.current;
403
- setHighlightedEntity(clickedEntity);
404
- analyticsController.onAnalyticsEvent?.("edit_entity_clicked", {
405
- path: clickedEntity.path,
406
- entityId: clickedEntity.id
407
- });
408
-
409
- if (collection) {
410
- addRecentId(collection.slug, clickedEntity.id);
411
- }
412
-
413
- const entityPath = path ?? clickedEntity.path;
414
- navigateToEntity({
415
- navigation: urlController,
416
- path: entityPath,
417
- sidePanelController,
418
- openEntityMode,
419
- collection,
420
- entityId: clickedEntity.id,
421
- replace: openEntityMode === "split" && selectedEntityIdProp !== undefined
422
- });
423
-
424
- }, [sidePanelController, openEntityMode, selectedEntityIdProp, path, urlController, analyticsController]);
425
-
426
- const onNewClick = useCallback(() => {
427
- const collection = collectionRef.current;
428
- analyticsController.onAnalyticsEvent?.("new_entity_click", {
429
- path: path
430
- });
431
- navigateToEntity({
432
- openEntityMode,
433
- collection,
434
- entityId: undefined,
435
- path: path,
436
- sidePanelController,
437
- navigation: urlController,
438
- onClose: unselectNavigatedEntity
439
- })
440
- }, [path, sidePanelController]);
441
-
442
- /**
443
- * Attach rows that already exist to this parent.
444
- *
445
- * Writing to the row's own address under this parent — `PUT
446
- * posts/1/tags/5` — is what asserts membership; the backend writes the
447
- * junction row idempotently. Sending no values keeps it a pure link:
448
- * the target's own fields are none of this tab's business.
449
- */
450
- const linkExistingEntities = useCallback(async (entities: Entity<M>[]) => {
451
- const alreadyLinked = new Set(tableController.data.map(e => String(e.id)));
452
- const toLink = entities.filter(e => !alreadyLinked.has(String(e.id)));
453
- if (toLink.length === 0) return;
454
-
455
- try {
456
- await Promise.all(toLink.map(entity =>
457
- saveEntityWithCallbacks<M>({
458
- path,
459
- entityId: entity.id,
460
- values: {} as Partial<M>,
461
- status: "existing",
462
- collection,
463
- data: dataClient,
464
- context
465
- })
466
- ));
467
- setLastDeleteTimestamp(Date.now());
468
- } catch (e) {
469
- context.snackbarController?.open({
470
- type: "error",
471
- message: e instanceof Error ? e.message : String(e)
472
- });
473
- }
474
- }, [dataClient, path, tableController.data, collection, context]);
475
-
476
- const selectionDialog = useSelectionDialog<M>({
477
- // The whole target collection, not this parent's slice of it —
478
- // otherwise the picker could only ever show rows already linked.
479
- path: linkedTargetSlug ?? false,
480
- multiselect: true,
481
- onMultipleEntitiesSelected: linkExistingEntities
482
- });
483
-
484
- const openNewDocument = useCallback((defaultValues?: Record<string, unknown>) => {
485
- const collection = collectionRef.current;
486
- analyticsController.onAnalyticsEvent?.("new_entity_click", {
487
- path: path
488
- });
489
- navigateToEntity({
490
- openEntityMode,
491
- collection,
492
- entityId: undefined,
493
- defaultValues,
494
- path: path,
495
- sidePanelController,
496
- navigation: urlController,
497
- onClose: unselectNavigatedEntity
498
- });
499
- }, [path, sidePanelController, openEntityMode, urlController, unselectNavigatedEntity]);
500
-
501
- const onMultipleDeleteClick = () => {
502
- analyticsController.onAnalyticsEvent?.("multiple_delete_dialog_open", {
503
- path: path
504
- });
505
- setDeleteEntityClicked(selectedEntities);
506
- };
507
-
508
- const internalOnEntityDelete = (_path: string, entity: Entity<M>) => {
509
- analyticsController.onAnalyticsEvent?.("single_entity_deleted", {
510
- path: path
511
- });
512
- setSelectedEntities((selectedEntities) => selectedEntities.filter((e) => e.id !== entity.id));
513
- setDeletedEntities(prev => [...prev, entity]);
514
- setLastDeleteTimestamp(Date.now());
515
- };
516
-
517
- const internalOnMultipleEntitiesDelete = (_path: string, entities: Entity<M>[]) => {
518
- analyticsController.onAnalyticsEvent?.("multiple_entities_deleted", {
519
- path: path
520
- });
521
- setSelectedEntities([]);
522
- setDeleteEntityClicked(undefined);
523
- setDeletedEntities(prev => [...prev, ...entities]);
524
- setLastDeleteTimestamp(Date.now());
525
- };
526
-
527
- const pluginAddColumnComponents = useSlot("collection.add-column", {
528
- path,
529
- parentCollectionSlugs: parentCollectionSlugs ?? EMPTY_ARRAY,
530
- parentEntityIds: parentEntityIds ?? EMPTY_ARRAY,
531
- collection,
532
- tableController
533
- });
534
-
535
- const pluginToolbarWidgets = useSlot("collection.toolbar", {
536
- path,
537
- parentCollectionSlugs: parentCollectionSlugs ?? EMPTY_ARRAY,
538
- parentEntityIds: parentEntityIds ?? EMPTY_ARRAY,
539
- collection: collection,
540
- tableController: tableController,
541
- selectionController: usedSelectionController
542
- });
543
-
544
- const pluginEmptyStates = useSlot("collection.empty-state", {
545
- path,
546
- parentCollectionSlugs: parentCollectionSlugs ?? EMPTY_ARRAY,
547
- parentEntityIds: parentEntityIds ?? EMPTY_ARRAY,
548
- collection,
549
- canCreate: canCreateEntities,
550
- onNewClick
551
- });
552
-
553
- const pluginWidgets = useSlot("collection.widgets", {
554
- path,
555
- parentCollectionSlugs: parentCollectionSlugs ?? EMPTY_ARRAY,
556
- parentEntityIds: parentEntityIds ?? EMPTY_ARRAY,
557
- collection
558
- });
559
-
560
- const onCollectionModifiedForUser = useCallback((path: string, partialCollection: PartialCollectionConfig<M>) => {
561
- if (userConfigPersistence) {
562
- const currentStoredConfig = userConfigPersistence.getCollectionConfig(path);
563
- const updatedConfig = mergeDeep(currentStoredConfig, partialCollection);
564
- userConfigPersistence.onCollectionModified(path, updatedConfig);
565
- }
566
- }, [userConfigPersistence]);
567
-
568
- const onColumnResize = useCallback(({
569
- width,
570
- key
571
- }: OnColumnResizeParams) => {
572
-
573
- const collection = collectionRef.current;
574
- // Only for property columns
575
- if (!getPropertyInPath(collection.properties, key)) return;
576
- const localCollection = buildPropertyWidthOverwrite(key, width) as PartialCollectionConfig<M>;
577
- onCollectionModifiedForUser(path, localCollection);
578
- }, [onCollectionModifiedForUser, path]);
579
-
580
- const onListSizeChanged = useCallback((size: CollectionSize) => {
581
- setListSize(size);
582
- if (userConfigPersistence)
583
- onCollectionModifiedForUser(path, { defaultSize: size })
584
- }, [onCollectionModifiedForUser, path, userConfigPersistence]);
585
-
586
- const onTableSizeChanged = useCallback((size: CollectionSize) => {
587
- setTableSize(size);
588
- if (userConfigPersistence)
589
- onCollectionModifiedForUser(path, { defaultSize: size })
590
- }, [onCollectionModifiedForUser, path, userConfigPersistence]);
591
-
592
- // View mode change: update URL + save to local persistence
593
- const onViewModeChange = useCallback((mode: ViewMode) => {
594
- analyticsController.onAnalyticsEvent?.("view_mode_changed", {
595
- path: path,
596
- from: viewMode,
597
- to: mode
598
- });
599
- setViewMode(mode);
600
- // Save to local persistence for next visit
601
- if (userConfigPersistence) {
602
- onCollectionModifiedForUser(path, { defaultViewMode: mode } as PartialCollectionConfig<M>);
603
- }
604
- }, [setViewMode, userConfigPersistence, onCollectionModifiedForUser, path, analyticsController, viewMode]);
605
-
606
- const createEnabled = canCreate(collection, path);
607
-
608
- const { onValueChange, uniqueFieldValidator } = useCollectionInlineEditor({
609
- path,
610
- collection,
611
- dataClient,
612
- context
613
- });
614
-
615
- // In v4, collections are already resolved, so we use collection directly
616
- const resolvedCollection = collection;
617
-
618
- // Check if Kanban view is available (needs kanban.columnProperty with enum)
619
- const kanbanEnabled = useMemo(() => {
620
- if (!collection.kanban?.columnProperty) return false;
621
- const property = getPropertyInPath(resolvedCollection.properties, collection.kanban.columnProperty);
622
- if (!property || property.type !== "string") return false;
623
- return Boolean("enum" in property && property.enum);
624
- }, [collection.kanban?.columnProperty, resolvedCollection.properties]);
625
-
626
- // Compute the effective enabled views:
627
- // - Start from collection.enabledViews (defaults to every built-in
628
- // plus every custom view the collection declared)
629
- // - Drop anything that resolves to nothing renderable
630
- // - Filter out kanban if no enum properties exist
631
- const hasEnumProperty = useMemo(() => {
632
- return Object.values(resolvedCollection.properties).some((p: Property) => p.type === "string" && "enum" in p && p.enum);
633
- }, [resolvedCollection.properties]);
634
-
635
- const enabledViews: ViewMode[] = useMemo(() => {
636
- // Declaring a custom view is enough to offer it — a collection
637
- // should not have to restate the built-ins in `enabledViews` just
638
- // to add one. Setting `enabledViews` explicitly still wins.
639
- const configured = collection.enabledViews
640
- ?? [...VIEW_MODES, ...customViewKeys];
641
- const renderable = configured.filter(v => isViewMode(v, customViewKeys));
642
- if (!hasEnumProperty) {
643
- return renderable.filter(v => v !== "kanban");
644
- }
645
- return renderable;
646
- }, [collection.enabledViews, hasEnumProperty, customViewKeys]);
647
-
648
- // Compute available enum properties for kanban column selection
649
- const kanbanPropertyOptions: KanbanPropertyOption[] = useMemo(() => {
650
- const options: KanbanPropertyOption[] = [];
651
- const properties = resolvedCollection.properties;
652
-
653
- for (const [key, property] of Object.entries(properties)) {
654
- const prop = property;
655
- if (prop && prop.type === "string" && "enum" in prop && prop.enum) {
656
- options.push({
657
- key,
658
- label: prop.name || key
659
- });
660
- }
661
- }
662
-
663
- return options;
664
- }, [resolvedCollection.properties]);
665
-
666
- // Get saved kanban property from user config
667
- const getSavedKanbanProperty = useCallback((): string | undefined => {
668
- const saved = userConfigPersistence?.getCollectionConfig<M>(path);
669
- return (saved as Record<string, unknown>)?.kanbanColumnProperty as string | undefined;
670
- }, [userConfigPersistence, path]);
671
-
672
- // Selected kanban property state - priority: saved config > collection default > first available
673
- const [selectedKanbanProperty, setSelectedKanbanProperty] = useState<string>(() => {
674
- const saved = getSavedKanbanProperty();
675
- if (saved && kanbanPropertyOptions.some(o => o.key === saved)) return saved;
676
- if (collection.kanban?.columnProperty) return collection.kanban.columnProperty;
677
- return kanbanPropertyOptions[0]?.key ?? "";
678
- });
679
-
680
- // Update selected property if options change and current selection is no longer valid
681
- useEffect(() => {
682
- if (kanbanPropertyOptions.length > 0 && !kanbanPropertyOptions.some(o => o.key === selectedKanbanProperty)) {
683
- const saved = getSavedKanbanProperty();
684
- if (saved && kanbanPropertyOptions.some(o => o.key === saved)) {
685
- setSelectedKanbanProperty(saved);
686
- } else if (collection.kanban?.columnProperty && kanbanPropertyOptions.some(o => o.key === collection.kanban?.columnProperty)) {
687
- setSelectedKanbanProperty(collection.kanban.columnProperty);
688
- } else {
689
- setSelectedKanbanProperty(kanbanPropertyOptions[0]?.key ?? "");
690
- }
691
- }
692
- }, [kanbanPropertyOptions, selectedKanbanProperty, getSavedKanbanProperty, collection.kanban?.columnProperty]);
693
-
694
- // Handle kanban property change
695
- const onKanbanPropertyChange = useCallback((property: string) => {
696
- analyticsController.onAnalyticsEvent?.("kanban_property_changed", {
697
- path: path,
698
- property
699
- });
700
- setSelectedKanbanProperty(property);
701
- // Save to local persistence
702
- if (userConfigPersistence) {
703
- onCollectionModifiedForUser(path, { kanbanColumnProperty: property } as PartialCollectionConfig<M>);
704
- }
705
- }, [userConfigPersistence, onCollectionModifiedForUser, path, analyticsController]);
706
-
707
- const getPropertyFor = useCallback(({
708
- propertyKey,
709
- entity
710
- }: GetPropertyForProps<M>) => {
711
- let property: Property | undefined = getPropertyInPath(collection.properties, propertyKey);
712
-
713
- // we might not find the property in the collection if combining property builders and map spread
714
- if (!property) {
715
- // these 2 properties are coming from the resolved collection with default values
716
- property = getPropertyInPath(resolvedCollection.properties, propertyKey);
717
- }
718
-
719
- // In v4, properties are already resolved, so we return them directly
720
- return property ?? null;
721
- }, [collection.properties, resolvedCollection.properties]);
722
-
723
- // Use a collection with local propertiesOrder for optimistic UI updates
724
- const collectionWithLocalOrder = useMemo(() => {
725
- if (localPropertiesOrder && localPropertiesOrder !== resolvedCollection.propertiesOrder) {
726
- return {
727
- ...resolvedCollection,
728
- // Runtime column order, not authored config: these ids come
729
- // from the user dragging columns and include additional-field
730
- // and `subcollection:` ids minted below. `propertiesOrder` is
731
- // typed against the collection's properties for *authoring*,
732
- // so widening back to that type is the intent here.
733
- propertiesOrder: localPropertiesOrder as typeof resolvedCollection.propertiesOrder
734
- };
735
- }
736
- return resolvedCollection;
737
- }, [resolvedCollection, localPropertiesOrder]);
738
-
739
- const displayedColumnIds = useColumnIds(collectionWithLocalOrder, true);
740
-
741
- const additionalFields = useMemo(() => {
742
- // Each child view gets a column that jumps to its tab. Keyed by the
743
- // view's collection slug, which is the relation key — two relations
744
- // to the same target used to collapse onto one column id here.
745
- //
746
- // Except where the relation was declared as a property and so already
747
- // has a column showing the child rows: the button then repeated that
748
- // column's heading verbatim. Filtered here as well as in
749
- // `useColumnIds` so no id is displayed without a delegate to build it,
750
- // and so the column picker does not offer the one back.
751
- const redundant = getRedundantChildViewColumnIds(collectionWithLocalOrder);
752
- const subcollectionsList = getEntityChildViews(collection)
753
- .map(v => v.collection as AdminCollection)
754
- .filter(child => !redundant.has(getSubcollectionColumnId(child)));
755
- const subcollectionColumns: AdditionalFieldDelegate<M, any>[] = subcollectionsList.map((subcollection: AdminCollection) => {
756
- return {
757
- key: getSubcollectionColumnId(subcollection),
758
- name: subcollection.name,
759
- width: 200,
760
- dependencies: [],
761
- Builder: ({ entity }: { entity: Entity }) => (
762
- <Button
763
- className={"max-w-full truncate justify-start"}
764
- startIcon={<ArrowRightToLineIcon size={iconSize.small}/>}
765
- onClick={(event: React.MouseEvent) => {
766
- event.stopPropagation();
767
- navigateToEntity({
768
- openEntityMode,
769
- collection,
770
- entityId: entity.id,
771
- selectedTab: subcollection.slug,
772
- path: path,
773
- navigation: urlController,
774
- sidePanelController
775
- })
776
- }}>
777
- {subcollection.name}
778
- </Button>
779
- )
780
- };
781
- }) ?? EMPTY_ARRAY;
782
-
783
- return [
784
- ...(collection.additionalFields ?? EMPTY_ARRAY),
785
- ...subcollectionColumns
786
- ];
787
- }, [collection, collectionWithLocalOrder, path, sidePanelController]);
788
-
789
- const updateLastDeleteTimestamp = useCallback(() => {
790
- setLastDeleteTimestamp(Date.now());
791
- }, []);
792
-
793
- const largeLayout = useLargeLayout();
794
-
795
- const isSplitLayout = openEntityMode === "split";
796
- const isCompact = isSplitLayout && selectedEntityIdProp !== undefined;
797
- const activeSelectionEnabled = !isCompact && selectionEnabled;
798
-
799
- const getActionsForEntity = useCallback(({
800
- entity,
801
- customEntityActions
802
- }: {
803
- entity?: Entity<M>,
804
- customEntityActions?: EntityAction[]
805
- }): EntityAction[] => {
806
- const deleteEnabled = entity ? canDelete(collection, path, entity) : true;
807
- const disableActions = collection.disableDefaultActions ?? [];
808
- const actions: EntityAction[] = [];
809
- if (!disableActions.includes("edit")) {
810
- actions.push(editEntityAction);
811
- }
812
- if (createEnabled && !disableActions.includes("copy"))
813
- actions.push(copyEntityAction);
814
- if (deleteEnabled && !disableActions.includes("delete")) {
815
- // On a junction-backed tab the server removes the link, not the
816
- // row. Same request either way — this is the button telling the
817
- // truth about what it will do.
818
- actions.push(isLinkedChildView ? unlinkEntityAction : deleteEntityAction);
819
- }
820
- if (customEntityActions)
821
- return mergeEntityActions(actions, customEntityActions);
822
- return actions;
823
- }, [canDelete, collection, path, createEnabled, isLinkedChildView]);
824
-
825
- const getIdColumnWidth = useCallback(() => {
826
- const entityActions = getActionsForEntity({});
827
- const collapsedActions = entityActions.filter(a => a.collapsed !== false);
828
- const uncollapsedActions = entityActions.filter(a => a.collapsed === false);
829
- const actionsWidth = uncollapsedActions.length * (largeLayout ? 40 : 30);
830
- return (largeLayout ? (80 + actionsWidth) : (70 + actionsWidth)) + (collapsedActions.length > 0 ? (largeLayout ? 40 : 30) : 0);
831
- }, [getActionsForEntity, largeLayout]);
832
-
833
- const tableRowActionsBuilder = useCallback(({
834
- entity,
835
- size,
836
- width,
837
- frozen
838
- }: {
839
- entity: Entity<any>,
840
- size: CollectionSize,
841
- width: number,
842
- frozen?: boolean
843
- }) => {
844
-
845
- const isSelected = Boolean(usedSelectionController.selectedEntities.find(e => e.id == entity.id && e.path == entity.path));
846
- const customEntityActions = (collection.entityActions ?? EMPTY_ARRAY)
847
- .map(action => resolveEntityAction(action, customizationController.entityActions))
848
- .filter(Boolean) as EntityAction<M>[];
849
-
850
- const actions = getActionsForEntity({
851
- entity,
852
- customEntityActions
853
- });
854
-
855
- return (
856
- <CollectionRowActions
857
- entity={entity}
858
- width={width}
859
- frozen={frozen}
860
- isSelected={isSelected}
861
- selectionEnabled={activeSelectionEnabled}
862
- size={size}
863
- highlightEntity={setHighlightedEntity}
864
- unhighlightEntity={unselectNavigatedEntity}
865
- collection={collection}
866
- path={path}
867
- actions={actions}
868
- hideId={collection?.hideIdFromCollection}
869
- onCollectionChange={updateLastDeleteTimestamp}
870
- selectionController={usedSelectionController}
871
- openEntityMode={openEntityMode}
872
- />
873
- );
874
-
875
- }, [updateLastDeleteTimestamp, usedSelectionController]);
876
-
877
- // EntitiesCount fetches the count and reports it — the toolbar renders it
878
- const countFetcher = <EntitiesCount
879
- path={path}
880
- collection={collection}
881
- filter={tableController.filterValues}
882
- sortBy={tableController.sortBy}
883
- searchString={tableController.searchString}
884
- onCountChange={setDocsCount}
885
- />;
886
-
887
- const { resolvedSlots } = customizationController;
888
-
889
- // Pre-compute header action slot contributions (avoid useSlot inside callback)
890
- const headerActionContributions = useMemo(() => resolvedSlots
891
- .filter(s => s.slot === "collection.header.action")
892
- .sort((a, b) => (a.order ?? 50) - (b.order ?? 50)), [resolvedSlots]);
893
-
894
- const buildAdditionalHeaderWidget = useCallback(({
895
- property,
896
- propertyKey,
897
- onHover
898
- }: {
899
- property: Property,
900
- propertyKey: string,
901
- onHover: boolean
902
- }) => {
903
- const collection = collectionRef.current;
904
- const headerSlotProps = {
905
- property,
906
- propertyKey,
907
- onHover,
908
- path,
909
- collection: collection as AdminCollection,
910
- tableController: tableController as EntityTableController,
911
- parentCollectionSlugs: parentCollectionSlugs ?? EMPTY_ARRAY,
912
- parentEntityIds: parentEntityIds ?? EMPTY_ARRAY
913
- };
914
- return <>{headerActionContributions.map((s, i) => (
915
- <ErrorBoundary key={`header_action_${propertyKey}_${i}`}>
916
- <s.Component {...headerSlotProps} {...(s.props ?? {})}/>
917
- </ErrorBoundary>
918
- ))}</>;
919
- }, [headerActionContributions, path, parentCollectionSlugs]);
920
-
921
- const addColumnComponentInternal = pluginAddColumnComponents.length > 0
922
- ? function () {
923
- return (
924
- <div className="flex flex-row items-center gap-2">
925
- {pluginAddColumnComponents}
926
- </div>
927
- );
928
- }
929
- : undefined;
930
-
931
- const onColumnsOrderChange = useCallback((newColumns: VirtualTableColumn[]) => {
932
- // Extract property keys from the new column order
933
- // Filter to only include actual property columns (not frozen columns, not additional fields, etc.)
934
- // Deduplicate to clean up any previously duplicated keys
935
- const seenKeys = new Set<string>();
936
- const newPropertiesOrder = newColumns
937
- .filter(col => !col.frozen && getPropertyInPath(collection.properties, col.key))
938
- .map(col => col.key)
939
- .filter(key => {
940
- if (seenKeys.has(key)) return false;
941
- seenKeys.add(key);
942
- return true;
943
- });
944
-
945
- // Optimistically update local state to prevent UI flickering
946
- setLocalPropertiesOrder(newPropertiesOrder);
947
-
948
- // Call each plugin's onColumnsReorder callback
949
- if (customizationController?.plugins) {
950
- customizationController.plugins
951
- .filter(plugin => plugin.hooks?.onColumnsReorder)
952
- .forEach(plugin => {
953
- plugin.hooks!.onColumnsReorder!({
954
- fullPath: path,
955
- parentCollectionSlugs: parentCollectionSlugs ?? EMPTY_ARRAY,
956
- parentEntityIds: parentEntityIds ?? EMPTY_ARRAY,
957
- collection,
958
- newPropertiesOrder
959
- });
960
- });
961
- }
962
-
963
- // Save to user configuration persistence (local storage)
964
- if (userConfigPersistence) {
965
- onCollectionModifiedForUser(path, { propertiesOrder: newPropertiesOrder } as PartialCollectionConfig<M>);
966
- }
967
- }, [collection, setLocalPropertiesOrder, customizationController, path, parentCollectionSlugs, parentEntityIds, userConfigPersistence, onCollectionModifiedForUser]);
968
-
969
- // Popover open state managed at parent level to prevent closing when view changes
970
- const [viewModePopoverOpen, setViewModePopoverOpen] = useState(false);
971
-
972
- // Create ViewModeToggle once to prevent remounting when view changes
973
- const viewModeToggleElement = (
974
- <ViewModeToggle
975
- viewMode={viewMode}
976
- onViewModeChange={onViewModeChange}
977
- enabledViews={enabledViews}
978
- customViews={resolvedCustomViews}
979
- size={activeCustomView ? (activeCustomView.sizeable ? customViewSize : undefined) : viewMode === "list" ? listSize : viewMode === "table" ? tableSize : viewMode === "cards" ? cardSize : undefined}
980
- onSizeChanged={activeCustomView ? (activeCustomView.sizeable ? setCustomViewSize : undefined) : viewMode === "list" ? onListSizeChanged : viewMode === "table" ? onTableSizeChanged : viewMode === "cards" ? setCardSize : undefined}
981
- open={viewModePopoverOpen}
982
- onOpenChange={setViewModePopoverOpen}
983
- kanbanPropertyOptions={kanbanPropertyOptions}
984
- selectedKanbanProperty={selectedKanbanProperty}
985
- onKanbanPropertyChange={onKanbanPropertyChange}
986
- />
987
- );
988
-
989
- // Compute plugin-provided error view for collection loading errors
990
- const pluginErrorViews = useSlot("collection.error", {
991
- path,
992
- collection,
993
- parentCollectionSlugs,
994
- parentEntityIds,
995
- error: tableController.dataLoadingError as Error
996
- });
997
- const pluginErrorView = tableController.dataLoadingError && pluginErrorViews.length > 0
998
- ? pluginErrorViews[0]
999
- : null;
1000
-
1001
- // Shared empty state — plugin slot takes priority, then override, then default
1002
- const isSearching = !!tableController.searchString;
1003
- const isFilteredOrSorted = tableController.filterValues !== undefined || tableController.sortBy !== undefined || isSearching;
1004
- const ResolvedEmptyState = useComponentOverride("Collection.EmptyState", DefaultCollectionEmptyState);
1005
- const ResolvedCollectionActions = useComponentOverride("Collection.Actions", CollectionViewActions);
1006
- const ResolvedCollectionTable = useComponentOverride("Collection.Table", CollectionTableBinding) as typeof CollectionTableBinding;
1007
- const emptyComponent = pluginEmptyStates.length > 0
1008
- ? <>{pluginEmptyStates}</>
1009
- : <ResolvedEmptyState
1010
- canCreate={canCreateEntities && !isFilteredOrSorted}
1011
- onNewClick={onNewClick}
1012
- isSearching={isSearching}
1013
- searchString={tableController.searchString ?? ""}
1014
- />;
1015
-
1016
- const toolbarNode = (
1017
- <CollectionTableToolbar
1018
- compact={isCompact}
1019
- loading={tableController.dataLoading}
1020
- onTextSearch={tableController.setSearchString}
1021
- initialSearchText={tableController.searchString}
1022
- viewModeToggle={viewModeToggleElement}
1023
- actionsStart={<CollectionViewStartActions
1024
- parentCollectionSlugs={parentCollectionSlugs ?? EMPTY_ARRAY} parentEntityIds={parentEntityIds ?? EMPTY_ARRAY}
1025
- collection={collection}
1026
- tableController={tableController}
1027
- path={path}
1028
- relativePath={getCollectionDataPath(collection)}
1029
- selectionController={usedSelectionController}
1030
- collectionEntitiesCount={docsCount ?? undefined}
1031
- resolvedProperties={resolvedCollection.properties}
1032
- viewMode={viewMode}
1033
- entitiesCount={docsCount}
1034
- openNewDocument={openNewDocument}
1035
- compact={isCompact}/>}
1036
- actions={
1037
- <ResolvedCollectionActions
1038
- parentCollectionSlugs={parentCollectionSlugs ?? EMPTY_ARRAY} parentEntityIds={parentEntityIds ?? EMPTY_ARRAY}
1039
- collection={collection}
1040
- tableController={tableController}
1041
- onMultipleDeleteClick={onMultipleDeleteClick}
1042
- onNewClick={onNewClick}
1043
- onAddExistingClick={linkedTargetSlug ? selectionDialog.open : undefined}
1044
- openNewDocument={openNewDocument}
1045
- path={path}
1046
- relativePath={getCollectionDataPath(collection)}
1047
- selectionController={usedSelectionController}
1048
- selectionEnabled={activeSelectionEnabled}
1049
- collectionEntitiesCount={docsCount ?? undefined}
1050
- compact={isCompact}
1051
- >
1052
- {pluginToolbarWidgets}
1053
- </ResolvedCollectionActions>
1054
- }
1055
- />
1056
- );
1057
-
1058
- // A custom view wins over the built-in chain, but only after the
1059
- // built-in keys have been ruled out — `resolveCollectionViews` refuses
1060
- // a view keyed "table", so this cannot shadow one.
1061
- const CustomViewBuilder = activeCustomView
1062
- ? resolveComponentRef<CollectionCustomViewParams<M>>(activeCustomView.Builder)
1063
- : undefined;
1064
-
1065
- const innerView = CustomViewBuilder ? (
1066
- <ErrorBoundary>
1067
- <CustomViewBuilder
1068
- key={`custom-view-${activeCustomView!.key}-${path}`}
1069
- collection={collection}
1070
- tableController={tableController}
1071
- path={path}
1072
- parentCollectionSlugs={parentCollectionSlugs}
1073
- parentEntityIds={parentEntityIds}
1074
- onEntityClick={onEntityClick}
1075
- onNewClick={canCreateEntities ? onNewClick : undefined}
1076
- canCreate={canCreateEntities}
1077
- selectionController={usedSelectionController}
1078
- selectionEnabled={selectionEnabled}
1079
- highlightedEntities={highlightedEntity ? [highlightedEntity] : []}
1080
- deletedEntities={deletedEntities}
1081
- emptyComponent={emptyComponent}
1082
- size={activeCustomView!.sizeable ? customViewSize : undefined}
1083
- viewMode={viewMode}
1084
- />
1085
- </ErrorBoundary>
1086
- ) : viewMode === "kanban" ? (
1087
- <CollectionBoardViewBinding
1088
- key={`kanban-view-${path}-${selectedKanbanProperty}`}
1089
- collection={collection}
1090
- tableController={tableController}
1091
- fullPath={path}
1092
- parentCollectionSlugs={parentCollectionSlugs} parentEntityIds={parentEntityIds}
1093
- columnProperty={selectedKanbanProperty}
1094
- onEntityClick={onEntityClick}
1095
- selectionController={usedSelectionController}
1096
- selectionEnabled={selectionEnabled}
1097
- highlightedEntities={highlightedEntity ? [highlightedEntity] : []}
1098
- deletedEntities={deletedEntities}
1099
- emptyComponent={emptyComponent}
1100
- />
1101
- ) : viewMode === "cards" ? (
1102
- <CollectionCardViewBinding
1103
- key={`cards-view-${path}`}
1104
- collection={collection}
1105
- tableController={tableController}
1106
- onEntityClick={onEntityClick}
1107
- selectionController={usedSelectionController}
1108
- selectionEnabled={selectionEnabled}
1109
- highlightedEntities={highlightedEntity ? [highlightedEntity] : []}
1110
- onScroll={tableController.onScroll}
1111
- initialScroll={tableController.initialScroll}
1112
- size={cardSize}
1113
- emptyComponent={emptyComponent}
1114
- />
1115
- ) : viewMode === "list" ? (
1116
- <CollectionListViewBinding
1117
- key={`list-view-${path}`}
1118
- collection={collection}
1119
- tableController={tableController}
1120
- onEntityClick={onEntityClick}
1121
- selectionController={usedSelectionController}
1122
- selectionEnabled={selectionEnabled}
1123
- highlightedEntities={highlightedEntity ? [highlightedEntity] : []}
1124
- size={listSize}
1125
- emptyComponent={emptyComponent}
1126
- selectedEntityId={selectedEntityIdProp}
1127
- getActionsForEntity={getActionsForEntity}
1128
- path={path}
1129
- openEntityMode={openEntityMode}
1130
- />
1131
- ) : (
1132
- <ResolvedCollectionTable
1133
- key={`collection_table_${path}`}
1134
- hideToolbar={true}
1135
- additionalFields={additionalFields}
1136
- tableController={tableController}
1137
- enablePopupIcon={true}
1138
- displayedColumnIds={displayedColumnIds}
1139
- onSizeChanged={onTableSizeChanged}
1140
- onEntityClick={onEntityClick}
1141
- onColumnResize={onColumnResize}
1142
- onValueChange={onValueChange}
1143
- tableRowActionsBuilder={tableRowActionsBuilder}
1144
- uniqueFieldValidator={uniqueFieldValidator}
1145
- selectionController={usedSelectionController}
1146
- highlightedEntities={highlightedEntity ? [highlightedEntity] : []}
1147
- defaultSize={tableSize}
1148
- properties={resolvedCollection.properties}
1149
- getPropertyFor={getPropertyFor}
1150
- onScroll={tableController.onScroll}
1151
- initialScroll={tableController.initialScroll}
1152
- emptyComponent={emptyComponent}
1153
- hoverRow={hoverRow}
1154
- inlineEditing={checkInlineEditing()}
1155
- AdditionalHeaderWidget={buildAdditionalHeaderWidget}
1156
- AddColumnComponent={addColumnComponentInternal}
1157
- getIdColumnWidth={getIdColumnWidth}
1158
- additionalIDHeaderWidget={<EntityIdHeaderWidget
1159
- path={path}
1160
- idPath={path}
1161
- collection={collection}
1162
- openEntityMode={openEntityMode}/>}
1163
- openEntityMode={openEntityMode}
1164
- onColumnsOrderChange={onColumnsOrderChange}
1165
- />
1166
- );
1167
-
1168
- const detailOpen = selectedEntityIdProp !== undefined;
1169
-
1170
- // List view gets a centered, titled reading surface. Opening a detail
1171
- // panel collapses the title and drops the centering so the list can
1172
- // shrink into the master column.
1173
- const collectionSurface = viewMode === "list"
1174
- ? (
1175
- <div
1176
- className={cls(
1177
- "flex flex-col w-full",
1178
- detailOpen ? "" : "max-w-6xl mx-auto px-3 md:px-4 lg:px-6 py-4"
1179
- )}
1180
- >
1181
- {/* Collapsible title — grid-rows for smooth height, transform for GPU */}
1182
- <div
1183
- className={cls(
1184
- "grid transition-[grid-template-rows,transform,margin] duration-150 ease-out",
1185
- detailOpen
1186
- ? "grid-rows-[0fr] -translate-y-2 mt-0 mb-0"
1187
- : "grid-rows-[1fr] translate-y-0 mt-12 mb-6"
1188
- )}
1189
- >
1190
- <div className="overflow-hidden flex items-center gap-4">
1191
- <Typography gutterBottom variant="h4" className="grow mb-0" component="h4">
1192
- {collection.name}
1193
- </Typography>
1194
- </div>
1195
- </div>
1196
- {pluginWidgets.length > 0 && (
1197
- <div
1198
- className={cls(
1199
- "grid transition-[grid-template-rows] duration-150 ease-out",
1200
- detailOpen ? "grid-rows-[0fr]" : "grid-rows-[1fr]"
1201
- )}
1202
- >
1203
- <div className="overflow-hidden flex-shrink-0">
1204
- {pluginWidgets}
1205
- </div>
1206
- </div>
1207
- )}
1208
- {innerView}
1209
- </div>
1210
- )
1211
- : innerView;
1212
-
1213
- const mainContent = (
1214
- <div className={cls("overflow-hidden h-full w-full rounded-md flex flex-col dark:bg-surface-800", className)}
1215
- ref={containerRef}>
1216
-
1217
- {countFetcher}
1218
-
1219
- {/* A plugin's error view takes priority over the built-in
1220
- banner, the same precedence the empty state uses. This was
1221
- computed above and then rendered nowhere, so `collection.error`
1222
- — a declared, documented slot with its own props interface —
1223
- did nothing at all, and a plugin author had no way to tell. */}
1224
- {pluginErrorView ?? (tableController.dataLoadingError && tableController.data.length > 0 && (
1225
- <div className="flex items-center gap-4 px-4 py-2 bg-red-50 dark:bg-red-900/20 border-b border-red-200 dark:border-red-800 flex-shrink-0">
1226
- <Typography variant="body2" className="text-red-700 dark:text-red-300 flex-1">
1227
- <strong>Warning:</strong> {tableController.dataLoadingError.message || "Failed to update data."}
1228
- </Typography>
1229
- </div>
1230
- ))}
1231
-
1232
- {/* When isSplitLayout, SplitListView is ALWAYS mounted — regardless
1233
- of viewMode. The toolbar + current view live in the left panel;
1234
- the right panel (entity detail) shows/hides based on selection.
1235
- This keeps the toolbar's React tree position stable across view
1236
- mode switches, preventing the ViewModeToggle popover from flashing. */}
1237
- {isSplitLayout ? (
1238
- <SplitListView
1239
- key={`split-list-view-${path}`}
1240
- collection={collection}
1241
- tableController={tableController}
1242
- onEntityClick={onEntityClick}
1243
- onNewClick={onNewClick}
1244
- selectionController={usedSelectionController}
1245
- selectionEnabled={selectionEnabled}
1246
- highlightedEntities={highlightedEntity ? [highlightedEntity] : []}
1247
- onScroll={tableController.onScroll}
1248
- initialScroll={tableController.initialScroll}
1249
- size={listSize}
1250
- emptyComponent={emptyComponent}
1251
- path={path}
1252
- parentCollectionSlugs={parentCollectionSlugs} parentEntityIds={parentEntityIds}
1253
- selectedEntityId={selectedEntityIdProp}
1254
- selectedTab={selectedTabProp}
1255
- toolbar={toolbarNode}
1256
- >
1257
- {/* The left panel keeps rendering whichever view mode is
1258
- active. Swapping it to a list on selection would make
1259
- a board or card grid vanish on every click. */}
1260
- {collectionSurface}
1261
- </SplitListView>
1262
- ) : (
1263
- <div className="flex flex-col w-full h-full">
1264
- {toolbarNode}
1265
- {/* `min-h-0` is what lets the board scroll its columns.
1266
- A flex item defaults to `min-height: auto`, so this
1267
- one grew to the board's full content height — 1230px
1268
- in a 883px area — and the ancestor's `overflow-hidden`
1269
- simply cut the rest off, with each column's own
1270
- scroller never reaching its limit. */}
1271
- <div className={cls(
1272
- "flex-1 min-h-0 flex flex-col",
1273
- (viewMode === "list" || viewMode === "cards") && "overflow-y-auto"
1274
- )}>
1275
- {collectionSurface}
1276
- </div>
1277
- </div>
1278
- )}
1279
-
1280
- {popupCell && <PopupFormField
1281
- key={`popup_form_${popupCell?.propertyKey}_${popupCell?.entityId}`}
1282
- open={Boolean(popupCell)}
1283
- onClose={onPopupClose}
1284
- cellRect={popupCell?.cellRect}
1285
- propertyKey={popupCell?.propertyKey}
1286
- collection={collection}
1287
- entityId={popupCell.entityId}
1288
- tableKey={tableKey.current}
1289
- customFieldValidator={uniqueFieldValidator}
1290
- path={path}
1291
- onCellValueChange={onValueChange}
1292
- container={containerRef.current}/>}
1293
-
1294
- {deleteEntityClicked &&
1295
- <DeleteEntityDialog
1296
- entityOrEntitiesToDelete={deleteEntityClicked}
1297
- path={path}
1298
- collection={collection}
1299
- callbacks={collection.callbacks}
1300
- open={Boolean(deleteEntityClicked)}
1301
- onEntityDelete={internalOnEntityDelete}
1302
- onMultipleEntitiesDelete={internalOnMultipleEntitiesDelete}
1303
- onClose={() => setDeleteEntityClicked(undefined)}/>}
1304
-
1305
- </div>
1306
- );
1307
-
1308
- return mainContent;
1309
- }, (a, b) => {
1310
- return equal(a.path, b.path) &&
1311
- equal(a.parentCollectionSlugs, b.parentCollectionSlugs) && equal(a.parentEntityIds, b.parentEntityIds) &&
1312
- equal(a.isSubCollection, b.isSubCollection) &&
1313
- equal(a.className, b.className) &&
1314
- equal(a.properties, b.properties) &&
1315
- equal(a.propertiesOrder, b.propertiesOrder) &&
1316
- equal(a.hideIdFromCollection, b.hideIdFromCollection) &&
1317
- equal(a.inlineEditing, b.inlineEditing) &&
1318
- equal(a.selectionEnabled, b.selectionEnabled) &&
1319
- equal(a.selectionController, b.selectionController) &&
1320
- equal(a.Actions, b.Actions) &&
1321
- equal(a.defaultSize, b.defaultSize) &&
1322
- equal(a.includeJsonView, b.includeJsonView) &&
1323
- equal(a.additionalFields, b.additionalFields) &&
1324
- equal(a.sideDialogWidth, b.sideDialogWidth) &&
1325
- equal(a.openEntityMode, b.openEntityMode) &&
1326
- equal(a.exportable, b.exportable) &&
1327
- equal(a.history, b.history) &&
1328
- equal(a.fixedFilter, b.fixedFilter) &&
1329
- equal(a.selectedEntityId, b.selectedEntityId) &&
1330
- equal(a.selectedTab, b.selectedTab);
1331
- }) as React.FunctionComponent<CollectionViewBindingProps<any>>;
1332
-
1333
- export const CollectionViewBinding = React.memo(
1334
- function CollectionViewBinding<M extends Record<string, unknown>>(props: CollectionViewBindingProps<M>) {
1335
- const collectionRegistry = useCollectionRegistryController();
1336
- const path = props.path ?? props.slug;
1337
- const collection = collectionRegistry.getCollection(path) ?? props;
1338
-
1339
- const content = <CollectionViewBindingInner {...props} />;
1340
-
1341
- if (collection) {
1342
- return (
1343
- <CollectionScopeProvider collection={collection as AdminCollection}>
1344
- {content}
1345
- </CollectionScopeProvider>
1346
- );
1347
- }
1348
- return content;
1349
- }
1350
- ) as React.FunctionComponent<CollectionViewBindingProps<any>>;
1351
-
1352
- /**
1353
- * Default empty state shown when a collection has no entities.
1354
- * Used as the fallback for the `"Collection.EmptyState"` component override.
1355
- *
1356
- * @internal
1357
- */
1358
- function DefaultCollectionEmptyState({
1359
- canCreate,
1360
- onNewClick,
1361
- isSearching,
1362
- searchString
1363
- }: {
1364
- canCreate: boolean;
1365
- onNewClick: () => void;
1366
- isSearching: boolean;
1367
- searchString: string;
1368
- }) {
1369
- const { t } = useTranslation();
1370
- if (canCreate) {
1371
- return (
1372
- <div className="flex flex-col items-center justify-center">
1373
- <Typography variant={"subtitle2"}>{t("so_empty")}</Typography>
1374
- <Button
1375
- onClick={onNewClick}
1376
- className="mt-4"
1377
- >
1378
- <PlusIcon/>
1379
- {t("create_your_first_entry")}
1380
- </Button>
1381
- </div>
1382
- );
1383
- }
1384
- return (
1385
- <Typography variant={"label"}>
1386
- {isSearching
1387
- ? t("no_results_search", { search: searchString })
1388
- : t("no_results_filter_sort")}
1389
- </Typography>
1390
- );
1391
- }
1392
-
1393
- /**
1394
- * Inflight count request deduplication map.
1395
- * Keyed by `path|filterKey|sort|search` so that concurrent
1396
- * callers (e.g. React StrictMode double-mount) share the same promise.
1397
- */
1398
- const inflightCountRequests = new Map<string, Promise<number>>();
1399
-
1400
- export function EntitiesCount({
1401
- path,
1402
- collection,
1403
- filter,
1404
- sortBy,
1405
- searchString,
1406
- onCountChange
1407
- }: {
1408
- path: string,
1409
- collection: AdminCollection,
1410
- filter?: FilterValues<any>,
1411
- sortBy?: OrderByTuple[],
1412
- /**
1413
- * Required, not optional. The term sits in the same scope as the element
1414
- * that mounts this and is passed to the toolbar and the empty state beside
1415
- * it — it was simply not passed here, so a searched list showed the count
1416
- * of the unsearched collection. A required prop is what stops the next
1417
- * caller doing the same; there is no useful "count without the search".
1418
- */
1419
- searchString: string | undefined,
1420
- onCountChange?: (count: number | null | undefined) => void,
1421
- }) {
1422
-
1423
- const dataClient = useData();
1424
-
1425
- // The whole sort as one string: it keys the dedup cache and drives the
1426
- // effect, and a `sortBy` array is a new reference on every render.
1427
- const sortKey = React.useMemo(() => sortBy ? serializeOrderBy(sortBy) ?? "" : "", [sortBy]);
1428
-
1429
- // Use refs for values that should NOT trigger re-fetches
1430
- const dataClientRef = React.useRef(dataClient);
1431
- dataClientRef.current = dataClient;
1432
- const onCountChangeRef = React.useRef(onCountChange);
1433
- onCountChangeRef.current = onCountChange;
1434
-
1435
- // Serialize filter to a stable string to avoid re-fetches on object identity changes
1436
- const filterKey = React.useMemo(() => filter ? JSON.stringify(filter) : "", [filter]);
1437
-
1438
- useEffect(() => {
1439
- const accessor = dataClientRef.current.collection(path);
1440
- if (!accessor.count) {
1441
- onCountChangeRef.current?.(undefined);
1442
- return;
1443
- }
1444
-
1445
- let cancelled = false;
1446
-
1447
- // filterValues is already FilterValues — pass directly
1448
- const whereParams = filter && Object.keys(filter).length > 0 ? filter : undefined;
1449
- const orderByParams = sortBy && sortBy.length > 0
1450
- ? sortBy.map(([field, direction]) => [String(field), direction] as OrderByTuple)
1451
- : undefined;
1452
-
1453
- // Deduplicate inflight count requests (e.g. React StrictMode double-mount)
1454
- // The search term is part of the query, so it is part of the key. The
1455
- // cache is module-level and outlives an unmount, so a key that omits it
1456
- // does not merely lose precision — it answers one search with a
1457
- // different search's total.
1458
- const cacheKey = `${path}|${filterKey}|${sortKey}|${searchString ?? ""}`;
1459
- let countPromise = inflightCountRequests.get(cacheKey);
1460
- if (!countPromise) {
1461
- countPromise = accessor.count({
1462
- where: whereParams,
1463
- orderBy: orderByParams,
1464
- searchString
1465
- });
1466
- inflightCountRequests.set(cacheKey, countPromise);
1467
- // Clean up the inflight entry once resolved/rejected
1468
- countPromise.finally(() => inflightCountRequests.delete(cacheKey));
1469
- }
1470
-
1471
- countPromise.then((c) => {
1472
- if (!cancelled) onCountChangeRef.current?.(c);
1473
- }).catch((e) => {
1474
- console.warn("Error fetching count", e);
1475
- if (!cancelled) onCountChangeRef.current?.(undefined);
1476
- });
1477
-
1478
- return () => { cancelled = true; };
1479
- // `filterKey` and `sortKey` ARE `filter` and `sortBy` — the memoized
1480
- // serializations a few lines up, and the only stable identity either
1481
- // has. Both arrive as freshly built objects on every render, so
1482
- // depending on them directly would re-run this effect each time and
1483
- // fire a count request per render. The keys change exactly when the
1484
- // values do, which is the condition this effect actually wants.
1485
- // eslint-disable-next-line react-hooks/exhaustive-deps
1486
- }, [path, filterKey, sortKey, searchString]);
1487
-
1488
- // The toolbar renders the count; this component only fetches and reports it
1489
- return null;
1490
- }
1491
-
1492
- function buildPropertyWidthOverwrite(key: string, width: number): PartialCollectionConfig {
1493
- if (key.includes(".")) {
1494
- const [parentKey, ...childKey] = key.split(".");
1495
- return { properties: { [parentKey]: buildPropertyWidthOverwrite(childKey.join("."), width) } } as PartialCollectionConfig;
1496
- }
1497
- return { properties: { [key]: { admin: { columnWidth: width } } } } as PartialCollectionConfig;
1498
- }
1499
-
1500
- function EntityIdHeaderWidget({
1501
- collection,
1502
- path,
1503
- idPath,
1504
- openEntityMode
1505
- }: {
1506
- collection: AdminCollection,
1507
- path: string,
1508
- idPath: string,
1509
- /**
1510
- * Resolved by the parent so that finding an entity by id lands it in the
1511
- * same surface a row click would — not a different one.
1512
- */
1513
- openEntityMode: OpenEntityMode
1514
- }) {
1515
-
1516
- const { t } = useTranslation();
1517
- const urlController = useUrlController();
1518
- const [openPopup, setOpenPopup] = React.useState(false);
1519
- const [searchString, setSearchString] = React.useState("");
1520
- const [recentIds, setRecentIds] = React.useState<string[]>(getRecentIds(collection.slug).map(String));
1521
- const sidePanelController = useSidePanel();
1522
-
1523
- return (
1524
- <Tooltip title={!openPopup ? t("find_by_id") : undefined} asChild={false}>
1525
- <Popover
1526
- open={openPopup}
1527
- onOpenChange={setOpenPopup}
1528
- sideOffset={0}
1529
- align={"start"}
1530
- alignOffset={-117}
1531
- trigger={
1532
- <IconButton size={"small"}>
1533
- <SearchIcon size={iconSize.small}/>
1534
- </IconButton>
1535
- }>
1536
- <div
1537
- className={cls("my-2 rounded-lg bg-surface-50 dark:bg-surface-800 text-surface-900 dark:text-white")}>
1538
- <form noValidate={true}
1539
- onSubmit={(e) => {
1540
- e.preventDefault();
1541
- if (!searchString) return;
1542
- setOpenPopup(false);
1543
- const entityId = searchString.trim();
1544
- setRecentIds(addRecentId(collection.slug, entityId).map(String));
1545
- navigateToEntity({
1546
- openEntityMode,
1547
- collection,
1548
- entityId,
1549
- path,
1550
-
1551
- sidePanelController,
1552
- navigation: urlController
1553
- })
1554
- }}
1555
- className={"w-96 max-w-full"}>
1556
-
1557
- <div className="flex p-2 w-full gap-2">
1558
- <TextField
1559
- autoFocus={openPopup}
1560
- aria-label={t("find_entity_by_id")}
1561
- placeholder={t("find_entity_by_id")}
1562
- size="small"
1563
- onChange={(e) => {
1564
- setSearchString(e.target.value);
1565
- }}
1566
- value={searchString}
1567
- className="flex-grow"
1568
- inputClassName={cls("rounded-lg bg-white dark:bg-surface-900", focusedDisabled)}
1569
- />
1570
- <Button variant={"text"}
1571
- disabled={!(searchString.trim())}
1572
- type={"submit"}
1573
- ><ArrowRightToLineIcon/></Button>
1574
- </div>
1575
- </form>
1576
- {recentIds && recentIds.length > 0 && <div className="flex flex-col gap-2 p-2">
1577
- {recentIds.map(id => (
1578
- <ReferencePreview reference={new EntityReference({ id,
1579
- path })}
1580
- key={id}
1581
- hover={true}
1582
- onClick={() => {
1583
- setOpenPopup(false);
1584
- navigateToEntity({
1585
- openEntityMode,
1586
- collection,
1587
- entityId: id,
1588
- path,
1589
-
1590
- sidePanelController,
1591
- navigation: urlController
1592
- })
1593
- }}
1594
- includeEntityLink={false}
1595
- size={"small"}/>
1596
- ))}
1597
- </div>}
1598
- </div>
1599
- </Popover>
1600
-
1601
- </Tooltip>
1602
- );
1603
- }