@firecms/core 3.0.1 → 3.1.0-canary.768c91f

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 (185) hide show
  1. package/README.md +1 -1
  2. package/dist/components/AIIcon.d.ts +16 -0
  3. package/dist/components/EntityCollectionTable/EntityCollectionRowActions.d.ts +7 -1
  4. package/dist/components/EntityCollectionTable/EntityCollectionTable.d.ts +1 -1
  5. package/dist/components/EntityCollectionTable/EntityCollectionTableProps.d.ts +14 -0
  6. package/dist/components/EntityCollectionTable/PropertyTableCell.d.ts +6 -0
  7. package/dist/components/EntityCollectionTable/internal/CollectionTableToolbar.d.ts +5 -4
  8. package/dist/components/EntityCollectionTable/internal/EntityTableCell.d.ts +6 -0
  9. package/dist/components/EntityCollectionTable/internal/popup_field/useDraggable.d.ts +2 -2
  10. package/dist/components/EntityCollectionView/Board.d.ts +2 -0
  11. package/dist/components/EntityCollectionView/BoardColumn.d.ts +42 -0
  12. package/dist/components/EntityCollectionView/BoardColumnTitle.d.ts +9 -0
  13. package/dist/components/EntityCollectionView/BoardSortableList.d.ts +14 -0
  14. package/dist/components/EntityCollectionView/EntityBoardCard.d.ts +26 -0
  15. package/dist/components/EntityCollectionView/EntityCard.d.ts +19 -0
  16. package/dist/components/EntityCollectionView/EntityCollectionBoardView.d.ts +20 -0
  17. package/dist/components/EntityCollectionView/EntityCollectionCardView.d.ts +31 -0
  18. package/dist/components/EntityCollectionView/EntityCollectionViewActions.d.ts +2 -2
  19. package/dist/components/EntityCollectionView/EntityCollectionViewStartActions.d.ts +7 -3
  20. package/dist/components/EntityCollectionView/FiltersDialog.d.ts +14 -0
  21. package/dist/components/EntityCollectionView/ViewModeToggle.d.ts +44 -0
  22. package/dist/components/EntityCollectionView/board_types.d.ts +105 -0
  23. package/dist/components/EntityCollectionView/useBoardDataController.d.ts +60 -0
  24. package/dist/components/ErrorBoundary.d.ts +1 -1
  25. package/dist/components/SelectableTable/SelectableTable.d.ts +5 -1
  26. package/dist/components/SelectableTable/filters/DateTimeFilterField.d.ts +2 -1
  27. package/dist/components/VirtualTable/VirtualTableCell.d.ts +6 -0
  28. package/dist/components/VirtualTable/VirtualTableHeader.d.ts +3 -1
  29. package/dist/components/VirtualTable/VirtualTableHeaderRow.d.ts +1 -1
  30. package/dist/components/VirtualTable/VirtualTableProps.d.ts +11 -0
  31. package/dist/components/VirtualTable/fields/VirtualTableDateField.d.ts +1 -0
  32. package/dist/components/VirtualTable/types.d.ts +2 -0
  33. package/dist/components/index.d.ts +3 -0
  34. package/dist/contexts/index.d.ts +10 -0
  35. package/dist/core/DrawerNavigationGroup.d.ts +45 -0
  36. package/dist/core/index.d.ts +1 -0
  37. package/dist/form/components/ErrorFocus.d.ts +1 -1
  38. package/dist/form/validation.d.ts +3 -2
  39. package/dist/hooks/useBreadcrumbsController.d.ts +16 -0
  40. package/dist/hooks/useCollapsedGroups.d.ts +4 -1
  41. package/dist/index.es.js +5266 -1578
  42. package/dist/index.es.js.map +1 -1
  43. package/dist/index.umd.js +5260 -1573
  44. package/dist/index.umd.js.map +1 -1
  45. package/dist/internal/useRestoreScroll.d.ts +1 -1
  46. package/dist/preview/PropertyPreviewProps.d.ts +5 -0
  47. package/dist/preview/components/DatePreview.d.ts +13 -3
  48. package/dist/preview/components/ImagePreview.d.ts +5 -1
  49. package/dist/preview/components/StorageThumbnail.d.ts +2 -1
  50. package/dist/preview/components/UrlComponentPreview.d.ts +2 -1
  51. package/dist/preview/property_previews/ArrayOfStorageComponentsPreview.d.ts +1 -1
  52. package/dist/preview/property_previews/ArrayOfStringsPreview.d.ts +1 -1
  53. package/dist/preview/property_previews/SkeletonPropertyComponent.d.ts +1 -1
  54. package/dist/types/analytics.d.ts +1 -1
  55. package/dist/types/collections.d.ts +50 -2
  56. package/dist/types/datasource.d.ts +0 -1
  57. package/dist/types/plugins.d.ts +62 -1
  58. package/dist/types/properties.d.ts +259 -4
  59. package/dist/util/__tests__/conditions.test.d.ts +1 -0
  60. package/dist/util/__tests__/objects.test.d.ts +1 -0
  61. package/dist/util/conditions.d.ts +26 -0
  62. package/dist/util/entities.d.ts +2 -3
  63. package/dist/util/index.d.ts +2 -1
  64. package/dist/util/property_utils.d.ts +2 -1
  65. package/dist/util/resolutions.d.ts +3 -3
  66. package/package.json +14 -11
  67. package/src/app/Scaffold.tsx +14 -15
  68. package/src/components/AIIcon.tsx +39 -0
  69. package/src/components/ArrayContainer.tsx +1 -4
  70. package/src/components/ClearFilterSortButton.tsx +19 -16
  71. package/src/components/ConfirmationDialog.tsx +0 -2
  72. package/src/components/DeleteEntityDialog.tsx +2 -4
  73. package/src/components/EntityCollectionTable/EntityCollectionRowActions.tsx +74 -41
  74. package/src/components/EntityCollectionTable/EntityCollectionTable.tsx +130 -79
  75. package/src/components/EntityCollectionTable/EntityCollectionTableProps.tsx +121 -104
  76. package/src/components/EntityCollectionTable/PropertyTableCell.tsx +132 -103
  77. package/src/components/EntityCollectionTable/internal/CollectionTableToolbar.tsx +20 -42
  78. package/src/components/EntityCollectionTable/internal/EntityTableCell.tsx +90 -49
  79. package/src/components/EntityCollectionTable/internal/EntityTableCellActions.tsx +1 -1
  80. package/src/components/EntityCollectionTable/internal/popup_field/useDraggable.tsx +11 -11
  81. package/src/components/EntityCollectionView/Board.tsx +324 -0
  82. package/src/components/EntityCollectionView/BoardColumn.tsx +158 -0
  83. package/src/components/EntityCollectionView/BoardColumnTitle.tsx +45 -0
  84. package/src/components/EntityCollectionView/BoardSortableList.tsx +172 -0
  85. package/src/components/EntityCollectionView/EntityBoardCard.tsx +212 -0
  86. package/src/components/EntityCollectionView/EntityCard.tsx +235 -0
  87. package/src/components/EntityCollectionView/EntityCollectionBoardView.tsx +733 -0
  88. package/src/components/EntityCollectionView/EntityCollectionCardView.tsx +244 -0
  89. package/src/components/EntityCollectionView/EntityCollectionView.tsx +519 -203
  90. package/src/components/EntityCollectionView/EntityCollectionViewActions.tsx +31 -19
  91. package/src/components/EntityCollectionView/EntityCollectionViewStartActions.tsx +84 -15
  92. package/src/components/EntityCollectionView/FiltersDialog.tsx +249 -0
  93. package/src/components/EntityCollectionView/ViewModeToggle.tsx +199 -0
  94. package/src/components/EntityCollectionView/board_types.ts +113 -0
  95. package/src/components/EntityCollectionView/useBoardDataController.tsx +490 -0
  96. package/src/components/ErrorTooltip.tsx +2 -1
  97. package/src/components/HomePage/DefaultHomePage.tsx +47 -10
  98. package/src/components/HomePage/HomePageDnD.tsx +56 -41
  99. package/src/components/HomePage/NavigationCard.tsx +20 -18
  100. package/src/components/HomePage/NavigationGroup.tsx +17 -16
  101. package/src/components/HomePage/RenameGroupDialog.tsx +0 -2
  102. package/src/components/HomePage/SmallNavigationCard.tsx +10 -9
  103. package/src/components/ReferenceTable/ReferenceSelectionTable.tsx +3 -10
  104. package/src/components/ReferenceWidget.tsx +2 -4
  105. package/src/components/SelectableTable/SelectableTable.tsx +75 -67
  106. package/src/components/SelectableTable/filters/BooleanFilterField.tsx +7 -6
  107. package/src/components/SelectableTable/filters/DateTimeFilterField.tsx +39 -40
  108. package/src/components/SelectableTable/filters/ReferenceFilterField.tsx +38 -38
  109. package/src/components/SelectableTable/filters/StringNumberFilterField.tsx +49 -58
  110. package/src/components/UnsavedChangesDialog.tsx +0 -2
  111. package/src/components/UserDisplay.tsx +4 -4
  112. package/src/components/VirtualTable/VirtualTable.tsx +272 -118
  113. package/src/components/VirtualTable/VirtualTableCell.tsx +18 -2
  114. package/src/components/VirtualTable/VirtualTableHeader.tsx +59 -50
  115. package/src/components/VirtualTable/VirtualTableHeaderRow.tsx +158 -42
  116. package/src/components/VirtualTable/VirtualTableProps.tsx +14 -1
  117. package/src/components/VirtualTable/VirtualTableRow.tsx +1 -1
  118. package/src/components/VirtualTable/fields/VirtualTableDateField.tsx +3 -0
  119. package/src/components/VirtualTable/fields/VirtualTableSelect.tsx +19 -6
  120. package/src/components/VirtualTable/types.tsx +2 -0
  121. package/src/components/common/useColumnsIds.tsx +95 -3
  122. package/src/components/index.tsx +4 -0
  123. package/src/contexts/BreacrumbsContext.tsx +15 -8
  124. package/src/contexts/index.ts +10 -0
  125. package/src/core/DefaultAppBar.tsx +40 -27
  126. package/src/core/DefaultDrawer.tsx +42 -56
  127. package/src/core/DrawerNavigationGroup.tsx +118 -0
  128. package/src/core/DrawerNavigationItem.tsx +4 -3
  129. package/src/core/EntityEditView.tsx +41 -43
  130. package/src/core/EntitySidePanel.tsx +28 -26
  131. package/src/core/SideDialogs.tsx +4 -2
  132. package/src/core/field_configs.tsx +14 -9
  133. package/src/core/index.tsx +1 -0
  134. package/src/form/EntityForm.tsx +69 -60
  135. package/src/form/PropertyFieldBinding.tsx +61 -46
  136. package/src/form/components/ErrorFocus.tsx +3 -3
  137. package/src/form/components/StorageItemPreview.tsx +2 -1
  138. package/src/form/field_bindings/ArrayOfReferencesFieldBinding.tsx +0 -1
  139. package/src/form/field_bindings/DateTimeFieldBinding.tsx +17 -16
  140. package/src/form/field_bindings/KeyValueFieldBinding.tsx +0 -1
  141. package/src/form/field_bindings/MapFieldBinding.tsx +69 -67
  142. package/src/form/field_bindings/MarkdownEditorFieldBinding.tsx +22 -18
  143. package/src/form/field_bindings/StorageUploadFieldBinding.tsx +83 -83
  144. package/src/form/field_bindings/TextFieldBinding.tsx +71 -35
  145. package/src/form/validation.ts +245 -160
  146. package/src/hooks/useBreadcrumbsController.tsx +18 -0
  147. package/src/hooks/useBuildNavigationController.tsx +46 -23
  148. package/src/hooks/useCollapsedGroups.ts +12 -4
  149. package/src/hooks/useValidateAuthenticator.tsx +1 -1
  150. package/src/internal/useBuildDataSource.ts +68 -34
  151. package/src/internal/useBuildSideDialogsController.tsx +11 -8
  152. package/src/internal/useBuildSideEntityController.tsx +2 -4
  153. package/src/internal/useRestoreScroll.tsx +26 -14
  154. package/src/preview/PropertyPreview.tsx +41 -32
  155. package/src/preview/PropertyPreviewProps.tsx +6 -0
  156. package/src/preview/components/DatePreview.tsx +72 -4
  157. package/src/preview/components/EmptyValue.tsx +1 -1
  158. package/src/preview/components/ImagePreview.tsx +37 -21
  159. package/src/preview/components/StorageThumbnail.tsx +16 -12
  160. package/src/preview/components/UrlComponentPreview.tsx +28 -25
  161. package/src/preview/property_previews/ArrayOfStorageComponentsPreview.tsx +9 -7
  162. package/src/preview/property_previews/ArrayOfStringsPreview.tsx +11 -9
  163. package/src/preview/property_previews/ArrayPropertyPreview.tsx +26 -24
  164. package/src/preview/property_previews/SkeletonPropertyComponent.tsx +61 -56
  165. package/src/routes/CustomCMSRoute.tsx +1 -0
  166. package/src/routes/FireCMSRoute.tsx +26 -13
  167. package/src/types/analytics.ts +10 -0
  168. package/src/types/collections.ts +57 -3
  169. package/src/types/datasource.ts +54 -56
  170. package/src/types/plugins.tsx +69 -1
  171. package/src/types/properties.ts +347 -27
  172. package/src/util/__tests__/conditions.test.ts +506 -0
  173. package/src/util/__tests__/objects.test.ts +196 -0
  174. package/src/util/callbacks.ts +6 -3
  175. package/src/util/collections.ts +51 -6
  176. package/src/util/conditions.ts +339 -0
  177. package/src/util/entities.ts +29 -30
  178. package/src/util/entity_cache.ts +2 -1
  179. package/src/util/index.ts +2 -1
  180. package/src/util/join_collections.ts +10 -8
  181. package/src/util/objects.ts +31 -13
  182. package/src/util/{references.ts → previews.ts} +16 -2
  183. package/src/util/property_utils.tsx +37 -11
  184. package/src/util/resolutions.ts +62 -58
  185. /package/dist/util/{references.d.ts → previews.d.ts} +0 -0
@@ -14,13 +14,15 @@ import {
14
14
  PartialEntityCollection,
15
15
  PropertyOrBuilder,
16
16
  ResolvedProperty,
17
- SaveEntityProps
17
+ SaveEntityProps,
18
+ ViewMode
18
19
  } from "../../types";
19
20
  import {
20
21
  EntityCollectionRowActions,
21
22
  EntityCollectionTable,
22
23
  useDataSourceTableController
23
24
  } from "../EntityCollectionTable";
25
+ import { CollectionTableToolbar } from "../EntityCollectionTable/internal/CollectionTableToolbar";
24
26
 
25
27
  import {
26
28
  canCreateEntity,
@@ -45,8 +47,12 @@ import {
45
47
  useNavigationController,
46
48
  useSideEntityController
47
49
  } from "../../hooks";
50
+ import { useBreadcrumbsController } from "../../hooks/useBreadcrumbsController";
48
51
  import { useUserConfigurationPersistence } from "../../hooks/useUserConfigurationPersistence";
49
52
  import { EntityCollectionViewActions } from "./EntityCollectionViewActions";
53
+ import { EntityCollectionCardView } from "./EntityCollectionCardView";
54
+ import { EntityCollectionBoardView } from "./EntityCollectionBoardView";
55
+ import { ViewModeToggle, KanbanPropertyOption } from "./ViewModeToggle";
50
56
  import {
51
57
  AddIcon,
52
58
  Button,
@@ -142,18 +148,19 @@ export type EntityCollectionViewProps<M extends Record<string, any>> = {
142
148
  */
143
149
  export const EntityCollectionView = React.memo(
144
150
  function EntityCollectionView<M extends Record<string, any>>({
145
- fullPath: fullPathProp,
146
- fullIdPath,
147
- parentCollectionIds,
148
- isSubCollection,
149
- className,
150
- updateUrl,
151
- ...collectionProp
152
- }: EntityCollectionViewProps<M>
151
+ fullPath: fullPathProp,
152
+ fullIdPath,
153
+ parentCollectionIds,
154
+ isSubCollection,
155
+ className,
156
+ updateUrl,
157
+ ...collectionProp
158
+ }: EntityCollectionViewProps<M>
153
159
  ) {
154
160
 
155
161
  const context = useFireCMSContext();
156
162
  const navigation = useNavigationController();
163
+ const breadcrumbs = useBreadcrumbsController();
157
164
  const fullPath = fullPathProp ?? collectionProp.path;
158
165
  const dataSource = useDataSource(collectionProp);
159
166
  const sideEntityController = useSideEntityController();
@@ -184,8 +191,19 @@ export const EntityCollectionView = React.memo(
184
191
 
185
192
  const [lastDeleteTimestamp, setLastDeleteTimestamp] = React.useState<number>(0);
186
193
 
187
- // number of entities in the collection
188
- const [docsCount, setDocsCount] = useState<number>(0);
194
+ // Track recently deleted entities for optimistic Kanban count updates
195
+ const [deletedEntities, setDeletedEntities] = React.useState<Entity<M>[]>([]);
196
+
197
+ // number of entities in the collection (undefined = loading)
198
+ const [docsCount, setDocsCount] = useState<number | undefined>(undefined);
199
+
200
+ // Optimistic state for column order to prevent UI flickering during persistence
201
+ const [localPropertiesOrder, setLocalPropertiesOrder] = useState<string[] | undefined>(collection.propertiesOrder);
202
+
203
+ // Sync local state with collection's propertiesOrder when it changes from external sources
204
+ useEffect(() => {
205
+ setLocalPropertiesOrder(collection.propertiesOrder);
206
+ }, [collection.propertiesOrder]);
189
207
 
190
208
  const unselectNavigatedEntity = useCallback(() => {
191
209
  const currentSelection = highlightedEntity;
@@ -208,6 +226,83 @@ export const EntityCollectionView = React.memo(
208
226
 
209
227
  const [popOverOpen, setPopOverOpen] = useState(false);
210
228
 
229
+ // View mode priority: URL > saved user config > collection.defaultViewMode
230
+ const defaultViewMode = collection.defaultViewMode ?? "table";
231
+
232
+ // Parse view from URL
233
+ const getViewFromUrl = useCallback((): ViewMode | null => {
234
+ const params = new URLSearchParams(window.location.search);
235
+ const urlView = params.get("__view");
236
+ if (urlView && ["table", "kanban", "cards"].includes(urlView)) {
237
+ return urlView as ViewMode;
238
+ }
239
+ return null;
240
+ }, []);
241
+
242
+ // Get saved view from local persistence
243
+ const getSavedView = useCallback((): ViewMode | null => {
244
+ const saved = userConfigPersistence?.getCollectionConfig<M>(fullPath)?.defaultViewMode;
245
+ return (saved as ViewMode) ?? null;
246
+ }, [userConfigPersistence, fullPath]);
247
+
248
+ const [viewMode, setViewModeState] = useState<ViewMode>(() => {
249
+ // Priority: URL > saved config > collection default
250
+ const urlView = getViewFromUrl();
251
+ if (urlView) return urlView;
252
+ const savedView = getSavedView();
253
+ if (savedView) return savedView;
254
+ return defaultViewMode;
255
+ });
256
+
257
+ // Sync URL with current view on init (if view came from saved config)
258
+ useEffect(() => {
259
+ const urlView = getViewFromUrl();
260
+ if (!urlView && viewMode !== "table") {
261
+ // View came from saved config but URL doesn't have it - update URL without push
262
+ const url = new URL(window.location.href);
263
+ url.searchParams.set("__view", viewMode);
264
+ window.history.replaceState({}, "", url.toString());
265
+ }
266
+ }, []); // Only on mount
267
+
268
+ // Update URL when view mode changes (user action)
269
+ const setViewMode = useCallback((newMode: ViewMode) => {
270
+ setViewModeState(newMode);
271
+
272
+ // Update URL with __view param
273
+ const url = new URL(window.location.href);
274
+ if (newMode === "table") {
275
+ url.searchParams.delete("__view");
276
+ } else {
277
+ url.searchParams.set("__view", newMode);
278
+ }
279
+ window.history.pushState({}, "", url.toString());
280
+ }, []);
281
+
282
+ // Listen for browser back/forward
283
+ useEffect(() => {
284
+ const handlePopState = () => {
285
+ const urlView = getViewFromUrl();
286
+ if (urlView) {
287
+ // URL has explicit view - use it
288
+ setViewModeState(urlView);
289
+ } else {
290
+ // No URL param - fallback to saved config or collection default
291
+ const savedView = getSavedView();
292
+ setViewModeState(savedView ?? defaultViewMode);
293
+ }
294
+ };
295
+
296
+ window.addEventListener("popstate", handlePopState);
297
+ return () => window.removeEventListener("popstate", handlePopState);
298
+ }, [getViewFromUrl, getSavedView, defaultViewMode]);
299
+
300
+ // Card view size state - controls the grid column count
301
+ const [cardSize, setCardSize] = useState<CollectionSize>(collection.defaultSize ?? "m");
302
+
303
+ // Table view size state - controls row height
304
+ const [tableSize, setTableSize] = useState<CollectionSize>(collection.defaultSize ?? "m");
305
+
211
306
  const selectionController = useSelectionController<M>();
212
307
  const usedSelectionController = collection.selectionController ?? selectionController;
213
308
  const {
@@ -284,6 +379,7 @@ export const EntityCollectionView = React.memo(
284
379
  path: fullPath
285
380
  });
286
381
  setSelectedEntities((selectedEntities) => selectedEntities.filter((e) => e.id !== entity.id));
382
+ setDeletedEntities(prev => [...prev, entity]);
287
383
  setLastDeleteTimestamp(Date.now());
288
384
  };
289
385
 
@@ -293,6 +389,7 @@ export const EntityCollectionView = React.memo(
293
389
  });
294
390
  setSelectedEntities([]);
295
391
  setDeleteEntityClicked(undefined);
392
+ setDeletedEntities(prev => [...prev, ...entities]);
296
393
  setLastDeleteTimestamp(Date.now());
297
394
  };
298
395
 
@@ -317,9 +414,9 @@ export const EntityCollectionView = React.memo(
317
414
  }, [userConfigPersistence]);
318
415
 
319
416
  const onColumnResize = useCallback(({
320
- width,
321
- key
322
- }: OnColumnResizeParams) => {
417
+ width,
418
+ key
419
+ }: OnColumnResizeParams) => {
323
420
 
324
421
  const collection = collectionRef.current;
325
422
  // Only for property columns
@@ -328,29 +425,44 @@ export const EntityCollectionView = React.memo(
328
425
  onCollectionModifiedForUser(fullPath, localCollection);
329
426
  }, [onCollectionModifiedForUser, fullPath]);
330
427
 
331
- const onSizeChanged = useCallback((size: CollectionSize) => {
428
+ const onTableSizeChanged = useCallback((size: CollectionSize) => {
429
+ setTableSize(size);
332
430
  if (userConfigPersistence)
333
431
  onCollectionModifiedForUser(fullPath, { defaultSize: size })
334
432
  }, [onCollectionModifiedForUser, fullPath, userConfigPersistence]);
335
433
 
434
+ // View mode change: update URL + save to local persistence
435
+ const onViewModeChange = useCallback((mode: ViewMode) => {
436
+ analyticsController.onAnalyticsEvent?.("view_mode_changed", {
437
+ path: fullPath,
438
+ from: viewMode,
439
+ to: mode
440
+ });
441
+ setViewMode(mode);
442
+ // Save to local persistence for next visit
443
+ if (userConfigPersistence) {
444
+ onCollectionModifiedForUser(fullPath, { defaultViewMode: mode } as PartialEntityCollection<M>);
445
+ }
446
+ }, [setViewMode, userConfigPersistence, onCollectionModifiedForUser, fullPath, analyticsController, viewMode]);
447
+
336
448
  const createEnabled = canCreateEntity(collection, authController, fullPath, null);
337
449
 
338
450
  const uniqueFieldValidator: UniqueFieldValidator = useCallback(
339
451
  ({
340
- name,
341
- value,
342
- property,
343
- entityId
344
- }) => dataSource.checkUniqueField(fullPath, name, value, entityId, collection),
452
+ name,
453
+ value,
454
+ property,
455
+ entityId
456
+ }) => dataSource.checkUniqueField(fullPath, name, value, entityId, collection),
345
457
  [fullPath]);
346
458
 
347
459
  const onValueChange: OnCellValueChange<any, any> = ({
348
- value,
349
- propertyKey,
350
- onValueUpdated,
351
- setError,
352
- data: entity,
353
- }) => {
460
+ value,
461
+ propertyKey,
462
+ onValueUpdated,
463
+ setError,
464
+ data: entity,
465
+ }) => {
354
466
 
355
467
  const updatedValues = setIn({ ...entity.values }, propertyKey, value);
356
468
 
@@ -394,10 +506,88 @@ export const EntityCollectionView = React.memo(
394
506
  authController,
395
507
  }), [collection, fullPath]);
396
508
 
509
+ // Check if Kanban view is possible (collection has at least one string enum property)
510
+ const hasEnumProperty = useMemo(() => {
511
+ const properties = resolvedCollection.properties;
512
+ return Object.values(properties).some((prop: any) =>
513
+ prop && prop.dataType === "string" && prop.enumValues
514
+ );
515
+ }, [resolvedCollection.properties]);
516
+
517
+ // Compute the effective enabled views:
518
+ // - Start from collection.enabledViews (defaults to all three)
519
+ // - Filter out kanban if no enum properties exist
520
+ const enabledViews: ViewMode[] = useMemo(() => {
521
+ const configured = collection.enabledViews ?? ["table", "cards", "kanban"];
522
+ if (!hasEnumProperty) {
523
+ return configured.filter(v => v !== "kanban");
524
+ }
525
+ return configured;
526
+ }, [collection.enabledViews, hasEnumProperty]);
527
+
528
+ // Compute available enum properties for kanban column selection
529
+ const kanbanPropertyOptions: KanbanPropertyOption[] = useMemo(() => {
530
+ const options: KanbanPropertyOption[] = [];
531
+ const properties = resolvedCollection.properties;
532
+
533
+ for (const [key, property] of Object.entries(properties)) {
534
+ const prop = property as any;
535
+ if (prop && prop.dataType === "string" && prop.enumValues) {
536
+ options.push({
537
+ key,
538
+ label: prop.name || key
539
+ });
540
+ }
541
+ }
542
+
543
+ return options;
544
+ }, [resolvedCollection.properties]);
545
+
546
+ // Get saved kanban property from user config
547
+ const getSavedKanbanProperty = useCallback((): string | undefined => {
548
+ const saved = userConfigPersistence?.getCollectionConfig<M>(fullPath);
549
+ return (saved as any)?.kanbanColumnProperty;
550
+ }, [userConfigPersistence, fullPath]);
551
+
552
+ // Selected kanban property state - priority: saved config > collection default > first available
553
+ const [selectedKanbanProperty, setSelectedKanbanProperty] = useState<string>(() => {
554
+ const saved = getSavedKanbanProperty();
555
+ if (saved && kanbanPropertyOptions.some(o => o.key === saved)) return saved;
556
+ if (collection.kanban?.columnProperty) return collection.kanban.columnProperty;
557
+ return kanbanPropertyOptions[0]?.key ?? "";
558
+ });
559
+
560
+ // Update selected property if options change and current selection is no longer valid
561
+ useEffect(() => {
562
+ if (kanbanPropertyOptions.length > 0 && !kanbanPropertyOptions.some(o => o.key === selectedKanbanProperty)) {
563
+ const saved = getSavedKanbanProperty();
564
+ if (saved && kanbanPropertyOptions.some(o => o.key === saved)) {
565
+ setSelectedKanbanProperty(saved);
566
+ } else if (collection.kanban?.columnProperty && kanbanPropertyOptions.some(o => o.key === collection.kanban?.columnProperty)) {
567
+ setSelectedKanbanProperty(collection.kanban.columnProperty);
568
+ } else {
569
+ setSelectedKanbanProperty(kanbanPropertyOptions[0]?.key ?? "");
570
+ }
571
+ }
572
+ }, [kanbanPropertyOptions, selectedKanbanProperty, getSavedKanbanProperty, collection.kanban?.columnProperty]);
573
+
574
+ // Handle kanban property change
575
+ const onKanbanPropertyChange = useCallback((property: string) => {
576
+ analyticsController.onAnalyticsEvent?.("kanban_property_changed", {
577
+ path: fullPath,
578
+ property
579
+ });
580
+ setSelectedKanbanProperty(property);
581
+ // Save to local persistence
582
+ if (userConfigPersistence) {
583
+ onCollectionModifiedForUser(fullPath, { kanbanColumnProperty: property } as any);
584
+ }
585
+ }, [userConfigPersistence, onCollectionModifiedForUser, fullPath, analyticsController]);
586
+
397
587
  const getPropertyFor = useCallback(({
398
- propertyKey,
399
- entity
400
- }: GetPropertyForProps<M>) => {
588
+ propertyKey,
589
+ entity
590
+ }: GetPropertyForProps<M>) => {
401
591
  let propertyOrBuilder: PropertyOrBuilder<any, M> | undefined = getPropertyInPath<M>(collection.properties, propertyKey);
402
592
 
403
593
  // we might not find the property in the collection if combining property builders and map spread
@@ -417,7 +607,18 @@ export const EntityCollectionView = React.memo(
417
607
  });
418
608
  }, [collection.properties, customizationController.propertyConfigs, resolvedCollection.properties]);
419
609
 
420
- const displayedColumnIds = useColumnIds(resolvedCollection, true);
610
+ // Use a collection with local propertiesOrder for optimistic UI updates
611
+ const collectionWithLocalOrder = useMemo(() => {
612
+ if (localPropertiesOrder && localPropertiesOrder !== resolvedCollection.propertiesOrder) {
613
+ return {
614
+ ...resolvedCollection,
615
+ propertiesOrder: localPropertiesOrder
616
+ };
617
+ }
618
+ return resolvedCollection;
619
+ }, [resolvedCollection, localPropertiesOrder]);
620
+
621
+ const displayedColumnIds = useColumnIds(collectionWithLocalOrder, true);
421
622
 
422
623
  const additionalFields = useMemo(() => {
423
624
  const subcollectionColumns: AdditionalFieldDelegate<M, any>[] = collection.subcollections?.map((subcollection) => {
@@ -427,23 +628,22 @@ export const EntityCollectionView = React.memo(
427
628
  width: 200,
428
629
  dependencies: [],
429
630
  Builder: ({ entity }) => (
430
- <Button color={"primary"}
431
- variant={"outlined"}
432
- className={"max-w-full truncate justify-start"}
433
- startIcon={<KeyboardTabIcon size={"small"}/>}
434
- onClick={(event: any) => {
435
- event.stopPropagation();
436
- navigateToEntity({
437
- openEntityMode,
438
- collection,
439
- entityId: entity.id,
440
- selectedTab: subcollection.id ?? subcollection.path,
441
- path: fullPath,
442
- fullIdPath,
443
- navigation,
444
- sideEntityController
445
- })
446
- }}>
631
+ <Button
632
+ className={"max-w-full truncate justify-start"}
633
+ startIcon={<KeyboardTabIcon size={"small"} />}
634
+ onClick={(event: any) => {
635
+ event.stopPropagation();
636
+ navigateToEntity({
637
+ openEntityMode,
638
+ collection,
639
+ entityId: entity.id,
640
+ selectedTab: subcollection.id ?? subcollection.path,
641
+ path: fullPath,
642
+ fullIdPath,
643
+ navigation,
644
+ sideEntityController
645
+ })
646
+ }}>
447
647
  {subcollection.name}
448
648
  </Button>
449
649
  )
@@ -465,7 +665,7 @@ export const EntityCollectionView = React.memo(
465
665
  <ReferencePreview
466
666
  key={reference.path + "/" + reference.id}
467
667
  reference={reference}
468
- size={"small"}/>
668
+ size={"small"} />
469
669
  );
470
670
  })}
471
671
  </div>
@@ -488,9 +688,9 @@ export const EntityCollectionView = React.memo(
488
688
  const largeLayout = useLargeLayout();
489
689
 
490
690
  const getActionsForEntity = ({
491
- entity,
492
- customEntityActions
493
- }: {
691
+ entity,
692
+ customEntityActions
693
+ }: {
494
694
  entity?: Entity<M>,
495
695
  customEntityActions?: EntityAction[]
496
696
  }): EntityAction[] => {
@@ -514,11 +714,11 @@ export const EntityCollectionView = React.memo(
514
714
  };
515
715
 
516
716
  const tableRowActionsBuilder = useCallback(({
517
- entity,
518
- size,
519
- width,
520
- frozen
521
- }: {
717
+ entity,
718
+ size,
719
+ width,
720
+ frozen
721
+ }: {
522
722
  entity: Entity<any>,
523
723
  size: CollectionSize,
524
724
  width: number,
@@ -558,45 +758,28 @@ export const EntityCollectionView = React.memo(
558
758
 
559
759
  }, [updateLastDeleteTimestamp, usedSelectionController]);
560
760
 
561
- const title = <Popover
562
- open={popOverOpen}
563
- onOpenChange={setPopOverOpen}
564
- enabled={Boolean(collection.description)}
565
- trigger={<div className="flex flex-col items-start">
566
- <Typography
567
- variant={"subtitle1"}
568
- className={`leading-none truncate max-w-[160px] lg:max-w-[240px] ${collection.description ? "cursor-pointer" : "cursor-auto"}`}
569
- onClick={collection.description
570
- ? (e) => {
571
- setPopOverOpen(true);
572
- e.stopPropagation();
573
- }
574
- : undefined}>
575
- {`${collection.name}`}
576
- </Typography>
577
-
578
- <EntitiesCount
579
- fullPath={fullPath}
580
- collection={collection}
581
- filter={tableController.filterValues}
582
- sortBy={tableController.sortBy}
583
- onCountChange={setDocsCount}
584
- />
585
-
586
- </div>}
587
- >
761
+ // Update breadcrumb count when count changes (only if loaded)
762
+ useEffect(() => {
763
+ if (docsCount !== undefined) {
764
+ breadcrumbs.updateCount(fullPath, docsCount);
765
+ }
766
+ }, [docsCount, fullPath, breadcrumbs.updateCount]);
588
767
 
589
- {collection.description && <div className="m-4 text-surface-900 dark:text-white">
590
- <Markdown source={collection.description}/>
591
- </div>}
768
+ // EntitiesCount fetches count and updates breadcrumb - no visual rendering needed here
769
+ const countFetcher = <EntitiesCount
770
+ fullPath={fullPath}
771
+ collection={collection}
772
+ filter={tableController.filterValues}
773
+ sortBy={tableController.sortBy}
774
+ onCountChange={setDocsCount}
775
+ />;
592
776
 
593
- </Popover>;
594
777
 
595
778
  const buildAdditionalHeaderWidget = useCallback(({
596
- property,
597
- propertyKey,
598
- onHover
599
- }: {
779
+ property,
780
+ propertyKey,
781
+ onHover
782
+ }: {
600
783
  property: ResolvedProperty,
601
784
  propertyKey: string,
602
785
  onHover: boolean
@@ -616,7 +799,7 @@ export const EntityCollectionView = React.memo(
616
799
  fullPath={fullPath}
617
800
  collection={collection}
618
801
  tableController={tableController}
619
- parentCollectionIds={parentCollectionIds ?? []}/>;
802
+ parentCollectionIds={parentCollectionIds ?? []} />;
620
803
  })}
621
804
  </>;
622
805
  }, [customizationController.plugins, fullPath, parentCollectionIds]);
@@ -625,9 +808,9 @@ export const EntityCollectionView = React.memo(
625
808
  ? function () {
626
809
  if (typeof AddColumnComponent === "function")
627
810
  return <AddColumnComponent fullPath={fullPath}
628
- parentCollectionIds={parentCollectionIds ?? []}
629
- collection={collection}
630
- tableController={tableController}/>;
811
+ parentCollectionIds={parentCollectionIds ?? []}
812
+ collection={collection}
813
+ tableController={tableController} />;
631
814
  return null;
632
815
  }
633
816
  : undefined;
@@ -643,32 +826,55 @@ export const EntityCollectionView = React.memo(
643
826
  parentCollectionIds
644
827
  });
645
828
 
829
+ // Popover open state managed at parent level to prevent closing when view changes
830
+ const [viewModePopoverOpen, setViewModePopoverOpen] = useState(false);
831
+
832
+ // Create ViewModeToggle once to prevent remounting when view changes
833
+ const viewModeToggleElement = (
834
+ <ViewModeToggle
835
+ viewMode={viewMode}
836
+ onViewModeChange={onViewModeChange}
837
+ enabledViews={enabledViews}
838
+ size={viewMode === "table" ? tableSize : viewMode === "cards" ? cardSize : undefined}
839
+ onSizeChanged={viewMode === "table" ? onTableSizeChanged : viewMode === "cards" ? setCardSize : undefined}
840
+ open={viewModePopoverOpen}
841
+ onOpenChange={setViewModePopoverOpen}
842
+ kanbanPropertyOptions={kanbanPropertyOptions}
843
+ selectedKanbanProperty={selectedKanbanProperty}
844
+ onKanbanPropertyChange={onKanbanPropertyChange}
845
+ />
846
+ );
847
+
848
+ // Compute plugin-provided error view for collection loading errors
849
+ const pluginErrorView = useMemo(() => {
850
+ const error = tableController.dataLoadingError;
851
+ if (!error || !customizationController.plugins) return null;
852
+ for (const plugin of customizationController.plugins) {
853
+ if (plugin.collectionView?.CollectionError) {
854
+ const CollectionError = plugin.collectionView.CollectionError;
855
+ return <CollectionError
856
+ path={fullPath}
857
+ collection={collection}
858
+ parentCollectionIds={parentCollectionIds}
859
+ error={error}
860
+ />;
861
+ }
862
+ }
863
+ return null;
864
+ }, [tableController.dataLoadingError, customizationController.plugins, fullPath, collection, parentCollectionIds]);
865
+
646
866
  return (
647
- <div className={cls("overflow-hidden h-full w-full rounded-md", className)}
648
- ref={containerRef}>
649
- <EntityCollectionTable
650
- key={`collection_table_${fullPath}`}
651
- additionalFields={additionalFields}
652
- tableController={tableController}
653
- enablePopupIcon={true}
654
- displayedColumnIds={displayedColumnIds}
655
- onSizeChanged={onSizeChanged}
656
- onEntityClick={onEntityClick}
657
- onColumnResize={onColumnResize}
658
- onValueChange={onValueChange}
659
- tableRowActionsBuilder={tableRowActionsBuilder}
660
- uniqueFieldValidator={uniqueFieldValidator}
661
- title={title}
662
- selectionController={usedSelectionController}
663
- highlightedEntities={highlightedEntity ? [highlightedEntity] : []}
664
- defaultSize={collection.defaultSize}
665
- properties={resolvedCollection.properties}
666
- getPropertyFor={getPropertyFor}
667
- onTextSearchClick={textSearchInitialised ? undefined : onTextSearchClick}
668
- onScroll={tableController.onScroll}
669
- initialScroll={tableController.initialScroll}
867
+ <div className={cls("overflow-hidden h-full w-full rounded-md flex flex-col", className)}
868
+ ref={containerRef}>
869
+
870
+ {/* Unified toolbar - rendered once, outside view conditionals */}
871
+ {countFetcher}
872
+ <CollectionTableToolbar
873
+ loading={tableController.dataLoading}
874
+ onTextSearch={textSearchEnabled && textSearchInitialised ? tableController.setSearchString : undefined}
875
+ onTextSearchClick={textSearchEnabled && !textSearchInitialised ? onTextSearchClick : undefined}
670
876
  textSearchLoading={textSearchLoading}
671
- textSearchEnabled={textSearchEnabled}
877
+ viewModeToggle={viewModeToggleElement}
672
878
  actionsStart={<EntityCollectionViewStartActions
673
879
  parentCollectionIds={parentCollectionIds ?? []}
674
880
  collection={collection}
@@ -676,7 +882,8 @@ export const EntityCollectionView = React.memo(
676
882
  path={fullPath}
677
883
  relativePath={collection.path}
678
884
  selectionController={usedSelectionController}
679
- collectionEntitiesCount={docsCount}/>}
885
+ collectionEntitiesCount={docsCount}
886
+ resolvedProperties={resolvedCollection.properties} />}
680
887
  actions={<EntityCollectionViewActions
681
888
  parentCollectionIds={parentCollectionIds ?? []}
682
889
  collection={collection}
@@ -689,33 +896,145 @@ export const EntityCollectionView = React.memo(
689
896
  selectionEnabled={selectionEnabled}
690
897
  collectionEntitiesCount={docsCount}
691
898
  />}
692
- emptyComponent={canCreateEntities && tableController.filterValues === undefined && tableController.sortBy === undefined
693
- ? <div className="flex flex-col items-center justify-center">
694
- <Typography variant={"subtitle2"}>So empty...</Typography>
695
- <Button
696
- color={"primary"}
697
- variant={"outlined"}
698
- onClick={onNewClick}
699
- className="mt-4"
700
- >
701
- <AddIcon/>
702
- Create your first entry
703
- </Button>
704
- </div>
705
- : <Typography variant={"label"}>No results with the applied filter/sort</Typography>
706
- }
707
- hoverRow={hoverRow}
708
- inlineEditing={checkInlineEditing()}
709
- AdditionalHeaderWidget={buildAdditionalHeaderWidget}
710
- AddColumnComponent={addColumnComponentInternal}
711
- getIdColumnWidth={getIdColumnWidth}
712
- additionalIDHeaderWidget={<EntityIdHeaderWidget
713
- path={fullPath}
714
- fullIdPath={fullIdPath ?? fullPath}
715
- collection={collection}/>}
716
- openEntityMode={openEntityMode}
717
899
  />
718
900
 
901
+ {/* View content - only the view-specific content changes */}
902
+ {tableController.dataLoadingError && pluginErrorView
903
+ ? pluginErrorView
904
+ : viewMode === "kanban" && enabledViews.includes("kanban") ? (
905
+ <EntityCollectionBoardView
906
+ key={`kanban-view-${fullPath}-${selectedKanbanProperty}`}
907
+ collection={collection}
908
+ tableController={tableController}
909
+ fullPath={fullPath}
910
+ parentCollectionIds={parentCollectionIds}
911
+ columnProperty={selectedKanbanProperty}
912
+ onEntityClick={onEntityClick}
913
+ selectionController={usedSelectionController}
914
+ selectionEnabled={selectionEnabled}
915
+ highlightedEntities={highlightedEntity ? [highlightedEntity] : []}
916
+ deletedEntities={deletedEntities}
917
+ emptyComponent={canCreateEntities && tableController.filterValues === undefined && tableController.sortBy === undefined
918
+ ? <div className="flex flex-col items-center justify-center">
919
+ <Typography variant={"subtitle2"}>So empty...</Typography>
920
+ <Button
921
+ onClick={onNewClick}
922
+ className="mt-4"
923
+ >
924
+ <AddIcon />
925
+ Create your first entry
926
+ </Button>
927
+ </div>
928
+ : <Typography variant={"label"}>No results with the applied filter/sort</Typography>
929
+ }
930
+ />
931
+ ) : viewMode === "cards" ? (
932
+ <EntityCollectionCardView
933
+ key={`cards-view-${fullPath}`}
934
+ collection={collection}
935
+ tableController={tableController}
936
+ onEntityClick={onEntityClick}
937
+ selectionController={usedSelectionController}
938
+ selectionEnabled={selectionEnabled}
939
+ highlightedEntities={highlightedEntity ? [highlightedEntity] : []}
940
+ onScroll={tableController.onScroll}
941
+ initialScroll={tableController.initialScroll}
942
+ size={cardSize}
943
+ emptyComponent={canCreateEntities && tableController.filterValues === undefined && tableController.sortBy === undefined
944
+ ? <div className="flex flex-col items-center justify-center">
945
+ <Typography variant={"subtitle2"}>So empty...</Typography>
946
+ <Button
947
+ onClick={onNewClick}
948
+ className="mt-4"
949
+ >
950
+ <AddIcon />
951
+ Create your first entry
952
+ </Button>
953
+ </div>
954
+ : <Typography variant={"label"}>No results with the applied filter/sort</Typography>
955
+ }
956
+ />
957
+ ) : (
958
+ <EntityCollectionTable
959
+ key={`collection_table_${fullPath}`}
960
+ hideToolbar={true}
961
+ additionalFields={additionalFields}
962
+ tableController={tableController}
963
+ enablePopupIcon={true}
964
+ displayedColumnIds={displayedColumnIds}
965
+ onSizeChanged={onTableSizeChanged}
966
+ onEntityClick={onEntityClick}
967
+ onColumnResize={onColumnResize}
968
+ onValueChange={onValueChange}
969
+ tableRowActionsBuilder={tableRowActionsBuilder}
970
+ uniqueFieldValidator={uniqueFieldValidator}
971
+ selectionController={usedSelectionController}
972
+ highlightedEntities={highlightedEntity ? [highlightedEntity] : []}
973
+ defaultSize={tableSize}
974
+ properties={resolvedCollection.properties}
975
+ getPropertyFor={getPropertyFor}
976
+ onTextSearchClick={textSearchInitialised ? undefined : onTextSearchClick}
977
+ onScroll={tableController.onScroll}
978
+ initialScroll={tableController.initialScroll}
979
+ textSearchLoading={textSearchLoading}
980
+ textSearchEnabled={textSearchEnabled}
981
+ emptyComponent={canCreateEntities && tableController.filterValues === undefined && tableController.sortBy === undefined
982
+ ? <div className="flex flex-col items-center justify-center">
983
+ <Typography variant={"subtitle2"}>So empty...</Typography>
984
+ <Button
985
+ onClick={onNewClick}
986
+ className="mt-4"
987
+ >
988
+ <AddIcon />
989
+ Create your first entry
990
+ </Button>
991
+ </div>
992
+ : <Typography variant={"label"}>No results with the applied filter/sort</Typography>
993
+ }
994
+ hoverRow={hoverRow}
995
+ inlineEditing={checkInlineEditing()}
996
+ AdditionalHeaderWidget={buildAdditionalHeaderWidget}
997
+ AddColumnComponent={addColumnComponentInternal}
998
+ getIdColumnWidth={getIdColumnWidth}
999
+ additionalIDHeaderWidget={<EntityIdHeaderWidget
1000
+ path={fullPath}
1001
+ fullIdPath={fullIdPath ?? fullPath}
1002
+ collection={collection} />}
1003
+ openEntityMode={openEntityMode}
1004
+ onColumnsOrderChange={(newColumns) => {
1005
+ // Extract property keys from the new column order
1006
+ // Filter to only include actual property columns (not frozen columns, not additional fields, etc.)
1007
+ // Deduplicate to clean up any previously duplicated keys
1008
+ const seenKeys = new Set<string>();
1009
+ const newPropertiesOrder = newColumns
1010
+ .filter(col => !col.frozen && getPropertyInPath(collection.properties, col.key))
1011
+ .map(col => col.key)
1012
+ .filter(key => {
1013
+ if (seenKeys.has(key)) return false;
1014
+ seenKeys.add(key);
1015
+ return true;
1016
+ });
1017
+
1018
+ // Optimistically update local state to prevent UI flickering
1019
+ setLocalPropertiesOrder(newPropertiesOrder);
1020
+
1021
+ // Call each plugin's onColumnsReorder callback
1022
+ if (customizationController?.plugins) {
1023
+ customizationController.plugins
1024
+ .filter(plugin => plugin.collectionView?.onColumnsReorder)
1025
+ .forEach(plugin => {
1026
+ plugin.collectionView!.onColumnsReorder!({
1027
+ fullPath,
1028
+ parentCollectionIds: parentCollectionIds ?? [],
1029
+ collection,
1030
+ newPropertiesOrder
1031
+ });
1032
+ });
1033
+ }
1034
+ }}
1035
+ />
1036
+ )}
1037
+
719
1038
  {popupCell && <PopupFormField
720
1039
  key={`popup_form_${popupCell?.propertyKey}_${popupCell?.entityId}`}
721
1040
  open={Boolean(popupCell)}
@@ -728,7 +1047,7 @@ export const EntityCollectionView = React.memo(
728
1047
  customFieldValidator={uniqueFieldValidator}
729
1048
  path={resolvedFullPath}
730
1049
  onCellValueChange={onValueChange}
731
- container={containerRef.current}/>}
1050
+ container={containerRef.current} />}
732
1051
 
733
1052
  {deleteEntityClicked &&
734
1053
  <DeleteEntityDialog
@@ -739,7 +1058,7 @@ export const EntityCollectionView = React.memo(
739
1058
  open={Boolean(deleteEntityClicked)}
740
1059
  onEntityDelete={internalOnEntityDelete}
741
1060
  onMultipleEntitiesDelete={internalOnMultipleEntitiesDelete}
742
- onClose={() => setDeleteEntityClicked(undefined)}/>}
1061
+ onClose={() => setDeleteEntityClicked(undefined)} />}
743
1062
 
744
1063
  </div>
745
1064
  );
@@ -769,12 +1088,12 @@ export const EntityCollectionView = React.memo(
769
1088
  }) as React.FunctionComponent<EntityCollectionViewProps<any>>
770
1089
 
771
1090
  function EntitiesCount({
772
- fullPath,
773
- collection,
774
- filter,
775
- sortBy,
776
- onCountChange
777
- }: {
1091
+ fullPath,
1092
+ collection,
1093
+ filter,
1094
+ sortBy,
1095
+ onCountChange
1096
+ }: {
778
1097
  fullPath: string,
779
1098
  collection: EntityCollection,
780
1099
  filter?: FilterValues<any>,
@@ -803,9 +1122,9 @@ function EntitiesCount({
803
1122
  }, [fullPath, dataSource.countEntities, resolvedPath, collection, filter, sortByProperty, currentSort]);
804
1123
 
805
1124
  useEffect(() => {
806
- if (onCountChange) {
1125
+ if (onCountChange && count !== undefined) {
807
1126
  setError(undefined);
808
- onCountChange(count ?? 0);
1127
+ onCountChange(count);
809
1128
  }
810
1129
  }, [onCountChange, count]);
811
1130
 
@@ -813,14 +1132,11 @@ function EntitiesCount({
813
1132
  return null;
814
1133
  }
815
1134
 
816
- return <Typography
817
- className="w-full text-ellipsis block overflow-hidden whitespace-nowrap max-w-xs text-left w-fit-content"
818
- variant={"caption"}
819
- color={"secondary"}>
820
- {count !== undefined ? `${count} entities` : <Skeleton className={"w-full max-w-[80px] mt-1"}/>}
821
- </Typography>;
1135
+ // Count is now displayed in the breadcrumb bar, this component only fetches and reports
1136
+ return null;
822
1137
  }
823
1138
 
1139
+
824
1140
  function buildPropertyWidthOverwrite(key: string, width: number): PartialEntityCollection {
825
1141
  if (key.includes(".")) {
826
1142
  const [parentKey, ...childKey] = key.split(".");
@@ -830,10 +1146,10 @@ function buildPropertyWidthOverwrite(key: string, width: number): PartialEntityC
830
1146
  }
831
1147
 
832
1148
  function EntityIdHeaderWidget({
833
- collection,
834
- path,
835
- fullIdPath
836
- }: {
1149
+ collection,
1150
+ path,
1151
+ fullIdPath
1152
+ }: {
837
1153
  collection: EntityCollection,
838
1154
  path: string,
839
1155
  fullIdPath: string
@@ -857,29 +1173,29 @@ function EntityIdHeaderWidget({
857
1173
  alignOffset={-117}
858
1174
  trigger={
859
1175
  <IconButton size={"small"}>
860
- <SearchIcon size={"small"}/>
1176
+ <SearchIcon size={"small"} />
861
1177
  </IconButton>
862
1178
  }>
863
1179
  <div
864
1180
  className={cls("my-2 rounded-lg bg-surface-50 dark:bg-surface-950 text-surface-900 dark:text-white")}>
865
1181
  <form noValidate={true}
866
- onSubmit={(e) => {
867
- e.preventDefault();
868
- if (!searchString) return;
869
- setOpenPopup(false);
870
- const entityId = searchString.trim();
871
- setRecentIds(addRecentId(collection.id, entityId));
872
- navigateToEntity({
873
- openEntityMode,
874
- collection,
875
- entityId,
876
- path,
877
- fullIdPath,
878
- sideEntityController,
879
- navigation
880
- })
881
- }}
882
- className={"w-96 max-w-full"}>
1182
+ onSubmit={(e) => {
1183
+ e.preventDefault();
1184
+ if (!searchString) return;
1185
+ setOpenPopup(false);
1186
+ const entityId = searchString.trim();
1187
+ setRecentIds(addRecentId(collection.id, entityId));
1188
+ navigateToEntity({
1189
+ openEntityMode,
1190
+ collection,
1191
+ entityId,
1192
+ path,
1193
+ fullIdPath,
1194
+ sideEntityController,
1195
+ navigation
1196
+ })
1197
+ }}
1198
+ className={"w-96 max-w-full"}>
883
1199
 
884
1200
  <div className="flex p-2 w-full gap-2">
885
1201
  <input
@@ -890,32 +1206,32 @@ function EntityIdHeaderWidget({
890
1206
  setSearchString(e.target.value);
891
1207
  }}
892
1208
  value={searchString}
893
- className={"rounded-lg bg-white dark:bg-surface-800 flex-grow bg-transparent outline-none p-2 " + focusedDisabled}/>
1209
+ className={"rounded-lg bg-white dark:bg-surface-800 flex-grow bg-transparent outline-none p-2 " + focusedDisabled} />
894
1210
  <Button variant={"text"}
895
- disabled={!(searchString.trim())}
896
- type={"submit"}
897
- ><KeyboardTabIcon/></Button>
1211
+ disabled={!(searchString.trim())}
1212
+ type={"submit"}
1213
+ ><KeyboardTabIcon /></Button>
898
1214
  </div>
899
1215
  </form>
900
1216
  {recentIds && recentIds.length > 0 && <div className="flex flex-col gap-2 p-2">
901
1217
  {recentIds.map(id => (
902
1218
  <ReferencePreview reference={new EntityReference(id, path)}
903
- key={id}
904
- hover={true}
905
- onClick={() => {
906
- setOpenPopup(false);
907
- navigateToEntity({
908
- openEntityMode,
909
- collection,
910
- entityId: id,
911
- path,
912
- fullIdPath,
913
- sideEntityController,
914
- navigation
915
- })
916
- }}
917
- includeEntityLink={false}
918
- size={"small"}/>
1219
+ key={id}
1220
+ hover={true}
1221
+ onClick={() => {
1222
+ setOpenPopup(false);
1223
+ navigateToEntity({
1224
+ openEntityMode,
1225
+ collection,
1226
+ entityId: id,
1227
+ path,
1228
+ fullIdPath,
1229
+ sideEntityController,
1230
+ navigation
1231
+ })
1232
+ }}
1233
+ includeEntityLink={false}
1234
+ size={"small"} />
919
1235
  ))}
920
1236
  </div>}
921
1237
  </div>