@firecms/core 3.4.0-canary.0ef7442 → 3.4.0-canary.2575a08
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/i18n/__tests__/FireCMSi18nProvider.test.d.ts +1 -0
- package/dist/i18n/__tests__/nested_provider_lifecycle.test.d.ts +1 -0
- package/dist/index.es.js +853 -600
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +852 -599
- 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/entity_cache.d.ts +11 -0
- 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/EntityCollectionTable/EntityCollectionRowActions.tsx +2 -2
- package/src/components/EntityCollectionView/EntityCollectionBoardView.tsx +8 -0
- package/src/components/EntityCollectionView/EntityCollectionView.tsx +40 -15
- 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 +22 -2
- package/src/components/common/useDataSourceTableController.tsx +26 -8
- package/src/components/common/useTableSearchHelper.ts +5 -0
- package/src/core/EntityEditView.tsx +8 -12
- package/src/core/EntityEditViewFormActions.tsx +3 -2
- package/src/core/EntitySidePanel.tsx +6 -4
- package/src/form/EntityForm.tsx +22 -7
- 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 +19 -6
- package/src/hooks/useBuildNavigationController.tsx +23 -8
- package/src/hooks/useResolvedNavigationFrom.tsx +1 -1
- package/src/i18n/FireCMSi18nProvider.tsx +48 -4
- package/src/i18n/__tests__/FireCMSi18nProvider.test.tsx +91 -0
- package/src/i18n/__tests__/nested_provider_lifecycle.test.tsx +67 -0
- package/src/internal/useBuildDataSource.ts +13 -12
- package/src/internal/useBuildSideEntityController.tsx +13 -5
- package/src/preview/components/ReferencePreview.tsx +7 -3
- 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/entity_cache.ts +18 -0
- package/src/util/navigation_from_path.ts +6 -1
- package/src/util/navigation_utils.ts +204 -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
|
@@ -90,6 +90,7 @@ import { useSelectionController } from "./useSelectionController";
|
|
|
90
90
|
import { EntityCollectionViewStartActions } from "./EntityCollectionViewStartActions";
|
|
91
91
|
import { addRecentId, getRecentIds } from "./utils";
|
|
92
92
|
import { useScrollRestoration } from "../common/useScrollRestoration";
|
|
93
|
+
import { entityCacheKey } from "../../util/entity_cache";
|
|
93
94
|
|
|
94
95
|
const DEFAULT_ENTITY_OPEN_MODE: "side_panel" | "full_screen" = "side_panel";
|
|
95
96
|
|
|
@@ -109,8 +110,9 @@ export type EntityCollectionViewProps<M extends Record<string, any>> = {
|
|
|
109
110
|
/**
|
|
110
111
|
* `fullPath` split at its real segment boundaries, e.g. `["nodes", "node/42", "edges"]`.
|
|
111
112
|
* Forwarded to the datasource so a parent entity id containing "/" stays unambiguous.
|
|
112
|
-
*
|
|
113
|
-
*
|
|
113
|
+
*
|
|
114
|
+
* Optional, and never derived by splitting `fullPath` — a guess would be wrong in
|
|
115
|
+
* exactly the case the field exists for. Absent means "not known here", not "no slashes".
|
|
114
116
|
*/
|
|
115
117
|
pathSegments?: string[];
|
|
116
118
|
/**
|
|
@@ -198,7 +200,7 @@ export const EntityCollectionView = React.memo(
|
|
|
198
200
|
collectionRef.current = collection;
|
|
199
201
|
}, [collection]);
|
|
200
202
|
|
|
201
|
-
const canCreateEntities = canCreateEntity(collection, authController, fullPath, null);
|
|
203
|
+
const canCreateEntities = canCreateEntity(collection, authController, fullPath, null, pathSegments);
|
|
202
204
|
const [highlightedEntity, setHighlightedEntity] = useState<Entity<M> | undefined>(undefined);
|
|
203
205
|
const [deleteEntityClicked, setDeleteEntityClicked] = React.useState<Entity<M> | Entity<M>[] | undefined>(undefined);
|
|
204
206
|
|
|
@@ -228,7 +230,7 @@ export const EntityCollectionView = React.memo(
|
|
|
228
230
|
|
|
229
231
|
const checkInlineEditing = useCallback((entity?: Entity<any>): boolean => {
|
|
230
232
|
const collection = collectionRef.current;
|
|
231
|
-
if (!canEditEntity(collection, authController, fullPath, entity ?? null)) {
|
|
233
|
+
if (!canEditEntity(collection, authController, fullPath, entity ?? null, pathSegments)) {
|
|
232
234
|
return false;
|
|
233
235
|
}
|
|
234
236
|
return collection.inlineEditing === undefined || collection.inlineEditing;
|
|
@@ -351,10 +353,16 @@ export const EntityCollectionView = React.memo(
|
|
|
351
353
|
addRecentId(collection.id, clickedEntity.id);
|
|
352
354
|
}
|
|
353
355
|
|
|
354
|
-
|
|
356
|
+
// In a collection group the row's own path is the real one, so the row's segments
|
|
357
|
+
// are the matching ones; otherwise both come from this view. `path` and
|
|
358
|
+
// `pathSegments` are picked from the same source so they cannot describe
|
|
359
|
+
// different chains.
|
|
360
|
+
const isCollectionGroup = Boolean(collection?.collectionGroup);
|
|
361
|
+
const path = isCollectionGroup ? clickedEntity.path : (fullPath ?? clickedEntity.path);
|
|
355
362
|
navigateToEntity({
|
|
356
363
|
navigation,
|
|
357
364
|
path,
|
|
365
|
+
pathSegments: isCollectionGroup ? clickedEntity.pathSegments : (pathSegments ?? clickedEntity.pathSegments),
|
|
358
366
|
fullIdPath,
|
|
359
367
|
sideEntityController,
|
|
360
368
|
openEntityMode,
|
|
@@ -461,7 +469,7 @@ export const EntityCollectionView = React.memo(
|
|
|
461
469
|
}
|
|
462
470
|
}, [setViewMode, userConfigPersistence, onCollectionModifiedForUser, fullPath, analyticsController, viewMode]);
|
|
463
471
|
|
|
464
|
-
const createEnabled = canCreateEntity(collection, authController, fullPath, null);
|
|
472
|
+
const createEnabled = canCreateEntity(collection, authController, fullPath, null, pathSegments);
|
|
465
473
|
|
|
466
474
|
const uniqueFieldValidator: UniqueFieldValidator = useCallback(
|
|
467
475
|
({
|
|
@@ -469,8 +477,8 @@ export const EntityCollectionView = React.memo(
|
|
|
469
477
|
value,
|
|
470
478
|
property,
|
|
471
479
|
entityId
|
|
472
|
-
}) => dataSource.checkUniqueField(fullPath, name, value, entityId, collection),
|
|
473
|
-
[fullPath]);
|
|
480
|
+
}) => dataSource.checkUniqueField(fullPath, name, value, entityId, collection, pathSegments),
|
|
481
|
+
[fullPath, pathSegments]);
|
|
474
482
|
|
|
475
483
|
const onValueChange: OnCellValueChange<any, any> = ({
|
|
476
484
|
value,
|
|
@@ -484,6 +492,8 @@ export const EntityCollectionView = React.memo(
|
|
|
484
492
|
|
|
485
493
|
const saveProps: SaveEntityProps = {
|
|
486
494
|
path: entity.path ?? fullPath,
|
|
495
|
+
// Paired with the `path` above: the row's segments describe the row's path.
|
|
496
|
+
pathSegments: entity.path ? entity.pathSegments : pathSegments,
|
|
487
497
|
entityId: entity.id,
|
|
488
498
|
values: updatedValues,
|
|
489
499
|
previousValues: entity.values,
|
|
@@ -514,7 +524,7 @@ export const EntityCollectionView = React.memo(
|
|
|
514
524
|
|
|
515
525
|
};
|
|
516
526
|
|
|
517
|
-
const resolvedFullPath = navigation.resolveIdsFrom(fullPath);
|
|
527
|
+
const resolvedFullPath = navigation.resolveIdsFrom(fullPath, pathSegments);
|
|
518
528
|
const resolvedCollection = useMemo(() => resolveCollection<M>({
|
|
519
529
|
collection,
|
|
520
530
|
path: fullPath,
|
|
@@ -676,13 +686,15 @@ export const EntityCollectionView = React.memo(
|
|
|
676
686
|
width: 260,
|
|
677
687
|
dependencies: [],
|
|
678
688
|
Builder: ({ entity }) => {
|
|
679
|
-
|
|
689
|
+
// `entity.path` is a RAW datasource path, so its segments are passed:
|
|
690
|
+
// splitting it would mis-read any parent id containing "/".
|
|
691
|
+
const collectionsWithPath = navigation.getParentReferencesFromPath(entity.path, entity.pathSegments);
|
|
680
692
|
return (
|
|
681
693
|
<div className={"flex flex-col gap-2 w-full"}>
|
|
682
694
|
{collectionsWithPath.map((reference) => {
|
|
683
695
|
return (
|
|
684
696
|
<ReferencePreview
|
|
685
|
-
key={reference.path
|
|
697
|
+
key={entityCacheKey(reference.path, reference.id)}
|
|
686
698
|
reference={reference}
|
|
687
699
|
size={"small"} />
|
|
688
700
|
);
|
|
@@ -713,7 +725,7 @@ export const EntityCollectionView = React.memo(
|
|
|
713
725
|
entity?: Entity<M>,
|
|
714
726
|
customEntityActions?: EntityAction[]
|
|
715
727
|
}): EntityAction[] => {
|
|
716
|
-
const deleteEnabled = entity ? canDeleteEntity(collection, authController, fullPath, entity) : true;
|
|
728
|
+
const deleteEnabled = entity ? canDeleteEntity(collection, authController, fullPath, entity, entity.pathSegments ?? pathSegments) : true;
|
|
717
729
|
const actions: EntityAction[] = [
|
|
718
730
|
{ ...editEntityAction, name: t("edit") }
|
|
719
731
|
];
|
|
@@ -843,6 +855,7 @@ export const EntityCollectionView = React.memo(
|
|
|
843
855
|
onTextSearchClick,
|
|
844
856
|
textSearchEnabled
|
|
845
857
|
} = useTableSearchHelper({
|
|
858
|
+
pathSegments,
|
|
846
859
|
collection,
|
|
847
860
|
fullPath: resolvedFullPath,
|
|
848
861
|
parentCollectionIds,
|
|
@@ -907,6 +920,7 @@ export const EntityCollectionView = React.memo(
|
|
|
907
920
|
collection={collection}
|
|
908
921
|
tableController={tableController}
|
|
909
922
|
path={fullPath}
|
|
923
|
+
pathSegments={pathSegments}
|
|
910
924
|
relativePath={collection.path}
|
|
911
925
|
selectionController={usedSelectionController}
|
|
912
926
|
collectionEntitiesCount={docsCount}
|
|
@@ -918,6 +932,7 @@ export const EntityCollectionView = React.memo(
|
|
|
918
932
|
onMultipleDeleteClick={onMultipleDeleteClick}
|
|
919
933
|
onNewClick={onNewClick}
|
|
920
934
|
path={fullPath}
|
|
935
|
+
pathSegments={pathSegments}
|
|
921
936
|
relativePath={collection.path}
|
|
922
937
|
selectionController={usedSelectionController}
|
|
923
938
|
selectionEnabled={selectionEnabled}
|
|
@@ -1026,6 +1041,7 @@ export const EntityCollectionView = React.memo(
|
|
|
1026
1041
|
getIdColumnWidth={getIdColumnWidth}
|
|
1027
1042
|
additionalIDHeaderWidget={<EntityIdHeaderWidget
|
|
1028
1043
|
path={fullPath}
|
|
1044
|
+
pathSegments={pathSegments}
|
|
1029
1045
|
fullIdPath={fullIdPath ?? fullPath}
|
|
1030
1046
|
collection={collection} />}
|
|
1031
1047
|
openEntityMode={openEntityMode}
|
|
@@ -1140,8 +1156,12 @@ function EntitiesCount({
|
|
|
1140
1156
|
|
|
1141
1157
|
const sortByProperty = sortBy ? sortBy[0] : undefined;
|
|
1142
1158
|
const currentSort = sortBy ? sortBy[1] : undefined;
|
|
1143
|
-
|
|
1144
|
-
|
|
1159
|
+
// Resolved together so the two representations always describe the same chain.
|
|
1160
|
+
// Segments are resolved, never derived (see useEntityFetch).
|
|
1161
|
+
const resolvedPath = useMemo(() => navigation.resolveIdsFrom(fullPath, pathSegments), [fullPath, pathSegments, navigation.resolveIdsFrom]);
|
|
1162
|
+
const resolvedPathSegments = useMemo(() => pathSegments
|
|
1163
|
+
? (navigation.resolveSegmentsFrom?.(pathSegments) ?? pathSegments)
|
|
1164
|
+
: undefined, [pathSegments, navigation.resolveSegmentsFrom]);
|
|
1145
1165
|
|
|
1146
1166
|
useEffect(() => {
|
|
1147
1167
|
if (dataSource.countEntities)
|
|
@@ -1182,10 +1202,13 @@ function buildPropertyWidthOverwrite(key: string, width: number): PartialEntityC
|
|
|
1182
1202
|
function EntityIdHeaderWidget({
|
|
1183
1203
|
collection,
|
|
1184
1204
|
path,
|
|
1205
|
+
pathSegments,
|
|
1185
1206
|
fullIdPath
|
|
1186
1207
|
}: {
|
|
1187
1208
|
collection: EntityCollection,
|
|
1188
1209
|
path: string,
|
|
1210
|
+
/** `path` split at its real segment boundaries, when the caller knows them. */
|
|
1211
|
+
pathSegments?: string[],
|
|
1189
1212
|
fullIdPath: string
|
|
1190
1213
|
}) {
|
|
1191
1214
|
|
|
@@ -1225,6 +1248,7 @@ function EntityIdHeaderWidget({
|
|
|
1225
1248
|
collection,
|
|
1226
1249
|
entityId,
|
|
1227
1250
|
path,
|
|
1251
|
+
pathSegments,
|
|
1228
1252
|
fullIdPath,
|
|
1229
1253
|
sideEntityController,
|
|
1230
1254
|
navigation
|
|
@@ -1250,7 +1274,7 @@ function EntityIdHeaderWidget({
|
|
|
1250
1274
|
</form>
|
|
1251
1275
|
{recentIds && recentIds.length > 0 && <div className="flex flex-col gap-2 p-2">
|
|
1252
1276
|
{recentIds.map(id => (
|
|
1253
|
-
<ReferencePreview reference={new EntityReference(id, path)}
|
|
1277
|
+
<ReferencePreview reference={new EntityReference(id, path, undefined, pathSegments)}
|
|
1254
1278
|
key={id}
|
|
1255
1279
|
hover={true}
|
|
1256
1280
|
onClick={() => {
|
|
@@ -1260,6 +1284,7 @@ function EntityIdHeaderWidget({
|
|
|
1260
1284
|
collection,
|
|
1261
1285
|
entityId: id,
|
|
1262
1286
|
path,
|
|
1287
|
+
pathSegments,
|
|
1263
1288
|
fullIdPath,
|
|
1264
1289
|
sideEntityController,
|
|
1265
1290
|
navigation
|
|
@@ -22,6 +22,8 @@ import { useTranslation } from "../../hooks/useTranslation";
|
|
|
22
22
|
export type EntityCollectionViewActionsProps<M extends Record<string, any>> = {
|
|
23
23
|
collection: EntityCollection<M>;
|
|
24
24
|
path: string;
|
|
25
|
+
/** `path` split at its real segment boundaries, when known. Never derived from `path`. */
|
|
26
|
+
pathSegments?: string[];
|
|
25
27
|
relativePath: string;
|
|
26
28
|
parentCollectionIds: string[];
|
|
27
29
|
selectionEnabled: boolean;
|
|
@@ -40,6 +42,7 @@ export function EntityCollectionViewActions<M extends Record<string, any>>({
|
|
|
40
42
|
onMultipleDeleteClick,
|
|
41
43
|
selectionEnabled,
|
|
42
44
|
path,
|
|
45
|
+
pathSegments,
|
|
43
46
|
selectionController,
|
|
44
47
|
tableController,
|
|
45
48
|
collectionEntitiesCount,
|
|
@@ -56,7 +59,7 @@ export function EntityCollectionViewActions<M extends Record<string, any>>({
|
|
|
56
59
|
|
|
57
60
|
const selectedEntities = selectionController.selectedEntities;
|
|
58
61
|
|
|
59
|
-
const addButton = canCreateEntity(collection, authController, path, null) &&
|
|
62
|
+
const addButton = canCreateEntity(collection, authController, path, null, pathSegments) &&
|
|
60
63
|
onNewClick && (largeLayout
|
|
61
64
|
? <Button
|
|
62
65
|
id={`add_entity_${path}`}
|
|
@@ -75,7 +78,7 @@ export function EntityCollectionViewActions<M extends Record<string, any>>({
|
|
|
75
78
|
<AddIcon size={"small"} />
|
|
76
79
|
</Button>);
|
|
77
80
|
|
|
78
|
-
const multipleDeleteEnabled = canDeleteEntity(collection, authController, path, null);
|
|
81
|
+
const multipleDeleteEnabled = canDeleteEntity(collection, authController, path, null, pathSegments);
|
|
79
82
|
|
|
80
83
|
let multipleDeleteButton: React.ReactNode | undefined;
|
|
81
84
|
if (selectionEnabled) {
|
|
@@ -107,6 +110,7 @@ export function EntityCollectionViewActions<M extends Record<string, any>>({
|
|
|
107
110
|
|
|
108
111
|
const actionProps: CollectionActionsProps = {
|
|
109
112
|
path,
|
|
113
|
+
pathSegments,
|
|
110
114
|
relativePath,
|
|
111
115
|
parentCollectionIds,
|
|
112
116
|
collection,
|
|
@@ -17,6 +17,8 @@ import { useTranslation } from "../../hooks/useTranslation";
|
|
|
17
17
|
export type EntityCollectionViewStartActionsProps<M extends Record<string, any>> = {
|
|
18
18
|
collection: EntityCollection<M>;
|
|
19
19
|
path: string;
|
|
20
|
+
/** `path` split at its real segment boundaries, when known. Never derived from `path`. */
|
|
21
|
+
pathSegments?: string[];
|
|
20
22
|
relativePath: string;
|
|
21
23
|
parentCollectionIds: string[];
|
|
22
24
|
selectionController: SelectionController<M>;
|
|
@@ -33,6 +35,7 @@ export function EntityCollectionViewStartActions<M extends Record<string, any>>(
|
|
|
33
35
|
relativePath,
|
|
34
36
|
parentCollectionIds,
|
|
35
37
|
path,
|
|
38
|
+
pathSegments,
|
|
36
39
|
selectionController,
|
|
37
40
|
tableController,
|
|
38
41
|
collectionEntitiesCount,
|
|
@@ -57,6 +60,7 @@ export function EntityCollectionViewStartActions<M extends Record<string, any>>(
|
|
|
57
60
|
|
|
58
61
|
const actionProps: CollectionActionsProps = {
|
|
59
62
|
path,
|
|
63
|
+
pathSegments,
|
|
60
64
|
relativePath,
|
|
61
65
|
parentCollectionIds,
|
|
62
66
|
collection,
|
|
@@ -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
|
-
|
|
86
|
-
|
|
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,8 @@ export const copyEntityAction: EntityAction = {
|
|
|
75
83
|
collection,
|
|
76
84
|
context,
|
|
77
85
|
fullPath,
|
|
86
|
+
pathSegments,
|
|
87
|
+
fullIdPath,
|
|
78
88
|
highlightEntity,
|
|
79
89
|
unhighlightEntity,
|
|
80
90
|
openEntityMode
|
|
@@ -89,13 +99,23 @@ export const copyEntityAction: EntityAction = {
|
|
|
89
99
|
});
|
|
90
100
|
|
|
91
101
|
const path = collection?.collectionGroup ? collection.path : (fullPath ?? collection?.path ?? entity.path);
|
|
92
|
-
|
|
102
|
+
// In a collection group `path` is the group's own root, which the entity's segments
|
|
103
|
+
// (its real nested location) do not describe — so they are deliberately not reused,
|
|
104
|
+
// and no substitute is invented.
|
|
105
|
+
const resolvedPathSegments = collection?.collectionGroup
|
|
106
|
+
? undefined
|
|
107
|
+
: (pathSegments ?? entity.pathSegments);
|
|
108
|
+
// `fullIdPath` is the ESCAPED chain and is what becomes the URL; `fullPath` is the raw
|
|
109
|
+
// datasource path, whose parent ids may contain "/". Using the latter here wrote a URL
|
|
110
|
+
// that read back as a different entity. Mirrors editEntityAction.
|
|
111
|
+
const newFullIdPath = collection?.collectionGroup ? collection.id : (fullIdPath ?? collection?.id);
|
|
93
112
|
navigateToEntity({
|
|
94
113
|
openEntityMode,
|
|
95
114
|
collection,
|
|
96
115
|
entityId: entity.id,
|
|
97
116
|
path,
|
|
98
|
-
|
|
117
|
+
pathSegments: resolvedPathSegments,
|
|
118
|
+
fullIdPath: newFullIdPath,
|
|
99
119
|
copy: true,
|
|
100
120
|
sideEntityController: context.sideEntityController,
|
|
101
121
|
onClose: () => unhighlightEntity?.(entity),
|
|
@@ -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.
|
|
31
|
-
*
|
|
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
|
-
|
|
98
|
-
//
|
|
99
|
-
const
|
|
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);
|
|
@@ -189,11 +194,23 @@ export function useDataSourceTableController<M extends Record<string, any> = any
|
|
|
189
194
|
|
|
190
195
|
useUpdateUrl(filterValues, sortBy, searchString, updateUrl);
|
|
191
196
|
|
|
192
|
-
|
|
193
|
-
|
|
197
|
+
// Keyed by `resolvedPath`, not `fullPath`, so the read agrees with the two writes below
|
|
198
|
+
// and in `onScroll`. What the cache holds is a slice of a dataset plus the offset into
|
|
199
|
+
// it, and the dataset is identified by what the datasource was actually queried with:
|
|
200
|
+
// `resolvedPath` + filters. Reading with the unresolved `fullPath` meant that for every
|
|
201
|
+
// collection reached through an id alias, or a subcollection whose parent ids need
|
|
202
|
+
// resolving, the entry was written under one key and looked up under another, so scroll
|
|
203
|
+
// restoration silently did nothing there.
|
|
204
|
+
const collectionScroll = scrollRestoration?.getCollectionScroll(resolvedPath, filterValues);
|
|
205
|
+
const initialItemCount = collectionScroll?.data.length || pageSize;
|
|
194
206
|
|
|
195
207
|
useEffect(() => {
|
|
196
|
-
|
|
208
|
+
// Only re-seed an entry we actually have something to restore for. Writing an empty
|
|
209
|
+
// `data` array here poisoned the cache: this is a module level Map that outlives the
|
|
210
|
+
// mount, so the next mount of the same collection read `data.length === 0` back as the
|
|
211
|
+
// initial item count and called the datasource with `limit: 0`. Custom datasources
|
|
212
|
+
// treat that as falsy and drop the limit altogether, loading the whole collection.
|
|
213
|
+
if (scrollRestoration && rawData.length > 0) {
|
|
197
214
|
scrollRestoration.updateCollectionScroll({
|
|
198
215
|
fullPath: resolvedPath,
|
|
199
216
|
scrollOffset: collectionScroll?.scrollOffset ?? 0,
|
|
@@ -247,6 +264,7 @@ export function useDataSourceTableController<M extends Record<string, any> = any
|
|
|
247
264
|
collection.callbacks!.onFetch!({
|
|
248
265
|
collection,
|
|
249
266
|
path: resolvedPath,
|
|
267
|
+
pathSegments: resolvedPathSegments,
|
|
250
268
|
entity,
|
|
251
269
|
context
|
|
252
270
|
})));
|
|
@@ -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,
|
|
@@ -28,7 +29,7 @@ import {
|
|
|
28
29
|
useLargeLayout
|
|
29
30
|
} from "../hooks";
|
|
30
31
|
import { CircularProgress, cls, CodeIcon, defaultBorderMixin, Tab, Tabs, Typography, Menu, MenuItem, ExpandMoreIcon } from "@firecms/ui";
|
|
31
|
-
import { getEntityFromMemoryCache } from "../util/entity_cache";
|
|
32
|
+
import { entityCacheKey, getEntityFromMemoryCache } from "../util/entity_cache";
|
|
32
33
|
import { EntityForm, EntityFormProps } from "../form";
|
|
33
34
|
import { EntityEditViewFormActions } from "./EntityEditViewFormActions";
|
|
34
35
|
import { EntityJsonPreview } from "../components/EntityJsonPreview";
|
|
@@ -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>
|
|
@@ -115,7 +118,7 @@ export function EntityEditView<M extends Record<string, any>, USER extends User>
|
|
|
115
118
|
});
|
|
116
119
|
|
|
117
120
|
const initialDirtyValues = entityId
|
|
118
|
-
? getEntityFromMemoryCache(props.path
|
|
121
|
+
? getEntityFromMemoryCache(entityCacheKey(props.path, entityId))
|
|
119
122
|
: getEntityFromMemoryCache(props.path + "#new");
|
|
120
123
|
|
|
121
124
|
const authController = useAuthController();
|
|
@@ -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
|
|
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);
|