@firecms/core 3.3.0 → 3.4.0-canary.2575a08

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 (155) hide show
  1. package/dist/app/useApp.d.ts +1 -0
  2. package/dist/components/EntityCollectionTable/column_utils.d.ts +4 -3
  3. package/dist/components/EntityCollectionTable/internal/CollectionTableToolbar.d.ts +6 -1
  4. package/dist/components/EntityCollectionTable/internal/popup_field/PopupFormField.d.ts +3 -1
  5. package/dist/components/EntityCollectionView/EntityCollectionBoardView.d.ts +3 -1
  6. package/dist/components/EntityCollectionView/EntityCollectionView.d.ts +8 -0
  7. package/dist/components/EntityCollectionView/EntityCollectionViewActions.d.ts +3 -1
  8. package/dist/components/EntityCollectionView/EntityCollectionViewStartActions.d.ts +3 -1
  9. package/dist/components/EntityCollectionView/FiltersDialog.d.ts +2 -1
  10. package/dist/components/EntityCollectionView/useBoardDataController.d.ts +3 -1
  11. package/dist/components/EntityPreview.d.ts +3 -1
  12. package/dist/components/ReferenceTable/ReferenceSelectionTable.d.ts +10 -1
  13. package/dist/components/SelectableTable/filters/filter_text_input.d.ts +30 -0
  14. package/dist/components/SelectableTable/filters/filter_text_input.test.d.ts +1 -0
  15. package/dist/components/common/table_url_params.d.ts +24 -0
  16. package/dist/components/common/useDataSourceTableController.d.ts +9 -1
  17. package/dist/components/common/useTableSearchHelper.d.ts +9 -1
  18. package/dist/core/DefaultDrawer.d.ts +10 -3
  19. package/dist/core/EntityEditView.d.ts +9 -1
  20. package/dist/core/EntityEditViewFormActions.d.ts +1 -1
  21. package/dist/core/field_configs.d.ts +1 -1
  22. package/dist/form/EntityForm.d.ts +8 -1
  23. package/dist/form/EntityFormActions.d.ts +2 -0
  24. package/dist/form/field_bindings/GeopointFieldBinding.d.ts +5 -0
  25. package/dist/form/index.d.ts +1 -0
  26. package/dist/hooks/data/delete.d.ts +1 -1
  27. package/dist/hooks/data/save.d.ts +1 -1
  28. package/dist/hooks/data/useCollectionFetch.d.ts +7 -1
  29. package/dist/hooks/data/useEntityFetch.d.ts +9 -1
  30. package/dist/i18n/__tests__/FireCMSi18nProvider.test.d.ts +1 -0
  31. package/dist/i18n/__tests__/nested_provider_lifecycle.test.d.ts +1 -0
  32. package/dist/index.es.js +3658 -1820
  33. package/dist/index.es.js.map +1 -1
  34. package/dist/index.umd.js +3655 -1817
  35. package/dist/index.umd.js.map +1 -1
  36. package/dist/locales/pl.d.ts +2 -0
  37. package/dist/preview/index.d.ts +1 -0
  38. package/dist/preview/property_previews/GeopointPropertyPreview.d.ts +4 -0
  39. package/dist/types/collections.d.ts +31 -0
  40. package/dist/types/datasource.d.ts +67 -4
  41. package/dist/types/entities.d.ts +21 -1
  42. package/dist/types/entity_actions.d.ts +6 -0
  43. package/dist/types/entity_callbacks.d.ts +21 -0
  44. package/dist/types/navigation.d.ts +34 -5
  45. package/dist/types/plugins.d.ts +13 -0
  46. package/dist/types/properties.d.ts +17 -1
  47. package/dist/types/side_entity_controller.d.ts +7 -0
  48. package/dist/types/translations.d.ts +6 -0
  49. package/dist/util/__tests__/collections.test.d.ts +1 -0
  50. package/dist/util/__tests__/urls.test.d.ts +1 -0
  51. package/dist/util/collections.d.ts +1 -1
  52. package/dist/util/entities.d.ts +1 -0
  53. package/dist/util/entity_cache.d.ts +11 -0
  54. package/dist/util/geopoint.d.ts +22 -0
  55. package/dist/util/index.d.ts +2 -0
  56. package/dist/util/navigation_blocking.d.ts +22 -0
  57. package/dist/util/navigation_from_path.d.ts +17 -0
  58. package/dist/util/navigation_utils.d.ts +107 -3
  59. package/dist/util/parent_references_from_path.d.ts +16 -0
  60. package/dist/util/paths.d.ts +28 -0
  61. package/dist/util/permissions.d.ts +10 -4
  62. package/dist/util/urls.d.ts +22 -0
  63. package/package.json +17 -10
  64. package/src/app/Scaffold.tsx +34 -44
  65. package/src/app/useApp.tsx +1 -0
  66. package/src/components/DeleteEntityDialog.tsx +2 -0
  67. package/src/components/EntityCollectionTable/EntityCollectionRowActions.tsx +2 -2
  68. package/src/components/EntityCollectionTable/EntityCollectionTable.tsx +3 -1
  69. package/src/components/EntityCollectionTable/column_utils.tsx +11 -19
  70. package/src/components/EntityCollectionTable/fields/TableReferenceField.tsx +1 -1
  71. package/src/components/EntityCollectionTable/internal/CollectionTableToolbar.tsx +7 -0
  72. package/src/components/EntityCollectionTable/internal/popup_field/PopupFormField.tsx +5 -1
  73. package/src/components/EntityCollectionView/EntityCollectionBoardView.tsx +12 -0
  74. package/src/components/EntityCollectionView/EntityCollectionView.tsx +66 -15
  75. package/src/components/EntityCollectionView/EntityCollectionViewActions.tsx +6 -2
  76. package/src/components/EntityCollectionView/EntityCollectionViewStartActions.tsx +8 -1
  77. package/src/components/EntityCollectionView/FiltersDialog.tsx +39 -28
  78. package/src/components/EntityCollectionView/useBoardDataController.tsx +17 -1
  79. package/src/components/EntityPreview.tsx +45 -41
  80. package/src/components/ReferenceTable/ReferenceSelectionTable.tsx +19 -2
  81. package/src/components/ReferenceWidget.tsx +1 -1
  82. package/src/components/SelectableTable/filters/ReferenceFilterField.tsx +2 -2
  83. package/src/components/SelectableTable/filters/StringNumberFilterField.tsx +33 -7
  84. package/src/components/SelectableTable/filters/filter_text_input.test.ts +184 -0
  85. package/src/components/SelectableTable/filters/filter_text_input.ts +71 -0
  86. package/src/components/VirtualTable/VirtualTable.tsx +19 -8
  87. package/src/components/common/default_entity_actions.tsx +22 -2
  88. package/src/components/common/table_url_params.ts +172 -0
  89. package/src/components/common/useDataSourceTableController.tsx +126 -163
  90. package/src/components/common/useTableSearchHelper.ts +26 -2
  91. package/src/contexts/SnackbarProvider.tsx +14 -1
  92. package/src/core/DefaultDrawer.tsx +150 -64
  93. package/src/core/DrawerNavigationGroup.tsx +27 -29
  94. package/src/core/DrawerNavigationItem.tsx +10 -12
  95. package/src/core/EntityEditView.tsx +77 -35
  96. package/src/core/EntityEditViewFormActions.tsx +3 -2
  97. package/src/core/EntitySidePanel.tsx +8 -4
  98. package/src/core/field_configs.tsx +15 -0
  99. package/src/form/EntityForm.tsx +31 -10
  100. package/src/form/EntityFormActions.tsx +2 -0
  101. package/src/form/field_bindings/ArrayOfReferencesFieldBinding.tsx +1 -1
  102. package/src/form/field_bindings/GeopointFieldBinding.tsx +139 -0
  103. package/src/form/index.tsx +1 -0
  104. package/src/hooks/data/delete.ts +8 -0
  105. package/src/hooks/data/save.ts +6 -1
  106. package/src/hooks/data/useCollectionFetch.tsx +21 -1
  107. package/src/hooks/data/useEntityFetch.tsx +35 -4
  108. package/src/hooks/useBuildNavigationController.tsx +23 -8
  109. package/src/hooks/useResolvedNavigationFrom.tsx +3 -1
  110. package/src/hooks/useValidateAuthenticator.tsx +4 -0
  111. package/src/i18n/FireCMSi18nProvider.tsx +50 -4
  112. package/src/i18n/__tests__/FireCMSi18nProvider.test.tsx +91 -0
  113. package/src/i18n/__tests__/nested_provider_lifecycle.test.tsx +67 -0
  114. package/src/internal/useBuildDataSource.ts +37 -15
  115. package/src/internal/useBuildSideEntityController.tsx +20 -5
  116. package/src/locales/de.ts +7 -2
  117. package/src/locales/en.ts +7 -2
  118. package/src/locales/es.ts +7 -2
  119. package/src/locales/fr.ts +7 -2
  120. package/src/locales/hi.ts +7 -2
  121. package/src/locales/it.ts +7 -2
  122. package/src/locales/pl.ts +735 -0
  123. package/src/locales/pt.ts +7 -2
  124. package/src/preview/PropertyPreview.tsx +12 -0
  125. package/src/preview/components/ReferencePreview.tsx +7 -3
  126. package/src/preview/components/StorageThumbnail.tsx +24 -2
  127. package/src/preview/index.ts +1 -0
  128. package/src/preview/property_previews/GeopointPropertyPreview.tsx +23 -0
  129. package/src/routes/FireCMSRoute.tsx +54 -27
  130. package/src/types/collections.ts +34 -0
  131. package/src/types/datasource.ts +67 -4
  132. package/src/types/entities.ts +24 -1
  133. package/src/types/entity_actions.tsx +6 -0
  134. package/src/types/entity_callbacks.ts +24 -0
  135. package/src/types/navigation.ts +35 -5
  136. package/src/types/plugins.tsx +13 -0
  137. package/src/types/properties.ts +17 -0
  138. package/src/types/side_entity_controller.tsx +7 -0
  139. package/src/types/translations.ts +6 -0
  140. package/src/util/__tests__/collections.test.ts +32 -0
  141. package/src/util/__tests__/urls.test.ts +131 -0
  142. package/src/util/collections.ts +1 -1
  143. package/src/util/entities.ts +16 -1
  144. package/src/util/entity_cache.ts +18 -0
  145. package/src/util/geopoint.ts +81 -0
  146. package/src/util/index.ts +2 -0
  147. package/src/util/navigation_blocking.ts +45 -0
  148. package/src/util/navigation_from_path.ts +47 -7
  149. package/src/util/navigation_utils.ts +244 -4
  150. package/src/util/parent_references_from_path.ts +45 -4
  151. package/src/util/paths.ts +40 -3
  152. package/src/util/permissions.ts +22 -10
  153. package/src/util/previews.ts +8 -1
  154. package/src/util/resolutions.ts +8 -0
  155. package/src/util/urls.ts +52 -0
@@ -26,6 +26,8 @@ import { OnCellValueChangeParams } from "../../../common";
26
26
  interface PopupFormFieldProps<M extends Record<string, any>> {
27
27
  customFieldValidator?: CustomFieldValidator;
28
28
  path: string;
29
+ /** `path` split at its real segment boundaries; forwarded to the datasource. */
30
+ pathSegments?: string[];
29
31
  entityId: string;
30
32
  tableKey: string;
31
33
  propertyKey?: Extract<keyof M, string>;
@@ -54,6 +56,7 @@ export function PopupFormFieldLoading<M extends Record<string, any>>({
54
56
  propertyKey,
55
57
  collection: inputCollection,
56
58
  path,
59
+ pathSegments,
57
60
  cellRect,
58
61
  open,
59
62
  onClose,
@@ -66,11 +69,12 @@ export function PopupFormFieldLoading<M extends Record<string, any>>({
66
69
  if (entityId && inputCollection) {
67
70
  dataSource.fetchEntity({
68
71
  path,
72
+ pathSegments,
69
73
  entityId,
70
74
  collection: inputCollection
71
75
  }).then(setEntity);
72
76
  }
73
- }, [entityId, inputCollection, dataSource, path]);
77
+ }, [entityId, inputCollection, dataSource, path, pathSegments]);
74
78
 
75
79
  if (!entity) return null;
76
80
  return <PopupFormFieldInternal {...{
@@ -42,6 +42,8 @@ export type EntityCollectionBoardViewProps<M extends Record<string, any> = any>
42
42
  collection: EntityCollection<M>;
43
43
  tableController: EntityTableController<M>;
44
44
  fullPath: string;
45
+ /** `fullPath` split at its real segment boundaries; forwarded to the datasource. */
46
+ pathSegments?: string[];
45
47
  parentCollectionIds?: string[];
46
48
  columnProperty: string;
47
49
  onEntityClick?: (entity: Entity<M>) => void;
@@ -60,6 +62,7 @@ export function EntityCollectionBoardView<M extends Record<string, any> = any>({
60
62
  collection,
61
63
  tableController,
62
64
  fullPath,
65
+ pathSegments,
63
66
  parentCollectionIds = [],
64
67
  columnProperty,
65
68
  onEntityClick,
@@ -169,6 +172,7 @@ export function EntityCollectionBoardView<M extends Record<string, any> = any>({
169
172
  // Use the new per-column data controller
170
173
  const boardDataController = useBoardDataController<M>({
171
174
  fullPath,
175
+ pathSegments,
172
176
  collection,
173
177
  columnProperty,
174
178
  columns,
@@ -270,6 +274,7 @@ export function EntityCollectionBoardView<M extends Record<string, any> = any>({
270
274
 
271
275
  currentDataSource.countEntities({
272
276
  path: fullPath,
277
+ pathSegments,
273
278
  collection: currentCollection
274
279
  }).then(count => {
275
280
  totalCount = count;
@@ -281,6 +286,7 @@ export function EntityCollectionBoardView<M extends Record<string, any> = any>({
281
286
 
282
287
  currentDataSource.countEntities({
283
288
  path: fullPath,
289
+ pathSegments,
284
290
  collection: currentCollection,
285
291
  filter: { [orderProperty]: ["!=", null] } as FilterValues<string>
286
292
  }).then(count => {
@@ -432,6 +438,8 @@ export function EntityCollectionBoardView<M extends Record<string, any> = any>({
432
438
 
433
439
  const saveProps: SaveEntityProps = {
434
440
  path: entity.path,
441
+ // Paired with `entity.path`: the segments the entity was loaded with.
442
+ pathSegments: entity.pathSegments,
435
443
  entityId: entity.id,
436
444
  values: updatedValues as M,
437
445
  previousValues: entity.values,
@@ -472,6 +480,7 @@ export function EntityCollectionBoardView<M extends Record<string, any> = any>({
472
480
  console.log("Fetching all documents from collection...");
473
481
  const allDocs = await dataSource.fetchCollection<M>({
474
482
  path: fullPath,
483
+ pathSegments,
475
484
  collection,
476
485
  limit: 10000 // Fetch all
477
486
  });
@@ -492,6 +501,8 @@ export function EntityCollectionBoardView<M extends Record<string, any> = any>({
492
501
 
493
502
  const saveProps: SaveEntityProps = {
494
503
  path: entity.path,
504
+ // Paired with `entity.path`: the segments the entity was loaded with.
505
+ pathSegments: entity.pathSegments,
495
506
  entityId: entity.id,
496
507
  values: updatedValues as M,
497
508
  previousValues: entity.values,
@@ -663,6 +674,7 @@ export function EntityCollectionBoardView<M extends Record<string, any> = any>({
663
674
  });
664
675
  sideEntityController.open({
665
676
  path: fullPath,
677
+ pathSegments,
666
678
  collection,
667
679
  entityId: undefined,
668
680
  updateUrl: true,
@@ -90,6 +90,7 @@ import { useSelectionController } from "./useSelectionController";
90
90
  import { EntityCollectionViewStartActions } from "./EntityCollectionViewStartActions";
91
91
  import { addRecentId, getRecentIds } from "./utils";
92
92
  import { useScrollRestoration } from "../common/useScrollRestoration";
93
+ import { entityCacheKey } from "../../util/entity_cache";
93
94
 
94
95
  const DEFAULT_ENTITY_OPEN_MODE: "side_panel" | "full_screen" = "side_panel";
95
96
 
@@ -106,6 +107,14 @@ export type EntityCollectionViewProps<M extends Record<string, any>> = {
106
107
  * Full path using navigation ids.
107
108
  */
108
109
  fullIdPath?: string;
110
+ /**
111
+ * `fullPath` split at its real segment boundaries, e.g. `["nodes", "node/42", "edges"]`.
112
+ * Forwarded to the datasource so a parent entity id containing "/" stays unambiguous.
113
+ *
114
+ * Optional, and never derived by splitting `fullPath` — a guess would be wrong in
115
+ * exactly the case the field exists for. Absent means "not known here", not "no slashes".
116
+ */
117
+ pathSegments?: string[];
109
118
  /**
110
119
  * If this is a subcollection, specify the parent collection ids.
111
120
  */
@@ -152,6 +161,7 @@ export const EntityCollectionView = React.memo(
152
161
  function EntityCollectionView<M extends Record<string, any>>({
153
162
  fullPath: fullPathProp,
154
163
  fullIdPath,
164
+ pathSegments,
155
165
  parentCollectionIds,
156
166
  isSubCollection,
157
167
  className,
@@ -190,7 +200,7 @@ export const EntityCollectionView = React.memo(
190
200
  collectionRef.current = collection;
191
201
  }, [collection]);
192
202
 
193
- const canCreateEntities = canCreateEntity(collection, authController, fullPath, null);
203
+ const canCreateEntities = canCreateEntity(collection, authController, fullPath, null, pathSegments);
194
204
  const [highlightedEntity, setHighlightedEntity] = useState<Entity<M> | undefined>(undefined);
195
205
  const [deleteEntityClicked, setDeleteEntityClicked] = React.useState<Entity<M> | Entity<M>[] | undefined>(undefined);
196
206
 
@@ -220,7 +230,7 @@ export const EntityCollectionView = React.memo(
220
230
 
221
231
  const checkInlineEditing = useCallback((entity?: Entity<any>): boolean => {
222
232
  const collection = collectionRef.current;
223
- if (!canEditEntity(collection, authController, fullPath, entity ?? null)) {
233
+ if (!canEditEntity(collection, authController, fullPath, entity ?? null, pathSegments)) {
224
234
  return false;
225
235
  }
226
236
  return collection.inlineEditing === undefined || collection.inlineEditing;
@@ -317,6 +327,7 @@ export const EntityCollectionView = React.memo(
317
327
 
318
328
  const tableController = useDataSourceTableController<M>({
319
329
  fullPath,
330
+ pathSegments,
320
331
  collection,
321
332
  lastDeleteTimestamp,
322
333
  scrollRestoration,
@@ -342,10 +353,16 @@ export const EntityCollectionView = React.memo(
342
353
  addRecentId(collection.id, clickedEntity.id);
343
354
  }
344
355
 
345
- const path = collection?.collectionGroup ? clickedEntity.path : (fullPath ?? clickedEntity.path);
356
+ // In a collection group the row's own path is the real one, so the row's segments
357
+ // are the matching ones; otherwise both come from this view. `path` and
358
+ // `pathSegments` are picked from the same source so they cannot describe
359
+ // different chains.
360
+ const isCollectionGroup = Boolean(collection?.collectionGroup);
361
+ const path = isCollectionGroup ? clickedEntity.path : (fullPath ?? clickedEntity.path);
346
362
  navigateToEntity({
347
363
  navigation,
348
364
  path,
365
+ pathSegments: isCollectionGroup ? clickedEntity.pathSegments : (pathSegments ?? clickedEntity.pathSegments),
349
366
  fullIdPath,
350
367
  sideEntityController,
351
368
  openEntityMode,
@@ -365,6 +382,7 @@ export const EntityCollectionView = React.memo(
365
382
  collection,
366
383
  entityId: undefined,
367
384
  path: fullPath,
385
+ pathSegments,
368
386
  fullIdPath,
369
387
  sideEntityController,
370
388
  navigation,
@@ -440,6 +458,7 @@ export const EntityCollectionView = React.memo(
440
458
  const onViewModeChange = useCallback((mode: ViewMode) => {
441
459
  analyticsController.onAnalyticsEvent?.("view_mode_changed", {
442
460
  path: fullPath,
461
+ pathSegments,
443
462
  from: viewMode,
444
463
  to: mode
445
464
  });
@@ -450,7 +469,7 @@ export const EntityCollectionView = React.memo(
450
469
  }
451
470
  }, [setViewMode, userConfigPersistence, onCollectionModifiedForUser, fullPath, analyticsController, viewMode]);
452
471
 
453
- const createEnabled = canCreateEntity(collection, authController, fullPath, null);
472
+ const createEnabled = canCreateEntity(collection, authController, fullPath, null, pathSegments);
454
473
 
455
474
  const uniqueFieldValidator: UniqueFieldValidator = useCallback(
456
475
  ({
@@ -458,8 +477,8 @@ export const EntityCollectionView = React.memo(
458
477
  value,
459
478
  property,
460
479
  entityId
461
- }) => dataSource.checkUniqueField(fullPath, name, value, entityId, collection),
462
- [fullPath]);
480
+ }) => dataSource.checkUniqueField(fullPath, name, value, entityId, collection, pathSegments),
481
+ [fullPath, pathSegments]);
463
482
 
464
483
  const onValueChange: OnCellValueChange<any, any> = ({
465
484
  value,
@@ -473,6 +492,8 @@ export const EntityCollectionView = React.memo(
473
492
 
474
493
  const saveProps: SaveEntityProps = {
475
494
  path: entity.path ?? fullPath,
495
+ // Paired with the `path` above: the row's segments describe the row's path.
496
+ pathSegments: entity.path ? entity.pathSegments : pathSegments,
476
497
  entityId: entity.id,
477
498
  values: updatedValues,
478
499
  previousValues: entity.values,
@@ -503,7 +524,7 @@ export const EntityCollectionView = React.memo(
503
524
 
504
525
  };
505
526
 
506
- const resolvedFullPath = navigation.resolveIdsFrom(fullPath);
527
+ const resolvedFullPath = navigation.resolveIdsFrom(fullPath, pathSegments);
507
528
  const resolvedCollection = useMemo(() => resolveCollection<M>({
508
529
  collection,
509
530
  path: fullPath,
@@ -581,6 +602,7 @@ export const EntityCollectionView = React.memo(
581
602
  const onKanbanPropertyChange = useCallback((property: string) => {
582
603
  analyticsController.onAnalyticsEvent?.("kanban_property_changed", {
583
604
  path: fullPath,
605
+ pathSegments,
584
606
  property
585
607
  });
586
608
  setSelectedKanbanProperty(property);
@@ -645,6 +667,7 @@ export const EntityCollectionView = React.memo(
645
667
  entityId: entity.id,
646
668
  selectedTab: subcollection.id ?? subcollection.path,
647
669
  path: fullPath,
670
+ pathSegments,
648
671
  fullIdPath,
649
672
  navigation,
650
673
  sideEntityController
@@ -663,13 +686,15 @@ export const EntityCollectionView = React.memo(
663
686
  width: 260,
664
687
  dependencies: [],
665
688
  Builder: ({ entity }) => {
666
- const collectionsWithPath = navigation.getParentReferencesFromPath(entity.path);
689
+ // `entity.path` is a RAW datasource path, so its segments are passed:
690
+ // splitting it would mis-read any parent id containing "/".
691
+ const collectionsWithPath = navigation.getParentReferencesFromPath(entity.path, entity.pathSegments);
667
692
  return (
668
693
  <div className={"flex flex-col gap-2 w-full"}>
669
694
  {collectionsWithPath.map((reference) => {
670
695
  return (
671
696
  <ReferencePreview
672
- key={reference.path + "/" + reference.id}
697
+ key={entityCacheKey(reference.path, reference.id)}
673
698
  reference={reference}
674
699
  size={"small"} />
675
700
  );
@@ -700,7 +725,7 @@ export const EntityCollectionView = React.memo(
700
725
  entity?: Entity<M>,
701
726
  customEntityActions?: EntityAction[]
702
727
  }): EntityAction[] => {
703
- const deleteEnabled = entity ? canDeleteEntity(collection, authController, fullPath, entity) : true;
728
+ const deleteEnabled = entity ? canDeleteEntity(collection, authController, fullPath, entity, entity.pathSegments ?? pathSegments) : true;
704
729
  const actions: EntityAction[] = [
705
730
  { ...editEntityAction, name: t("edit") }
706
731
  ];
@@ -776,6 +801,7 @@ export const EntityCollectionView = React.memo(
776
801
  // EntitiesCount fetches count and updates breadcrumb - no visual rendering needed here
777
802
  const countFetcher = <EntitiesCount
778
803
  fullPath={fullPath}
804
+ pathSegments={pathSegments}
779
805
  collection={collection}
780
806
  filter={tableController.filterValues}
781
807
  sortBy={tableController.sortBy}
@@ -829,9 +855,14 @@ export const EntityCollectionView = React.memo(
829
855
  onTextSearchClick,
830
856
  textSearchEnabled
831
857
  } = useTableSearchHelper({
858
+ pathSegments,
832
859
  collection,
833
860
  fullPath: resolvedFullPath,
834
- parentCollectionIds
861
+ parentCollectionIds,
862
+ // When the search term comes from the URL instead of the search bar, text search
863
+ // has never been initialised for this collection. Initialise it so the search bar
864
+ // is usable (editable and clearable) right away.
865
+ initialSearchString: tableController.searchString
835
866
  });
836
867
 
837
868
  // Popover open state managed at parent level to prevent closing when view changes
@@ -881,6 +912,7 @@ export const EntityCollectionView = React.memo(
881
912
  loading={tableController.dataLoading}
882
913
  onTextSearch={textSearchEnabled && textSearchInitialised ? tableController.setSearchString : undefined}
883
914
  onTextSearchClick={textSearchEnabled && !textSearchInitialised ? onTextSearchClick : undefined}
915
+ initialSearchString={tableController.searchString}
884
916
  textSearchLoading={textSearchLoading}
885
917
  viewModeToggle={viewModeToggleElement}
886
918
  actionsStart={<EntityCollectionViewStartActions
@@ -888,6 +920,7 @@ export const EntityCollectionView = React.memo(
888
920
  collection={collection}
889
921
  tableController={tableController}
890
922
  path={fullPath}
923
+ pathSegments={pathSegments}
891
924
  relativePath={collection.path}
892
925
  selectionController={usedSelectionController}
893
926
  collectionEntitiesCount={docsCount}
@@ -899,6 +932,7 @@ export const EntityCollectionView = React.memo(
899
932
  onMultipleDeleteClick={onMultipleDeleteClick}
900
933
  onNewClick={onNewClick}
901
934
  path={fullPath}
935
+ pathSegments={pathSegments}
902
936
  relativePath={collection.path}
903
937
  selectionController={usedSelectionController}
904
938
  selectionEnabled={selectionEnabled}
@@ -915,6 +949,7 @@ export const EntityCollectionView = React.memo(
915
949
  collection={collection}
916
950
  tableController={tableController}
917
951
  fullPath={fullPath}
952
+ pathSegments={pathSegments}
918
953
  parentCollectionIds={parentCollectionIds}
919
954
  columnProperty={selectedKanbanProperty}
920
955
  onEntityClick={onEntityClick}
@@ -1006,6 +1041,7 @@ export const EntityCollectionView = React.memo(
1006
1041
  getIdColumnWidth={getIdColumnWidth}
1007
1042
  additionalIDHeaderWidget={<EntityIdHeaderWidget
1008
1043
  path={fullPath}
1044
+ pathSegments={pathSegments}
1009
1045
  fullIdPath={fullIdPath ?? fullPath}
1010
1046
  collection={collection} />}
1011
1047
  openEntityMode={openEntityMode}
@@ -1045,6 +1081,7 @@ export const EntityCollectionView = React.memo(
1045
1081
 
1046
1082
  {popupCell && <PopupFormField
1047
1083
  key={`popup_form_${popupCell?.propertyKey}_${popupCell?.entityId}`}
1084
+ pathSegments={pathSegments}
1048
1085
  open={Boolean(popupCell)}
1049
1086
  onClose={onPopupClose}
1050
1087
  cellRect={popupCell?.cellRect}
@@ -1100,13 +1137,16 @@ function EntitiesCount({
1100
1137
  collection,
1101
1138
  filter,
1102
1139
  sortBy,
1103
- onCountChange
1140
+ onCountChange,
1141
+ pathSegments
1104
1142
  }: {
1105
1143
  fullPath: string,
1106
1144
  collection: EntityCollection,
1107
1145
  filter?: FilterValues<any>,
1108
1146
  sortBy?: [string, "asc" | "desc"],
1109
1147
  onCountChange?: (count: number) => void,
1148
+ /** `fullPath` split at its real segment boundaries; forwarded to countEntities. */
1149
+ pathSegments?: string[],
1110
1150
  }) {
1111
1151
 
1112
1152
  const dataSource = useDataSource(collection);
@@ -1116,18 +1156,24 @@ function EntitiesCount({
1116
1156
 
1117
1157
  const sortByProperty = sortBy ? sortBy[0] : undefined;
1118
1158
  const currentSort = sortBy ? sortBy[1] : undefined;
1119
- const resolvedPath = useMemo(() => navigation.resolveIdsFrom(fullPath), [fullPath, navigation.resolveIdsFrom]);
1159
+ // Resolved together so the two representations always describe the same chain.
1160
+ // Segments are resolved, never derived (see useEntityFetch).
1161
+ const resolvedPath = useMemo(() => navigation.resolveIdsFrom(fullPath, pathSegments), [fullPath, pathSegments, navigation.resolveIdsFrom]);
1162
+ const resolvedPathSegments = useMemo(() => pathSegments
1163
+ ? (navigation.resolveSegmentsFrom?.(pathSegments) ?? pathSegments)
1164
+ : undefined, [pathSegments, navigation.resolveSegmentsFrom]);
1120
1165
 
1121
1166
  useEffect(() => {
1122
1167
  if (dataSource.countEntities)
1123
1168
  dataSource.countEntities({
1124
1169
  path: resolvedPath,
1170
+ pathSegments: resolvedPathSegments,
1125
1171
  collection,
1126
1172
  filter,
1127
1173
  orderBy: sortByProperty,
1128
1174
  order: currentSort
1129
1175
  }).then(setCount).catch(setError);
1130
- }, [fullPath, dataSource.countEntities, resolvedPath, collection, filter, sortByProperty, currentSort]);
1176
+ }, [fullPath, dataSource.countEntities, resolvedPath, resolvedPathSegments, collection, filter, sortByProperty, currentSort]);
1131
1177
 
1132
1178
  useEffect(() => {
1133
1179
  if (onCountChange && count !== undefined) {
@@ -1156,10 +1202,13 @@ function buildPropertyWidthOverwrite(key: string, width: number): PartialEntityC
1156
1202
  function EntityIdHeaderWidget({
1157
1203
  collection,
1158
1204
  path,
1205
+ pathSegments,
1159
1206
  fullIdPath
1160
1207
  }: {
1161
1208
  collection: EntityCollection,
1162
1209
  path: string,
1210
+ /** `path` split at its real segment boundaries, when the caller knows them. */
1211
+ pathSegments?: string[],
1163
1212
  fullIdPath: string
1164
1213
  }) {
1165
1214
 
@@ -1199,6 +1248,7 @@ function EntityIdHeaderWidget({
1199
1248
  collection,
1200
1249
  entityId,
1201
1250
  path,
1251
+ pathSegments,
1202
1252
  fullIdPath,
1203
1253
  sideEntityController,
1204
1254
  navigation
@@ -1224,7 +1274,7 @@ function EntityIdHeaderWidget({
1224
1274
  </form>
1225
1275
  {recentIds && recentIds.length > 0 && <div className="flex flex-col gap-2 p-2">
1226
1276
  {recentIds.map(id => (
1227
- <ReferencePreview reference={new EntityReference(id, path)}
1277
+ <ReferencePreview reference={new EntityReference(id, path, undefined, pathSegments)}
1228
1278
  key={id}
1229
1279
  hover={true}
1230
1280
  onClick={() => {
@@ -1234,6 +1284,7 @@ function EntityIdHeaderWidget({
1234
1284
  collection,
1235
1285
  entityId: id,
1236
1286
  path,
1287
+ pathSegments,
1237
1288
  fullIdPath,
1238
1289
  sideEntityController,
1239
1290
  navigation
@@ -22,6 +22,8 @@ import { useTranslation } from "../../hooks/useTranslation";
22
22
  export type EntityCollectionViewActionsProps<M extends Record<string, any>> = {
23
23
  collection: EntityCollection<M>;
24
24
  path: string;
25
+ /** `path` split at its real segment boundaries, when known. Never derived from `path`. */
26
+ pathSegments?: string[];
25
27
  relativePath: string;
26
28
  parentCollectionIds: string[];
27
29
  selectionEnabled: boolean;
@@ -40,6 +42,7 @@ export function EntityCollectionViewActions<M extends Record<string, any>>({
40
42
  onMultipleDeleteClick,
41
43
  selectionEnabled,
42
44
  path,
45
+ pathSegments,
43
46
  selectionController,
44
47
  tableController,
45
48
  collectionEntitiesCount,
@@ -56,7 +59,7 @@ export function EntityCollectionViewActions<M extends Record<string, any>>({
56
59
 
57
60
  const selectedEntities = selectionController.selectedEntities;
58
61
 
59
- const addButton = canCreateEntity(collection, authController, path, null) &&
62
+ const addButton = canCreateEntity(collection, authController, path, null, pathSegments) &&
60
63
  onNewClick && (largeLayout
61
64
  ? <Button
62
65
  id={`add_entity_${path}`}
@@ -75,7 +78,7 @@ export function EntityCollectionViewActions<M extends Record<string, any>>({
75
78
  <AddIcon size={"small"} />
76
79
  </Button>);
77
80
 
78
- const multipleDeleteEnabled = canDeleteEntity(collection, authController, path, null);
81
+ const multipleDeleteEnabled = canDeleteEntity(collection, authController, path, null, pathSegments);
79
82
 
80
83
  let multipleDeleteButton: React.ReactNode | undefined;
81
84
  if (selectionEnabled) {
@@ -107,6 +110,7 @@ export function EntityCollectionViewActions<M extends Record<string, any>>({
107
110
 
108
111
  const actionProps: CollectionActionsProps = {
109
112
  path,
113
+ pathSegments,
110
114
  relativePath,
111
115
  parentCollectionIds,
112
116
  collection,
@@ -17,6 +17,8 @@ import { useTranslation } from "../../hooks/useTranslation";
17
17
  export type EntityCollectionViewStartActionsProps<M extends Record<string, any>> = {
18
18
  collection: EntityCollection<M>;
19
19
  path: string;
20
+ /** `path` split at its real segment boundaries, when known. Never derived from `path`. */
21
+ pathSegments?: string[];
20
22
  relativePath: string;
21
23
  parentCollectionIds: string[];
22
24
  selectionController: SelectionController<M>;
@@ -33,6 +35,7 @@ export function EntityCollectionViewStartActions<M extends Record<string, any>>(
33
35
  relativePath,
34
36
  parentCollectionIds,
35
37
  path,
38
+ pathSegments,
36
39
  selectionController,
37
40
  tableController,
38
41
  collectionEntitiesCount,
@@ -57,6 +60,7 @@ export function EntityCollectionViewStartActions<M extends Record<string, any>>(
57
60
 
58
61
  const actionProps: CollectionActionsProps = {
59
62
  path,
63
+ pathSegments,
60
64
  relativePath,
61
65
  parentCollectionIds,
62
66
  collection,
@@ -66,8 +70,10 @@ export function EntityCollectionViewStartActions<M extends Record<string, any>>(
66
70
  collectionEntitiesCount
67
71
  };
68
72
 
73
+ const hasAnyAllowedFilters = !tableController.allowedFilters || tableController.allowedFilters.length > 0;
74
+
69
75
  // Filters button
70
- const filtersButton = resolvedProperties && tableController.setFilterValues && (
76
+ const filtersButton = resolvedProperties && tableController.setFilterValues && hasAnyAllowedFilters && (
71
77
  <Tooltip title={t("filters")}
72
78
  key={"filters_tooltip"}>
73
79
  <Badge
@@ -131,6 +137,7 @@ export function EntityCollectionViewStartActions<M extends Record<string, any>>(
131
137
  filterValues={tableController.filterValues}
132
138
  setFilterValues={(filterValues) => tableController.setFilterValues?.(filterValues ?? {})}
133
139
  forceFilter={collection.forceFilter}
140
+ allowedFilters={tableController.allowedFilters?.map(key => key.toString())}
134
141
  />
135
142
  )}
136
143
  </>
@@ -12,7 +12,6 @@ import {
12
12
  DialogActions,
13
13
  DialogContent,
14
14
  DialogTitle,
15
- FilterListIcon,
16
15
  Typography
17
16
  } from "@firecms/ui";
18
17
  import { useTranslation } from "../../hooks/useTranslation";
@@ -30,6 +29,7 @@ export interface FiltersDialogProps {
30
29
  filterValues: FilterValues<any> | undefined;
31
30
  setFilterValues: (filterValues?: FilterValues<any>) => void;
32
31
  forceFilter?: FilterValues<any>;
32
+ allowedFilters?: string[];
33
33
  }
34
34
 
35
35
  /**
@@ -42,7 +42,8 @@ export function FiltersDialog({
42
42
  properties,
43
43
  filterValues,
44
44
  setFilterValues,
45
- forceFilter
45
+ forceFilter,
46
+ allowedFilters
46
47
  }: FiltersDialogProps) {
47
48
  const { t } = useTranslation();
48
49
 
@@ -59,18 +60,18 @@ export function FiltersDialog({
59
60
  }
60
61
  }, [open, filterValues]);
61
62
 
62
- // Get list of filterable properties
63
- const filterableProperties = useMemo(() => {
64
- return Object.entries(properties).filter(([key, property]) => {
65
- if (!property) return false;
66
- // Force filter properties should not be editable
67
- if (forceFilter && key in forceFilter) return false;
68
- // Check if property type is filterable
69
- const baseProperty = property.dataType === "array" ? property.of : property;
70
- if (!baseProperty) return false;
71
- return ["string", "number", "boolean", "date", "reference"].includes(baseProperty.dataType);
63
+ // Get list of editable filter properties
64
+ const editableFilterProperties = useMemo(() => {
65
+ return Object.entries(properties).filter(([key]) => {
66
+ const isFilterAllowed = !allowedFilters || allowedFilters.includes(key);
67
+
68
+ const isFilterForced = Boolean(forceFilter && Object.keys(forceFilter).includes(key));
69
+
70
+ return isFilterAllowed && !isFilterForced;
72
71
  });
73
- }, [properties, forceFilter]);
72
+ }, [properties, allowedFilters, forceFilter]);
73
+
74
+ const hasEditableFilterProperties = editableFilterProperties.length > 0;
74
75
 
75
76
  const handleFilterChange = useCallback((propertyKey: string, value?: [VirtualTableWhereFilterOp, any]) => {
76
77
  setLocalFilters(prev => {
@@ -103,7 +104,13 @@ export function FiltersDialog({
103
104
 
104
105
  // Check if any reference field's dialog is currently open (should hide this dialog)
105
106
  const isAnyFieldHidden = Object.values(hiddenFields).some(hidden => hidden);
106
- const activeFilterCount = Object.keys(localFilters).length;
107
+
108
+ const getActiveFilterCount = () => {
109
+ const editableLocalFilters = Object.keys(localFilters).filter((key) => !forceFilter || !(key in forceFilter));
110
+ return editableLocalFilters.length;
111
+ }
112
+
113
+ const activeFilterCount = getActiveFilterCount();
107
114
 
108
115
  const renderFilterField = useCallback((propertyKey: string, property: ResolvedProperty) => {
109
116
  const isArray = property.dataType === "array";
@@ -185,14 +192,14 @@ export function FiltersDialog({
185
192
  </DialogTitle>
186
193
 
187
194
  <DialogContent >
188
- {filterableProperties.length === 0 ? (
195
+ {!hasEditableFilterProperties ? (
189
196
  <Typography color="secondary" className="py-8 text-center">
190
197
  {t("no_filterable_properties")}
191
198
  </Typography>
192
199
  ) : (
193
200
  <table className="w-full border-collapse">
194
201
  <tbody>
195
- {filterableProperties.map(([propertyKey, property], index) => {
202
+ {editableFilterProperties.map(([propertyKey, property], index) => {
196
203
  const hasFilter = propertyKey in localFilters;
197
204
 
198
205
  return (
@@ -234,18 +241,22 @@ export function FiltersDialog({
234
241
  {t("clear")}
235
242
  </Button>
236
243
  <div className="flex-grow" />
237
- <Button
238
- variant="text"
239
- onClick={() => onOpenChange(false)}
240
- >
241
- {t("cancel")}
242
- </Button>
243
- <Button
244
- variant="filled"
245
- onClick={handleApply}
246
- >
247
- {t("apply_filters")}
248
- </Button>
244
+ {hasEditableFilterProperties && (
245
+ <>
246
+ <Button
247
+ variant="text"
248
+ onClick={() => onOpenChange(false)}
249
+ >
250
+ {t("cancel")}
251
+ </Button>
252
+ <Button
253
+ variant="filled"
254
+ onClick={handleApply}
255
+ >
256
+ {t("apply_filters")}
257
+ </Button>
258
+ </>
259
+ )}
249
260
  </DialogActions>
250
261
  </Dialog>
251
262
  );