@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,1430 +0,0 @@
1
-
2
- import type { OrderByTuple, Properties, Property } from "@rebasepro/types";
3
- import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
4
- import { Entity } from "@rebasepro/types";
5
- import { CollectionSize, EntityAction, EntityTableController, SelectionController, AdminCollection } from "@rebasepro/cms-types";
6
- import {
7
- ArrowDownIcon,
8
- ArrowUpIcon,
9
- Checkbox,
10
- Chip,
11
- cls,
12
- defaultBorderMixin,
13
- IconButton,
14
- Tooltip,
15
- Typography,
16
- ListView
17
- } from "@rebasepro/ui";
18
- import { PropertyPreview } from "../../preview";
19
- import {
20
- useAuthController,
21
- useCustomizationController
22
- } from "@rebasepro/app";
23
- import { useAnalyticsController } from "@rebasepro/app";
24
- import { IconForView } from "@rebasepro/app";
25
- import { getIcon } from "@rebasepro/app";
26
- import { hasDeclaredDisplay } from "@rebasepro/app";
27
- import { useTranslation } from "@rebasepro/app";
28
- import { formatRelativeTime, getValueInPath } from "@rebasepro/utils";
29
- import { useCollectionSlotKeys, useEntitySlots, type CollectionSlotKeys, type EntityPreviewSlots } from "./usePreviewSlots";
30
- import { SlotValue, TagChips } from "./SlotValue";
31
- import { Highlighted } from "./SearchHighlight";
32
- import { useSearchExplanation, MatchExplanation, fieldLabel } from "./SearchExplanation";
33
- import { useAdminContext } from "../../hooks/useAdminContext";
34
- import { resolveEntityAction } from "../../util/resolutions";
35
- import { getSortablePropertyOptions } from "../CollectionTableBinding/column_utils";
36
- import { getResolvedPropertyInPath } from "../../util/property_utils";
37
-
38
- export type CollectionListViewBindingProps<M extends Record<string, unknown> = Record<string, unknown>> = {
39
- collection: AdminCollection<M>;
40
- tableController: EntityTableController<M>;
41
- onEntityClick?: (entity: Entity<M>) => void;
42
- selectionController?: SelectionController<M>;
43
- selectionEnabled?: boolean;
44
- highlightedEntities?: Entity<M>[];
45
- emptyComponent?: React.ReactNode;
46
-
47
- /**
48
- * Size of the list rows.
49
- * - "xs": Most compact, single-line rows
50
- * - "s": Compact with minimal info
51
- * - "m": Balanced (default) — title + subtitle
52
- * - "l": Spacious with more preview fields
53
- * - "xl": Full detail with all preview fields
54
- */
55
- size?: CollectionSize;
56
- /**
57
- * ID of the currently selected/active entity. When set, the matching
58
- * row is visually highlighted with a primary accent.
59
- */
60
- selectedEntityId?: string | number;
61
-
62
- /**
63
- * Callback to get entity actions for a given entity.
64
- * Only actions with `showActionsInListView: true` will be rendered.
65
- */
66
- getActionsForEntity?: (params: { entity?: Entity<M>, customEntityActions?: EntityAction[] }) => EntityAction[];
67
-
68
- /**
69
- * Full path of the collection, used as context for action handlers.
70
- */
71
- path?: string;
72
-
73
- /**
74
- * How entities open when an action triggers navigation.
75
- */
76
- openEntityMode?: "side_panel" | "full_screen" | "split" | "dialog";
77
- };
78
-
79
- /**
80
- * Which display slot fills a column, when a slot does rather than a property.
81
- * A status is an enum chip and a date a relative timestamp wherever they come
82
- * from, so the cell renders the slot; the column exists either way, because a
83
- * header needs something to sit over.
84
- */
85
- type ListColumnSlot = "tags" | "status" | "date";
86
-
87
- /**
88
- * One column of the list, in the header and in every row.
89
- *
90
- * The header and the cells read the same definition — the same width, the same
91
- * alignment, the same decision about being shown at this container width — so a
92
- * label cannot end up over a column that is not there, or over the wrong one.
93
- */
94
- type ListColumn = {
95
- key: string;
96
- label: string;
97
- /** Set when a display slot fills the cell; absent when the record does. */
98
- slot?: ListColumnSlot;
99
- property?: Property;
100
- align: "left" | "center" | "right";
101
- width: string;
102
- /**
103
- * What {@link width} costs in pixels, so the fit calculation can budget in
104
- * the same units the row lays out in. A flat per-column estimate treated a
105
- * relation (w-64) as costing the same as a date (w-28), which is where a row
106
- * ran out of width for the title while still claiming everything fit.
107
- */
108
- widthPx: number;
109
- /** Whether clicking this header can order the collection by it. */
110
- sortable: boolean;
111
- /**
112
- * What survives when the row runs out of width: the lowest goes first.
113
- */
114
- priority: number;
115
- };
116
-
117
- // ── Column priorities ─────────────────────────────────────────────────
118
- // Read as "what a narrowing row gives up first".
119
- //
120
- // Three bands. The sort or filter *in force* comes first: it is the reason the
121
- // rows are in the order they are in, and hiding the column it names leaves that
122
- // order unexplained. The collection's own columns come next — its `listProperties`,
123
- // or the status and date it was read as having. A column left over from a
124
- // request since cleared comes last: it is worth keeping while there is room for
125
- // it, and it is not worth the collection's own status.
126
-
127
- /** A column left over from a sort or filter that is no longer applied. */
128
- const PRIORITY_STALE_REQUEST = 1000;
129
- /** The collection's own columns. Slots sit just above it, in give-way order. */
130
- const PRIORITY_DECLARED = 2000;
131
- const PRIORITY_DATE = PRIORITY_DECLARED;
132
- const PRIORITY_STATUS = PRIORITY_DECLARED + 1;
133
- const PRIORITY_TAGS = PRIORITY_DECLARED + 2;
134
- /** The sort or filter currently in force. */
135
- const PRIORITY_ACTIVE_REQUEST = 3000;
136
-
137
- /**
138
- * How wide a column of this property wants to be — the Tailwind class the cell
139
- * gets, and the pixel cost of that class. The two are returned together because
140
- * they must not drift: the budget is only meaningful if it describes the widths
141
- * actually rendered. `lg:` variants key off the viewport, not this panel, so a
142
- * narrow split on a wide screen gets the larger of the two — the cost we budget.
143
- */
144
- function getIdealColumnWidth(prop: Property): { width: string, widthPx: number } {
145
- if (prop.type === "string" && "enum" in prop) return { width: "flex-shrink-0 w-32", widthPx: 128 };
146
- if (prop.type === "date" || prop.type === "number" || prop.type === "boolean") return { width: "flex-shrink-0 w-28", widthPx: 112 };
147
- if (prop.type === "reference" || prop.type === "relation") return { width: "flex-shrink-0 w-56 lg:w-64", widthPx: 256 };
148
- if (prop.type === "string") return { width: "flex-shrink-0 w-40 lg:w-48", widthPx: 192 };
149
- return { width: "flex-shrink-0 w-40", widthPx: 160 };
150
- }
151
-
152
- /**
153
- * Get row padding/spacing classes based on size
154
- */
155
- function getRowClasses(size: CollectionSize): string {
156
- switch (size) {
157
- case "xs": return "py-2 px-5";
158
- case "s": return "py-2.5 px-5";
159
- case "m": return "py-3 px-5";
160
- case "l": return "py-4 px-5";
161
- case "xl": return "py-5 px-5";
162
- default: return "py-3 px-5";
163
- }
164
- }
165
-
166
- // ── Row layout budget ─────────────────────────────────────────────────
167
- // What the row spends before a single column is placed. These mirror the
168
- // classes on the row itself (`px-5`, `gap-4`, `w-8` checkbox, `w-10` image);
169
- // they are the same layout described twice, so a change to one is a change to
170
- // the other.
171
-
172
- /** `px-5` on both sides. */
173
- const ROW_PADDING_WIDTH = 40;
174
- /** The checkbox cell (`w-8`). */
175
- const CHECKBOX_WIDTH = 32;
176
- /** The thumbnail / icon square (`w-10`). */
177
- const IMAGE_WIDTH = 40;
178
- /** `gap-4` between every cell of the row. */
179
- const COLUMN_GAP = 16;
180
-
181
- /**
182
- * The title's share of a row, taken before any column bids for width.
183
- *
184
- * A title is a name, and a name truncated to "Lider Sr. en…" has stopped being
185
- * one. Columns are a scanner's convenience; the title is the row's identity, so
186
- * a narrow list spends its width on the title and simply shows fewer columns.
187
- */
188
- const TITLE_COMFORTABLE_WIDTH = 320;
189
-
190
- /** An `IconButton size="small"` (`w-8`), which is what a row action renders as. */
191
- const ACTION_BUTTON_WIDTH = 32;
192
- /** `gap-0.5` between row actions. */
193
- const ACTION_BUTTON_GAP = 2;
194
- /**
195
- * How many rows are read to size the actions cell.
196
- *
197
- * Actions are resolved per record, so the width they need is only knowable by
198
- * asking — and asking every loaded row would mean re-resolving thousands of
199
- * them on every page. The first rows are representative: a collection whose
200
- * actions vary at all varies within its first screenful.
201
- */
202
- const ACTIONS_SAMPLE_SIZE = 20;
203
-
204
- /** Stable empty array for when no list-view actions are available. */
205
- const EMPTY_LIST_VIEW_ACTIONS: EntityAction[] = [];
206
-
207
- /**
208
- * Returns true if a property type should NOT be rendered via
209
- * PropertyPreview in list row columns (because it would blow up height).
210
- */
211
- function isComplexPropertyType(property: Property): boolean {
212
- if (property.type === "array") {
213
- const ofProp = "of" in property ? property.of : undefined;
214
- const innerProp = ofProp ? (Array.isArray(ofProp) ? ofProp[0] : ofProp) : undefined;
215
- if (innerProp && typeof innerProp === "object" && "enum" in innerProp && innerProp.enum) {
216
- return false;
217
- }
218
- }
219
- return property.type === "array"
220
- || property.type === "map"
221
- || property.type === "reference"
222
- || property.type === "relation";
223
- }
224
-
225
- /**
226
- * Render a complex value as a compact, single-line string.
227
- * - Arrays → "Item, Item +3"
228
- * - Maps → "4 fields"
229
- * - Refs → entity ID
230
- * - Relations → entity ID or name
231
- */
232
- function compactValueSummary(value: unknown, property: Property): string | null {
233
- if (value === undefined || value === null) return null;
234
-
235
- if (property.type === "array") {
236
- if (!Array.isArray(value)) return null;
237
- if (value.length === 0) return null;
238
-
239
- const of = "of" in property ? property.of : undefined;
240
- const innerProp = of ? (Array.isArray(of) ? of[0] : of) : undefined;
241
-
242
- // String/number arrays → join
243
- const labels = value.map((v: unknown) => {
244
- if (typeof v === "string") return v;
245
- if (typeof v === "number") return String(v);
246
- // Reference inside array
247
- if (v && typeof v === "object" && "id" in v) return String((v as Record<string, unknown>).id);
248
- // Enum label lookup
249
- if (innerProp && "enum" in innerProp && innerProp.enum && typeof v === "string") {
250
- const enumValues = innerProp.enum;
251
- if (Array.isArray(enumValues)) {
252
- const match = enumValues.find((e: unknown) =>
253
- typeof e === "object" && e !== null && "id" in e && (e as Record<string, unknown>).id === v
254
- );
255
- if (match && typeof match === "object" && "label" in match) return String((match as Record<string, unknown>).label);
256
- } else if (typeof enumValues === "object") {
257
- const label = (enumValues as Record<string, unknown>)[v];
258
- if (typeof label === "string") return label;
259
- if (label && typeof label === "object" && "label" in label) return String((label as Record<string, unknown>).label);
260
- }
261
- return v;
262
- }
263
- return "•";
264
- });
265
-
266
- const MAX_SHOWN = 2;
267
- const shown = labels.slice(0, MAX_SHOWN).join(", ");
268
- const remaining = labels.length - MAX_SHOWN;
269
- return remaining > 0 ? `${shown} +${remaining}` : shown;
270
- }
271
-
272
- if (property.type === "map") {
273
- if (typeof value !== "object" || value === null) return null;
274
- const count = Object.keys(value).length;
275
- return count === 0 ? null : `${count} field${count !== 1 ? "s" : ""}`;
276
- }
277
-
278
- if (property.type === "reference") {
279
- if (typeof value === "string") return value;
280
- if (value && typeof value === "object" && "id" in value) return String((value as Record<string, unknown>).id);
281
- return null;
282
- }
283
-
284
- if (property.type === "relation") {
285
- if (typeof value === "string" || typeof value === "number") return String(value);
286
- if (value && typeof value === "object") {
287
- const obj = value as Record<string, unknown>;
288
- // EntityRelation.data is a Entity: { id, path, values: { name, ... } }
289
- if (obj.data && typeof obj.data === "object") {
290
- const data = obj.data as Record<string, unknown>;
291
- const values = (data.values && typeof data.values === "object")
292
- ? data.values as Record<string, unknown>
293
- : data;
294
- const name = values.name ?? values.title ?? values.display_name
295
- ?? values.displayName ?? values.label ?? values.email;
296
- if (name && typeof name !== "object") return String(name);
297
- }
298
- if ("id" in obj) return String(obj.id);
299
- }
300
- return null;
301
- }
302
-
303
- return null;
304
- }
305
-
306
- /**
307
- * Format a date value for display in the list.
308
- *
309
- * A date column holds whatever the developer put in it, so the value is as
310
- * likely to be ahead of now as behind it. The relative phrase therefore comes
311
- * from {@link formatRelativeTime}, which reads the sign: a row due this
312
- * afternoon says "in 2h" rather than the "-1d ago" that flooring a negative
313
- * difference used to produce.
314
- */
315
- function formatDateValue(value: unknown, now: Date = new Date()): string | null {
316
- if (!value) return null;
317
- let date: Date | null = null;
318
- if (value instanceof Date) {
319
- date = value;
320
- } else if (typeof value === "string" || typeof value === "number") {
321
- date = new Date(value);
322
- }
323
- if (!date || isNaN(date.getTime())) return null;
324
-
325
- const diffMs = now.getTime() - date.getTime();
326
- const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
327
-
328
- // Today and yesterday get their own wording; both are strictly in the past,
329
- // because a negative difference floors to -1 and falls through to below.
330
- if (diffDays === 0) {
331
- return date.toLocaleTimeString(undefined, { hour: "2-digit",
332
- minute: "2-digit" });
333
- }
334
- if (diffDays === 1) return "Yesterday";
335
-
336
- const relative = formatRelativeTime(date, { now });
337
- if (relative) return relative;
338
-
339
- // Comparing years rather than counting days: a date six weeks back across a
340
- // new year needs the year, and one 400 days ahead needs it just as much.
341
- return date.toLocaleDateString(undefined, { month: "short",
342
- day: "numeric",
343
- year: date.getFullYear() !== now.getFullYear() ? "numeric" : undefined });
344
- }
345
-
346
- /**
347
- * Classic admin list view for displaying entities.
348
- * Designed to be the most familiar, stereotypical admin content management view:
349
- * - Clean rows with checkbox, icon/avatar, title, metadata, and actions
350
- * - Column-sortable headers
351
- * - Infinite scroll
352
- */
353
- export function CollectionListViewBinding<M extends Record<string, unknown> = Record<string, unknown>>({
354
- collection,
355
- tableController,
356
- onEntityClick,
357
- selectionController,
358
- selectionEnabled = true,
359
- highlightedEntities,
360
- emptyComponent,
361
-
362
- size = "m",
363
- selectedEntityId,
364
- getActionsForEntity,
365
- path,
366
- openEntityMode
367
- }: CollectionListViewBindingProps<M>) {
368
- const authController = useAuthController();
369
- const customizationController = useCustomizationController();
370
- const analyticsController = useAnalyticsController();
371
- const context = useAdminContext();
372
-
373
- const containerRef = useRef<HTMLDivElement>(null);
374
- // `undefined` is "not measured yet", and it renders the title alone. The
375
- // seed used to be a 1200px guess, which is a claim about a list that may be
376
- // a 300px column of a split — and, because the ref below was never attached
377
- // to anything, a guess nothing ever corrected.
378
- const [containerWidth, setContainerWidth] = useState<number | undefined>(undefined);
379
-
380
- // Track container width for responsive column visibility
381
- useEffect(() => {
382
- const el = containerRef.current;
383
- if (!el) return;
384
- const measure = (width: number) => setContainerWidth(width > 0 ? width : undefined);
385
- measure(el.getBoundingClientRect().width);
386
- if (typeof ResizeObserver === "undefined") return;
387
- const ro = new ResizeObserver(([entry]) => {
388
- if (entry) measure(entry.contentRect.width);
389
- });
390
- ro.observe(el);
391
- return () => ro.disconnect();
392
- }, []);
393
-
394
- const {
395
- data,
396
- dataLoading,
397
- noMoreToLoad,
398
- dataLoadingError,
399
- itemCount,
400
- setItemCount,
401
- pageSize = 50,
402
- paginationEnabled,
403
- sortBy,
404
- setSortBy,
405
- filterValues,
406
- checkFilterCombination,
407
- searchString
408
- } = tableController;
409
-
410
- const resolvedCollection = collection;
411
-
412
- // ── Shared slot resolution (replaces 4 individual useMemo calls) ──
413
- const slotKeys = useCollectionSlotKeys(
414
- resolvedCollection as AdminCollection<Record<string, unknown>>,
415
- authController,
416
- customizationController.propertyConfigs
417
- );
418
- const { titleKey: titlePropertyKey, imageKey: imagePropertyKey, subtitleKey, statusKey: statusPropertyKey, dateKey: datePropertyKey } = slotKeys;
419
-
420
- const columnMode = !!resolvedCollection.listProperties && resolvedCollection.listProperties.length > 0;
421
-
422
- /**
423
- * The properties an `ORDER BY` can be written for, under the same authority
424
- * the table headers and the sort menu ask — so a header that offers a sort
425
- * here offers it there, and none of the three invents one the driver would
426
- * silently drop.
427
- */
428
- const sortableKeys = useMemo(
429
- () => new Set(getSortablePropertyOptions(resolvedCollection.properties as Properties).map(option => option.key)),
430
- [resolvedCollection.properties]
431
- );
432
-
433
- /** The row's identity cell: the leading column, or the title slot. */
434
- const titleColumn = useMemo<ListColumn | undefined>(() => {
435
- const key = columnMode ? resolvedCollection.listProperties?.[0] : titlePropertyKey;
436
- if (!key) return undefined;
437
- const property = resolvedCollection.properties[key] as Property | undefined;
438
- return {
439
- key,
440
- label: property?.name || (columnMode ? key : "Name"),
441
- property,
442
- align: "left",
443
- width: "flex-1 min-w-0",
444
- widthPx: TITLE_COMFORTABLE_WIDTH,
445
- sortable: sortableKeys.has(key),
446
- priority: Number.MAX_SAFE_INTEGER
447
- };
448
- }, [columnMode, resolvedCollection, titlePropertyKey, sortableKeys]);
449
-
450
- /**
451
- * The trailing columns the collection itself implies: the developer's
452
- * `listProperties` when there are any, otherwise the display slots the row
453
- * has always shown to the right of the title.
454
- */
455
- const declaredColumns = useMemo<ListColumn[]>(() => {
456
- if (columnMode) {
457
- const keys = resolvedCollection.listProperties!.slice(1);
458
- return keys.flatMap((key, index) => {
459
- const property = resolvedCollection.properties[key] as Property | undefined;
460
- if (!property) return [];
461
- return [{
462
- key,
463
- label: property.name || key,
464
- property,
465
- align: (property.type === "number" || property.type === "date" ? "right" : "left") as ListColumn["align"],
466
- ...getIdealColumnWidth(property),
467
- sortable: sortableKeys.has(key),
468
- // Declared order is a statement of importance: the first
469
- // column after the title outlives the last one.
470
- priority: PRIORITY_DECLARED + (keys.length - index)
471
- }];
472
- });
473
- }
474
-
475
- const cols: ListColumn[] = [];
476
-
477
- // A slot column exists when the collection has something to put in it —
478
- // a property to read, or a resolver that computes one. Whether a given
479
- // record fills it is a per-row question, and the wrong one to ask here:
480
- // a column that came and went with the values under it would leave its
481
- // header over a different cell on every row.
482
- const slotColumn = (
483
- slot: ListColumnSlot,
484
- key: string | undefined,
485
- fallbackLabel: string,
486
- layout: { align: ListColumn["align"], width: string, widthPx: number },
487
- priority: number
488
- ): ListColumn | undefined => {
489
- const declared = hasDeclaredDisplay(resolvedCollection, slot);
490
- if (!key && !declared) return undefined;
491
- const property = key ? resolvedCollection.properties[key] as Property | undefined : undefined;
492
- return {
493
- key: key ?? `display:${slot}`,
494
- label: property?.name || fallbackLabel,
495
- slot,
496
- property,
497
- ...layout,
498
- // A computed slot has no column to order by, whatever it renders.
499
- sortable: !!key && sortableKeys.has(key),
500
- priority
501
- };
502
- };
503
-
504
- const tags = slotColumn(
505
- "tags",
506
- slotKeys.tagsKey,
507
- "Tags",
508
- { align: "left", width: "flex-shrink-0 w-40", widthPx: 160 },
509
- PRIORITY_TAGS
510
- );
511
- if (tags) cols.push(tags);
512
-
513
- const status = slotColumn(
514
- "status",
515
- statusPropertyKey,
516
- "Status",
517
- { align: "left", width: "flex-shrink-0 w-32", widthPx: 128 },
518
- PRIORITY_STATUS
519
- );
520
- if (status) cols.push(status);
521
-
522
- const date = slotColumn(
523
- "date",
524
- datePropertyKey,
525
- "Modified",
526
- { align: "right", width: "flex-shrink-0 w-20", widthPx: 80 },
527
- PRIORITY_DATE
528
- );
529
- if (date) cols.push(date);
530
-
531
- return cols;
532
- }, [columnMode, resolvedCollection, slotKeys.tagsKey, statusPropertyKey, datePropertyKey, sortableKeys]);
533
-
534
- /** What the row already shows without a column of its own. */
535
- const shownKeys = useMemo(() => new Set<string | undefined>([
536
- titleColumn?.key,
537
- imagePropertyKey,
538
- subtitleKey,
539
- ...declaredColumns.map(col => col.key),
540
- // The relation chips under the title are as visible as any column.
541
- ...(columnMode ? [] : slotKeys.relationKeys)
542
- ]), [titleColumn, imagePropertyKey, subtitleKey, declaredColumns, columnMode, slotKeys.relationKeys]);
543
-
544
- /** The properties the sort names, minus the ones a cell already shows. */
545
- const sortedKeys = useMemo(
546
- () => (sortBy ?? []).map(([key]) => key as string).filter(key => !shownKeys.has(key)),
547
- [sortBy, shownKeys]
548
- );
549
-
550
- /** The properties the filters name, minus the ones a cell already shows. */
551
- const filteredKeys = useMemo(
552
- () => Object.keys(filterValues ?? {}).filter(key => !shownKeys.has(key)),
553
- [filterValues, shownKeys]
554
- );
555
-
556
- /**
557
- * The last property the sort named, and the last set the filters named.
558
- *
559
- * A column earns its place when a sort or a filter names it, and keeps it
560
- * once that sort or filter is cleared: taking it back would move every
561
- * column right of it at the moment the user is comparing values across rows,
562
- * and clearing a sort says nothing about wanting to stop seeing the property
563
- * it sorted on.
564
- *
565
- * What it does *not* do is accumulate. A sort names the properties it names
566
- * *now*, so ordering by four properties in turn is changing one's mind four
567
- * times, not asking for four columns — and asking for four is how the row
568
- * lost its status and its date to a wall of numbers. A new sort therefore
569
- * replaces the properties the last one left behind, and a new set of filters
570
- * replaces the set before it. At most one sort's columns and one set of
571
- * filter columns outlive their request.
572
- *
573
- * The list remounts per collection (it is keyed by path), so this is the
574
- * memory of one visit to one collection, not a preference.
575
- */
576
- const [lastSortedKeys, setLastSortedKeys] = useState<string[]>([]);
577
- const [lastFilteredKeys, setLastFilteredKeys] = useState<string[]>([]);
578
-
579
- useEffect(() => {
580
- if (sortedKeys.length === 0) return;
581
- // Same members, same array — see the filter effect below.
582
- setLastSortedKeys(previous =>
583
- previous.length === sortedKeys.length && previous.every((key, i) => key === sortedKeys[i])
584
- ? previous
585
- : sortedKeys);
586
- }, [sortedKeys]);
587
-
588
- useEffect(() => {
589
- if (filteredKeys.length === 0) return;
590
- // Same members, same array: `filteredKeys` is rebuilt every render, and
591
- // storing an equal copy would re-render every consumer of the columns.
592
- setLastFilteredKeys(previous =>
593
- previous.length === filteredKeys.length && previous.every((key, i) => key === filteredKeys[i])
594
- ? previous
595
- : filteredKeys);
596
- }, [filteredKeys]);
597
-
598
- /**
599
- * Columns for what the user asked to sort or filter by and cannot see.
600
- *
601
- * Ordering a list by a property no cell shows produces a shuffle with no
602
- * explanation in it — the rows move and nothing on screen says why. So the
603
- * property joins the row as its own column, last: the filters' first, in
604
- * their own order, then the sort's.
605
- */
606
- const requestedColumns = useMemo<ListColumn[]>(() => {
607
- // The live request where there is one, the remembered one otherwise —
608
- // read in that order rather than from state alone, because the effects
609
- // above have not run yet on the render that first sees a request, and
610
- // waiting a frame would flash the row without its new column.
611
- const active = new Set([...filteredKeys, ...sortedKeys]);
612
- const keys: string[] = [];
613
- const request = (key: string | undefined) => {
614
- if (!key || keys.includes(key) || shownKeys.has(key)) return;
615
- keys.push(key);
616
- };
617
- (filteredKeys.length > 0 ? filteredKeys : lastFilteredKeys).forEach(request);
618
- (sortedKeys.length > 0 ? sortedKeys : lastSortedKeys).forEach(request);
619
-
620
- return keys.flatMap((key, index) => {
621
- const property = getResolvedPropertyInPath(resolvedCollection.properties, key) as Property | undefined;
622
- if (!property) return [];
623
- return [{
624
- key,
625
- label: property.name || key,
626
- property,
627
- align: (property.type === "number" || property.type === "date" ? "right" : "left") as ListColumn["align"],
628
- ...getIdealColumnWidth(property),
629
- sortable: sortableKeys.has(key),
630
- priority: (active.has(key) ? PRIORITY_ACTIVE_REQUEST : PRIORITY_STALE_REQUEST) - index
631
- }];
632
- });
633
- }, [filteredKeys, sortedKeys, lastFilteredKeys, lastSortedKeys, shownKeys, resolvedCollection, sortableKeys]);
634
-
635
- // ── Compute list-view-visible actions per entity ──
636
- const getListViewActions = useCallback((entity: Entity<M>): EntityAction[] => {
637
- if (!getActionsForEntity) return EMPTY_LIST_VIEW_ACTIONS;
638
- const customEntityActions = (collection.entityActions ?? [])
639
- .map(action => resolveEntityAction(action, customizationController.entityActions))
640
- .filter(Boolean) as EntityAction<M>[];
641
- const allActions = getActionsForEntity({ entity,
642
- customEntityActions });
643
- return allActions.filter(a => a.showActionsInListView);
644
- }, [getActionsForEntity, collection.entityActions, customizationController.entityActions]);
645
-
646
- const showImage = size !== "xs";
647
-
648
- /**
649
- * Does the checkbox ride on the thumbnail rather than sit beside it?
650
- *
651
- * Only in the split's compact list — the one layout where the list is a
652
- * column beside a record rather than the page itself, and where the 48px of
653
- * checkbox cell is 48px the title does not get. Everywhere else the list has
654
- * the room, and a checkbox that is always there is easier to find than one
655
- * that appears under the pointer.
656
- *
657
- * Needs a thumbnail to ride on: at `xs` there is none, so the checkbox keeps
658
- * its cell.
659
- */
660
- const combineSelection = openEntityMode === "split" && selectedEntityId !== undefined && showImage;
661
-
662
- /**
663
- * How much room the action buttons take, reserved identically in the header
664
- * and in every row so the columns line up above one another whether or not a
665
- * given record has actions.
666
- */
667
- const actionsWidth = useMemo(() => {
668
- if (!getActionsForEntity) return 0;
669
- let most = 0;
670
- for (const entity of data.slice(0, ACTIONS_SAMPLE_SIZE)) {
671
- most = Math.max(most, getListViewActions(entity).length);
672
- }
673
- return most === 0 ? 0 : most * ACTION_BUTTON_WIDTH + (most - 1) * ACTION_BUTTON_GAP;
674
- }, [data, getActionsForEntity, getListViewActions]);
675
-
676
- // Responsive: keep only the columns the row can actually pay for.
677
- //
678
- // The title is what a row is *for* — it is the one cell that identifies the
679
- // record — so it is not one competitor among the columns: it takes its
680
- // comfortable share off the top, and the columns divide what is left. What
681
- // gives way when there is not enough is decided by `priority` rather than by
682
- // position, so the column the user just sorted by outlives the inferred
683
- // ones; the survivors keep their declared order, because a row that
684
- // reordered itself as it resized would be unreadable.
685
- const visibleColumns = useMemo(() => {
686
- const columns = [...declaredColumns, ...requestedColumns];
687
- if (columns.length === 0) return columns;
688
-
689
- // Unmeasured: the title alone, which is the answer we would rather flash
690
- // than a row of columns squeezing it down to an ellipsis.
691
- if (containerWidth === undefined) return [];
692
-
693
- const chrome = ROW_PADDING_WIDTH
694
- + (selectionEnabled && !combineSelection ? CHECKBOX_WIDTH + COLUMN_GAP : 0)
695
- + (showImage ? IMAGE_WIDTH + COLUMN_GAP : 0)
696
- + (actionsWidth > 0 ? actionsWidth + COLUMN_GAP : 0);
697
- const available = containerWidth - chrome - TITLE_COMFORTABLE_WIDTH;
698
-
699
- const cost = (col: ListColumn) => col.widthPx + COLUMN_GAP;
700
- let total = columns.reduce((acc, col) => acc + cost(col), 0);
701
- if (total <= available) return columns;
702
-
703
- const dropped = new Set<string>();
704
- for (const col of [...columns].sort((a, b) => a.priority - b.priority)) {
705
- if (total <= available) break;
706
- dropped.add(col.key);
707
- total -= cost(col);
708
- }
709
- return columns.filter(col => !dropped.has(col.key));
710
- }, [declaredColumns, requestedColumns, containerWidth, selectionEnabled, combineSelection, showImage, actionsWidth]);
711
-
712
- const handleEntityClick = useCallback((entity: Entity<M>) => {
713
- analyticsController.onAnalyticsEvent?.("entity_click", {
714
- path: entity.path,
715
- entityId: entity.id
716
- });
717
- onEntityClick?.(entity);
718
- }, [onEntityClick, analyticsController]);
719
-
720
- const handleSelectionChange = useCallback((entity: Entity<M>, selected: boolean) => {
721
- selectionController?.toggleEntitySelection(entity, selected);
722
- }, [selectionController]);
723
-
724
- const isEntitySelected = useCallback((entity: Entity<M>) => {
725
- return selectionController?.isEntitySelected(entity) ?? false;
726
- }, [selectionController]);
727
-
728
- const isEntityHighlighted = useCallback((entity: Entity<M>) => {
729
- return highlightedEntities?.some(e => e.id === entity.id && e.path === entity.path) ?? false;
730
- }, [highlightedEntities]);
731
-
732
-
733
- const rowClasses = getRowClasses(size);
734
-
735
- const selectedIds = useMemo(() => new Set(selectionController?.selectedEntities.map(e => e.id)), [selectionController?.selectedEntities]);
736
- const highlightedIds = useMemo(() => new Set(highlightedEntities?.map(e => e.id)), [highlightedEntities]);
737
-
738
- const handleRowSelectionChange = useCallback((entity: Entity<M>, selected: boolean) => {
739
- handleSelectionChange(entity, selected);
740
- }, [handleSelectionChange]);
741
-
742
- /** Where each sorted column sits in the order, so a header can show its rank. */
743
- const sortIndex = useMemo(() => {
744
- const index = new Map<string, { direction: "asc" | "desc"; position: number }>();
745
- (sortBy ?? []).forEach(([key, direction], position) => {
746
- if (!index.has(key as string)) index.set(key as string, { direction,
747
- position });
748
- });
749
- return index;
750
- }, [sortBy]);
751
-
752
- /**
753
- * Order by a column, on the same cycle a table header runs: unordered →
754
- * ascending → descending → unordered, and shift-click to add the column
755
- * under the sort already in place. Clicking a header here and clicking it
756
- * in the table view have to mean the same thing, because they are the same
757
- * collection under the same controller.
758
- */
759
- const onColumnSort = useCallback((key: string, additive = false) => {
760
- if (!setSortBy) return;
761
- const active = sortIndex.get(key)?.direction;
762
- const next: "asc" | "desc" | undefined = active === "asc"
763
- ? "desc"
764
- : active === "desc"
765
- ? undefined
766
- : "asc";
767
-
768
- const existing = (sortBy ?? []) as OrderByTuple[];
769
- let updated: OrderByTuple[] | undefined;
770
- if (!additive) {
771
- updated = next ? [[key, next]] : undefined;
772
- } else if (next === undefined) {
773
- updated = existing.filter(([existingKey]) => existingKey !== key);
774
- } else if (active === undefined) {
775
- updated = [...existing, [key, next]];
776
- } else {
777
- updated = existing.map((entry) => entry[0] === key ? [key, next] as OrderByTuple : entry);
778
- }
779
- if (updated?.length === 0) updated = undefined;
780
-
781
- setSortBy(updated as Parameters<NonNullable<typeof setSortBy>>[0]);
782
- // Re-ordering a partially loaded collection invalidates the pages
783
- // already fetched — they are no longer the rows the query answers with —
784
- // so pagination starts over, as it does for the table and the sort menu.
785
- setItemCount?.(pageSize);
786
- }, [setSortBy, sortBy, sortIndex, setItemCount, pageSize]);
787
-
788
- /**
789
- * Whether a sort can be offered at all next to the filter already applied.
790
- * A driver may refuse the combination; the table clears the filter to make
791
- * room, which is a heavier answer than a header click asks for, so the
792
- * header simply stops offering it — the same choice the sort menu makes.
793
- */
794
- const sortIsAvailable = useCallback((key: string) => {
795
- if (!setSortBy) return false;
796
- if (!checkFilterCombination) return true;
797
- const active = sortIndex.get(key)?.direction;
798
- // Clearing the sort is always available: it asks nothing of the driver.
799
- if (active === "desc") return true;
800
- return checkFilterCombination(filterValues ?? {}, [[key, active === "asc" ? "desc" : "asc"]]);
801
- }, [setSortBy, checkFilterCombination, filterValues, sortIndex]);
802
-
803
- const header = (titleColumn || visibleColumns.length > 0) && (
804
- <ListHeader
805
- titleColumn={titleColumn}
806
- columns={visibleColumns}
807
- selectionEnabled={selectionEnabled && !combineSelection}
808
- showImage={showImage}
809
- actionsWidth={actionsWidth}
810
- sortIndex={sortIndex}
811
- onColumnSort={onColumnSort}
812
- sortIsAvailable={sortIsAvailable}
813
- />
814
- );
815
-
816
- return (
817
- <div ref={containerRef} className="w-full">
818
- <ListView<Entity<M>>
819
- data={data}
820
- dataLoading={dataLoading}
821
- noMoreToLoad={noMoreToLoad}
822
- dataLoadingError={dataLoadingError}
823
- itemCount={itemCount}
824
- setItemCount={setItemCount}
825
- pageSize={pageSize}
826
- paginationEnabled={paginationEnabled}
827
- onItemClick={handleEntityClick}
828
- selectedIds={selectedIds}
829
- highlightedIds={highlightedIds}
830
- selectionEnabled={selectionEnabled}
831
- emptyComponent={emptyComponent}
832
- size={size}
833
- selectedEntityId={selectedEntityId}
834
- header={header}
835
- renderRow={useCallback(({ item: entity, style, className, selected, highlighted, isLast, onClick, onSelectionChange }) => {
836
- return (
837
- <div
838
- key={entity.id}
839
- style={style}
840
- className={className}
841
- >
842
- <ListRow
843
- entity={entity}
844
- collection={resolvedCollection}
845
- searchString={searchString}
846
- onClick={handleEntityClick}
847
- selected={selected}
848
- highlighted={highlighted}
849
- onSelectionChange={handleRowSelectionChange}
850
- selectionEnabled={selectionEnabled}
851
- combineSelection={combineSelection}
852
- columns={visibleColumns}
853
- slotKeys={slotKeys}
854
- rowClasses={rowClasses}
855
- showImage={showImage}
856
- size={size}
857
- isLast={isLast}
858
- isActive={selectedEntityId !== undefined && entity.id === selectedEntityId}
859
- listViewActions={getListViewActions(entity)}
860
- actionsWidth={actionsWidth}
861
- context={context}
862
- path={path}
863
- selectionController={selectionController}
864
- openEntityMode={openEntityMode}
865
- />
866
- </div>
867
- );
868
- }, [resolvedCollection, selectionEnabled, combineSelection, visibleColumns, slotKeys, rowClasses, showImage, size, selectedEntityId, getListViewActions, actionsWidth, context, path, selectionController, openEntityMode, handleRowSelectionChange, handleEntityClick, searchString])}
869
- />
870
- </div>
871
- );
872
- }
873
-
874
- /**
875
- * Single row in the list view.
876
- * Uses the shared slot system for a fixed editorial layout:
877
- * [Checkbox] [Image] [Title + Subtitle] → spacer → [Status] [Date]
878
- *
879
- * When collection.listProperties is explicitly set, falls back to
880
- * the column system for developer-defined table layouts.
881
- */
882
- const ListRow = React.memo(function ListRow<M extends Record<string, unknown>>({
883
- entity,
884
- collection,
885
- onClick,
886
- selected,
887
- highlighted,
888
- onSelectionChange,
889
- selectionEnabled,
890
- combineSelection = false,
891
- columns,
892
- slotKeys,
893
- rowClasses,
894
- showImage,
895
- size,
896
- isLast,
897
- isActive = false,
898
- listViewActions = [],
899
- actionsWidth = 0,
900
- context,
901
- path,
902
- selectionController,
903
- openEntityMode,
904
- searchString
905
- }: {
906
- entity: Entity<M>;
907
- /** The active search, so a row can show where it matched. */
908
- searchString?: string;
909
- collection: AdminCollection<M>;
910
- onClick?: (entity: Entity<M>) => void;
911
- selected?: boolean;
912
- highlighted?: boolean;
913
- onSelectionChange?: (entity: Entity<M>, selected: boolean) => void;
914
- selectionEnabled?: boolean;
915
- /**
916
- * Fold the selection checkbox onto the thumbnail instead of giving it a cell
917
- * of its own, buying the row back 48px for its title. Set where the row is
918
- * paying for width it does not have — see the split's compact list in
919
- * {@link CollectionListViewBinding}.
920
- */
921
- combineSelection?: boolean;
922
- columns: ListColumn[];
923
- slotKeys: CollectionSlotKeys;
924
- rowClasses: string;
925
- showImage: boolean;
926
- size: CollectionSize;
927
- isLast: boolean;
928
- isActive?: boolean;
929
- listViewActions?: EntityAction[];
930
- /** Reserved width of the actions cell, shared with the header. */
931
- actionsWidth?: number;
932
- context?: ReturnType<typeof useAdminContext>;
933
- path?: string;
934
- selectionController?: SelectionController<M>;
935
- openEntityMode?: "side_panel" | "full_screen" | "split" | "dialog";
936
- }) {
937
- // ── Resolve slots ──
938
- // A hook now, not a pure call: a `display` resolver may have to fetch what
939
- // fills a slot, so the row subscribes to the result and re-renders when it
940
- // lands. `ListRow` is memoized, which is what keeps that to the one row.
941
- const slots = useEntitySlots(
942
- entity as Entity<Record<string, unknown>>,
943
- collection as AdminCollection<Record<string, unknown>>,
944
- slotKeys
945
- );
946
-
947
- // Why is this row here? Shared with cards and board — see SearchExplanation.
948
- const { terms, offSlot } = useSearchExplanation(
949
- entity,
950
- collection.properties as Record<string, unknown>,
951
- [slotKeys.titleKey, slotKeys.subtitleKey],
952
- searchString
953
- );
954
-
955
- const handleClick = useCallback((e: React.MouseEvent) => {
956
- // Cmd+click (Mac) or Ctrl+click (Windows) toggles selection
957
- if ((e.metaKey || e.ctrlKey) && selectionEnabled) {
958
- e.preventDefault();
959
- onSelectionChange?.(entity, !selected);
960
- return;
961
- }
962
- onClick?.(entity);
963
- }, [entity, onClick, selected, selectionEnabled, onSelectionChange]);
964
-
965
- const handleCheckboxClick = useCallback((e: React.MouseEvent) => {
966
- e.stopPropagation();
967
- }, []);
968
-
969
- const handleCheckboxChange = useCallback((checked: boolean) => {
970
- onSelectionChange?.(entity, checked);
971
- }, [entity, onSelectionChange]);
972
-
973
- /** The thumbnail-as-checkbox cell: the square toggles, and never opens the record. */
974
- const handleSelectionCellClick = useCallback((e: React.MouseEvent) => {
975
- e.stopPropagation();
976
- onSelectionChange?.(entity, !selected);
977
- }, [entity, onSelectionChange, selected]);
978
-
979
- // Developer-defined column mode (listProperties is explicitly set)
980
- const useColumnMode = !!collection.listProperties && collection.listProperties.length > 0;
981
-
982
- return (
983
- <div
984
- className={cls(
985
- "@container flex items-center gap-4 cursor-pointer group transition-colors duration-200 relative h-full",
986
- rowClasses,
987
- isActive
988
- ? "bg-surface-accent-100 dark:bg-surface-accent-950 hover:bg-surface-accent-200 dark:hover:bg-surface-accent-950"
989
- : selected
990
- ? "bg-surface-accent-50 dark:bg-surface-accent-900 hover:bg-surface-accent-100 dark:hover:bg-surface-accent-950"
991
- : highlighted
992
- ? "bg-surface-accent-50 dark:bg-surface-accent-900 hover:bg-surface-50 dark:hover:bg-surface-800/40"
993
- : "bg-white dark:bg-surface-900 hover:bg-surface-50 dark:hover:bg-surface-800/40"
994
- )}
995
- onClick={handleClick}
996
- >
997
- {/* Selection indicator line */}
998
- {selected && !isActive && (
999
- <div className="absolute left-0 top-0 bottom-0 w-0.5 bg-primary-500 rounded-r-full"/>
1000
- )}
1001
-
1002
- {/* Selection Checkbox — its own cell only when it is not riding on
1003
- the thumbnail. See {@link ListRowProps.combineSelection}. */}
1004
- {selectionEnabled && !combineSelection && (
1005
- <div
1006
- className="flex-shrink-0 w-8"
1007
- onClick={handleCheckboxClick}
1008
- >
1009
- <Checkbox
1010
- checked={selected ?? false}
1011
- onCheckedChange={handleCheckboxChange}
1012
- size="smallest"
1013
- />
1014
- </div>
1015
- )}
1016
-
1017
- {/* MEDIA slot → Image / Icon */}
1018
- {showImage && (
1019
- <div className="flex-shrink-0 relative w-10 h-10">
1020
- {slots.image ? (
1021
- <div className={cls("w-10 h-10 rounded-lg border relative overflow-hidden bg-surface-100 dark:bg-surface-900", defaultBorderMixin)}>
1022
- <SlotValue slot={slots.image} size="small" fill={true}/>
1023
- </div>
1024
- ) : (
1025
- <div className={cls("w-10 h-10 rounded-lg bg-surface-100 dark:bg-surface-900 flex items-center justify-center border", defaultBorderMixin)}>
1026
- <IconForView
1027
- collectionOrView={collection}
1028
- className="text-surface-500 dark:text-surface-400"
1029
- size="small"
1030
- />
1031
- </div>
1032
- )}
1033
-
1034
- {/* The checkbox, over the thumbnail rather than beside it.
1035
- Hidden until the row is hovered or the record is already
1036
- selected: at rest the cell is the picture it was, and the
1037
- row spends the reclaimed width on its title instead. The
1038
- scrim is what makes an unchecked box legible over a photo. */}
1039
- {selectionEnabled && combineSelection && (
1040
- <div
1041
- className={cls(
1042
- "absolute inset-0 flex items-center justify-center rounded-lg transition-opacity duration-150",
1043
- "bg-surface-950/55",
1044
- selected
1045
- ? "opacity-100"
1046
- : "opacity-0 group-hover:opacity-100 focus-within:opacity-100"
1047
- )}
1048
- // The whole square toggles, not just the box drawn in
1049
- // it: at 40px the cell is the target the pointer is
1050
- // already over, and a click that lands beside a 16px
1051
- // checkbox and does nothing reads as a broken row.
1052
- // The box itself takes no pointer events, so one
1053
- // click is one toggle — it stays focusable, and space
1054
- // still works, for the keyboard.
1055
- onClick={handleSelectionCellClick}
1056
- >
1057
- <div className="pointer-events-none">
1058
- <Checkbox
1059
- checked={selected ?? false}
1060
- onCheckedChange={handleCheckboxChange}
1061
- size="smallest"
1062
- />
1063
- </div>
1064
- </div>
1065
- )}
1066
- </div>
1067
- )}
1068
-
1069
- {/* PRIMARY slot → Title + subtitle + byline */}
1070
- <div className="flex-1 min-w-0 overflow-hidden">
1071
- <div className="truncate">
1072
- {slots.title?.value !== undefined ? (
1073
- <Typography component="div" variant="body2" className="font-semibold text-surface-900 dark:text-surface-50 truncate transition-colors group-hover:text-primary-600 dark:group-hover:text-primary-400">
1074
- {terms.length > 0 && typeof slots.title?.value === "string"
1075
- ? <Highlighted text={slots.title.value} terms={terms}/>
1076
- : <SlotValue slot={slots.title} size="small"/>}
1077
- </Typography>
1078
- ) : (
1079
- <Typography component="div" variant="body2" className="font-semibold text-surface-500 dark:text-surface-400 font-mono text-xs transition-colors group-hover:text-primary-600 dark:group-hover:text-primary-400">
1080
- {entity.id}
1081
- </Typography>
1082
- )}
1083
- </div>
1084
-
1085
- {/* SUBTITLE slot — or, while searching, where the match was.
1086
- A hit in a field the row does not display makes the row look
1087
- arbitrary; the subtitle is the one line already reserved for
1088
- secondary context, so it carries the explanation instead. */}
1089
- {offSlot ? (
1090
- <MatchExplanation
1091
- match={offSlot}
1092
- label={fieldLabel(collection.properties as Record<string, unknown>, offSlot.field)}
1093
- />
1094
- ) : slots.subtitle && (
1095
- <div className="truncate mt-0.5">
1096
- <Typography variant="caption" component="div" className="text-surface-500 dark:text-surface-400 truncate">
1097
- {terms.length > 0 && typeof slots.subtitle.value === "string"
1098
- ? <Highlighted text={slots.subtitle.value} terms={terms}/>
1099
- : <SlotValue slot={slots.subtitle} size="small"/>}
1100
- </Typography>
1101
- </div>
1102
- )}
1103
-
1104
- {/* RELATION CHIPS slot — compact chips for all relations */}
1105
- {!useColumnMode && slots.relations.length > 0 && (
1106
- <div className="flex items-center gap-1 mt-1 overflow-hidden max-w-full @max-[350px]:hidden">
1107
- {slots.relations.map((rel) => (
1108
- rel.items.map((item) => (
1109
- <Chip
1110
- key={`${rel.propertyKey}-${item.id}`}
1111
- size="smallest"
1112
- colorScheme={rel.colorScheme}
1113
- className="!text-[10px] !leading-tight !py-0 shrink-0 max-w-[120px] truncate"
1114
- >
1115
- {item.displayName}
1116
- </Chip>
1117
- ))
1118
- ))}
1119
- {slots.relations.some(r => r.totalCount > r.items.length) && (
1120
- <span className="text-[10px] text-surface-400 dark:text-surface-500 shrink-0">
1121
- +{slots.relations.reduce((acc, r) => acc + Math.max(0, r.totalCount - r.items.length), 0)}
1122
- </span>
1123
- )}
1124
- </div>
1125
- )}
1126
- </div>
1127
-
1128
- {/* TRAILING COLUMNS — the same definitions the header labels, in the
1129
- same order and at the same widths, so a label and the values
1130
- under it cannot come apart. */}
1131
- {columns.length > 0 && (
1132
- <div className="flex items-center gap-4 flex-shrink-0 ml-auto">
1133
- {columns.map((col) => (
1134
- <div
1135
- key={col.key}
1136
- className={cls(
1137
- col.width,
1138
- "flex items-center overflow-hidden",
1139
- col.align === "center" ? "justify-center" : col.align === "right" ? "justify-end" : "justify-start"
1140
- )}
1141
- >
1142
- <ListCell column={col} entity={entity} slots={slots}/>
1143
- </div>
1144
- ))}
1145
- </div>
1146
- )}
1147
-
1148
- {/* LIST VIEW ACTIONS — always visible on each row.
1149
- Width is reserved from the widest row rather than taken from
1150
- this one, so a record with fewer actions does not slide its
1151
- columns out from under the header. */}
1152
- {actionsWidth > 0 && (
1153
- <div className="flex items-center justify-end gap-0.5 flex-shrink-0 ml-auto"
1154
- style={{ minWidth: actionsWidth }}
1155
- onClick={(e) => e.stopPropagation()}>
1156
- {listViewActions.map((action, index) => (
1157
- <Tooltip key={action.key ?? index} title={action.name} asChild>
1158
- <IconButton
1159
- size="small"
1160
- onClick={(e: React.MouseEvent) => {
1161
- e.stopPropagation();
1162
- action.onClick({
1163
- view: "collection",
1164
- entity,
1165
- path,
1166
- collection,
1167
- context: context!,
1168
- sidePanelController: context?.sidePanelController,
1169
- selectionController,
1170
- openEntityMode: openEntityMode ?? collection?.openEntityMode ?? "full_screen"
1171
- });
1172
- }}>
1173
- {getIcon(action.icon, undefined, undefined, "smallest")}
1174
- </IconButton>
1175
- </Tooltip>
1176
- ))}
1177
- </div>
1178
- )}
1179
- </div>
1180
- );
1181
- }) as <M extends Record<string, unknown>>(props: {
1182
- entity: Entity<M>;
1183
- collection: AdminCollection<M>;
1184
- /** The active search, so a row can show where it matched. */
1185
- searchString?: string;
1186
- onClick?: (entity: Entity<M>) => void;
1187
- selected?: boolean;
1188
- highlighted?: boolean;
1189
- onSelectionChange?: (entity: Entity<M>, selected: boolean) => void;
1190
- selectionEnabled?: boolean;
1191
- combineSelection?: boolean;
1192
- columns: ListColumn[];
1193
- slotKeys: CollectionSlotKeys;
1194
- rowClasses: string;
1195
- showImage: boolean;
1196
- size: CollectionSize;
1197
- isLast: boolean;
1198
- isActive?: boolean;
1199
- listViewActions?: EntityAction[];
1200
- /** Reserved width of the actions cell, shared with the header. */
1201
- actionsWidth?: number;
1202
- context?: ReturnType<typeof useAdminContext>;
1203
- path?: string;
1204
- selectionController?: SelectionController<M>;
1205
- openEntityMode?: "side_panel" | "full_screen" | "split" | "dialog";
1206
- }) => React.ReactElement;
1207
-
1208
- /**
1209
- * One cell of one row: whatever the column says fills it.
1210
- *
1211
- * A slot column renders the slot — a status keeps its coloured chip and a date
1212
- * its relative wording wherever the value came from — and a property column
1213
- * reads the record. Both go through here so the header above them is labelling
1214
- * one thing, not two that happen to line up.
1215
- */
1216
- function ListCell<M extends Record<string, unknown>>({
1217
- column,
1218
- entity,
1219
- slots
1220
- }: {
1221
- column: ListColumn;
1222
- entity: Entity<M>;
1223
- slots: EntityPreviewSlots;
1224
- }) {
1225
- if (column.slot === "tags") {
1226
- if (!slots.tags) return null;
1227
- return (
1228
- <div className="flex items-center gap-1 overflow-hidden">
1229
- <TagChips slot={slots.tags} max={3}/>
1230
- </div>
1231
- );
1232
- }
1233
-
1234
- if (column.slot === "status") {
1235
- if (!slots.status) return <EmptyCell/>;
1236
- return <SlotValue slot={slots.status} size="small"/>;
1237
- }
1238
-
1239
- if (column.slot === "date") {
1240
- return (
1241
- <Typography variant="caption" className="whitespace-nowrap text-surface-400 dark:text-surface-500 font-medium">
1242
- {slots.date?.formatted ?? "—"}
1243
- </Typography>
1244
- );
1245
- }
1246
-
1247
- const property = column.property;
1248
- if (!property) return null;
1249
-
1250
- const value = getValueInPath(entity.values, column.key);
1251
-
1252
- if (property.type === "date") {
1253
- return (
1254
- <Typography variant="caption" className="whitespace-nowrap text-surface-400 dark:text-surface-500 font-medium">
1255
- {formatDateValue(value) ?? "—"}
1256
- </Typography>
1257
- );
1258
- }
1259
-
1260
- // Complex types → compact single-line summary, so a row keeps its height.
1261
- if (isComplexPropertyType(property)) {
1262
- const summary = compactValueSummary(value, property);
1263
- return (
1264
- <Typography variant="caption" className="text-surface-500 dark:text-surface-400 truncate">
1265
- {summary ?? "—"}
1266
- </Typography>
1267
- );
1268
- }
1269
-
1270
- if (value === undefined || value === null) return <EmptyCell/>;
1271
-
1272
- return (
1273
- <Typography component="div" variant="body2" className="text-surface-600 dark:text-surface-300 truncate">
1274
- <PropertyPreview propertyKey={column.key} value={value} property={property} size="small"/>
1275
- </Typography>
1276
- );
1277
- }
1278
-
1279
- /** What a column shows for a record that has nothing in it. */
1280
- function EmptyCell() {
1281
- return <span className="text-surface-400 dark:text-surface-600">—</span>;
1282
- }
1283
-
1284
- /**
1285
- * The list's column header.
1286
- *
1287
- * Quiet by design: a list is read row by row, and the header is there to name
1288
- * the columns and to be clicked, not to compete with the rows. It lays out
1289
- * against the same widths the rows do, and skips the checkbox and the image —
1290
- * neither is a column, and neither has anything to be called.
1291
- */
1292
- function ListHeader({
1293
- titleColumn,
1294
- columns,
1295
- selectionEnabled,
1296
- showImage,
1297
- actionsWidth,
1298
- sortIndex,
1299
- onColumnSort,
1300
- sortIsAvailable
1301
- }: {
1302
- titleColumn?: ListColumn;
1303
- columns: ListColumn[];
1304
- selectionEnabled?: boolean;
1305
- showImage: boolean;
1306
- actionsWidth: number;
1307
- sortIndex: Map<string, { direction: "asc" | "desc"; position: number }>;
1308
- onColumnSort: (key: string, additive?: boolean) => void;
1309
- sortIsAvailable: (key: string) => boolean;
1310
- }) {
1311
- const headerCell = (column: ListColumn) => (
1312
- <ListHeaderLabel
1313
- column={column}
1314
- direction={sortIndex.get(column.key)?.direction}
1315
- position={sortIndex.size > 1 ? sortIndex.get(column.key)?.position : undefined}
1316
- onSort={column.sortable && sortIsAvailable(column.key)
1317
- ? (additive: boolean) => onColumnSort(column.key, additive)
1318
- : undefined}
1319
- />
1320
- );
1321
-
1322
- // No `role="columnheader"` / `aria-sort` here, tempting as they are: both
1323
- // are only valid inside a `row` inside a `table`/`grid`, and neither this
1324
- // header nor the virtualized `ListView` under it declares one. An orphaned
1325
- // columnheader is worse than none. The sort state and the rank ride on the
1326
- // header button's `aria-label` instead — see `ListHeaderLabel`.
1327
- return (
1328
- <div className={cls(
1329
- "flex items-center gap-4 px-5 py-1.5 select-none border-b bg-surface-50 dark:bg-surface-900",
1330
- defaultBorderMixin
1331
- )}>
1332
- {selectionEnabled && <div className="flex-shrink-0 w-8"/>}
1333
- {showImage && <div className="flex-shrink-0 w-10"/>}
1334
-
1335
- <div className="flex-1 min-w-0 flex items-center">
1336
- {titleColumn && headerCell(titleColumn)}
1337
- </div>
1338
-
1339
- {columns.length > 0 && (
1340
- <div className="flex items-center gap-4 flex-shrink-0 ml-auto">
1341
- {columns.map(column => (
1342
- <div
1343
- key={column.key}
1344
- className={cls(
1345
- column.width,
1346
- "flex items-center overflow-hidden",
1347
- column.align === "center" ? "justify-center" : column.align === "right" ? "justify-end" : "justify-start"
1348
- )}
1349
- >
1350
- {headerCell(column)}
1351
- </div>
1352
- ))}
1353
- </div>
1354
- )}
1355
-
1356
- {actionsWidth > 0 && <div className="flex-shrink-0 ml-auto" style={{ minWidth: actionsWidth }}/>}
1357
- </div>
1358
- );
1359
- }
1360
-
1361
- /**
1362
- * One header label, clickable when the collection can be ordered by it.
1363
- *
1364
- * The arrow shows only on the column currently ordering the list: three arrows
1365
- * in a header row are three invitations, and only one of them is ever the
1366
- * answer to "how is this sorted".
1367
- */
1368
- function ListHeaderLabel({
1369
- column,
1370
- direction,
1371
- position,
1372
- onSort
1373
- }: {
1374
- column: ListColumn;
1375
- direction?: "asc" | "desc";
1376
- /** Rank in a multi-key sort, or `undefined` when one key says it all. */
1377
- position?: number;
1378
- onSort?: (additive: boolean) => void;
1379
- }) {
1380
- const { t } = useTranslation();
1381
-
1382
- const content = (
1383
- <>
1384
- <span className="truncate">{column.label}</span>
1385
- {direction === "asc" && <ArrowUpIcon size={12} className="flex-shrink-0"/>}
1386
- {direction === "desc" && <ArrowDownIcon size={12} className="flex-shrink-0"/>}
1387
- {direction && position !== undefined && (
1388
- <span className="flex-shrink-0 text-[9px] font-semibold leading-none tabular-nums">
1389
- {position + 1}
1390
- </span>
1391
- )}
1392
- </>
1393
- );
1394
-
1395
- const base = "inline-flex items-center gap-1 max-w-full text-[11px] leading-none uppercase tracking-wider font-medium";
1396
- const tone = direction
1397
- ? "text-surface-600 dark:text-surface-300"
1398
- : "text-surface-400 dark:text-surface-500";
1399
-
1400
- if (!onSort) {
1401
- return <span className={cls(base, tone)}>{content}</span>;
1402
- }
1403
-
1404
- // What the *next* click does, which is the only thing a user needs from a
1405
- // control with three states. This said "Sort by <column>" in every state,
1406
- // so on a descending column the tooltip promised a sort where the click
1407
- // removed one — and it said it in English regardless of the panel's
1408
- // language, alone among the toolbar's sort controls.
1409
- const nextAction = direction === "asc"
1410
- ? t("sort_descending")
1411
- : direction === "desc"
1412
- ? t("sort_remove")
1413
- : t("sort_ascending");
1414
- const rank = direction && position !== undefined
1415
- ? ` (${t("sort_key_position", { position: position + 1 })})`
1416
- : "";
1417
- const label = `${column.label}${rank} — ${nextAction}. ${t("sort_shift_click_hint")}`;
1418
-
1419
- return (
1420
- <button
1421
- type="button"
1422
- title={label}
1423
- aria-label={label}
1424
- onClick={(event) => onSort(event.shiftKey)}
1425
- className={cls(base, tone, "cursor-pointer hover:text-surface-700 dark:hover:text-surface-200 transition-colors")}
1426
- >
1427
- {content}
1428
- </button>
1429
- );
1430
- }