@firecms/core 3.4.0-canary.d20cc85 → 3.4.0-canary.e2466ba

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 (64) hide show
  1. package/dist/components/EntityCollectionView/EntityCollectionView.d.ts +3 -2
  2. package/dist/components/EntityCollectionView/EntityCollectionViewActions.d.ts +3 -1
  3. package/dist/components/EntityCollectionView/EntityCollectionViewStartActions.d.ts +3 -1
  4. package/dist/components/common/useDataSourceTableController.d.ts +4 -2
  5. package/dist/components/common/useTableSearchHelper.d.ts +3 -1
  6. package/dist/core/EntityEditView.d.ts +2 -0
  7. package/dist/core/EntityEditViewFormActions.d.ts +1 -1
  8. package/dist/form/EntityForm.d.ts +2 -0
  9. package/dist/form/EntityFormActions.d.ts +2 -0
  10. package/dist/hooks/data/delete.d.ts +1 -1
  11. package/dist/hooks/data/useCollectionFetch.d.ts +1 -1
  12. package/dist/hooks/data/useEntityFetch.d.ts +4 -1
  13. package/dist/index.es.js +663 -451
  14. package/dist/index.es.js.map +1 -1
  15. package/dist/index.umd.js +663 -451
  16. package/dist/index.umd.js.map +1 -1
  17. package/dist/types/collections.d.ts +7 -0
  18. package/dist/types/datasource.d.ts +8 -0
  19. package/dist/types/entity_actions.d.ts +6 -0
  20. package/dist/types/entity_callbacks.d.ts +21 -0
  21. package/dist/types/navigation.d.ts +34 -5
  22. package/dist/types/plugins.d.ts +13 -0
  23. package/dist/types/side_entity_controller.d.ts +3 -2
  24. package/dist/util/navigation_utils.d.ts +84 -2
  25. package/dist/util/parent_references_from_path.d.ts +15 -0
  26. package/dist/util/paths.d.ts +28 -0
  27. package/dist/util/permissions.d.ts +10 -4
  28. package/package.json +3 -3
  29. package/src/components/DeleteEntityDialog.tsx +2 -0
  30. package/src/components/EntityCollectionView/EntityCollectionBoardView.tsx +8 -0
  31. package/src/components/EntityCollectionView/EntityCollectionView.tsx +38 -14
  32. package/src/components/EntityCollectionView/EntityCollectionViewActions.tsx +6 -2
  33. package/src/components/EntityCollectionView/EntityCollectionViewStartActions.tsx +4 -0
  34. package/src/components/EntityCollectionView/useBoardDataController.tsx +7 -2
  35. package/src/components/EntityPreview.tsx +4 -1
  36. package/src/components/ReferenceTable/ReferenceSelectionTable.tsx +8 -2
  37. package/src/components/common/default_entity_actions.tsx +16 -0
  38. package/src/components/common/useDataSourceTableController.tsx +11 -5
  39. package/src/components/common/useTableSearchHelper.ts +5 -0
  40. package/src/core/EntityEditView.tsx +6 -10
  41. package/src/core/EntityEditViewFormActions.tsx +3 -2
  42. package/src/core/EntitySidePanel.tsx +4 -2
  43. package/src/form/EntityForm.tsx +16 -2
  44. package/src/form/EntityFormActions.tsx +2 -0
  45. package/src/hooks/data/delete.ts +8 -2
  46. package/src/hooks/data/save.ts +4 -1
  47. package/src/hooks/data/useCollectionFetch.tsx +9 -2
  48. package/src/hooks/data/useEntityFetch.tsx +15 -3
  49. package/src/hooks/useBuildNavigationController.tsx +23 -8
  50. package/src/hooks/useResolvedNavigationFrom.tsx +1 -1
  51. package/src/internal/useBuildDataSource.ts +13 -12
  52. package/src/preview/components/ReferencePreview.tsx +4 -1
  53. package/src/routes/FireCMSRoute.tsx +9 -4
  54. package/src/types/collections.ts +8 -0
  55. package/src/types/datasource.ts +8 -0
  56. package/src/types/entity_actions.tsx +6 -0
  57. package/src/types/entity_callbacks.ts +24 -0
  58. package/src/types/navigation.ts +35 -5
  59. package/src/types/plugins.tsx +13 -0
  60. package/src/types/side_entity_controller.tsx +3 -2
  61. package/src/util/navigation_utils.ts +197 -2
  62. package/src/util/parent_references_from_path.ts +32 -1
  63. package/src/util/paths.ts +40 -3
  64. package/src/util/permissions.ts +22 -10
@@ -109,8 +109,9 @@ export type EntityCollectionViewProps<M extends Record<string, any>> = {
109
109
  /**
110
110
  * `fullPath` split at its real segment boundaries, e.g. `["nodes", "node/42", "edges"]`.
111
111
  * 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 "/".
112
+ *
113
+ * Optional, and never derived by splitting `fullPath` — a guess would be wrong in
114
+ * exactly the case the field exists for. Absent means "not known here", not "no slashes".
114
115
  */
115
116
  pathSegments?: string[];
116
117
  /**
@@ -198,7 +199,7 @@ export const EntityCollectionView = React.memo(
198
199
  collectionRef.current = collection;
199
200
  }, [collection]);
200
201
 
201
- const canCreateEntities = canCreateEntity(collection, authController, fullPath, null);
202
+ const canCreateEntities = canCreateEntity(collection, authController, fullPath, null, pathSegments);
202
203
  const [highlightedEntity, setHighlightedEntity] = useState<Entity<M> | undefined>(undefined);
203
204
  const [deleteEntityClicked, setDeleteEntityClicked] = React.useState<Entity<M> | Entity<M>[] | undefined>(undefined);
204
205
 
@@ -228,7 +229,7 @@ export const EntityCollectionView = React.memo(
228
229
 
229
230
  const checkInlineEditing = useCallback((entity?: Entity<any>): boolean => {
230
231
  const collection = collectionRef.current;
231
- if (!canEditEntity(collection, authController, fullPath, entity ?? null)) {
232
+ if (!canEditEntity(collection, authController, fullPath, entity ?? null, pathSegments)) {
232
233
  return false;
233
234
  }
234
235
  return collection.inlineEditing === undefined || collection.inlineEditing;
@@ -351,10 +352,16 @@ export const EntityCollectionView = React.memo(
351
352
  addRecentId(collection.id, clickedEntity.id);
352
353
  }
353
354
 
354
- const path = collection?.collectionGroup ? clickedEntity.path : (fullPath ?? clickedEntity.path);
355
+ // In a collection group the row's own path is the real one, so the row's segments
356
+ // are the matching ones; otherwise both come from this view. `path` and
357
+ // `pathSegments` are picked from the same source so they cannot describe
358
+ // different chains.
359
+ const isCollectionGroup = Boolean(collection?.collectionGroup);
360
+ const path = isCollectionGroup ? clickedEntity.path : (fullPath ?? clickedEntity.path);
355
361
  navigateToEntity({
356
362
  navigation,
357
363
  path,
364
+ pathSegments: isCollectionGroup ? clickedEntity.pathSegments : (pathSegments ?? clickedEntity.pathSegments),
358
365
  fullIdPath,
359
366
  sideEntityController,
360
367
  openEntityMode,
@@ -461,7 +468,7 @@ export const EntityCollectionView = React.memo(
461
468
  }
462
469
  }, [setViewMode, userConfigPersistence, onCollectionModifiedForUser, fullPath, analyticsController, viewMode]);
463
470
 
464
- const createEnabled = canCreateEntity(collection, authController, fullPath, null);
471
+ const createEnabled = canCreateEntity(collection, authController, fullPath, null, pathSegments);
465
472
 
466
473
  const uniqueFieldValidator: UniqueFieldValidator = useCallback(
467
474
  ({
@@ -469,8 +476,8 @@ export const EntityCollectionView = React.memo(
469
476
  value,
470
477
  property,
471
478
  entityId
472
- }) => dataSource.checkUniqueField(fullPath, name, value, entityId, collection),
473
- [fullPath]);
479
+ }) => dataSource.checkUniqueField(fullPath, name, value, entityId, collection, pathSegments),
480
+ [fullPath, pathSegments]);
474
481
 
475
482
  const onValueChange: OnCellValueChange<any, any> = ({
476
483
  value,
@@ -484,6 +491,8 @@ export const EntityCollectionView = React.memo(
484
491
 
485
492
  const saveProps: SaveEntityProps = {
486
493
  path: entity.path ?? fullPath,
494
+ // Paired with the `path` above: the row's segments describe the row's path.
495
+ pathSegments: entity.path ? entity.pathSegments : pathSegments,
487
496
  entityId: entity.id,
488
497
  values: updatedValues,
489
498
  previousValues: entity.values,
@@ -514,7 +523,7 @@ export const EntityCollectionView = React.memo(
514
523
 
515
524
  };
516
525
 
517
- const resolvedFullPath = navigation.resolveIdsFrom(fullPath);
526
+ const resolvedFullPath = navigation.resolveIdsFrom(fullPath, pathSegments);
518
527
  const resolvedCollection = useMemo(() => resolveCollection<M>({
519
528
  collection,
520
529
  path: fullPath,
@@ -676,7 +685,9 @@ export const EntityCollectionView = React.memo(
676
685
  width: 260,
677
686
  dependencies: [],
678
687
  Builder: ({ entity }) => {
679
- const collectionsWithPath = navigation.getParentReferencesFromPath(entity.path);
688
+ // `entity.path` is a RAW datasource path, so its segments are passed:
689
+ // splitting it would mis-read any parent id containing "/".
690
+ const collectionsWithPath = navigation.getParentReferencesFromPath(entity.path, entity.pathSegments);
680
691
  return (
681
692
  <div className={"flex flex-col gap-2 w-full"}>
682
693
  {collectionsWithPath.map((reference) => {
@@ -713,7 +724,7 @@ export const EntityCollectionView = React.memo(
713
724
  entity?: Entity<M>,
714
725
  customEntityActions?: EntityAction[]
715
726
  }): EntityAction[] => {
716
- const deleteEnabled = entity ? canDeleteEntity(collection, authController, fullPath, entity) : true;
727
+ const deleteEnabled = entity ? canDeleteEntity(collection, authController, fullPath, entity, entity.pathSegments ?? pathSegments) : true;
717
728
  const actions: EntityAction[] = [
718
729
  { ...editEntityAction, name: t("edit") }
719
730
  ];
@@ -843,6 +854,7 @@ export const EntityCollectionView = React.memo(
843
854
  onTextSearchClick,
844
855
  textSearchEnabled
845
856
  } = useTableSearchHelper({
857
+ pathSegments,
846
858
  collection,
847
859
  fullPath: resolvedFullPath,
848
860
  parentCollectionIds,
@@ -907,6 +919,7 @@ export const EntityCollectionView = React.memo(
907
919
  collection={collection}
908
920
  tableController={tableController}
909
921
  path={fullPath}
922
+ pathSegments={pathSegments}
910
923
  relativePath={collection.path}
911
924
  selectionController={usedSelectionController}
912
925
  collectionEntitiesCount={docsCount}
@@ -918,6 +931,7 @@ export const EntityCollectionView = React.memo(
918
931
  onMultipleDeleteClick={onMultipleDeleteClick}
919
932
  onNewClick={onNewClick}
920
933
  path={fullPath}
934
+ pathSegments={pathSegments}
921
935
  relativePath={collection.path}
922
936
  selectionController={usedSelectionController}
923
937
  selectionEnabled={selectionEnabled}
@@ -1026,6 +1040,7 @@ export const EntityCollectionView = React.memo(
1026
1040
  getIdColumnWidth={getIdColumnWidth}
1027
1041
  additionalIDHeaderWidget={<EntityIdHeaderWidget
1028
1042
  path={fullPath}
1043
+ pathSegments={pathSegments}
1029
1044
  fullIdPath={fullIdPath ?? fullPath}
1030
1045
  collection={collection} />}
1031
1046
  openEntityMode={openEntityMode}
@@ -1140,8 +1155,12 @@ function EntitiesCount({
1140
1155
 
1141
1156
  const sortByProperty = sortBy ? sortBy[0] : undefined;
1142
1157
  const currentSort = sortBy ? sortBy[1] : undefined;
1143
- const resolvedPath = useMemo(() => navigation.resolveIdsFrom(fullPath), [fullPath, navigation.resolveIdsFrom]);
1144
- const resolvedPathSegments = pathSegments;
1158
+ // Resolved together so the two representations always describe the same chain.
1159
+ // Segments are resolved, never derived (see useEntityFetch).
1160
+ const resolvedPath = useMemo(() => navigation.resolveIdsFrom(fullPath, pathSegments), [fullPath, pathSegments, navigation.resolveIdsFrom]);
1161
+ const resolvedPathSegments = useMemo(() => pathSegments
1162
+ ? (navigation.resolveSegmentsFrom?.(pathSegments) ?? pathSegments)
1163
+ : undefined, [pathSegments, navigation.resolveSegmentsFrom]);
1145
1164
 
1146
1165
  useEffect(() => {
1147
1166
  if (dataSource.countEntities)
@@ -1182,10 +1201,13 @@ function buildPropertyWidthOverwrite(key: string, width: number): PartialEntityC
1182
1201
  function EntityIdHeaderWidget({
1183
1202
  collection,
1184
1203
  path,
1204
+ pathSegments,
1185
1205
  fullIdPath
1186
1206
  }: {
1187
1207
  collection: EntityCollection,
1188
1208
  path: string,
1209
+ /** `path` split at its real segment boundaries, when the caller knows them. */
1210
+ pathSegments?: string[],
1189
1211
  fullIdPath: string
1190
1212
  }) {
1191
1213
 
@@ -1225,6 +1247,7 @@ function EntityIdHeaderWidget({
1225
1247
  collection,
1226
1248
  entityId,
1227
1249
  path,
1250
+ pathSegments,
1228
1251
  fullIdPath,
1229
1252
  sideEntityController,
1230
1253
  navigation
@@ -1250,7 +1273,7 @@ function EntityIdHeaderWidget({
1250
1273
  </form>
1251
1274
  {recentIds && recentIds.length > 0 && <div className="flex flex-col gap-2 p-2">
1252
1275
  {recentIds.map(id => (
1253
- <ReferencePreview reference={new EntityReference(id, path)}
1276
+ <ReferencePreview reference={new EntityReference(id, path, undefined, pathSegments)}
1254
1277
  key={id}
1255
1278
  hover={true}
1256
1279
  onClick={() => {
@@ -1260,6 +1283,7 @@ function EntityIdHeaderWidget({
1260
1283
  collection,
1261
1284
  entityId: id,
1262
1285
  path,
1286
+ pathSegments,
1263
1287
  fullIdPath,
1264
1288
  sideEntityController,
1265
1289
  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,
@@ -82,8 +82,12 @@ export function useBoardDataController<M extends Record<string, any> = any, COLU
82
82
  const context = useFireCMSContext();
83
83
  const dataSource = useDataSource(collection);
84
84
  const navigation = useNavigationController();
85
- const resolvedPath = useMemo(() => navigation.resolveIdsFrom(fullPath), [fullPath, navigation.resolveIdsFrom]);
86
- const resolvedPathSegments = pathSegments;
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]);
87
91
 
88
92
  // Stable refs for objects that shouldn't trigger re-subscriptions
89
93
  const dataSourceRef = useRef(dataSource);
@@ -212,6 +216,7 @@ export function useBoardDataController<M extends Record<string, any> = any, COLU
212
216
  currentCollection.callbacks!.onFetch!({
213
217
  collection: currentCollection,
214
218
  path: currentResolvedPath,
219
+ pathSegments: currentPathSegments,
215
220
  entity,
216
221
  context: currentContext
217
222
  })
@@ -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
  });
@@ -120,7 +120,7 @@ export function ReferenceSelectionTable<M extends Record<string, any>>(
120
120
  const analyticsController = useAnalyticsController();
121
121
  const customizationController = useCustomizationController();
122
122
 
123
- const fullPath = navigation.resolveIdsFrom(pathInput);
123
+ const fullPath = navigation.resolveIdsFrom(pathInput, pathSegments);
124
124
 
125
125
  const dataSource = useDataSource(collection);
126
126
 
@@ -215,6 +215,7 @@ export function ReferenceSelectionTable<M extends Record<string, any>>(
215
215
  });
216
216
  sideEntityController.open({
217
217
  path: fullPath,
218
+ pathSegments,
218
219
  collection,
219
220
  updateUrl: true,
220
221
  onUpdate: ({ entity }) => {
@@ -288,6 +289,7 @@ export function ReferenceSelectionTable<M extends Record<string, any>>(
288
289
  textSearchEnabled
289
290
  } =
290
291
  useTableSearchHelper({
292
+ pathSegments,
291
293
  collection,
292
294
  fullPath,
293
295
  });
@@ -326,6 +328,7 @@ export function ReferenceSelectionTable<M extends Record<string, any>>(
326
328
  actions={<ReferenceDialogActions
327
329
  collection={collection}
328
330
  path={fullPath}
331
+ pathSegments={pathSegments}
329
332
  onNewClick={onNewClick}
330
333
  onClear={onClear}/>
331
334
  }
@@ -352,11 +355,14 @@ export function ReferenceSelectionTable<M extends Record<string, any>>(
352
355
  function ReferenceDialogActions({
353
356
  collection,
354
357
  path,
358
+ pathSegments,
355
359
  onClear,
356
360
  onNewClick
357
361
  }: {
358
362
  collection: EntityCollection<any>,
359
363
  path: string,
364
+ /** `path` split at its real segment boundaries, when known. Never derived from `path`. */
365
+ pathSegments?: string[],
360
366
  onClear: () => void,
361
367
  onNewClick: () => void
362
368
  }) {
@@ -371,7 +377,7 @@ function ReferenceDialogActions({
371
377
  onNewClick();
372
378
  }
373
379
  : undefined;
374
- const addButton = canCreateEntity(collection, authController, path, null) &&
380
+ const addButton = canCreateEntity(collection, authController, path, null, pathSegments) &&
375
381
  onClick && (largeLayout
376
382
  ? <Button
377
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,7 @@ export const copyEntityAction: EntityAction = {
75
83
  collection,
76
84
  context,
77
85
  fullPath,
86
+ pathSegments,
78
87
  highlightEntity,
79
88
  unhighlightEntity,
80
89
  openEntityMode
@@ -89,12 +98,19 @@ export const copyEntityAction: EntityAction = {
89
98
  });
90
99
 
91
100
  const path = collection?.collectionGroup ? collection.path : (fullPath ?? collection?.path ?? entity.path);
101
+ // In a collection group `path` is the group's own root, which the entity's segments
102
+ // (its real nested location) do not describe — so they are deliberately not reused,
103
+ // and no substitute is invented.
104
+ const resolvedPathSegments = collection?.collectionGroup
105
+ ? undefined
106
+ : (pathSegments ?? entity.pathSegments);
92
107
  const fullIdPath = collection?.collectionGroup ? collection.id : (fullPath ?? collection?.id ?? entity.path);
93
108
  navigateToEntity({
94
109
  openEntityMode,
95
110
  collection,
96
111
  entityId: entity.id,
97
112
  path,
113
+ pathSegments: resolvedPathSegments,
98
114
  fullIdPath,
99
115
  copy: true,
100
116
  sideEntityController: context.sideEntityController,
@@ -27,8 +27,10 @@ export type DataSourceTableControllerProps<M extends Record<string, any> = any>
27
27
  fullPath: string;
28
28
  /**
29
29
  * `fullPath` split at its real segment boundaries. Passed through to the datasource so
30
- * a parent entity id containing "/" stays unambiguous. Defaults to splitting
31
- * `fullPath`, which is correct for any backend whose ids cannot contain "/".
30
+ * a parent entity id containing "/" stays unambiguous.
31
+ *
32
+ * Optional, and never derived by splitting `fullPath` — a guess would be wrong in
33
+ * exactly the case the field exists for. Absent means "not known here", not "no slashes".
32
34
  */
33
35
  pathSegments?: string[];
34
36
  /**
@@ -94,9 +96,12 @@ export function useDataSourceTableController<M extends Record<string, any> = any
94
96
  const [popupCell, setPopupCell] = React.useState<SelectedCellProps<M> | undefined>(undefined);
95
97
  const navigation = useNavigationController();
96
98
  const dataSource = useDataSource(collection);
97
- const resolvedPath = useMemo(() => navigation.resolveIdsFrom(fullPath), [fullPath, navigation.resolveIdsFrom]);
98
- // Passed through as-is; never derived from `resolvedPath` (see useEntityFetch).
99
- const resolvedPathSegments = pathSegments;
99
+ // Resolved together so the two representations always describe the same chain.
100
+ // Segments are resolved, never derived (see useEntityFetch).
101
+ const resolvedPath = useMemo(() => navigation.resolveIdsFrom(fullPath, pathSegments), [fullPath, pathSegments, navigation.resolveIdsFrom]);
102
+ const resolvedPathSegments = useMemo(() => pathSegments
103
+ ? (navigation.resolveSegmentsFrom?.(pathSegments) ?? pathSegments)
104
+ : undefined, [pathSegments, navigation.resolveSegmentsFrom]);
100
105
 
101
106
  const forceFilter = forceFilterFromProps ?? forceFilterFromCollection;
102
107
  const paginationEnabled = collection.pagination === undefined || Boolean(collection.pagination);
@@ -247,6 +252,7 @@ export function useDataSourceTableController<M extends Record<string, any> = any
247
252
  collection.callbacks!.onFetch!({
248
253
  collection,
249
254
  path: resolvedPath,
255
+ pathSegments: resolvedPathSegments,
250
256
  entity,
251
257
  context
252
258
  })));
@@ -6,6 +6,8 @@ import { useCustomizationController, useDataSource, useFireCMSContext } from "..
6
6
  export interface UseTableSearchHelperParams<M extends Record<string, any>> {
7
7
  collection: EntityCollection<M>;
8
8
  fullPath: string;
9
+ /** `fullPath` split at its real segment boundaries, when known. Never derived from it. */
10
+ pathSegments?: string[];
9
11
  parentCollectionIds?: string[];
10
12
  /**
11
13
  * Search term that is already being applied without the user having gone through the
@@ -18,6 +20,7 @@ export interface UseTableSearchHelperParams<M extends Record<string, any>> {
18
20
  export function useTableSearchHelper<M extends Record<string, any>>({
19
21
  collection,
20
22
  fullPath,
23
+ pathSegments,
21
24
  parentCollectionIds,
22
25
  initialSearchString
23
26
  }: UseTableSearchHelperParams<M>) {
@@ -36,6 +39,7 @@ export function useTableSearchHelper<M extends Record<string, any>>({
36
39
  const props = {
37
40
  context,
38
41
  path: fullPath,
42
+ pathSegments,
39
43
  databaseId: collection.databaseId,
40
44
  collection,
41
45
  parentCollectionIds
@@ -62,6 +66,7 @@ export function useTableSearchHelper<M extends Record<string, any>>({
62
66
  promises.push(p.collectionView.onTextSearchClick({
63
67
  context,
64
68
  path: fullPath,
69
+ pathSegments,
65
70
  collection,
66
71
  parentCollectionIds
67
72
  }));
@@ -12,6 +12,7 @@ import {
12
12
 
13
13
  import { CircularProgressCenter, EntityCollectionView, EntityView, ErrorBoundary } from "../components";
14
14
  import {
15
+ buildSubcollectionPathSegments,
15
16
  canEditEntity,
16
17
  encodeEntityId,
17
18
  removeInitialAndTrailingSlashes,
@@ -42,6 +43,8 @@ export type OnUpdateParams = {
42
43
  entity: Entity<any>,
43
44
  status: EntityStatus,
44
45
  path: string,
46
+ /** `path` split at its real segment boundaries, when known. Never derived from `path`. */
47
+ pathSegments?: string[],
45
48
  entityId?: string;
46
49
  selectedTab?: string;
47
50
  collection: EntityCollection<any>
@@ -127,7 +130,7 @@ export function EntityEditView<M extends Record<string, any>, USER extends User>
127
130
  if (status === "new" || status === "copy") {
128
131
  return true;
129
132
  } else {
130
- return entity ? canEditEntity(props.collection, authController, props.path, entity ?? null) : undefined;
133
+ return entity ? canEditEntity(props.collection, authController, props.path, entity ?? null, props.pathSegments) : undefined;
131
134
  }
132
135
  }, [authController, entity, status]);
133
136
 
@@ -202,6 +205,7 @@ export function EntityEditViewInner<M extends Record<string, any>>({
202
205
  entityId,
203
206
  parentCollectionIds,
204
207
  path,
208
+ pathSegments,
205
209
  status,
206
210
  collection,
207
211
  context,
@@ -359,17 +363,9 @@ export function EntityEditViewInner<M extends Record<string, any>>({
359
363
  const newFullPath = usedEntity ? `${path}/${usedEntity?.id}/${removeInitialAndTrailingSlashes(subcollection.path)}` : undefined;
360
364
  // `newFullIdPath` is fed to buildUrlCollectionPath downstream, so it stays escaped.
361
365
  const newFullIdPath = fullIdPath && usedEntity ? `${fullIdPath}/${encodeEntityId(usedEntity.id)}/${removeInitialAndTrailingSlashes(subcollectionId)}` : undefined;
362
- // The subcollection sits under this entity, so its segments are ours plus this
363
- // entity's id (kept whole, slashes and all) plus the subcollection's own path.
364
366
  // Only extendable when we were given real segments — deriving the parent chain
365
367
  // from the flattened `path` would drop the boundary of a slash-bearing parent id.
366
- const newPathSegments = usedEntity && pathSegments
367
- ? [
368
- ...pathSegments,
369
- usedEntity.id,
370
- ...removeInitialAndTrailingSlashes(subcollection.path).split("/")
371
- ]
372
- : undefined;
368
+ const newPathSegments = buildSubcollectionPathSegments(pathSegments, usedEntity?.id, subcollection.path);
373
369
 
374
370
  if (selectedTab !== subcollectionId) return null;
375
371
  return (
@@ -39,6 +39,7 @@ import { ErrorTooltip } from "../components/ErrorTooltip";
39
39
  export function EntityEditViewFormActions({
40
40
  collection,
41
41
  path,
42
+ pathSegments,
42
43
  entity,
43
44
  layout,
44
45
  savingError,
@@ -63,8 +64,8 @@ export function EntityEditViewFormActions({
63
64
  const customEntityActions = (collection.entityActions ?? [])
64
65
  .map(action => resolveEntityAction(action, customizationController.entityActions))
65
66
  .filter(Boolean) as EntityAction[];
66
- const createEnabled = canCreateEntity(collection, authController, path, null);
67
- const deleteEnabled = entity ? canDeleteEntity(collection, authController, path, entity) : false;
67
+ const createEnabled = canCreateEntity(collection, authController, path, null, pathSegments);
68
+ const deleteEnabled = entity ? canDeleteEntity(collection, authController, path, entity, entity.pathSegments ?? pathSegments) : false;
68
69
  const actions: EntityAction[] = [];
69
70
  if (createEnabled)
70
71
  actions.push(copyEntityAction);
@@ -60,6 +60,7 @@ export function EntitySidePanel(props: EntitySidePanelProps) {
60
60
  if (params.status !== "existing") {
61
61
  sideEntityController.replace({
62
62
  path: params.path,
63
+ pathSegments: params.pathSegments ?? pathSegments,
63
64
  entityId: params.entityId,
64
65
  fullIdPath: props.fullIdPath,
65
66
  selectedTab: params.selectedTab,
@@ -76,10 +77,10 @@ export function EntitySidePanel(props: EntitySidePanelProps) {
76
77
  }
77
78
 
78
79
  const parentCollectionIds = useMemo(() => {
79
- return navigationController.getParentCollectionIds(path);
80
+ return navigationController.getParentCollectionIds(path, pathSegments);
80
81
  }, [navigationController, path]);
81
82
 
82
- const collection = props.collection ?? navigationController.getCollection(fullIdPath ?? path);
83
+ const collection = props.collection ?? navigationController.getCollection(fullIdPath ?? path, false, pathSegments);
83
84
 
84
85
  useEffect(() => {
85
86
  function beforeunload(e: any) {
@@ -154,6 +155,7 @@ export function EntitySidePanel(props: EntitySidePanelProps) {
154
155
  }) => {
155
156
  sideEntityController.replace({
156
157
  path,
158
+ pathSegments,
157
159
  entityId,
158
160
  fullIdPath: props.fullIdPath,
159
161
  selectedTab,