@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.
- package/dist/components/EntityCollectionView/EntityCollectionView.d.ts +3 -2
- package/dist/components/EntityCollectionView/EntityCollectionViewActions.d.ts +3 -1
- package/dist/components/EntityCollectionView/EntityCollectionViewStartActions.d.ts +3 -1
- package/dist/components/common/useDataSourceTableController.d.ts +4 -2
- package/dist/components/common/useTableSearchHelper.d.ts +3 -1
- package/dist/core/EntityEditView.d.ts +2 -0
- package/dist/core/EntityEditViewFormActions.d.ts +1 -1
- package/dist/form/EntityForm.d.ts +2 -0
- package/dist/form/EntityFormActions.d.ts +2 -0
- package/dist/hooks/data/delete.d.ts +1 -1
- package/dist/hooks/data/useCollectionFetch.d.ts +1 -1
- package/dist/hooks/data/useEntityFetch.d.ts +4 -1
- package/dist/index.es.js +663 -451
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +663 -451
- package/dist/index.umd.js.map +1 -1
- package/dist/types/collections.d.ts +7 -0
- package/dist/types/datasource.d.ts +8 -0
- package/dist/types/entity_actions.d.ts +6 -0
- package/dist/types/entity_callbacks.d.ts +21 -0
- package/dist/types/navigation.d.ts +34 -5
- package/dist/types/plugins.d.ts +13 -0
- package/dist/types/side_entity_controller.d.ts +3 -2
- package/dist/util/navigation_utils.d.ts +84 -2
- package/dist/util/parent_references_from_path.d.ts +15 -0
- package/dist/util/paths.d.ts +28 -0
- package/dist/util/permissions.d.ts +10 -4
- package/package.json +3 -3
- package/src/components/DeleteEntityDialog.tsx +2 -0
- package/src/components/EntityCollectionView/EntityCollectionBoardView.tsx +8 -0
- package/src/components/EntityCollectionView/EntityCollectionView.tsx +38 -14
- package/src/components/EntityCollectionView/EntityCollectionViewActions.tsx +6 -2
- package/src/components/EntityCollectionView/EntityCollectionViewStartActions.tsx +4 -0
- package/src/components/EntityCollectionView/useBoardDataController.tsx +7 -2
- package/src/components/EntityPreview.tsx +4 -1
- package/src/components/ReferenceTable/ReferenceSelectionTable.tsx +8 -2
- package/src/components/common/default_entity_actions.tsx +16 -0
- package/src/components/common/useDataSourceTableController.tsx +11 -5
- package/src/components/common/useTableSearchHelper.ts +5 -0
- package/src/core/EntityEditView.tsx +6 -10
- package/src/core/EntityEditViewFormActions.tsx +3 -2
- package/src/core/EntitySidePanel.tsx +4 -2
- package/src/form/EntityForm.tsx +16 -2
- package/src/form/EntityFormActions.tsx +2 -0
- package/src/hooks/data/delete.ts +8 -2
- package/src/hooks/data/save.ts +4 -1
- package/src/hooks/data/useCollectionFetch.tsx +9 -2
- package/src/hooks/data/useEntityFetch.tsx +15 -3
- package/src/hooks/useBuildNavigationController.tsx +23 -8
- package/src/hooks/useResolvedNavigationFrom.tsx +1 -1
- package/src/internal/useBuildDataSource.ts +13 -12
- package/src/preview/components/ReferencePreview.tsx +4 -1
- package/src/routes/FireCMSRoute.tsx +9 -4
- package/src/types/collections.ts +8 -0
- package/src/types/datasource.ts +8 -0
- package/src/types/entity_actions.tsx +6 -0
- package/src/types/entity_callbacks.ts +24 -0
- package/src/types/navigation.ts +35 -5
- package/src/types/plugins.tsx +13 -0
- package/src/types/side_entity_controller.tsx +3 -2
- package/src/util/navigation_utils.ts +197 -2
- package/src/util/parent_references_from_path.ts +32 -1
- package/src/util/paths.ts +40 -3
- package/src/util/permissions.ts +22 -10
package/src/form/EntityForm.tsx
CHANGED
|
@@ -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;
|
package/src/hooks/data/delete.ts
CHANGED
|
@@ -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
|
-
|
|
85
|
-
pathSegments: entity.pathSegments,
|
|
91
|
+
pathSegments: resolvedPathSegments,
|
|
86
92
|
entity,
|
|
87
93
|
collection
|
|
88
94
|
}).then(() => {
|
package/src/hooks/data/save.ts
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
524
|
-
|
|
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
|
-
|
|
291
|
-
|
|
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
|
-
|
|
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={
|
|
265
|
+
pathSegments={pathSegments}
|
|
261
266
|
collection={collection}
|
|
262
267
|
layout={"full_screen"}
|
|
263
268
|
path={collectionPath}
|
package/src/types/collections.ts
CHANGED
|
@@ -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
|
*/
|
package/src/types/datasource.ts
CHANGED
|
@@ -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
|
*/
|
package/src/types/navigation.ts
CHANGED
|
@@ -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
|