@firecms/core 3.4.0-canary.63041d0 → 3.4.0-canary.cfeed0b
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/EntityCollectionTable/internal/popup_field/PopupFormField.d.ts +3 -1
- package/dist/components/EntityCollectionView/EntityCollectionBoardView.d.ts +3 -1
- 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/EntityCollectionView/useBoardDataController.d.ts +3 -1
- package/dist/components/ReferenceTable/ReferenceSelectionTable.d.ts +10 -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 +8 -1
- package/dist/form/EntityFormActions.d.ts +2 -0
- package/dist/hooks/data/delete.d.ts +1 -1
- package/dist/hooks/data/useCollectionFetch.d.ts +7 -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 +967 -649
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +966 -648
- package/dist/index.umd.js.map +1 -1
- package/dist/types/collections.d.ts +7 -0
- package/dist/types/datasource.d.ts +25 -4
- package/dist/types/entities.d.ts +21 -1
- 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 +16 -0
- package/dist/util/paths.d.ts +28 -0
- package/dist/util/permissions.d.ts +10 -4
- package/package.json +4 -4
- package/src/components/DeleteEntityDialog.tsx +2 -0
- package/src/components/EntityCollectionTable/EntityCollectionRowActions.tsx +2 -2
- package/src/components/EntityCollectionTable/internal/popup_field/PopupFormField.tsx +5 -1
- package/src/components/EntityCollectionView/EntityCollectionBoardView.tsx +12 -0
- package/src/components/EntityCollectionView/EntityCollectionView.tsx +49 -16
- package/src/components/EntityCollectionView/EntityCollectionViewActions.tsx +6 -2
- package/src/components/EntityCollectionView/EntityCollectionViewStartActions.tsx +4 -0
- package/src/components/EntityCollectionView/useBoardDataController.tsx +17 -1
- package/src/components/EntityPreview.tsx +4 -1
- package/src/components/ReferenceTable/ReferenceSelectionTable.tsx +19 -2
- package/src/components/common/default_entity_actions.tsx +22 -2
- package/src/components/common/useDataSourceTableController.tsx +17 -5
- package/src/components/common/useTableSearchHelper.ts +5 -0
- package/src/core/EntityEditView.tsx +11 -12
- package/src/core/EntityEditViewFormActions.tsx +3 -2
- package/src/core/EntitySidePanel.tsx +6 -4
- package/src/form/EntityForm.tsx +30 -9
- package/src/form/EntityFormActions.tsx +2 -0
- package/src/hooks/data/delete.ts +8 -0
- package/src/hooks/data/save.ts +5 -2
- package/src/hooks/data/useCollectionFetch.tsx +21 -1
- package/src/hooks/data/useEntityFetch.tsx +28 -6
- package/src/hooks/useBuildNavigationController.tsx +23 -8
- package/src/hooks/useResolvedNavigationFrom.tsx +3 -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 +26 -15
- 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 +25 -4
- package/src/types/entities.ts +24 -1
- 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/entities.ts +3 -1
- 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 +42 -3
- package/src/util/paths.ts +40 -3
- package/src/util/permissions.ts +22 -10
|
@@ -42,6 +42,8 @@ export type EntityCollectionBoardViewProps<M extends Record<string, any> = any>
|
|
|
42
42
|
collection: EntityCollection<M>;
|
|
43
43
|
tableController: EntityTableController<M>;
|
|
44
44
|
fullPath: string;
|
|
45
|
+
/** `fullPath` split at its real segment boundaries; forwarded to the datasource. */
|
|
46
|
+
pathSegments?: string[];
|
|
45
47
|
parentCollectionIds?: string[];
|
|
46
48
|
columnProperty: string;
|
|
47
49
|
onEntityClick?: (entity: Entity<M>) => void;
|
|
@@ -60,6 +62,7 @@ export function EntityCollectionBoardView<M extends Record<string, any> = any>({
|
|
|
60
62
|
collection,
|
|
61
63
|
tableController,
|
|
62
64
|
fullPath,
|
|
65
|
+
pathSegments,
|
|
63
66
|
parentCollectionIds = [],
|
|
64
67
|
columnProperty,
|
|
65
68
|
onEntityClick,
|
|
@@ -169,6 +172,7 @@ export function EntityCollectionBoardView<M extends Record<string, any> = any>({
|
|
|
169
172
|
// Use the new per-column data controller
|
|
170
173
|
const boardDataController = useBoardDataController<M>({
|
|
171
174
|
fullPath,
|
|
175
|
+
pathSegments,
|
|
172
176
|
collection,
|
|
173
177
|
columnProperty,
|
|
174
178
|
columns,
|
|
@@ -270,6 +274,7 @@ export function EntityCollectionBoardView<M extends Record<string, any> = any>({
|
|
|
270
274
|
|
|
271
275
|
currentDataSource.countEntities({
|
|
272
276
|
path: fullPath,
|
|
277
|
+
pathSegments,
|
|
273
278
|
collection: currentCollection
|
|
274
279
|
}).then(count => {
|
|
275
280
|
totalCount = count;
|
|
@@ -281,6 +286,7 @@ export function EntityCollectionBoardView<M extends Record<string, any> = any>({
|
|
|
281
286
|
|
|
282
287
|
currentDataSource.countEntities({
|
|
283
288
|
path: fullPath,
|
|
289
|
+
pathSegments,
|
|
284
290
|
collection: currentCollection,
|
|
285
291
|
filter: { [orderProperty]: ["!=", null] } as FilterValues<string>
|
|
286
292
|
}).then(count => {
|
|
@@ -432,6 +438,8 @@ export function EntityCollectionBoardView<M extends Record<string, any> = any>({
|
|
|
432
438
|
|
|
433
439
|
const saveProps: SaveEntityProps = {
|
|
434
440
|
path: entity.path,
|
|
441
|
+
// Paired with `entity.path`: the segments the entity was loaded with.
|
|
442
|
+
pathSegments: entity.pathSegments,
|
|
435
443
|
entityId: entity.id,
|
|
436
444
|
values: updatedValues as M,
|
|
437
445
|
previousValues: entity.values,
|
|
@@ -472,6 +480,7 @@ export function EntityCollectionBoardView<M extends Record<string, any> = any>({
|
|
|
472
480
|
console.log("Fetching all documents from collection...");
|
|
473
481
|
const allDocs = await dataSource.fetchCollection<M>({
|
|
474
482
|
path: fullPath,
|
|
483
|
+
pathSegments,
|
|
475
484
|
collection,
|
|
476
485
|
limit: 10000 // Fetch all
|
|
477
486
|
});
|
|
@@ -492,6 +501,8 @@ export function EntityCollectionBoardView<M extends Record<string, any> = any>({
|
|
|
492
501
|
|
|
493
502
|
const saveProps: SaveEntityProps = {
|
|
494
503
|
path: entity.path,
|
|
504
|
+
// Paired with `entity.path`: the segments the entity was loaded with.
|
|
505
|
+
pathSegments: entity.pathSegments,
|
|
495
506
|
entityId: entity.id,
|
|
496
507
|
values: updatedValues as M,
|
|
497
508
|
previousValues: entity.values,
|
|
@@ -663,6 +674,7 @@ export function EntityCollectionBoardView<M extends Record<string, any> = any>({
|
|
|
663
674
|
});
|
|
664
675
|
sideEntityController.open({
|
|
665
676
|
path: fullPath,
|
|
677
|
+
pathSegments,
|
|
666
678
|
collection,
|
|
667
679
|
entityId: undefined,
|
|
668
680
|
updateUrl: true,
|
|
@@ -90,6 +90,7 @@ import { useSelectionController } from "./useSelectionController";
|
|
|
90
90
|
import { EntityCollectionViewStartActions } from "./EntityCollectionViewStartActions";
|
|
91
91
|
import { addRecentId, getRecentIds } from "./utils";
|
|
92
92
|
import { useScrollRestoration } from "../common/useScrollRestoration";
|
|
93
|
+
import { entityCacheKey } from "../../util/entity_cache";
|
|
93
94
|
|
|
94
95
|
const DEFAULT_ENTITY_OPEN_MODE: "side_panel" | "full_screen" = "side_panel";
|
|
95
96
|
|
|
@@ -109,8 +110,9 @@ export type EntityCollectionViewProps<M extends Record<string, any>> = {
|
|
|
109
110
|
/**
|
|
110
111
|
* `fullPath` split at its real segment boundaries, e.g. `["nodes", "node/42", "edges"]`.
|
|
111
112
|
* Forwarded to the datasource so a parent entity id containing "/" stays unambiguous.
|
|
112
|
-
*
|
|
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
|
];
|
|
@@ -789,6 +801,7 @@ export const EntityCollectionView = React.memo(
|
|
|
789
801
|
// EntitiesCount fetches count and updates breadcrumb - no visual rendering needed here
|
|
790
802
|
const countFetcher = <EntitiesCount
|
|
791
803
|
fullPath={fullPath}
|
|
804
|
+
pathSegments={pathSegments}
|
|
792
805
|
collection={collection}
|
|
793
806
|
filter={tableController.filterValues}
|
|
794
807
|
sortBy={tableController.sortBy}
|
|
@@ -842,6 +855,7 @@ export const EntityCollectionView = React.memo(
|
|
|
842
855
|
onTextSearchClick,
|
|
843
856
|
textSearchEnabled
|
|
844
857
|
} = useTableSearchHelper({
|
|
858
|
+
pathSegments,
|
|
845
859
|
collection,
|
|
846
860
|
fullPath: resolvedFullPath,
|
|
847
861
|
parentCollectionIds,
|
|
@@ -906,6 +920,7 @@ export const EntityCollectionView = React.memo(
|
|
|
906
920
|
collection={collection}
|
|
907
921
|
tableController={tableController}
|
|
908
922
|
path={fullPath}
|
|
923
|
+
pathSegments={pathSegments}
|
|
909
924
|
relativePath={collection.path}
|
|
910
925
|
selectionController={usedSelectionController}
|
|
911
926
|
collectionEntitiesCount={docsCount}
|
|
@@ -917,6 +932,7 @@ export const EntityCollectionView = React.memo(
|
|
|
917
932
|
onMultipleDeleteClick={onMultipleDeleteClick}
|
|
918
933
|
onNewClick={onNewClick}
|
|
919
934
|
path={fullPath}
|
|
935
|
+
pathSegments={pathSegments}
|
|
920
936
|
relativePath={collection.path}
|
|
921
937
|
selectionController={usedSelectionController}
|
|
922
938
|
selectionEnabled={selectionEnabled}
|
|
@@ -933,6 +949,7 @@ export const EntityCollectionView = React.memo(
|
|
|
933
949
|
collection={collection}
|
|
934
950
|
tableController={tableController}
|
|
935
951
|
fullPath={fullPath}
|
|
952
|
+
pathSegments={pathSegments}
|
|
936
953
|
parentCollectionIds={parentCollectionIds}
|
|
937
954
|
columnProperty={selectedKanbanProperty}
|
|
938
955
|
onEntityClick={onEntityClick}
|
|
@@ -1024,6 +1041,7 @@ export const EntityCollectionView = React.memo(
|
|
|
1024
1041
|
getIdColumnWidth={getIdColumnWidth}
|
|
1025
1042
|
additionalIDHeaderWidget={<EntityIdHeaderWidget
|
|
1026
1043
|
path={fullPath}
|
|
1044
|
+
pathSegments={pathSegments}
|
|
1027
1045
|
fullIdPath={fullIdPath ?? fullPath}
|
|
1028
1046
|
collection={collection} />}
|
|
1029
1047
|
openEntityMode={openEntityMode}
|
|
@@ -1063,6 +1081,7 @@ export const EntityCollectionView = React.memo(
|
|
|
1063
1081
|
|
|
1064
1082
|
{popupCell && <PopupFormField
|
|
1065
1083
|
key={`popup_form_${popupCell?.propertyKey}_${popupCell?.entityId}`}
|
|
1084
|
+
pathSegments={pathSegments}
|
|
1066
1085
|
open={Boolean(popupCell)}
|
|
1067
1086
|
onClose={onPopupClose}
|
|
1068
1087
|
cellRect={popupCell?.cellRect}
|
|
@@ -1118,13 +1137,16 @@ function EntitiesCount({
|
|
|
1118
1137
|
collection,
|
|
1119
1138
|
filter,
|
|
1120
1139
|
sortBy,
|
|
1121
|
-
onCountChange
|
|
1140
|
+
onCountChange,
|
|
1141
|
+
pathSegments
|
|
1122
1142
|
}: {
|
|
1123
1143
|
fullPath: string,
|
|
1124
1144
|
collection: EntityCollection,
|
|
1125
1145
|
filter?: FilterValues<any>,
|
|
1126
1146
|
sortBy?: [string, "asc" | "desc"],
|
|
1127
1147
|
onCountChange?: (count: number) => void,
|
|
1148
|
+
/** `fullPath` split at its real segment boundaries; forwarded to countEntities. */
|
|
1149
|
+
pathSegments?: string[],
|
|
1128
1150
|
}) {
|
|
1129
1151
|
|
|
1130
1152
|
const dataSource = useDataSource(collection);
|
|
@@ -1134,18 +1156,24 @@ function EntitiesCount({
|
|
|
1134
1156
|
|
|
1135
1157
|
const sortByProperty = sortBy ? sortBy[0] : undefined;
|
|
1136
1158
|
const currentSort = sortBy ? sortBy[1] : undefined;
|
|
1137
|
-
|
|
1159
|
+
// Resolved together so the two representations always describe the same chain.
|
|
1160
|
+
// Segments are resolved, never derived (see useEntityFetch).
|
|
1161
|
+
const resolvedPath = useMemo(() => navigation.resolveIdsFrom(fullPath, pathSegments), [fullPath, pathSegments, navigation.resolveIdsFrom]);
|
|
1162
|
+
const resolvedPathSegments = useMemo(() => pathSegments
|
|
1163
|
+
? (navigation.resolveSegmentsFrom?.(pathSegments) ?? pathSegments)
|
|
1164
|
+
: undefined, [pathSegments, navigation.resolveSegmentsFrom]);
|
|
1138
1165
|
|
|
1139
1166
|
useEffect(() => {
|
|
1140
1167
|
if (dataSource.countEntities)
|
|
1141
1168
|
dataSource.countEntities({
|
|
1142
1169
|
path: resolvedPath,
|
|
1170
|
+
pathSegments: resolvedPathSegments,
|
|
1143
1171
|
collection,
|
|
1144
1172
|
filter,
|
|
1145
1173
|
orderBy: sortByProperty,
|
|
1146
1174
|
order: currentSort
|
|
1147
1175
|
}).then(setCount).catch(setError);
|
|
1148
|
-
}, [fullPath, dataSource.countEntities, resolvedPath, collection, filter, sortByProperty, currentSort]);
|
|
1176
|
+
}, [fullPath, dataSource.countEntities, resolvedPath, resolvedPathSegments, collection, filter, sortByProperty, currentSort]);
|
|
1149
1177
|
|
|
1150
1178
|
useEffect(() => {
|
|
1151
1179
|
if (onCountChange && count !== undefined) {
|
|
@@ -1174,10 +1202,13 @@ function buildPropertyWidthOverwrite(key: string, width: number): PartialEntityC
|
|
|
1174
1202
|
function EntityIdHeaderWidget({
|
|
1175
1203
|
collection,
|
|
1176
1204
|
path,
|
|
1205
|
+
pathSegments,
|
|
1177
1206
|
fullIdPath
|
|
1178
1207
|
}: {
|
|
1179
1208
|
collection: EntityCollection,
|
|
1180
1209
|
path: string,
|
|
1210
|
+
/** `path` split at its real segment boundaries, when the caller knows them. */
|
|
1211
|
+
pathSegments?: string[],
|
|
1181
1212
|
fullIdPath: string
|
|
1182
1213
|
}) {
|
|
1183
1214
|
|
|
@@ -1217,6 +1248,7 @@ function EntityIdHeaderWidget({
|
|
|
1217
1248
|
collection,
|
|
1218
1249
|
entityId,
|
|
1219
1250
|
path,
|
|
1251
|
+
pathSegments,
|
|
1220
1252
|
fullIdPath,
|
|
1221
1253
|
sideEntityController,
|
|
1222
1254
|
navigation
|
|
@@ -1242,7 +1274,7 @@ function EntityIdHeaderWidget({
|
|
|
1242
1274
|
</form>
|
|
1243
1275
|
{recentIds && recentIds.length > 0 && <div className="flex flex-col gap-2 p-2">
|
|
1244
1276
|
{recentIds.map(id => (
|
|
1245
|
-
<ReferencePreview reference={new EntityReference(id, path)}
|
|
1277
|
+
<ReferencePreview reference={new EntityReference(id, path, undefined, pathSegments)}
|
|
1246
1278
|
key={id}
|
|
1247
1279
|
hover={true}
|
|
1248
1280
|
onClick={() => {
|
|
@@ -1252,6 +1284,7 @@ function EntityIdHeaderWidget({
|
|
|
1252
1284
|
collection,
|
|
1253
1285
|
entityId: id,
|
|
1254
1286
|
path,
|
|
1287
|
+
pathSegments,
|
|
1255
1288
|
fullIdPath,
|
|
1256
1289
|
sideEntityController,
|
|
1257
1290
|
navigation
|
|
@@ -22,6 +22,8 @@ import { useTranslation } from "../../hooks/useTranslation";
|
|
|
22
22
|
export type EntityCollectionViewActionsProps<M extends Record<string, any>> = {
|
|
23
23
|
collection: EntityCollection<M>;
|
|
24
24
|
path: string;
|
|
25
|
+
/** `path` split at its real segment boundaries, when known. Never derived from `path`. */
|
|
26
|
+
pathSegments?: string[];
|
|
25
27
|
relativePath: string;
|
|
26
28
|
parentCollectionIds: string[];
|
|
27
29
|
selectionEnabled: boolean;
|
|
@@ -40,6 +42,7 @@ export function EntityCollectionViewActions<M extends Record<string, any>>({
|
|
|
40
42
|
onMultipleDeleteClick,
|
|
41
43
|
selectionEnabled,
|
|
42
44
|
path,
|
|
45
|
+
pathSegments,
|
|
43
46
|
selectionController,
|
|
44
47
|
tableController,
|
|
45
48
|
collectionEntitiesCount,
|
|
@@ -56,7 +59,7 @@ export function EntityCollectionViewActions<M extends Record<string, any>>({
|
|
|
56
59
|
|
|
57
60
|
const selectedEntities = selectionController.selectedEntities;
|
|
58
61
|
|
|
59
|
-
const addButton = canCreateEntity(collection, authController, path, null) &&
|
|
62
|
+
const addButton = canCreateEntity(collection, authController, path, null, pathSegments) &&
|
|
60
63
|
onNewClick && (largeLayout
|
|
61
64
|
? <Button
|
|
62
65
|
id={`add_entity_${path}`}
|
|
@@ -75,7 +78,7 @@ export function EntityCollectionViewActions<M extends Record<string, any>>({
|
|
|
75
78
|
<AddIcon size={"small"} />
|
|
76
79
|
</Button>);
|
|
77
80
|
|
|
78
|
-
const multipleDeleteEnabled = canDeleteEntity(collection, authController, path, null);
|
|
81
|
+
const multipleDeleteEnabled = canDeleteEntity(collection, authController, path, null, pathSegments);
|
|
79
82
|
|
|
80
83
|
let multipleDeleteButton: React.ReactNode | undefined;
|
|
81
84
|
if (selectionEnabled) {
|
|
@@ -107,6 +110,7 @@ export function EntityCollectionViewActions<M extends Record<string, any>>({
|
|
|
107
110
|
|
|
108
111
|
const actionProps: CollectionActionsProps = {
|
|
109
112
|
path,
|
|
113
|
+
pathSegments,
|
|
110
114
|
relativePath,
|
|
111
115
|
parentCollectionIds,
|
|
112
116
|
collection,
|
|
@@ -17,6 +17,8 @@ import { useTranslation } from "../../hooks/useTranslation";
|
|
|
17
17
|
export type EntityCollectionViewStartActionsProps<M extends Record<string, any>> = {
|
|
18
18
|
collection: EntityCollection<M>;
|
|
19
19
|
path: string;
|
|
20
|
+
/** `path` split at its real segment boundaries, when known. Never derived from `path`. */
|
|
21
|
+
pathSegments?: string[];
|
|
20
22
|
relativePath: string;
|
|
21
23
|
parentCollectionIds: string[];
|
|
22
24
|
selectionController: SelectionController<M>;
|
|
@@ -33,6 +35,7 @@ export function EntityCollectionViewStartActions<M extends Record<string, any>>(
|
|
|
33
35
|
relativePath,
|
|
34
36
|
parentCollectionIds,
|
|
35
37
|
path,
|
|
38
|
+
pathSegments,
|
|
36
39
|
selectionController,
|
|
37
40
|
tableController,
|
|
38
41
|
collectionEntitiesCount,
|
|
@@ -57,6 +60,7 @@ export function EntityCollectionViewStartActions<M extends Record<string, any>>(
|
|
|
57
60
|
|
|
58
61
|
const actionProps: CollectionActionsProps = {
|
|
59
62
|
path,
|
|
63
|
+
pathSegments,
|
|
60
64
|
relativePath,
|
|
61
65
|
parentCollectionIds,
|
|
62
66
|
collection,
|
|
@@ -45,6 +45,8 @@ export interface BoardDataController<M extends Record<string, any> = any, COLUMN
|
|
|
45
45
|
export interface UseBoardDataControllerProps<M extends Record<string, any> = any> {
|
|
46
46
|
/** Full path to the collection */
|
|
47
47
|
fullPath: string;
|
|
48
|
+
/** `fullPath` split at its real segment boundaries; forwarded to the datasource. */
|
|
49
|
+
pathSegments?: string[];
|
|
48
50
|
/** The entity collection configuration */
|
|
49
51
|
collection: EntityCollection<M>;
|
|
50
52
|
/** Property key used for column assignment */
|
|
@@ -67,6 +69,7 @@ export interface UseBoardDataControllerProps<M extends Record<string, any> = any
|
|
|
67
69
|
*/
|
|
68
70
|
export function useBoardDataController<M extends Record<string, any> = any, COLUMN extends string = string>({
|
|
69
71
|
fullPath,
|
|
72
|
+
pathSegments,
|
|
70
73
|
collection,
|
|
71
74
|
columnProperty,
|
|
72
75
|
columns,
|
|
@@ -79,7 +82,12 @@ export function useBoardDataController<M extends Record<string, any> = any, COLU
|
|
|
79
82
|
const context = useFireCMSContext();
|
|
80
83
|
const dataSource = useDataSource(collection);
|
|
81
84
|
const navigation = useNavigationController();
|
|
82
|
-
|
|
85
|
+
// Resolved together so the two representations always describe the same chain.
|
|
86
|
+
// Segments are resolved, never derived (see useEntityFetch).
|
|
87
|
+
const resolvedPath = useMemo(() => navigation.resolveIdsFrom(fullPath, pathSegments), [fullPath, pathSegments, navigation.resolveIdsFrom]);
|
|
88
|
+
const resolvedPathSegments = useMemo(() => pathSegments
|
|
89
|
+
? (navigation.resolveSegmentsFrom?.(pathSegments) ?? pathSegments)
|
|
90
|
+
: undefined, [pathSegments, navigation.resolveSegmentsFrom]);
|
|
83
91
|
|
|
84
92
|
// Stable refs for objects that shouldn't trigger re-subscriptions
|
|
85
93
|
const dataSourceRef = useRef(dataSource);
|
|
@@ -95,11 +103,13 @@ export function useBoardDataController<M extends Record<string, any> = any, COLU
|
|
|
95
103
|
const orderPropertyRef = useRef(orderProperty);
|
|
96
104
|
const searchStringRef = useRef(searchString);
|
|
97
105
|
const resolvedPathRef = useRef(resolvedPath);
|
|
106
|
+
const resolvedPathSegmentsRef = useRef(resolvedPathSegments);
|
|
98
107
|
filterValuesRef.current = filterValues;
|
|
99
108
|
columnPropertyRef.current = columnProperty;
|
|
100
109
|
orderPropertyRef.current = orderProperty;
|
|
101
110
|
searchStringRef.current = searchString;
|
|
102
111
|
resolvedPathRef.current = resolvedPath;
|
|
112
|
+
resolvedPathSegmentsRef.current = resolvedPathSegments;
|
|
103
113
|
|
|
104
114
|
// Track item count per column for pagination
|
|
105
115
|
const [columnItemCounts, setColumnItemCounts] = useState<Record<string, number>>(() => {
|
|
@@ -168,6 +178,7 @@ export function useBoardDataController<M extends Record<string, any> = any, COLU
|
|
|
168
178
|
const currentOrderProperty = orderPropertyRef.current;
|
|
169
179
|
const currentSearchString = searchStringRef.current;
|
|
170
180
|
const currentResolvedPath = resolvedPathRef.current;
|
|
181
|
+
const currentPathSegments = resolvedPathSegmentsRef.current;
|
|
171
182
|
|
|
172
183
|
// Build filter for this column
|
|
173
184
|
const columnFilter: FilterValues<string> = {
|
|
@@ -205,6 +216,7 @@ export function useBoardDataController<M extends Record<string, any> = any, COLU
|
|
|
205
216
|
currentCollection.callbacks!.onFetch!({
|
|
206
217
|
collection: currentCollection,
|
|
207
218
|
path: currentResolvedPath,
|
|
219
|
+
pathSegments: currentPathSegments,
|
|
208
220
|
entity,
|
|
209
221
|
context: currentContext
|
|
210
222
|
})
|
|
@@ -248,6 +260,7 @@ export function useBoardDataController<M extends Record<string, any> = any, COLU
|
|
|
248
260
|
if (currentDataSource.listenCollection) {
|
|
249
261
|
const unsubscribe = currentDataSource.listenCollection<M>({
|
|
250
262
|
path: currentResolvedPath,
|
|
263
|
+
pathSegments: currentPathSegments,
|
|
251
264
|
collection: currentCollection,
|
|
252
265
|
onUpdate,
|
|
253
266
|
onError,
|
|
@@ -262,6 +275,7 @@ export function useBoardDataController<M extends Record<string, any> = any, COLU
|
|
|
262
275
|
} else {
|
|
263
276
|
currentDataSource.fetchCollection<M>({
|
|
264
277
|
path: currentResolvedPath,
|
|
278
|
+
pathSegments: currentPathSegments,
|
|
265
279
|
collection: currentCollection,
|
|
266
280
|
searchString: currentSearchString,
|
|
267
281
|
filter: columnFilter,
|
|
@@ -297,6 +311,7 @@ export function useBoardDataController<M extends Record<string, any> = any, COLU
|
|
|
297
311
|
const currentColumnProperty = columnPropertyRef.current;
|
|
298
312
|
const currentSearchString = searchStringRef.current;
|
|
299
313
|
const currentResolvedPath = resolvedPathRef.current;
|
|
314
|
+
const currentPathSegments = resolvedPathSegmentsRef.current;
|
|
300
315
|
const currentColumns = columns;
|
|
301
316
|
const currentColumnItemCounts = columnItemCounts;
|
|
302
317
|
|
|
@@ -316,6 +331,7 @@ export function useBoardDataController<M extends Record<string, any> = any, COLU
|
|
|
316
331
|
};
|
|
317
332
|
currentDataSource.countEntities({
|
|
318
333
|
path: currentResolvedPath,
|
|
334
|
+
pathSegments: currentPathSegments,
|
|
319
335
|
collection: currentCollection,
|
|
320
336
|
filter: columnFilter,
|
|
321
337
|
searchString: currentSearchString
|
|
@@ -63,7 +63,7 @@ export function EntityPreview({
|
|
|
63
63
|
|
|
64
64
|
const navigationController = useNavigationController();
|
|
65
65
|
|
|
66
|
-
const collection = collectionProp ?? navigationController.getCollection(entity.path);
|
|
66
|
+
const collection = collectionProp ?? navigationController.getCollection(entity.path, false, entity.pathSegments);
|
|
67
67
|
|
|
68
68
|
if (!collection) {
|
|
69
69
|
throw Error(`Couldn't find the corresponding collection view for the path: ${entity.path}`);
|
|
@@ -177,6 +177,9 @@ export function EntityPreview({
|
|
|
177
177
|
sideEntityController.open({
|
|
178
178
|
entityId: entity.id,
|
|
179
179
|
path: entity.path,
|
|
180
|
+
// Paired with `entity.path` above; undefined if the entity was
|
|
181
|
+
// loaded without them.
|
|
182
|
+
pathSegments: entity.pathSegments,
|
|
180
183
|
collection,
|
|
181
184
|
updateUrl: true
|
|
182
185
|
});
|
|
@@ -44,6 +44,15 @@ export interface ReferenceSelectionInnerProps<M extends Record<string, any>> {
|
|
|
44
44
|
* dynamic. If not set, the dialog won't open.
|
|
45
45
|
*/
|
|
46
46
|
path: string;
|
|
47
|
+
/**
|
|
48
|
+
* `path` split at its real segment boundaries, when the caller knows them.
|
|
49
|
+
*
|
|
50
|
+
* Left undefined by default and deliberately NOT derived from `path`: a reference
|
|
51
|
+
* path comes from a property's configuration, so there is no entity chain to recover
|
|
52
|
+
* the boundaries from. Splitting it would silently produce the wrong segments for a
|
|
53
|
+
* subcollection under a slash-bearing parent id, which is worse than not answering.
|
|
54
|
+
*/
|
|
55
|
+
pathSegments?: string[];
|
|
47
56
|
|
|
48
57
|
/**
|
|
49
58
|
* If you are opening the dialog for the first time, you can select some
|
|
@@ -96,6 +105,7 @@ export function ReferenceSelectionTable<M extends Record<string, any>>(
|
|
|
96
105
|
multiselect,
|
|
97
106
|
collection,
|
|
98
107
|
path: pathInput,
|
|
108
|
+
pathSegments,
|
|
99
109
|
selectedEntityIds: selectedEntityIdsProp,
|
|
100
110
|
description,
|
|
101
111
|
forceFilter,
|
|
@@ -110,7 +120,7 @@ export function ReferenceSelectionTable<M extends Record<string, any>>(
|
|
|
110
120
|
const analyticsController = useAnalyticsController();
|
|
111
121
|
const customizationController = useCustomizationController();
|
|
112
122
|
|
|
113
|
-
const fullPath = navigation.resolveIdsFrom(pathInput);
|
|
123
|
+
const fullPath = navigation.resolveIdsFrom(pathInput, pathSegments);
|
|
114
124
|
|
|
115
125
|
const dataSource = useDataSource(collection);
|
|
116
126
|
|
|
@@ -153,6 +163,7 @@ export function ReferenceSelectionTable<M extends Record<string, any>>(
|
|
|
153
163
|
selectedEntityIds.map((entityId) =>
|
|
154
164
|
dataSource.fetchEntity({
|
|
155
165
|
path: fullPath,
|
|
166
|
+
pathSegments,
|
|
156
167
|
entityId,
|
|
157
168
|
collection
|
|
158
169
|
})))
|
|
@@ -204,6 +215,7 @@ export function ReferenceSelectionTable<M extends Record<string, any>>(
|
|
|
204
215
|
});
|
|
205
216
|
sideEntityController.open({
|
|
206
217
|
path: fullPath,
|
|
218
|
+
pathSegments,
|
|
207
219
|
collection,
|
|
208
220
|
updateUrl: true,
|
|
209
221
|
onUpdate: ({ entity }) => {
|
|
@@ -277,6 +289,7 @@ export function ReferenceSelectionTable<M extends Record<string, any>>(
|
|
|
277
289
|
textSearchEnabled
|
|
278
290
|
} =
|
|
279
291
|
useTableSearchHelper({
|
|
292
|
+
pathSegments,
|
|
280
293
|
collection,
|
|
281
294
|
fullPath,
|
|
282
295
|
});
|
|
@@ -315,6 +328,7 @@ export function ReferenceSelectionTable<M extends Record<string, any>>(
|
|
|
315
328
|
actions={<ReferenceDialogActions
|
|
316
329
|
collection={collection}
|
|
317
330
|
path={fullPath}
|
|
331
|
+
pathSegments={pathSegments}
|
|
318
332
|
onNewClick={onNewClick}
|
|
319
333
|
onClear={onClear}/>
|
|
320
334
|
}
|
|
@@ -341,11 +355,14 @@ export function ReferenceSelectionTable<M extends Record<string, any>>(
|
|
|
341
355
|
function ReferenceDialogActions({
|
|
342
356
|
collection,
|
|
343
357
|
path,
|
|
358
|
+
pathSegments,
|
|
344
359
|
onClear,
|
|
345
360
|
onNewClick
|
|
346
361
|
}: {
|
|
347
362
|
collection: EntityCollection<any>,
|
|
348
363
|
path: string,
|
|
364
|
+
/** `path` split at its real segment boundaries, when known. Never derived from `path`. */
|
|
365
|
+
pathSegments?: string[],
|
|
349
366
|
onClear: () => void,
|
|
350
367
|
onNewClick: () => void
|
|
351
368
|
}) {
|
|
@@ -360,7 +377,7 @@ function ReferenceDialogActions({
|
|
|
360
377
|
onNewClick();
|
|
361
378
|
}
|
|
362
379
|
: undefined;
|
|
363
|
-
const addButton = canCreateEntity(collection, authController, path, null) &&
|
|
380
|
+
const addButton = canCreateEntity(collection, authController, path, null, pathSegments) &&
|
|
364
381
|
onClick && (largeLayout
|
|
365
382
|
? <Button
|
|
366
383
|
onClick={onClick}
|
|
@@ -18,6 +18,7 @@ export const editEntityAction: EntityAction = {
|
|
|
18
18
|
entity,
|
|
19
19
|
collection,
|
|
20
20
|
fullPath,
|
|
21
|
+
pathSegments,
|
|
21
22
|
fullIdPath,
|
|
22
23
|
context,
|
|
23
24
|
highlightEntity,
|
|
@@ -41,6 +42,12 @@ export const editEntityAction: EntityAction = {
|
|
|
41
42
|
}
|
|
42
43
|
|
|
43
44
|
const path = collection?.collectionGroup ? entity.path : (fullPath ?? collection?.path ?? entity.path);
|
|
45
|
+
// In a collection group `path` is the entity's own, so the entity's segments describe
|
|
46
|
+
// it. Otherwise the caller's segments win, falling back to the ones the entity was
|
|
47
|
+
// loaded with — both describe the same collection. Never derived from `path`.
|
|
48
|
+
const resolvedPathSegments = collection?.collectionGroup
|
|
49
|
+
? entity.pathSegments
|
|
50
|
+
: (pathSegments ?? entity.pathSegments);
|
|
44
51
|
const newFullIdPath = collection?.collectionGroup ? collection.id : (fullIdPath ?? collection?.id ?? entity.path);
|
|
45
52
|
const defaultSelectedView = resolveDefaultSelectedView(
|
|
46
53
|
collection ? collection.defaultSelectedView : undefined,
|
|
@@ -54,6 +61,7 @@ export const editEntityAction: EntityAction = {
|
|
|
54
61
|
collection,
|
|
55
62
|
entityId: entity.id,
|
|
56
63
|
path,
|
|
64
|
+
pathSegments: resolvedPathSegments,
|
|
57
65
|
fullIdPath: newFullIdPath,
|
|
58
66
|
sideEntityController: context.sideEntityController,
|
|
59
67
|
onClose: () => unhighlightEntity?.(entity),
|
|
@@ -75,6 +83,8 @@ export const copyEntityAction: EntityAction = {
|
|
|
75
83
|
collection,
|
|
76
84
|
context,
|
|
77
85
|
fullPath,
|
|
86
|
+
pathSegments,
|
|
87
|
+
fullIdPath,
|
|
78
88
|
highlightEntity,
|
|
79
89
|
unhighlightEntity,
|
|
80
90
|
openEntityMode
|
|
@@ -89,13 +99,23 @@ export const copyEntityAction: EntityAction = {
|
|
|
89
99
|
});
|
|
90
100
|
|
|
91
101
|
const path = collection?.collectionGroup ? collection.path : (fullPath ?? collection?.path ?? entity.path);
|
|
92
|
-
|
|
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),
|