@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
@@ -64,6 +64,8 @@ export type OnUpdateParams = {
64
64
  entity: Entity<any>,
65
65
  status: EntityStatus,
66
66
  path: string,
67
+ /** `path` split at its real segment boundaries, when known. Never derived from `path`. */
68
+ pathSegments?: string[],
67
69
  entityId?: string;
68
70
  selectedTab?: string;
69
71
  collection: EntityCollection<any>
@@ -276,6 +278,7 @@ export function EntityForm<M extends Record<string, any>>({
276
278
  entityId: entityIdProp,
277
279
  collection,
278
280
  path,
281
+ pathSegments,
279
282
  values: valuesToBeSaved
280
283
  });
281
284
  }, false, 2000);
@@ -497,6 +500,7 @@ export function EntityForm<M extends Record<string, any>>({
497
500
  entity: updatedEntity,
498
501
  status,
499
502
  path,
503
+ pathSegments,
500
504
  entityId: updatedEntity.id,
501
505
  collection
502
506
  });
@@ -517,16 +521,20 @@ export function EntityForm<M extends Record<string, any>>({
517
521
  previousValues,
518
522
  entityId,
519
523
  collection,
520
- path
524
+ path,
525
+ pathSegments
521
526
  }: {
522
527
  collection: EntityCollection<M>,
523
528
  path: string,
529
+ /** `path` split at its real segment boundaries, travelling with the `path` above. */
530
+ pathSegments?: string[],
524
531
  entityId: string | undefined,
525
532
  values: M,
526
533
  previousValues?: M,
527
534
  }) => {
528
535
  return saveEntityWithCallbacks({
529
536
  path,
537
+ pathSegments,
530
538
  entityId,
531
539
  values,
532
540
  previousValues,
@@ -544,6 +552,7 @@ export function EntityForm<M extends Record<string, any>>({
544
552
  type EntityFormSaveParams<M extends Record<string, any>> = {
545
553
  collection: ResolvedEntityCollection<M>,
546
554
  path: string,
555
+ pathSegments?: string[],
547
556
  entityId: string | undefined,
548
557
  values: EntityValues<M>,
549
558
  previousValues?: EntityValues<M>,
@@ -553,6 +562,7 @@ export function EntityForm<M extends Record<string, any>>({
553
562
  const onSaveEntityRequest = async ({
554
563
  collection,
555
564
  path,
565
+ pathSegments,
556
566
  entityId,
557
567
  values,
558
568
  previousValues,
@@ -566,6 +576,7 @@ export function EntityForm<M extends Record<string, any>>({
566
576
  return saveEntity({
567
577
  collection,
568
578
  path,
579
+ pathSegments,
569
580
  entityId,
570
581
  values,
571
582
  previousValues
@@ -579,6 +590,7 @@ export function EntityForm<M extends Record<string, any>>({
579
590
  return onSaveEntityRequest({
580
591
  collection: resolvedCollection,
581
592
  path,
593
+ pathSegments,
582
594
  entityId,
583
595
  values,
584
596
  previousValues: entity?.values,
@@ -629,13 +641,14 @@ export function EntityForm<M extends Record<string, any>>({
629
641
  const plugins = customizationController.plugins;
630
642
 
631
643
  const actionsDisabled = disabled || formex.isSubmitting || (status === "existing" && !formex.dirty) || Boolean(disabledProp);
632
- const parentCollectionIds = navigationController.getParentCollectionIds(path);
644
+ const parentCollectionIds = navigationController.getParentCollectionIds(path, pathSegments);
633
645
 
634
646
  if (plugins && collection) {
635
647
  const actionProps: PluginFormActionProps = {
636
648
  entityId,
637
649
  parentCollectionIds,
638
650
  path,
651
+ pathSegments,
639
652
  status,
640
653
  collection,
641
654
  context,
@@ -896,6 +909,7 @@ export function EntityForm<M extends Record<string, any>>({
896
909
  const dialogActions = <EntityFormActionsComponent
897
910
  collection={resolvedCollection}
898
911
  path={path}
912
+ pathSegments={pathSegments}
899
913
  fullPath={path}
900
914
  fullIdPath={fullIdPath}
901
915
  entity={entity}
@@ -26,6 +26,8 @@ export interface EntityFormActionsProps {
26
26
  fullIdPath?: string;
27
27
  collection: ResolvedEntityCollection;
28
28
  path: string;
29
+ /** `path` split at its real segment boundaries, when known. Never derived from `path`. */
30
+ pathSegments?: string[];
29
31
  entity?: Entity;
30
32
  layout: "bottom" | "side";
31
33
  savingError?: Error;
@@ -46,6 +46,7 @@ export type DeleteEntityWithCallbacksProps<M extends Record<string, any>, USER e
46
46
  export async function deleteEntityWithCallbacks<M extends Record<string, any>, USER extends User>({
47
47
  dataSource,
48
48
  entity,
49
+ pathSegments,
49
50
  collection,
50
51
  callbacks,
51
52
  onDeleteSuccess,
@@ -62,11 +63,17 @@ export async function deleteEntityWithCallbacks<M extends Record<string, any>, U
62
63
 
63
64
  console.debug("Deleting entity", entity.path, entity.id);
64
65
 
66
+ // An explicit `pathSegments` wins, then the ones the entity was loaded with. The prop
67
+ // used to be accepted and silently ignored, which is the same class of bug this field
68
+ // exists to prevent.
69
+ const resolvedPathSegments = pathSegments ?? entity.pathSegments;
70
+
65
71
  const entityDeleteProps: EntityOnDeleteProps<M, any> = {
66
72
  entity,
67
73
  collection,
68
74
  entityId: entity.id,
69
75
  path: entity.path,
76
+ pathSegments: resolvedPathSegments,
70
77
  context
71
78
  };
72
79
 
@@ -81,8 +88,7 @@ export async function deleteEntityWithCallbacks<M extends Record<string, any>, U
81
88
  }
82
89
  }
83
90
  return dataSource.deleteEntity({
84
- // Carried by the entity from the fetch that produced it; undefined if unknown.
85
- pathSegments: entity.pathSegments,
91
+ pathSegments: resolvedPathSegments,
86
92
  entity,
87
93
  collection
88
94
  }).then(() => {
@@ -68,7 +68,7 @@ export async function saveEntityWithCallbacks<M extends Record<string, any>, USE
68
68
 
69
69
  const customizationController = context.customizationController;
70
70
 
71
- const resolvedPath = context.navigation.resolveIdsFrom(path);
71
+ const resolvedPath = context.navigation.resolveIdsFrom(path, pathSegments);
72
72
 
73
73
  const callbacks = collection.callbacks;
74
74
  if (callbacks?.onPreSave) {
@@ -84,6 +84,7 @@ export async function saveEntityWithCallbacks<M extends Record<string, any>, USE
84
84
  updatedValues = await callbacks.onPreSave({
85
85
  collection: resolvedCollection,
86
86
  path,
87
+ pathSegments,
87
88
  resolvedPath,
88
89
  entityId,
89
90
  values,
@@ -123,6 +124,7 @@ export async function saveEntityWithCallbacks<M extends Record<string, any>, USE
123
124
  callbacks.onSaveSuccess({
124
125
  collection: resolvedCollection,
125
126
  path,
127
+ pathSegments,
126
128
  resolvedPath,
127
129
  entityId: entity.id,
128
130
  values: updatedValues,
@@ -152,6 +154,7 @@ export async function saveEntityWithCallbacks<M extends Record<string, any>, USE
152
154
  callbacks.onSaveFailure({
153
155
  collection: resolvedCollection,
154
156
  path,
157
+ pathSegments,
155
158
  resolvedPath,
156
159
  entityId,
157
160
  values: updatedValues,
@@ -69,7 +69,7 @@ export interface CollectionFetchResult<M extends Record<string, any>> {
69
69
  export function useCollectionFetch<M extends Record<string, any>, USER extends User>(
70
70
  {
71
71
  path: inputPath,
72
- pathSegments,
72
+ pathSegments: inputPathSegments,
73
73
  collection,
74
74
  filterValues,
75
75
  sortBy,
@@ -80,7 +80,13 @@ export function useCollectionFetch<M extends Record<string, any>, USER extends U
80
80
  const dataSource = useDataSource(collection);
81
81
  const navigationController = useNavigationController();
82
82
 
83
- const path = navigationController.resolveIdsFrom(inputPath);
83
+ // Resolved together so the two representations always describe the same chain.
84
+ // Segments are resolved, never derived: only collection ids are swapped for their
85
+ // real paths, and entity ids are carried through whole (see useEntityFetch).
86
+ const pathSegments = inputPathSegments
87
+ ? (navigationController.resolveSegmentsFrom?.(inputPathSegments) ?? inputPathSegments)
88
+ : undefined;
89
+ const path = navigationController.resolveIdsFrom(inputPath, inputPathSegments);
84
90
 
85
91
  const sortByProperty = sortBy ? sortBy[0] : undefined;
86
92
  const currentSort = sortBy ? sortBy[1] : undefined;
@@ -109,6 +115,7 @@ export function useCollectionFetch<M extends Record<string, any>, USER extends U
109
115
  collection.callbacks!.onFetch!({
110
116
  collection,
111
117
  path,
118
+ pathSegments,
112
119
  entity,
113
120
  context
114
121
  })));
@@ -11,7 +11,10 @@ export interface EntityFetchProps<M extends Record<string, any>, USER extends Us
11
11
  path: string;
12
12
  /**
13
13
  * `path` split at its real segment boundaries. Forwarded to the datasource so a parent
14
- * entity id containing "/" stays unambiguous. Defaults to splitting the resolved path.
14
+ * entity id containing "/" stays unambiguous.
15
+ *
16
+ * Optional, and never derived by splitting `path` — a guess would be wrong in exactly
17
+ * the case the field exists for. Absent means "not known here", not "no slashes".
15
18
  */
16
19
  pathSegments?: string[];
17
20
  entityId?: string;
@@ -54,11 +57,19 @@ export function useEntityFetch<M extends Record<string, any>, USER extends User>
54
57
  const dataSource = useDataSource(collection);
55
58
  const navigationController = useNavigationController();
56
59
 
57
- const path = navigationController.resolveIdsFrom(inputPath);
58
60
  // Never fabricate segments. If the caller did not thread them, pass undefined so a
59
61
  // delegate can tell "not provided" from a real answer — splitting `path` here would
60
62
  // produce a confidently wrong array for any id containing "/".
61
- const pathSegments = inputPathSegments;
63
+ //
64
+ // When they were threaded, both representations are resolved from them together, so
65
+ // `path` cannot end up describing a different chain than `pathSegments`.
66
+ // `resolveSegmentsFrom` is optional on the controller: a host app may be supplying one
67
+ // built against an earlier version. Falling back to the segments as given is what
68
+ // happened before it existed — they are used unresolved, never invented.
69
+ const pathSegments = inputPathSegments
70
+ ? (navigationController.resolveSegmentsFrom?.(inputPathSegments) ?? inputPathSegments)
71
+ : undefined;
72
+ const path = navigationController.resolveIdsFrom(inputPath, inputPathSegments);
62
73
 
63
74
  const context: FireCMSContext<USER> = useFireCMSContext();
64
75
 
@@ -82,6 +93,7 @@ export function useEntityFetch<M extends Record<string, any>, USER extends User>
82
93
  updatedEntity = await collection.callbacks.onFetch({
83
94
  collection,
84
95
  path,
96
+ pathSegments,
85
97
  entity: updatedEntity,
86
98
  context
87
99
  });
@@ -22,11 +22,14 @@ import {
22
22
  } from "../types";
23
23
  import {
24
24
  applyPermissionsFunctionIfEmpty,
25
+ collectionSegmentsFrom,
26
+ fullPathToCollectionSegments,
25
27
  getCollectionByPathOrId,
26
28
  mergeDeep,
27
29
  removeFunctions,
28
30
  removeInitialAndTrailingSlashes,
29
31
  resolveCollectionPathIds,
32
+ resolveCollectionPathSegments,
30
33
  resolvePermissions
31
34
  } from "../util";
32
35
  import { getParentReferencesFromPath } from "../util/parent_references_from_path";
@@ -405,13 +408,14 @@ export function useBuildNavigationController<EC extends EntityCollection, USER e
405
408
 
406
409
  const getCollection = useCallback((
407
410
  idOrPath: string,
408
- includeUserOverride = false
411
+ includeUserOverride = false,
412
+ pathSegments?: string[]
409
413
  ): EC | undefined => {
410
414
  const collections = collectionsRef.current;
411
415
  if (!collections)
412
416
  return undefined;
413
417
 
414
- const baseCollection = getCollectionByPathOrId(removeInitialAndTrailingSlashes(idOrPath), collections);
418
+ const baseCollection = getCollectionByPathOrId(removeInitialAndTrailingSlashes(idOrPath), collections, pathSegments);
415
419
 
416
420
  const userOverride = includeUserOverride ? userConfigPersistence?.getCollectionConfig(idOrPath) : undefined;
417
421
  let overriddenCollection = baseCollection;
@@ -505,23 +509,33 @@ export function useBuildNavigationController<EC extends EntityCollection, USER e
505
509
  throw Error("Expected path starting with " + fullCollectionPath);
506
510
  }, [fullCollectionPath]);
507
511
 
508
- const resolveIdsFrom = useCallback((path: string): string => {
512
+ const resolveIdsFrom = useCallback((path: string, pathSegments?: string[]): string => {
509
513
  const collections = collectionsRef.current ?? [];
510
- return resolveCollectionPathIds(path, collections);
514
+ return resolveCollectionPathIds(path, collections, pathSegments);
511
515
  }, []);
512
516
 
513
- const getAllParentReferencesForPath = useCallback((path: string): EntityReference[] => {
517
+ const resolveSegmentsFrom = useCallback((pathSegments: string[]): string[] => {
518
+ const collections = collectionsRef.current ?? [];
519
+ return resolveCollectionPathSegments(pathSegments, collections);
520
+ }, []);
521
+
522
+ const getAllParentReferencesForPath = useCallback((path: string, pathSegments?: string[]): EntityReference[] => {
514
523
  const collections = collectionsRef.current ?? [];
515
524
  return getParentReferencesFromPath({
516
525
  path,
526
+ pathSegments,
517
527
  collections
518
528
  });
519
529
  }, []);
520
530
 
521
- const getParentCollectionIds = useCallback((path: string): string[] => {
531
+ const getParentCollectionIds = useCallback((path: string, pathSegments?: string[]): string[] => {
522
532
 
523
- const strings = path.split("/");
524
- const oddPathSegments = strings.filter((_, i) => i % 2 === 0);
533
+ // Positional filtering only picks out the collection segments while no entity id
534
+ // contains "/"; with one, every following segment shifts. Given the real boundaries
535
+ // the same rule applies correctly; without them this is the historical reading.
536
+ const oddPathSegments = pathSegments
537
+ ? collectionSegmentsFrom(pathSegments)
538
+ : fullPathToCollectionSegments(path);
525
539
  oddPathSegments.pop();
526
540
 
527
541
  const result: string[][] = [];
@@ -567,6 +581,7 @@ export function useBuildNavigationController<EC extends EntityCollection, USER e
567
581
  urlPathToDataPath,
568
582
  buildUrlCollectionPath,
569
583
  resolveIdsFrom,
584
+ resolveSegmentsFrom,
570
585
  topLevelNavigation,
571
586
  refreshNavigation,
572
587
  getParentReferencesFromPath: getAllParentReferencesForPath,
@@ -80,7 +80,7 @@ export function resolveNavigationFrom<M extends Record<string, any>, USER extend
80
80
  if (entry.type === "collection") {
81
81
  return Promise.resolve(entry);
82
82
  } else if (entry.type === "entity") {
83
- const collection = navigation.getCollection(entry.path);
83
+ const collection = navigation.getCollection(entry.path, false, entry.pathSegments);
84
84
  if (!collection) {
85
85
  throw Error(`No collection defined in the navigation for the entity with path ${entry.path}`);
86
86
  }
@@ -112,7 +112,7 @@ export function useBuildDataSource({
112
112
  }: ListenCollectionProps<M>
113
113
  ): () => void => {
114
114
 
115
- const collection = collectionProp ?? navigationController.getCollection(path);
115
+ const collection = collectionProp ?? navigationController.getCollection(path, false, pathSegments);
116
116
  const usedDelegate = collection?.overrides?.dataSourceDelegate ?? delegate;
117
117
 
118
118
  if (!usedDelegate.listenCollection)
@@ -213,7 +213,7 @@ export function useBuildDataSource({
213
213
  status
214
214
  }: SaveEntityProps<M>): Promise<Entity<M>> => {
215
215
 
216
- const collection = collectionProp ?? navigationController.getCollection(path);
216
+ const collection = collectionProp ?? navigationController.getCollection(path, false, pathSegments);
217
217
  const usedDelegate = collection?.overrides?.dataSourceDelegate ?? delegate;
218
218
 
219
219
  const resolvedCollection = collection
@@ -286,9 +286,15 @@ export function useBuildDataSource({
286
286
  values: finalValues,
287
287
  status
288
288
  }).then((res) => {
289
+ // Keep everything the delegate resolved — `databaseId`, and `pathSegments` in
290
+ // particular — so a just-saved entity is as resolvable as a fetched one.
291
+ // Only the values are re-mapped back to the CMS model.
289
292
  return {
290
- id: res.id,
291
- path: res.path,
293
+ ...res,
294
+ // A delegate that predates `pathSegments` returns none; the segments we
295
+ // just saved with describe exactly this entity's collection, so they are
296
+ // the authoritative answer rather than a guess.
297
+ pathSegments: res.pathSegments ?? pathSegments,
292
298
  values: usedDelegate.delegateToCMSModel(finalValues)
293
299
  } as Entity<M>;
294
300
  });
@@ -343,6 +349,8 @@ export function useBuildDataSource({
343
349
  return usedDelegate.generateEntityId(path, collection, pathSegments);
344
350
  }, [delegate.generateEntityId]),
345
351
 
352
+ // Typed with the declared props rather than an inline duplicate: the duplicate is
353
+ // free to drift from the interface, and a field it forgets is dropped in silence.
346
354
  countEntities: delegate.countEntities ? async ({
347
355
  path,
348
356
  pathSegments,
@@ -350,14 +358,7 @@ export function useBuildDataSource({
350
358
  filter,
351
359
  order,
352
360
  orderBy
353
- }: {
354
- path: string,
355
- pathSegments?: string[],
356
- collection: EntityCollection<any>,
357
- filter?: FilterValues<Extract<keyof any, string>>,
358
- orderBy?: string,
359
- order?: "desc" | "asc",
360
- }): Promise<number> => {
361
+ }: FetchCollectionProps<any>): Promise<number> => {
361
362
  const usedDelegate = collection?.overrides?.dataSourceDelegate ?? delegate;
362
363
  return usedDelegate.countEntities!({
363
364
  path,
@@ -53,7 +53,7 @@ function ReferencePreviewInternal({
53
53
 
54
54
  const navigationController = useNavigationController();
55
55
 
56
- const collection = navigationController.getCollection(reference.path);
56
+ const collection = navigationController.getCollection(reference.path, false, reference.pathSegments);
57
57
  if (!collection) {
58
58
  if (customizationController.components?.missingReference) {
59
59
  return <customizationController.components.missingReference path={reference.path}/>;
@@ -99,6 +99,9 @@ function ReferencePreviewExisting<M extends Record<string, any> = any>({
99
99
  dataLoadingError
100
100
  } = useEntityFetch({
101
101
  path: reference.path,
102
+ // References carry the segments of the collection they point at, when the code that
103
+ // built them knew them (see getReferenceFrom).
104
+ pathSegments: reference.pathSegments,
102
105
  entityId: reference.id,
103
106
  collection,
104
107
  useCache: true
@@ -90,7 +90,7 @@ export function FireCMSRoute() {
90
90
  let collection: EntityCollection<any> | undefined;
91
91
  collection = navigation.getCollectionById(navigationEntries[0].id);
92
92
  if (!collection)
93
- collection = navigation.getCollection(navigationEntries[0].path);
93
+ collection = navigation.getCollection(navigationEntries[0].path, false, navigationEntries[0].pathSegments);
94
94
  if (!collection)
95
95
  return null;
96
96
  return <React.Suspense fallback={null}>
@@ -114,7 +114,7 @@ export function FireCMSRoute() {
114
114
  const firstEntry = navigationEntries[0] as NavigationViewCollectionInternal<any>;
115
115
  collection = navigation.getCollectionById(firstEntry.id);
116
116
  if (!collection)
117
- collection = navigation.getCollection(firstEntry.path);
117
+ collection = navigation.getCollection(firstEntry.path, false, firstEntry.pathSegments);
118
118
  if (!collection)
119
119
  return null;
120
120
  return <React.Suspense fallback={null}>
@@ -186,6 +186,8 @@ function EntityFullScreenRoute({
186
186
  const urlTab = getSelectedTabFromUrl(isNew, lastCustomView);
187
187
  const [selectedTab, setSelectedTab] = useState<string | undefined>(urlTab);
188
188
 
189
+ // `navigationPath` comes from the URL, where entity ids are still escaped (see
190
+ // encodeEntityId), so splitting it on "/" inside is correct and no segments are needed.
189
191
  const parentCollectionIds = navigation.getParentCollectionIds(navigationPath);
190
192
  useEffect(() => {
191
193
  if (urlTab !== selectedTab) {
@@ -250,14 +252,17 @@ function EntityFullScreenRoute({
250
252
  // `fullIdPath` is used downstream to build URLs, so it carries the escaped chain;
251
253
  // `collectionPath` addresses the datasource, so it is resolved from the raw one.
252
254
  const fullIdPath = isNew ? lastCollectionEntry!.fullPath : (parentCollectionEntry?.fullPath ?? lastEntityEntry!.fullPath);
253
- const collectionPath = navigation.resolveIdsFrom(isNew ? lastCollectionEntry!.path : lastEntityEntry!.path);
255
+ // The navigation entries know the real segment boundaries, so the path is resolved
256
+ // from them rather than re-parsed out of the flattened string.
257
+ const pathSegments = isNew ? lastCollectionEntry!.pathSegments : lastEntityEntry!.pathSegments;
258
+ const collectionPath = navigation.resolveIdsFrom(isNew ? lastCollectionEntry!.path : lastEntityEntry!.path, pathSegments);
254
259
  return <>
255
260
  <React.Suspense fallback={null}>
256
261
  <EntityEditView
257
262
  key={collection.id + "_" + (isNew ? "new" : (isCopy ? entityId + "_copy" : entityId))}
258
263
  entityId={isNew ? undefined : entityId}
259
264
  fullIdPath={fullIdPath}
260
- pathSegments={isNew ? lastCollectionEntry!.pathSegments : lastEntityEntry!.pathSegments}
265
+ pathSegments={pathSegments}
261
266
  collection={collection}
262
267
  layout={"full_screen"}
263
268
  path={collectionPath}
@@ -497,6 +497,14 @@ export interface CollectionActionsProps<M extends Record<string, any> = any, USE
497
497
  */
498
498
  path: string;
499
499
 
500
+ /**
501
+ * `path` split at its real segment boundaries, e.g. `["nodes", "node/42", "edges"]`.
502
+ *
503
+ * Optional, and never derived by splitting `path` — a parent entity id may itself
504
+ * contain "/". Absent means "not known here", not "no slashes".
505
+ */
506
+ pathSegments?: string[];
507
+
500
508
  /**
501
509
  * Path of the last collection, like `addresses`
502
510
  */
@@ -283,6 +283,10 @@ export interface DataSource {
283
283
  initTextSearch?: (props: {
284
284
  context: FireCMSContext,
285
285
  path: string,
286
+ /**
287
+ * `path` split at its real segment boundaries, when known. Never derived from `path`.
288
+ */
289
+ pathSegments?: string[],
286
290
  collection: EntityCollection,
287
291
  parentCollectionIds?: string[]
288
292
  }) => Promise<boolean>;
@@ -453,6 +457,10 @@ export interface DataSourceDelegate {
453
457
  initTextSearch?: (props: {
454
458
  context: FireCMSContext,
455
459
  path: string,
460
+ /**
461
+ * `path` split at its real segment boundaries, when known. Never derived from `path`.
462
+ */
463
+ pathSegments?: string[],
456
464
  databaseId?: string,
457
465
  collection: EntityCollection,
458
466
  parentCollectionIds?: string[]
@@ -65,6 +65,12 @@ export type EntityActionClickProps<M extends object, USER extends User = User> =
65
65
  context: FireCMSContext<USER>;
66
66
 
67
67
  fullPath?: string;
68
+ /**
69
+ * `fullPath` split at its real segment boundaries, e.g. `["nodes", "node/42", "edges"]`,
70
+ * when the caller knows them. Absent means "not known here" — never derived from
71
+ * `fullPath`, since a parent id may itself contain "/".
72
+ */
73
+ pathSegments?: string[];
68
74
  fullIdPath?: string;
69
75
  collection?: EntityCollection<M>;
70
76
 
@@ -86,6 +86,14 @@ export interface EntityOnFetchProps<M extends Record<string, any> = any, USER ex
86
86
  */
87
87
  path: string;
88
88
 
89
+ /**
90
+ * `path` split at its real segment boundaries, e.g. `["nodes", "node/42", "edges"]`.
91
+ *
92
+ * Optional, and never derived by splitting `path` — a parent entity id may itself
93
+ * contain "/". Absent means "not known here", not "no slashes".
94
+ */
95
+ pathSegments?: string[];
96
+
89
97
  /**
90
98
  * Fetched entity
91
99
  */
@@ -133,6 +141,14 @@ export interface EntityOnSaveProps<M extends Record<string, any> = any, USER ext
133
141
  */
134
142
  path: string;
135
143
 
144
+ /**
145
+ * `path` split at its real segment boundaries, e.g. `["nodes", "node/42", "edges"]`.
146
+ *
147
+ * Optional, and never derived by splitting `path` — a parent entity id may itself
148
+ * contain "/". Absent means "not known here", not "no slashes".
149
+ */
150
+ pathSegments?: string[];
151
+
136
152
  /**
137
153
  * Full path where this entity is being saved, with alias resolved
138
154
  */
@@ -180,6 +196,14 @@ export interface EntityOnDeleteProps<M extends Record<string, any> = any, USER e
180
196
  */
181
197
  path: string;
182
198
 
199
+ /**
200
+ * `path` split at its real segment boundaries, e.g. `["nodes", "node/42", "edges"]`.
201
+ *
202
+ * Optional, and never derived by splitting `path` — a parent entity id may itself
203
+ * contain "/". Absent means "not known here", not "no slashes".
204
+ */
205
+ pathSegments?: string[];
206
+
183
207
  /**
184
208
  * Deleted entity id
185
209
  */
@@ -57,8 +57,12 @@ export type NavigationController<EC extends EntityCollection = EntityCollection<
57
57
  /**
58
58
  * Get the collection configuration for a given path.
59
59
  * The collection is resolved from the given path or alias.
60
+ *
61
+ * Pass `pathSegments` when you have them: without them the path is split on "/" and
62
+ * asserted to have an odd number of parts, which a chain containing a slash-bearing
63
+ * entity id fails — it throws instead of resolving.
60
64
  */
61
- getCollection: (pathOrId: string, includeUserOverride?: boolean) => EC | undefined;
65
+ getCollection: (pathOrId: string, includeUserOverride?: boolean, pathSegments?: string[]) => EC | undefined;
62
66
 
63
67
  /**
64
68
  * Get the top level collection configuration for a given id
@@ -118,9 +122,30 @@ export type NavigationController<EC extends EntityCollection = EntityCollection<
118
122
  /**
119
123
  * Turn a path with collection ids into a resolved path.
120
124
  * The ids (typically used in urls) will be replaced with relative paths (typically used in database paths)
125
+ *
126
+ * Pass `pathSegments` whenever you have them. Without them this has to find the entity
127
+ * ids inside the flattened string by reading up to the next "/", which is wrong for any
128
+ * id that contains one — the chain then shifts by a segment and resolution fails.
129
+ *
121
130
  * @param pathWithAliases
131
+ * @param pathSegments `pathWithAliases` split at its real segment boundaries, if known
122
132
  */
123
- resolveIdsFrom: (pathWithAliases: string) => string;
133
+ resolveIdsFrom: (pathWithAliases: string, pathSegments?: string[]) => string;
134
+
135
+ /**
136
+ * The segment-wise counterpart of {@link resolveIdsFrom}: replaces collection ids with
137
+ * their real paths while carrying every entity id through whole, slashes included.
138
+ *
139
+ * Idempotent — segments that are already resolved pass through unchanged.
140
+ *
141
+ * Optional so that a `navigationController` built against an earlier version keeps
142
+ * working: callers fall back to using the segments as given, which is what happened
143
+ * before this method existed. Controllers built by `useBuildNavigationController`
144
+ * always provide it.
145
+ *
146
+ * @param pathSegments
147
+ */
148
+ resolveSegmentsFrom?: (pathSegments: string[]) => string[];
124
149
 
125
150
  /**
126
151
  * Call this method to recalculate the navigation
@@ -128,16 +153,21 @@ export type NavigationController<EC extends EntityCollection = EntityCollection<
128
153
  refreshNavigation: () => void;
129
154
 
130
155
  /**
131
- * Retrieve all the related parent references for a given path
156
+ * Retrieve all the related parent references for a given path.
157
+ *
158
+ * `path` is expected escaped, as it comes from the URL. Pass `pathSegments` when the
159
+ * path at hand is a raw datasource path, whose ids may contain a bare "/".
160
+ *
132
161
  * @param path
162
+ * @param pathSegments `path` split at its real segment boundaries, with raw ids
133
163
  */
134
- getParentReferencesFromPath: (path: string) => EntityReference[];
164
+ getParentReferencesFromPath: (path: string, pathSegments?: string[]) => EntityReference[];
135
165
 
136
166
  /**
137
167
  * Retrieve all the related parent collection ids for a given path
138
168
  * @param path
139
169
  */
140
- getParentCollectionIds: (path: string) => string[];
170
+ getParentCollectionIds: (path: string, pathSegments?: string[]) => string[];
141
171
 
142
172
  /**
143
173
  * Resolve paths from a list of ids