@firecms/core 3.3.0-canary.d3242eb → 3.4.0-canary.00f9be5

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 (221) hide show
  1. package/README.md +4 -0
  2. package/dist/app/useApp.d.ts +1 -0
  3. package/dist/components/EntityCollectionTable/column_utils.d.ts +4 -3
  4. package/dist/components/EntityCollectionTable/internal/CollectionTableToolbar.d.ts +6 -1
  5. package/dist/components/EntityCollectionTable/internal/popup_field/PopupFormField.d.ts +3 -1
  6. package/dist/components/EntityCollectionView/EntityCollectionBoardView.d.ts +3 -1
  7. package/dist/components/EntityCollectionView/EntityCollectionView.d.ts +8 -0
  8. package/dist/components/EntityCollectionView/EntityCollectionViewActions.d.ts +3 -1
  9. package/dist/components/EntityCollectionView/EntityCollectionViewStartActions.d.ts +3 -1
  10. package/dist/components/EntityCollectionView/FiltersDialog.d.ts +2 -1
  11. package/dist/components/EntityCollectionView/useBoardDataController.d.ts +3 -1
  12. package/dist/components/EntityPreview.d.ts +3 -1
  13. package/dist/components/ErrorBoundary.d.ts +1 -3
  14. package/dist/components/LicenseBanner.d.ts +23 -0
  15. package/dist/components/ReferenceTable/ReferenceSelectionTable.d.ts +10 -1
  16. package/dist/components/SelectableTable/filters/filter_text_input.d.ts +30 -0
  17. package/dist/components/SelectableTable/filters/filter_text_input.test.d.ts +1 -0
  18. package/dist/components/common/table_url_params.d.ts +24 -0
  19. package/dist/components/common/useDataSourceTableController.d.ts +9 -1
  20. package/dist/components/common/useTableSearchHelper.d.ts +9 -1
  21. package/dist/components/index.d.ts +1 -0
  22. package/dist/contexts/LicenseStatusContext.d.ts +16 -0
  23. package/dist/core/DefaultDrawer.d.ts +10 -3
  24. package/dist/core/EntityEditView.d.ts +9 -1
  25. package/dist/core/EntityEditViewFormActions.d.ts +1 -1
  26. package/dist/core/field_configs.d.ts +1 -1
  27. package/dist/core/index.d.ts +1 -0
  28. package/dist/core/plugin_translations.d.ts +38 -0
  29. package/dist/core/pro_plugins.d.ts +40 -0
  30. package/dist/de-BF5vcQPY.js +703 -0
  31. package/dist/de-BF5vcQPY.js.map +1 -0
  32. package/dist/editor/extensions/Image.d.ts +11 -0
  33. package/dist/editor/index.d.ts +2 -1
  34. package/dist/editor/lazy_editor.d.ts +6 -0
  35. package/dist/editor-BSo_aS1I.js +4563 -0
  36. package/dist/editor-BSo_aS1I.js.map +1 -0
  37. package/dist/form/EntityForm.d.ts +8 -1
  38. package/dist/form/EntityFormActions.d.ts +2 -0
  39. package/dist/form/field_bindings/GeopointFieldBinding.d.ts +5 -0
  40. package/dist/form/index.d.ts +1 -0
  41. package/dist/fr-DH9_wyFI.js +703 -0
  42. package/dist/fr-DH9_wyFI.js.map +1 -0
  43. package/dist/hi-BM8dm1Lu.js +703 -0
  44. package/dist/hi-BM8dm1Lu.js.map +1 -0
  45. package/dist/hooks/data/delete.d.ts +1 -1
  46. package/dist/hooks/data/save.d.ts +1 -1
  47. package/dist/hooks/data/useCollectionFetch.d.ts +7 -1
  48. package/dist/hooks/data/useEntityFetch.d.ts +9 -1
  49. package/dist/hooks/index.d.ts +2 -0
  50. package/dist/hooks/useLicenseStatus.d.ts +16 -0
  51. package/dist/hooks/useProjectLog.d.ts +11 -6
  52. package/dist/hooks/useTranslation.d.ts +1 -1
  53. package/dist/i18n/__tests__/FireCMSi18nProvider.test.d.ts +1 -0
  54. package/dist/i18n/__tests__/nested_provider_lifecycle.test.d.ts +1 -0
  55. package/dist/index.es.js +4429 -10479
  56. package/dist/index.es.js.map +1 -1
  57. package/dist/index.umd.js +9313 -6582
  58. package/dist/index.umd.js.map +1 -1
  59. package/dist/it-D3K1gfe4.js +703 -0
  60. package/dist/it-D3K1gfe4.js.map +1 -0
  61. package/dist/locales/pl.d.ts +2 -0
  62. package/dist/pl-CP1zeLzn.js +705 -0
  63. package/dist/pl-CP1zeLzn.js.map +1 -0
  64. package/dist/preview/index.d.ts +1 -0
  65. package/dist/preview/property_previews/GeopointPropertyPreview.d.ts +4 -0
  66. package/dist/pt-Cus7eKQR.js +705 -0
  67. package/dist/pt-Cus7eKQR.js.map +1 -0
  68. package/dist/types/analytics.d.ts +1 -1
  69. package/dist/types/collections.d.ts +31 -0
  70. package/dist/types/datasource.d.ts +67 -4
  71. package/dist/types/entities.d.ts +21 -1
  72. package/dist/types/entity_actions.d.ts +6 -0
  73. package/dist/types/entity_callbacks.d.ts +21 -0
  74. package/dist/types/firecms.d.ts +18 -1
  75. package/dist/types/firecms_context.d.ts +8 -0
  76. package/dist/types/index.d.ts +1 -0
  77. package/dist/types/license.d.ts +55 -0
  78. package/dist/types/navigation.d.ts +34 -5
  79. package/dist/types/plugins.d.ts +22 -1
  80. package/dist/types/properties.d.ts +17 -1
  81. package/dist/types/side_entity_controller.d.ts +7 -0
  82. package/dist/types/translations.d.ts +54 -5
  83. package/dist/util/__tests__/collections.test.d.ts +1 -0
  84. package/dist/util/__tests__/urls.test.d.ts +1 -0
  85. package/dist/util/collections.d.ts +1 -1
  86. package/dist/util/date_fns_locales.d.ts +18 -0
  87. package/dist/util/entities.d.ts +1 -0
  88. package/dist/util/entity_cache.d.ts +11 -0
  89. package/dist/util/geopoint.d.ts +22 -0
  90. package/dist/util/index.d.ts +3 -0
  91. package/dist/util/lazy_eager.d.ts +3 -3
  92. package/dist/util/navigation_blocking.d.ts +22 -0
  93. package/dist/util/navigation_from_path.d.ts +17 -0
  94. package/dist/util/navigation_utils.d.ts +107 -3
  95. package/dist/util/parent_references_from_path.d.ts +16 -0
  96. package/dist/util/paths.d.ts +28 -0
  97. package/dist/util/permissions.d.ts +10 -4
  98. package/dist/util/urls.d.ts +22 -0
  99. package/package.json +17 -11
  100. package/src/app/Scaffold.tsx +54 -45
  101. package/src/app/useApp.tsx +1 -0
  102. package/src/components/DeleteEntityDialog.tsx +2 -0
  103. package/src/components/EntityCollectionTable/EntityCollectionRowActions.tsx +2 -2
  104. package/src/components/EntityCollectionTable/EntityCollectionTable.tsx +3 -1
  105. package/src/components/EntityCollectionTable/column_utils.tsx +11 -19
  106. package/src/components/EntityCollectionTable/fields/TableReferenceField.tsx +1 -1
  107. package/src/components/EntityCollectionTable/internal/CollectionTableToolbar.tsx +7 -0
  108. package/src/components/EntityCollectionTable/internal/popup_field/PopupFormField.tsx +5 -1
  109. package/src/components/EntityCollectionView/EntityCollectionBoardView.tsx +12 -0
  110. package/src/components/EntityCollectionView/EntityCollectionView.tsx +71 -17
  111. package/src/components/EntityCollectionView/EntityCollectionViewActions.tsx +6 -2
  112. package/src/components/EntityCollectionView/EntityCollectionViewStartActions.tsx +8 -1
  113. package/src/components/EntityCollectionView/FiltersDialog.tsx +39 -28
  114. package/src/components/EntityCollectionView/useBoardDataController.tsx +17 -1
  115. package/src/components/EntityPreview.tsx +45 -41
  116. package/src/components/ErrorBoundary.tsx +12 -18
  117. package/src/components/HomePage/NavigationCardBinding.tsx +6 -3
  118. package/src/components/LicenseBanner.tsx +207 -0
  119. package/src/components/ReferenceTable/ReferenceSelectionTable.tsx +19 -2
  120. package/src/components/ReferenceWidget.tsx +1 -1
  121. package/src/components/SelectableTable/filters/ReferenceFilterField.tsx +2 -2
  122. package/src/components/SelectableTable/filters/StringNumberFilterField.tsx +33 -7
  123. package/src/components/SelectableTable/filters/filter_text_input.test.ts +184 -0
  124. package/src/components/SelectableTable/filters/filter_text_input.ts +71 -0
  125. package/src/components/VirtualTable/VirtualTable.tsx +19 -8
  126. package/src/components/common/default_entity_actions.tsx +22 -2
  127. package/src/components/common/table_url_params.ts +172 -0
  128. package/src/components/common/useDataSourceTableController.tsx +126 -163
  129. package/src/components/common/useTableSearchHelper.ts +26 -2
  130. package/src/components/index.tsx +1 -0
  131. package/src/contexts/LicenseStatusContext.tsx +50 -0
  132. package/src/contexts/SnackbarProvider.tsx +14 -1
  133. package/src/core/DefaultAppBar.tsx +28 -16
  134. package/src/core/DefaultDrawer.tsx +151 -65
  135. package/src/core/DrawerNavigationGroup.tsx +27 -29
  136. package/src/core/DrawerNavigationItem.tsx +10 -12
  137. package/src/core/EntityEditView.tsx +77 -35
  138. package/src/core/EntityEditViewFormActions.tsx +3 -2
  139. package/src/core/EntitySidePanel.tsx +8 -4
  140. package/src/core/FireCMS.tsx +73 -47
  141. package/src/core/SideDialogs.tsx +44 -1
  142. package/src/core/field_configs.tsx +15 -0
  143. package/src/core/index.tsx +1 -0
  144. package/src/core/plugin_translations.ts +87 -0
  145. package/src/core/pro_plugins.ts +79 -0
  146. package/src/editor/components/SlashCommandMenu.tsx +4 -1
  147. package/src/editor/extensions/Image.ts +17 -1
  148. package/src/editor/hooks/useProseMirror.ts +5 -0
  149. package/src/editor/index.ts +11 -1
  150. package/src/editor/lazy_editor.tsx +33 -0
  151. package/src/form/EntityForm.tsx +31 -10
  152. package/src/form/EntityFormActions.tsx +2 -0
  153. package/src/form/field_bindings/ArrayOfReferencesFieldBinding.tsx +1 -1
  154. package/src/form/field_bindings/GeopointFieldBinding.tsx +139 -0
  155. package/src/form/index.tsx +1 -0
  156. package/src/hooks/data/delete.ts +8 -0
  157. package/src/hooks/data/save.ts +6 -1
  158. package/src/hooks/data/useCollectionFetch.tsx +21 -1
  159. package/src/hooks/data/useEntityFetch.tsx +35 -4
  160. package/src/hooks/index.tsx +2 -0
  161. package/src/hooks/useBuildNavigationController.tsx +75 -16
  162. package/src/hooks/useFireCMSContext.tsx +7 -3
  163. package/src/hooks/useLicenseStatus.tsx +32 -0
  164. package/src/hooks/useProjectLog.tsx +27 -9
  165. package/src/hooks/useResolvedNavigationFrom.tsx +3 -1
  166. package/src/hooks/useTranslation.ts +3 -1
  167. package/src/hooks/useValidateAuthenticator.tsx +4 -0
  168. package/src/i18n/FireCMSi18nProvider.tsx +199 -23
  169. package/src/i18n/__tests__/FireCMSi18nProvider.test.tsx +91 -0
  170. package/src/i18n/__tests__/nested_provider_lifecycle.test.tsx +67 -0
  171. package/src/internal/useBuildDataSource.ts +37 -15
  172. package/src/internal/useBuildSideEntityController.tsx +20 -5
  173. package/src/locales/de.ts +37 -7
  174. package/src/locales/en.ts +41 -9
  175. package/src/locales/es.ts +37 -7
  176. package/src/locales/fr.ts +37 -7
  177. package/src/locales/hi.ts +37 -7
  178. package/src/locales/it.ts +37 -7
  179. package/src/locales/pl.ts +752 -0
  180. package/src/locales/pt.ts +37 -7
  181. package/src/preview/PropertyPreview.tsx +12 -0
  182. package/src/preview/components/DatePreview.tsx +2 -4
  183. package/src/preview/components/ReferencePreview.tsx +7 -3
  184. package/src/preview/components/StorageThumbnail.tsx +24 -2
  185. package/src/preview/index.ts +1 -0
  186. package/src/preview/property_previews/GeopointPropertyPreview.tsx +23 -0
  187. package/src/routes/FireCMSRoute.tsx +54 -27
  188. package/src/types/analytics.ts +3 -0
  189. package/src/types/collections.ts +34 -0
  190. package/src/types/datasource.ts +67 -4
  191. package/src/types/entities.ts +24 -1
  192. package/src/types/entity_actions.tsx +6 -0
  193. package/src/types/entity_callbacks.ts +24 -0
  194. package/src/types/firecms.tsx +19 -1
  195. package/src/types/firecms_context.tsx +9 -0
  196. package/src/types/index.ts +1 -0
  197. package/src/types/license.ts +63 -0
  198. package/src/types/navigation.ts +35 -5
  199. package/src/types/plugins.tsx +23 -1
  200. package/src/types/properties.ts +17 -0
  201. package/src/types/side_entity_controller.tsx +7 -0
  202. package/src/types/translations.ts +54 -5
  203. package/src/util/__tests__/collections.test.ts +32 -0
  204. package/src/util/__tests__/urls.test.ts +131 -0
  205. package/src/util/collections.ts +1 -1
  206. package/src/util/date_fns_locales.ts +168 -0
  207. package/src/util/entities.ts +16 -1
  208. package/src/util/entity_cache.ts +18 -0
  209. package/src/util/geopoint.ts +81 -0
  210. package/src/util/index.ts +3 -0
  211. package/src/util/lazy_eager.tsx +30 -8
  212. package/src/util/navigation_blocking.ts +45 -0
  213. package/src/util/navigation_from_path.ts +47 -7
  214. package/src/util/navigation_utils.ts +250 -5
  215. package/src/util/parent_references_from_path.ts +45 -4
  216. package/src/util/paths.ts +40 -3
  217. package/src/util/permissions.ts +22 -10
  218. package/src/util/previews.ts +8 -1
  219. package/src/util/resolutions.ts +11 -0
  220. package/src/util/urls.ts +52 -0
  221. package/src/util/useStorageUploadController.tsx +6 -1
@@ -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,
@@ -178,7 +188,9 @@ export const EntityCollectionView = React.memo(
178
188
 
179
189
  const collection = useMemo(() => {
180
190
  const userOverride = userConfigPersistence?.getCollectionConfig<M>(fullPath);
181
- return (userOverride ? mergeDeep(collectionProp, userOverride) : collectionProp) as EntityCollection<M>;
191
+ if (!userOverride) return collectionProp;
192
+ const { properties, ...rest } = userOverride;
193
+ return mergeDeep(collectionProp, rest) as EntityCollection<M>;
182
194
  }, [collectionProp, fullPath, userConfigPersistence?.getCollectionConfig]);
183
195
 
184
196
  const openEntityMode = collection?.openEntityMode ?? DEFAULT_ENTITY_OPEN_MODE;
@@ -188,7 +200,7 @@ export const EntityCollectionView = React.memo(
188
200
  collectionRef.current = collection;
189
201
  }, [collection]);
190
202
 
191
- const canCreateEntities = canCreateEntity(collection, authController, fullPath, null);
203
+ const canCreateEntities = canCreateEntity(collection, authController, fullPath, null, pathSegments);
192
204
  const [highlightedEntity, setHighlightedEntity] = useState<Entity<M> | undefined>(undefined);
193
205
  const [deleteEntityClicked, setDeleteEntityClicked] = React.useState<Entity<M> | Entity<M>[] | undefined>(undefined);
194
206
 
@@ -218,7 +230,7 @@ export const EntityCollectionView = React.memo(
218
230
 
219
231
  const checkInlineEditing = useCallback((entity?: Entity<any>): boolean => {
220
232
  const collection = collectionRef.current;
221
- if (!canEditEntity(collection, authController, fullPath, entity ?? null)) {
233
+ if (!canEditEntity(collection, authController, fullPath, entity ?? null, pathSegments)) {
222
234
  return false;
223
235
  }
224
236
  return collection.inlineEditing === undefined || collection.inlineEditing;
@@ -315,6 +327,7 @@ export const EntityCollectionView = React.memo(
315
327
 
316
328
  const tableController = useDataSourceTableController<M>({
317
329
  fullPath,
330
+ pathSegments,
318
331
  collection,
319
332
  lastDeleteTimestamp,
320
333
  scrollRestoration,
@@ -340,10 +353,16 @@ export const EntityCollectionView = React.memo(
340
353
  addRecentId(collection.id, clickedEntity.id);
341
354
  }
342
355
 
343
- 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);
344
362
  navigateToEntity({
345
363
  navigation,
346
364
  path,
365
+ pathSegments: isCollectionGroup ? clickedEntity.pathSegments : (pathSegments ?? clickedEntity.pathSegments),
347
366
  fullIdPath,
348
367
  sideEntityController,
349
368
  openEntityMode,
@@ -363,6 +382,7 @@ export const EntityCollectionView = React.memo(
363
382
  collection,
364
383
  entityId: undefined,
365
384
  path: fullPath,
385
+ pathSegments,
366
386
  fullIdPath,
367
387
  sideEntityController,
368
388
  navigation,
@@ -438,6 +458,7 @@ export const EntityCollectionView = React.memo(
438
458
  const onViewModeChange = useCallback((mode: ViewMode) => {
439
459
  analyticsController.onAnalyticsEvent?.("view_mode_changed", {
440
460
  path: fullPath,
461
+ pathSegments,
441
462
  from: viewMode,
442
463
  to: mode
443
464
  });
@@ -448,7 +469,7 @@ export const EntityCollectionView = React.memo(
448
469
  }
449
470
  }, [setViewMode, userConfigPersistence, onCollectionModifiedForUser, fullPath, analyticsController, viewMode]);
450
471
 
451
- const createEnabled = canCreateEntity(collection, authController, fullPath, null);
472
+ const createEnabled = canCreateEntity(collection, authController, fullPath, null, pathSegments);
452
473
 
453
474
  const uniqueFieldValidator: UniqueFieldValidator = useCallback(
454
475
  ({
@@ -456,8 +477,8 @@ export const EntityCollectionView = React.memo(
456
477
  value,
457
478
  property,
458
479
  entityId
459
- }) => dataSource.checkUniqueField(fullPath, name, value, entityId, collection),
460
- [fullPath]);
480
+ }) => dataSource.checkUniqueField(fullPath, name, value, entityId, collection, pathSegments),
481
+ [fullPath, pathSegments]);
461
482
 
462
483
  const onValueChange: OnCellValueChange<any, any> = ({
463
484
  value,
@@ -471,6 +492,8 @@ export const EntityCollectionView = React.memo(
471
492
 
472
493
  const saveProps: SaveEntityProps = {
473
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,
474
497
  entityId: entity.id,
475
498
  values: updatedValues,
476
499
  previousValues: entity.values,
@@ -501,13 +524,14 @@ export const EntityCollectionView = React.memo(
501
524
 
502
525
  };
503
526
 
504
- const resolvedFullPath = navigation.resolveIdsFrom(fullPath);
527
+ const resolvedFullPath = navigation.resolveIdsFrom(fullPath, pathSegments);
505
528
  const resolvedCollection = useMemo(() => resolveCollection<M>({
506
529
  collection,
507
530
  path: fullPath,
508
531
  propertyConfigs: customizationController.propertyConfigs,
509
532
  authController,
510
- }), [collection, fullPath]);
533
+ userConfigPersistence,
534
+ }), [collection, fullPath, userConfigPersistence]);
511
535
 
512
536
  // Check if Kanban view is possible (collection has at least one string enum property)
513
537
  const hasEnumProperty = useMemo(() => {
@@ -578,6 +602,7 @@ export const EntityCollectionView = React.memo(
578
602
  const onKanbanPropertyChange = useCallback((property: string) => {
579
603
  analyticsController.onAnalyticsEvent?.("kanban_property_changed", {
580
604
  path: fullPath,
605
+ pathSegments,
581
606
  property
582
607
  });
583
608
  setSelectedKanbanProperty(property);
@@ -642,6 +667,7 @@ export const EntityCollectionView = React.memo(
642
667
  entityId: entity.id,
643
668
  selectedTab: subcollection.id ?? subcollection.path,
644
669
  path: fullPath,
670
+ pathSegments,
645
671
  fullIdPath,
646
672
  navigation,
647
673
  sideEntityController
@@ -660,13 +686,15 @@ export const EntityCollectionView = React.memo(
660
686
  width: 260,
661
687
  dependencies: [],
662
688
  Builder: ({ entity }) => {
663
- 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);
664
692
  return (
665
693
  <div className={"flex flex-col gap-2 w-full"}>
666
694
  {collectionsWithPath.map((reference) => {
667
695
  return (
668
696
  <ReferencePreview
669
- key={reference.path + "/" + reference.id}
697
+ key={entityCacheKey(reference.path, reference.id)}
670
698
  reference={reference}
671
699
  size={"small"} />
672
700
  );
@@ -697,7 +725,7 @@ export const EntityCollectionView = React.memo(
697
725
  entity?: Entity<M>,
698
726
  customEntityActions?: EntityAction[]
699
727
  }): EntityAction[] => {
700
- const deleteEnabled = entity ? canDeleteEntity(collection, authController, fullPath, entity) : true;
728
+ const deleteEnabled = entity ? canDeleteEntity(collection, authController, fullPath, entity, entity.pathSegments ?? pathSegments) : true;
701
729
  const actions: EntityAction[] = [
702
730
  { ...editEntityAction, name: t("edit") }
703
731
  ];
@@ -773,6 +801,7 @@ export const EntityCollectionView = React.memo(
773
801
  // EntitiesCount fetches count and updates breadcrumb - no visual rendering needed here
774
802
  const countFetcher = <EntitiesCount
775
803
  fullPath={fullPath}
804
+ pathSegments={pathSegments}
776
805
  collection={collection}
777
806
  filter={tableController.filterValues}
778
807
  sortBy={tableController.sortBy}
@@ -826,9 +855,14 @@ export const EntityCollectionView = React.memo(
826
855
  onTextSearchClick,
827
856
  textSearchEnabled
828
857
  } = useTableSearchHelper({
858
+ pathSegments,
829
859
  collection,
830
860
  fullPath: resolvedFullPath,
831
- 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
832
866
  });
833
867
 
834
868
  // Popover open state managed at parent level to prevent closing when view changes
@@ -878,6 +912,7 @@ export const EntityCollectionView = React.memo(
878
912
  loading={tableController.dataLoading}
879
913
  onTextSearch={textSearchEnabled && textSearchInitialised ? tableController.setSearchString : undefined}
880
914
  onTextSearchClick={textSearchEnabled && !textSearchInitialised ? onTextSearchClick : undefined}
915
+ initialSearchString={tableController.searchString}
881
916
  textSearchLoading={textSearchLoading}
882
917
  viewModeToggle={viewModeToggleElement}
883
918
  actionsStart={<EntityCollectionViewStartActions
@@ -885,6 +920,7 @@ export const EntityCollectionView = React.memo(
885
920
  collection={collection}
886
921
  tableController={tableController}
887
922
  path={fullPath}
923
+ pathSegments={pathSegments}
888
924
  relativePath={collection.path}
889
925
  selectionController={usedSelectionController}
890
926
  collectionEntitiesCount={docsCount}
@@ -896,6 +932,7 @@ export const EntityCollectionView = React.memo(
896
932
  onMultipleDeleteClick={onMultipleDeleteClick}
897
933
  onNewClick={onNewClick}
898
934
  path={fullPath}
935
+ pathSegments={pathSegments}
899
936
  relativePath={collection.path}
900
937
  selectionController={usedSelectionController}
901
938
  selectionEnabled={selectionEnabled}
@@ -912,6 +949,7 @@ export const EntityCollectionView = React.memo(
912
949
  collection={collection}
913
950
  tableController={tableController}
914
951
  fullPath={fullPath}
952
+ pathSegments={pathSegments}
915
953
  parentCollectionIds={parentCollectionIds}
916
954
  columnProperty={selectedKanbanProperty}
917
955
  onEntityClick={onEntityClick}
@@ -1003,6 +1041,7 @@ export const EntityCollectionView = React.memo(
1003
1041
  getIdColumnWidth={getIdColumnWidth}
1004
1042
  additionalIDHeaderWidget={<EntityIdHeaderWidget
1005
1043
  path={fullPath}
1044
+ pathSegments={pathSegments}
1006
1045
  fullIdPath={fullIdPath ?? fullPath}
1007
1046
  collection={collection} />}
1008
1047
  openEntityMode={openEntityMode}
@@ -1042,6 +1081,7 @@ export const EntityCollectionView = React.memo(
1042
1081
 
1043
1082
  {popupCell && <PopupFormField
1044
1083
  key={`popup_form_${popupCell?.propertyKey}_${popupCell?.entityId}`}
1084
+ pathSegments={pathSegments}
1045
1085
  open={Boolean(popupCell)}
1046
1086
  onClose={onPopupClose}
1047
1087
  cellRect={popupCell?.cellRect}
@@ -1097,13 +1137,16 @@ function EntitiesCount({
1097
1137
  collection,
1098
1138
  filter,
1099
1139
  sortBy,
1100
- onCountChange
1140
+ onCountChange,
1141
+ pathSegments
1101
1142
  }: {
1102
1143
  fullPath: string,
1103
1144
  collection: EntityCollection,
1104
1145
  filter?: FilterValues<any>,
1105
1146
  sortBy?: [string, "asc" | "desc"],
1106
1147
  onCountChange?: (count: number) => void,
1148
+ /** `fullPath` split at its real segment boundaries; forwarded to countEntities. */
1149
+ pathSegments?: string[],
1107
1150
  }) {
1108
1151
 
1109
1152
  const dataSource = useDataSource(collection);
@@ -1113,18 +1156,24 @@ function EntitiesCount({
1113
1156
 
1114
1157
  const sortByProperty = sortBy ? sortBy[0] : undefined;
1115
1158
  const currentSort = sortBy ? sortBy[1] : undefined;
1116
- 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]);
1117
1165
 
1118
1166
  useEffect(() => {
1119
1167
  if (dataSource.countEntities)
1120
1168
  dataSource.countEntities({
1121
1169
  path: resolvedPath,
1170
+ pathSegments: resolvedPathSegments,
1122
1171
  collection,
1123
1172
  filter,
1124
1173
  orderBy: sortByProperty,
1125
1174
  order: currentSort
1126
1175
  }).then(setCount).catch(setError);
1127
- }, [fullPath, dataSource.countEntities, resolvedPath, collection, filter, sortByProperty, currentSort]);
1176
+ }, [fullPath, dataSource.countEntities, resolvedPath, resolvedPathSegments, collection, filter, sortByProperty, currentSort]);
1128
1177
 
1129
1178
  useEffect(() => {
1130
1179
  if (onCountChange && count !== undefined) {
@@ -1153,10 +1202,13 @@ function buildPropertyWidthOverwrite(key: string, width: number): PartialEntityC
1153
1202
  function EntityIdHeaderWidget({
1154
1203
  collection,
1155
1204
  path,
1205
+ pathSegments,
1156
1206
  fullIdPath
1157
1207
  }: {
1158
1208
  collection: EntityCollection,
1159
1209
  path: string,
1210
+ /** `path` split at its real segment boundaries, when the caller knows them. */
1211
+ pathSegments?: string[],
1160
1212
  fullIdPath: string
1161
1213
  }) {
1162
1214
 
@@ -1196,6 +1248,7 @@ function EntityIdHeaderWidget({
1196
1248
  collection,
1197
1249
  entityId,
1198
1250
  path,
1251
+ pathSegments,
1199
1252
  fullIdPath,
1200
1253
  sideEntityController,
1201
1254
  navigation
@@ -1221,7 +1274,7 @@ function EntityIdHeaderWidget({
1221
1274
  </form>
1222
1275
  {recentIds && recentIds.length > 0 && <div className="flex flex-col gap-2 p-2">
1223
1276
  {recentIds.map(id => (
1224
- <ReferencePreview reference={new EntityReference(id, path)}
1277
+ <ReferencePreview reference={new EntityReference(id, path, undefined, pathSegments)}
1225
1278
  key={id}
1226
1279
  hover={true}
1227
1280
  onClick={() => {
@@ -1231,6 +1284,7 @@ function EntityIdHeaderWidget({
1231
1284
  collection,
1232
1285
  entityId: id,
1233
1286
  path,
1287
+ pathSegments,
1234
1288
  fullIdPath,
1235
1289
  sideEntityController,
1236
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
  );
@@ -45,6 +45,8 @@ export interface BoardDataController<M extends Record<string, any> = any, COLUMN
45
45
  export interface UseBoardDataControllerProps<M extends Record<string, any> = any> {
46
46
  /** Full path to the collection */
47
47
  fullPath: string;
48
+ /** `fullPath` split at its real segment boundaries; forwarded to the datasource. */
49
+ pathSegments?: string[];
48
50
  /** The entity collection configuration */
49
51
  collection: EntityCollection<M>;
50
52
  /** Property key used for column assignment */
@@ -67,6 +69,7 @@ export interface UseBoardDataControllerProps<M extends Record<string, any> = any
67
69
  */
68
70
  export function useBoardDataController<M extends Record<string, any> = any, COLUMN extends string = string>({
69
71
  fullPath,
72
+ pathSegments,
70
73
  collection,
71
74
  columnProperty,
72
75
  columns,
@@ -79,7 +82,12 @@ export function useBoardDataController<M extends Record<string, any> = any, COLU
79
82
  const context = useFireCMSContext();
80
83
  const dataSource = useDataSource(collection);
81
84
  const navigation = useNavigationController();
82
- const resolvedPath = useMemo(() => navigation.resolveIdsFrom(fullPath), [fullPath, navigation.resolveIdsFrom]);
85
+ // Resolved together so the two representations always describe the same chain.
86
+ // Segments are resolved, never derived (see useEntityFetch).
87
+ const resolvedPath = useMemo(() => navigation.resolveIdsFrom(fullPath, pathSegments), [fullPath, pathSegments, navigation.resolveIdsFrom]);
88
+ const resolvedPathSegments = useMemo(() => pathSegments
89
+ ? (navigation.resolveSegmentsFrom?.(pathSegments) ?? pathSegments)
90
+ : undefined, [pathSegments, navigation.resolveSegmentsFrom]);
83
91
 
84
92
  // Stable refs for objects that shouldn't trigger re-subscriptions
85
93
  const dataSourceRef = useRef(dataSource);
@@ -95,11 +103,13 @@ export function useBoardDataController<M extends Record<string, any> = any, COLU
95
103
  const orderPropertyRef = useRef(orderProperty);
96
104
  const searchStringRef = useRef(searchString);
97
105
  const resolvedPathRef = useRef(resolvedPath);
106
+ const resolvedPathSegmentsRef = useRef(resolvedPathSegments);
98
107
  filterValuesRef.current = filterValues;
99
108
  columnPropertyRef.current = columnProperty;
100
109
  orderPropertyRef.current = orderProperty;
101
110
  searchStringRef.current = searchString;
102
111
  resolvedPathRef.current = resolvedPath;
112
+ resolvedPathSegmentsRef.current = resolvedPathSegments;
103
113
 
104
114
  // Track item count per column for pagination
105
115
  const [columnItemCounts, setColumnItemCounts] = useState<Record<string, number>>(() => {
@@ -168,6 +178,7 @@ export function useBoardDataController<M extends Record<string, any> = any, COLU
168
178
  const currentOrderProperty = orderPropertyRef.current;
169
179
  const currentSearchString = searchStringRef.current;
170
180
  const currentResolvedPath = resolvedPathRef.current;
181
+ const currentPathSegments = resolvedPathSegmentsRef.current;
171
182
 
172
183
  // Build filter for this column
173
184
  const columnFilter: FilterValues<string> = {
@@ -205,6 +216,7 @@ export function useBoardDataController<M extends Record<string, any> = any, COLU
205
216
  currentCollection.callbacks!.onFetch!({
206
217
  collection: currentCollection,
207
218
  path: currentResolvedPath,
219
+ pathSegments: currentPathSegments,
208
220
  entity,
209
221
  context: currentContext
210
222
  })
@@ -248,6 +260,7 @@ export function useBoardDataController<M extends Record<string, any> = any, COLU
248
260
  if (currentDataSource.listenCollection) {
249
261
  const unsubscribe = currentDataSource.listenCollection<M>({
250
262
  path: currentResolvedPath,
263
+ pathSegments: currentPathSegments,
251
264
  collection: currentCollection,
252
265
  onUpdate,
253
266
  onError,
@@ -262,6 +275,7 @@ export function useBoardDataController<M extends Record<string, any> = any, COLU
262
275
  } else {
263
276
  currentDataSource.fetchCollection<M>({
264
277
  path: currentResolvedPath,
278
+ pathSegments: currentPathSegments,
265
279
  collection: currentCollection,
266
280
  searchString: currentSearchString,
267
281
  filter: columnFilter,
@@ -297,6 +311,7 @@ export function useBoardDataController<M extends Record<string, any> = any, COLU
297
311
  const currentColumnProperty = columnPropertyRef.current;
298
312
  const currentSearchString = searchStringRef.current;
299
313
  const currentResolvedPath = resolvedPathRef.current;
314
+ const currentPathSegments = resolvedPathSegmentsRef.current;
300
315
  const currentColumns = columns;
301
316
  const currentColumnItemCounts = columnItemCounts;
302
317
 
@@ -316,6 +331,7 @@ export function useBoardDataController<M extends Record<string, any> = any, COLU
316
331
  };
317
332
  currentDataSource.countEntities({
318
333
  path: currentResolvedPath,
334
+ pathSegments: currentPathSegments,
319
335
  collection: currentCollection,
320
336
  filter: columnFilter,
321
337
  searchString: currentSearchString