@firecms/core 3.4.0-canary.63041d0 → 3.4.0-canary.cfeed0b

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 (82) hide show
  1. package/dist/components/EntityCollectionTable/internal/popup_field/PopupFormField.d.ts +3 -1
  2. package/dist/components/EntityCollectionView/EntityCollectionBoardView.d.ts +3 -1
  3. package/dist/components/EntityCollectionView/EntityCollectionView.d.ts +3 -2
  4. package/dist/components/EntityCollectionView/EntityCollectionViewActions.d.ts +3 -1
  5. package/dist/components/EntityCollectionView/EntityCollectionViewStartActions.d.ts +3 -1
  6. package/dist/components/EntityCollectionView/useBoardDataController.d.ts +3 -1
  7. package/dist/components/ReferenceTable/ReferenceSelectionTable.d.ts +10 -1
  8. package/dist/components/common/useDataSourceTableController.d.ts +4 -2
  9. package/dist/components/common/useTableSearchHelper.d.ts +3 -1
  10. package/dist/core/EntityEditView.d.ts +2 -0
  11. package/dist/core/EntityEditViewFormActions.d.ts +1 -1
  12. package/dist/form/EntityForm.d.ts +8 -1
  13. package/dist/form/EntityFormActions.d.ts +2 -0
  14. package/dist/hooks/data/delete.d.ts +1 -1
  15. package/dist/hooks/data/useCollectionFetch.d.ts +7 -1
  16. package/dist/hooks/data/useEntityFetch.d.ts +4 -1
  17. package/dist/i18n/__tests__/FireCMSi18nProvider.test.d.ts +1 -0
  18. package/dist/i18n/__tests__/nested_provider_lifecycle.test.d.ts +1 -0
  19. package/dist/index.es.js +967 -649
  20. package/dist/index.es.js.map +1 -1
  21. package/dist/index.umd.js +966 -648
  22. package/dist/index.umd.js.map +1 -1
  23. package/dist/types/collections.d.ts +7 -0
  24. package/dist/types/datasource.d.ts +25 -4
  25. package/dist/types/entities.d.ts +21 -1
  26. package/dist/types/entity_actions.d.ts +6 -0
  27. package/dist/types/entity_callbacks.d.ts +21 -0
  28. package/dist/types/navigation.d.ts +34 -5
  29. package/dist/types/plugins.d.ts +13 -0
  30. package/dist/types/side_entity_controller.d.ts +3 -2
  31. package/dist/util/entity_cache.d.ts +11 -0
  32. package/dist/util/navigation_utils.d.ts +84 -2
  33. package/dist/util/parent_references_from_path.d.ts +16 -0
  34. package/dist/util/paths.d.ts +28 -0
  35. package/dist/util/permissions.d.ts +10 -4
  36. package/package.json +4 -4
  37. package/src/components/DeleteEntityDialog.tsx +2 -0
  38. package/src/components/EntityCollectionTable/EntityCollectionRowActions.tsx +2 -2
  39. package/src/components/EntityCollectionTable/internal/popup_field/PopupFormField.tsx +5 -1
  40. package/src/components/EntityCollectionView/EntityCollectionBoardView.tsx +12 -0
  41. package/src/components/EntityCollectionView/EntityCollectionView.tsx +49 -16
  42. package/src/components/EntityCollectionView/EntityCollectionViewActions.tsx +6 -2
  43. package/src/components/EntityCollectionView/EntityCollectionViewStartActions.tsx +4 -0
  44. package/src/components/EntityCollectionView/useBoardDataController.tsx +17 -1
  45. package/src/components/EntityPreview.tsx +4 -1
  46. package/src/components/ReferenceTable/ReferenceSelectionTable.tsx +19 -2
  47. package/src/components/common/default_entity_actions.tsx +22 -2
  48. package/src/components/common/useDataSourceTableController.tsx +17 -5
  49. package/src/components/common/useTableSearchHelper.ts +5 -0
  50. package/src/core/EntityEditView.tsx +11 -12
  51. package/src/core/EntityEditViewFormActions.tsx +3 -2
  52. package/src/core/EntitySidePanel.tsx +6 -4
  53. package/src/form/EntityForm.tsx +30 -9
  54. package/src/form/EntityFormActions.tsx +2 -0
  55. package/src/hooks/data/delete.ts +8 -0
  56. package/src/hooks/data/save.ts +5 -2
  57. package/src/hooks/data/useCollectionFetch.tsx +21 -1
  58. package/src/hooks/data/useEntityFetch.tsx +28 -6
  59. package/src/hooks/useBuildNavigationController.tsx +23 -8
  60. package/src/hooks/useResolvedNavigationFrom.tsx +3 -1
  61. package/src/i18n/FireCMSi18nProvider.tsx +48 -4
  62. package/src/i18n/__tests__/FireCMSi18nProvider.test.tsx +91 -0
  63. package/src/i18n/__tests__/nested_provider_lifecycle.test.tsx +67 -0
  64. package/src/internal/useBuildDataSource.ts +26 -15
  65. package/src/internal/useBuildSideEntityController.tsx +13 -5
  66. package/src/preview/components/ReferencePreview.tsx +7 -3
  67. package/src/routes/FireCMSRoute.tsx +9 -4
  68. package/src/types/collections.ts +8 -0
  69. package/src/types/datasource.ts +25 -4
  70. package/src/types/entities.ts +24 -1
  71. package/src/types/entity_actions.tsx +6 -0
  72. package/src/types/entity_callbacks.ts +24 -0
  73. package/src/types/navigation.ts +35 -5
  74. package/src/types/plugins.tsx +13 -0
  75. package/src/types/side_entity_controller.tsx +3 -2
  76. package/src/util/entities.ts +3 -1
  77. package/src/util/entity_cache.ts +18 -0
  78. package/src/util/navigation_from_path.ts +6 -1
  79. package/src/util/navigation_utils.ts +204 -2
  80. package/src/util/parent_references_from_path.ts +42 -3
  81. package/src/util/paths.ts +40 -3
  82. package/src/util/permissions.ts +22 -10
@@ -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
 
@@ -109,8 +110,9 @@ export type EntityCollectionViewProps<M extends Record<string, any>> = {
109
110
  /**
110
111
  * `fullPath` split at its real segment boundaries, e.g. `["nodes", "node/42", "edges"]`.
111
112
  * Forwarded to the datasource so a parent entity id containing "/" stays unambiguous.
112
- * Defaults to splitting `fullPath`, which is correct for any backend whose ids cannot
113
- * contain "/".
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".
114
116
  */
115
117
  pathSegments?: string[];
116
118
  /**
@@ -198,7 +200,7 @@ export const EntityCollectionView = React.memo(
198
200
  collectionRef.current = collection;
199
201
  }, [collection]);
200
202
 
201
- const canCreateEntities = canCreateEntity(collection, authController, fullPath, null);
203
+ const canCreateEntities = canCreateEntity(collection, authController, fullPath, null, pathSegments);
202
204
  const [highlightedEntity, setHighlightedEntity] = useState<Entity<M> | undefined>(undefined);
203
205
  const [deleteEntityClicked, setDeleteEntityClicked] = React.useState<Entity<M> | Entity<M>[] | undefined>(undefined);
204
206
 
@@ -228,7 +230,7 @@ export const EntityCollectionView = React.memo(
228
230
 
229
231
  const checkInlineEditing = useCallback((entity?: Entity<any>): boolean => {
230
232
  const collection = collectionRef.current;
231
- if (!canEditEntity(collection, authController, fullPath, entity ?? null)) {
233
+ if (!canEditEntity(collection, authController, fullPath, entity ?? null, pathSegments)) {
232
234
  return false;
233
235
  }
234
236
  return collection.inlineEditing === undefined || collection.inlineEditing;
@@ -351,10 +353,16 @@ export const EntityCollectionView = React.memo(
351
353
  addRecentId(collection.id, clickedEntity.id);
352
354
  }
353
355
 
354
- 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);
355
362
  navigateToEntity({
356
363
  navigation,
357
364
  path,
365
+ pathSegments: isCollectionGroup ? clickedEntity.pathSegments : (pathSegments ?? clickedEntity.pathSegments),
358
366
  fullIdPath,
359
367
  sideEntityController,
360
368
  openEntityMode,
@@ -461,7 +469,7 @@ export const EntityCollectionView = React.memo(
461
469
  }
462
470
  }, [setViewMode, userConfigPersistence, onCollectionModifiedForUser, fullPath, analyticsController, viewMode]);
463
471
 
464
- const createEnabled = canCreateEntity(collection, authController, fullPath, null);
472
+ const createEnabled = canCreateEntity(collection, authController, fullPath, null, pathSegments);
465
473
 
466
474
  const uniqueFieldValidator: UniqueFieldValidator = useCallback(
467
475
  ({
@@ -469,8 +477,8 @@ export const EntityCollectionView = React.memo(
469
477
  value,
470
478
  property,
471
479
  entityId
472
- }) => dataSource.checkUniqueField(fullPath, name, value, entityId, collection),
473
- [fullPath]);
480
+ }) => dataSource.checkUniqueField(fullPath, name, value, entityId, collection, pathSegments),
481
+ [fullPath, pathSegments]);
474
482
 
475
483
  const onValueChange: OnCellValueChange<any, any> = ({
476
484
  value,
@@ -484,6 +492,8 @@ export const EntityCollectionView = React.memo(
484
492
 
485
493
  const saveProps: SaveEntityProps = {
486
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,
487
497
  entityId: entity.id,
488
498
  values: updatedValues,
489
499
  previousValues: entity.values,
@@ -514,7 +524,7 @@ export const EntityCollectionView = React.memo(
514
524
 
515
525
  };
516
526
 
517
- const resolvedFullPath = navigation.resolveIdsFrom(fullPath);
527
+ const resolvedFullPath = navigation.resolveIdsFrom(fullPath, pathSegments);
518
528
  const resolvedCollection = useMemo(() => resolveCollection<M>({
519
529
  collection,
520
530
  path: fullPath,
@@ -676,13 +686,15 @@ export const EntityCollectionView = React.memo(
676
686
  width: 260,
677
687
  dependencies: [],
678
688
  Builder: ({ entity }) => {
679
- 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);
680
692
  return (
681
693
  <div className={"flex flex-col gap-2 w-full"}>
682
694
  {collectionsWithPath.map((reference) => {
683
695
  return (
684
696
  <ReferencePreview
685
- key={reference.path + "/" + reference.id}
697
+ key={entityCacheKey(reference.path, reference.id)}
686
698
  reference={reference}
687
699
  size={"small"} />
688
700
  );
@@ -713,7 +725,7 @@ export const EntityCollectionView = React.memo(
713
725
  entity?: Entity<M>,
714
726
  customEntityActions?: EntityAction[]
715
727
  }): EntityAction[] => {
716
- const deleteEnabled = entity ? canDeleteEntity(collection, authController, fullPath, entity) : true;
728
+ const deleteEnabled = entity ? canDeleteEntity(collection, authController, fullPath, entity, entity.pathSegments ?? pathSegments) : true;
717
729
  const actions: EntityAction[] = [
718
730
  { ...editEntityAction, name: t("edit") }
719
731
  ];
@@ -789,6 +801,7 @@ export const EntityCollectionView = React.memo(
789
801
  // EntitiesCount fetches count and updates breadcrumb - no visual rendering needed here
790
802
  const countFetcher = <EntitiesCount
791
803
  fullPath={fullPath}
804
+ pathSegments={pathSegments}
792
805
  collection={collection}
793
806
  filter={tableController.filterValues}
794
807
  sortBy={tableController.sortBy}
@@ -842,6 +855,7 @@ export const EntityCollectionView = React.memo(
842
855
  onTextSearchClick,
843
856
  textSearchEnabled
844
857
  } = useTableSearchHelper({
858
+ pathSegments,
845
859
  collection,
846
860
  fullPath: resolvedFullPath,
847
861
  parentCollectionIds,
@@ -906,6 +920,7 @@ export const EntityCollectionView = React.memo(
906
920
  collection={collection}
907
921
  tableController={tableController}
908
922
  path={fullPath}
923
+ pathSegments={pathSegments}
909
924
  relativePath={collection.path}
910
925
  selectionController={usedSelectionController}
911
926
  collectionEntitiesCount={docsCount}
@@ -917,6 +932,7 @@ export const EntityCollectionView = React.memo(
917
932
  onMultipleDeleteClick={onMultipleDeleteClick}
918
933
  onNewClick={onNewClick}
919
934
  path={fullPath}
935
+ pathSegments={pathSegments}
920
936
  relativePath={collection.path}
921
937
  selectionController={usedSelectionController}
922
938
  selectionEnabled={selectionEnabled}
@@ -933,6 +949,7 @@ export const EntityCollectionView = React.memo(
933
949
  collection={collection}
934
950
  tableController={tableController}
935
951
  fullPath={fullPath}
952
+ pathSegments={pathSegments}
936
953
  parentCollectionIds={parentCollectionIds}
937
954
  columnProperty={selectedKanbanProperty}
938
955
  onEntityClick={onEntityClick}
@@ -1024,6 +1041,7 @@ export const EntityCollectionView = React.memo(
1024
1041
  getIdColumnWidth={getIdColumnWidth}
1025
1042
  additionalIDHeaderWidget={<EntityIdHeaderWidget
1026
1043
  path={fullPath}
1044
+ pathSegments={pathSegments}
1027
1045
  fullIdPath={fullIdPath ?? fullPath}
1028
1046
  collection={collection} />}
1029
1047
  openEntityMode={openEntityMode}
@@ -1063,6 +1081,7 @@ export const EntityCollectionView = React.memo(
1063
1081
 
1064
1082
  {popupCell && <PopupFormField
1065
1083
  key={`popup_form_${popupCell?.propertyKey}_${popupCell?.entityId}`}
1084
+ pathSegments={pathSegments}
1066
1085
  open={Boolean(popupCell)}
1067
1086
  onClose={onPopupClose}
1068
1087
  cellRect={popupCell?.cellRect}
@@ -1118,13 +1137,16 @@ function EntitiesCount({
1118
1137
  collection,
1119
1138
  filter,
1120
1139
  sortBy,
1121
- onCountChange
1140
+ onCountChange,
1141
+ pathSegments
1122
1142
  }: {
1123
1143
  fullPath: string,
1124
1144
  collection: EntityCollection,
1125
1145
  filter?: FilterValues<any>,
1126
1146
  sortBy?: [string, "asc" | "desc"],
1127
1147
  onCountChange?: (count: number) => void,
1148
+ /** `fullPath` split at its real segment boundaries; forwarded to countEntities. */
1149
+ pathSegments?: string[],
1128
1150
  }) {
1129
1151
 
1130
1152
  const dataSource = useDataSource(collection);
@@ -1134,18 +1156,24 @@ function EntitiesCount({
1134
1156
 
1135
1157
  const sortByProperty = sortBy ? sortBy[0] : undefined;
1136
1158
  const currentSort = sortBy ? sortBy[1] : undefined;
1137
- 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]);
1138
1165
 
1139
1166
  useEffect(() => {
1140
1167
  if (dataSource.countEntities)
1141
1168
  dataSource.countEntities({
1142
1169
  path: resolvedPath,
1170
+ pathSegments: resolvedPathSegments,
1143
1171
  collection,
1144
1172
  filter,
1145
1173
  orderBy: sortByProperty,
1146
1174
  order: currentSort
1147
1175
  }).then(setCount).catch(setError);
1148
- }, [fullPath, dataSource.countEntities, resolvedPath, collection, filter, sortByProperty, currentSort]);
1176
+ }, [fullPath, dataSource.countEntities, resolvedPath, resolvedPathSegments, collection, filter, sortByProperty, currentSort]);
1149
1177
 
1150
1178
  useEffect(() => {
1151
1179
  if (onCountChange && count !== undefined) {
@@ -1174,10 +1202,13 @@ function buildPropertyWidthOverwrite(key: string, width: number): PartialEntityC
1174
1202
  function EntityIdHeaderWidget({
1175
1203
  collection,
1176
1204
  path,
1205
+ pathSegments,
1177
1206
  fullIdPath
1178
1207
  }: {
1179
1208
  collection: EntityCollection,
1180
1209
  path: string,
1210
+ /** `path` split at its real segment boundaries, when the caller knows them. */
1211
+ pathSegments?: string[],
1181
1212
  fullIdPath: string
1182
1213
  }) {
1183
1214
 
@@ -1217,6 +1248,7 @@ function EntityIdHeaderWidget({
1217
1248
  collection,
1218
1249
  entityId,
1219
1250
  path,
1251
+ pathSegments,
1220
1252
  fullIdPath,
1221
1253
  sideEntityController,
1222
1254
  navigation
@@ -1242,7 +1274,7 @@ function EntityIdHeaderWidget({
1242
1274
  </form>
1243
1275
  {recentIds && recentIds.length > 0 && <div className="flex flex-col gap-2 p-2">
1244
1276
  {recentIds.map(id => (
1245
- <ReferencePreview reference={new EntityReference(id, path)}
1277
+ <ReferencePreview reference={new EntityReference(id, path, undefined, pathSegments)}
1246
1278
  key={id}
1247
1279
  hover={true}
1248
1280
  onClick={() => {
@@ -1252,6 +1284,7 @@ function EntityIdHeaderWidget({
1252
1284
  collection,
1253
1285
  entityId: id,
1254
1286
  path,
1287
+ pathSegments,
1255
1288
  fullIdPath,
1256
1289
  sideEntityController,
1257
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,
@@ -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
@@ -63,7 +63,7 @@ export function EntityPreview({
63
63
 
64
64
  const navigationController = useNavigationController();
65
65
 
66
- const collection = collectionProp ?? navigationController.getCollection(entity.path);
66
+ const collection = collectionProp ?? navigationController.getCollection(entity.path, false, entity.pathSegments);
67
67
 
68
68
  if (!collection) {
69
69
  throw Error(`Couldn't find the corresponding collection view for the path: ${entity.path}`);
@@ -177,6 +177,9 @@ export function EntityPreview({
177
177
  sideEntityController.open({
178
178
  entityId: entity.id,
179
179
  path: entity.path,
180
+ // Paired with `entity.path` above; undefined if the entity was
181
+ // loaded without them.
182
+ pathSegments: entity.pathSegments,
180
183
  collection,
181
184
  updateUrl: true
182
185
  });
@@ -44,6 +44,15 @@ export interface ReferenceSelectionInnerProps<M extends Record<string, any>> {
44
44
  * dynamic. If not set, the dialog won't open.
45
45
  */
46
46
  path: string;
47
+ /**
48
+ * `path` split at its real segment boundaries, when the caller knows them.
49
+ *
50
+ * Left undefined by default and deliberately NOT derived from `path`: a reference
51
+ * path comes from a property's configuration, so there is no entity chain to recover
52
+ * the boundaries from. Splitting it would silently produce the wrong segments for a
53
+ * subcollection under a slash-bearing parent id, which is worse than not answering.
54
+ */
55
+ pathSegments?: string[];
47
56
 
48
57
  /**
49
58
  * If you are opening the dialog for the first time, you can select some
@@ -96,6 +105,7 @@ export function ReferenceSelectionTable<M extends Record<string, any>>(
96
105
  multiselect,
97
106
  collection,
98
107
  path: pathInput,
108
+ pathSegments,
99
109
  selectedEntityIds: selectedEntityIdsProp,
100
110
  description,
101
111
  forceFilter,
@@ -110,7 +120,7 @@ export function ReferenceSelectionTable<M extends Record<string, any>>(
110
120
  const analyticsController = useAnalyticsController();
111
121
  const customizationController = useCustomizationController();
112
122
 
113
- const fullPath = navigation.resolveIdsFrom(pathInput);
123
+ const fullPath = navigation.resolveIdsFrom(pathInput, pathSegments);
114
124
 
115
125
  const dataSource = useDataSource(collection);
116
126
 
@@ -153,6 +163,7 @@ export function ReferenceSelectionTable<M extends Record<string, any>>(
153
163
  selectedEntityIds.map((entityId) =>
154
164
  dataSource.fetchEntity({
155
165
  path: fullPath,
166
+ pathSegments,
156
167
  entityId,
157
168
  collection
158
169
  })))
@@ -204,6 +215,7 @@ export function ReferenceSelectionTable<M extends Record<string, any>>(
204
215
  });
205
216
  sideEntityController.open({
206
217
  path: fullPath,
218
+ pathSegments,
207
219
  collection,
208
220
  updateUrl: true,
209
221
  onUpdate: ({ entity }) => {
@@ -277,6 +289,7 @@ export function ReferenceSelectionTable<M extends Record<string, any>>(
277
289
  textSearchEnabled
278
290
  } =
279
291
  useTableSearchHelper({
292
+ pathSegments,
280
293
  collection,
281
294
  fullPath,
282
295
  });
@@ -315,6 +328,7 @@ export function ReferenceSelectionTable<M extends Record<string, any>>(
315
328
  actions={<ReferenceDialogActions
316
329
  collection={collection}
317
330
  path={fullPath}
331
+ pathSegments={pathSegments}
318
332
  onNewClick={onNewClick}
319
333
  onClear={onClear}/>
320
334
  }
@@ -341,11 +355,14 @@ export function ReferenceSelectionTable<M extends Record<string, any>>(
341
355
  function ReferenceDialogActions({
342
356
  collection,
343
357
  path,
358
+ pathSegments,
344
359
  onClear,
345
360
  onNewClick
346
361
  }: {
347
362
  collection: EntityCollection<any>,
348
363
  path: string,
364
+ /** `path` split at its real segment boundaries, when known. Never derived from `path`. */
365
+ pathSegments?: string[],
349
366
  onClear: () => void,
350
367
  onNewClick: () => void
351
368
  }) {
@@ -360,7 +377,7 @@ function ReferenceDialogActions({
360
377
  onNewClick();
361
378
  }
362
379
  : undefined;
363
- const addButton = canCreateEntity(collection, authController, path, null) &&
380
+ const addButton = canCreateEntity(collection, authController, path, null, pathSegments) &&
364
381
  onClick && (largeLayout
365
382
  ? <Button
366
383
  onClick={onClick}
@@ -18,6 +18,7 @@ export const editEntityAction: EntityAction = {
18
18
  entity,
19
19
  collection,
20
20
  fullPath,
21
+ pathSegments,
21
22
  fullIdPath,
22
23
  context,
23
24
  highlightEntity,
@@ -41,6 +42,12 @@ export const editEntityAction: EntityAction = {
41
42
  }
42
43
 
43
44
  const path = collection?.collectionGroup ? entity.path : (fullPath ?? collection?.path ?? entity.path);
45
+ // In a collection group `path` is the entity's own, so the entity's segments describe
46
+ // it. Otherwise the caller's segments win, falling back to the ones the entity was
47
+ // loaded with — both describe the same collection. Never derived from `path`.
48
+ const resolvedPathSegments = collection?.collectionGroup
49
+ ? entity.pathSegments
50
+ : (pathSegments ?? entity.pathSegments);
44
51
  const newFullIdPath = collection?.collectionGroup ? collection.id : (fullIdPath ?? collection?.id ?? entity.path);
45
52
  const defaultSelectedView = resolveDefaultSelectedView(
46
53
  collection ? collection.defaultSelectedView : undefined,
@@ -54,6 +61,7 @@ export const editEntityAction: EntityAction = {
54
61
  collection,
55
62
  entityId: entity.id,
56
63
  path,
64
+ pathSegments: resolvedPathSegments,
57
65
  fullIdPath: newFullIdPath,
58
66
  sideEntityController: context.sideEntityController,
59
67
  onClose: () => unhighlightEntity?.(entity),
@@ -75,6 +83,8 @@ export const copyEntityAction: EntityAction = {
75
83
  collection,
76
84
  context,
77
85
  fullPath,
86
+ pathSegments,
87
+ fullIdPath,
78
88
  highlightEntity,
79
89
  unhighlightEntity,
80
90
  openEntityMode
@@ -89,13 +99,23 @@ export const copyEntityAction: EntityAction = {
89
99
  });
90
100
 
91
101
  const path = collection?.collectionGroup ? collection.path : (fullPath ?? collection?.path ?? entity.path);
92
- const fullIdPath = collection?.collectionGroup ? collection.id : (fullPath ?? collection?.id ?? entity.path);
102
+ // In a collection group `path` is the group's own root, which the entity's segments
103
+ // (its real nested location) do not describe — so they are deliberately not reused,
104
+ // and no substitute is invented.
105
+ const resolvedPathSegments = collection?.collectionGroup
106
+ ? undefined
107
+ : (pathSegments ?? entity.pathSegments);
108
+ // `fullIdPath` is the ESCAPED chain and is what becomes the URL; `fullPath` is the raw
109
+ // datasource path, whose parent ids may contain "/". Using the latter here wrote a URL
110
+ // that read back as a different entity. Mirrors editEntityAction.
111
+ const newFullIdPath = collection?.collectionGroup ? collection.id : (fullIdPath ?? collection?.id);
93
112
  navigateToEntity({
94
113
  openEntityMode,
95
114
  collection,
96
115
  entityId: entity.id,
97
116
  path,
98
- fullIdPath,
117
+ pathSegments: resolvedPathSegments,
118
+ fullIdPath: newFullIdPath,
99
119
  copy: true,
100
120
  sideEntityController: context.sideEntityController,
101
121
  onClose: () => unhighlightEntity?.(entity),