@firecms/core 3.4.0-canary.63041d0 → 3.4.0-canary.d20cc85
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/useBoardDataController.d.ts +3 -1
- package/dist/components/ReferenceTable/ReferenceSelectionTable.d.ts +10 -1
- package/dist/form/EntityForm.d.ts +6 -1
- package/dist/hooks/data/useCollectionFetch.d.ts +7 -1
- package/dist/index.es.js +177 -112
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +177 -112
- package/dist/index.umd.js.map +1 -1
- package/dist/types/datasource.d.ts +17 -4
- package/dist/types/entities.d.ts +21 -1
- package/dist/util/parent_references_from_path.d.ts +1 -0
- package/package.json +4 -4
- package/src/components/EntityCollectionTable/internal/popup_field/PopupFormField.tsx +5 -1
- package/src/components/EntityCollectionView/EntityCollectionBoardView.tsx +4 -0
- package/src/components/EntityCollectionView/EntityCollectionView.tsx +10 -2
- package/src/components/EntityCollectionView/useBoardDataController.tsx +11 -0
- package/src/components/ReferenceTable/ReferenceSelectionTable.tsx +11 -0
- package/src/components/common/useDataSourceTableController.tsx +8 -2
- package/src/core/EntityEditView.tsx +5 -2
- package/src/form/EntityForm.tsx +8 -2
- package/src/hooks/data/delete.ts +2 -0
- package/src/hooks/data/save.ts +1 -1
- package/src/hooks/data/useCollectionFetch.tsx +13 -0
- package/src/hooks/data/useEntityFetch.tsx +10 -1
- package/src/hooks/useResolvedNavigationFrom.tsx +2 -0
- package/src/internal/useBuildDataSource.ts +14 -4
- package/src/types/datasource.ts +17 -4
- package/src/types/entities.ts +24 -1
- package/src/util/entities.ts +3 -1
- package/src/util/parent_references_from_path.ts +10 -2
|
@@ -96,6 +96,11 @@ export interface SaveEntityProps<M extends Record<string, any> = any> {
|
|
|
96
96
|
*/
|
|
97
97
|
export interface DeleteEntityProps<M extends Record<string, any> = any> {
|
|
98
98
|
entity: Entity<M>;
|
|
99
|
+
/**
|
|
100
|
+
* `entity.path` split at its real segment boundaries, when the caller knows them.
|
|
101
|
+
* Never derived from `entity.path` — see `pathSegments` on FetchCollectionProps.
|
|
102
|
+
*/
|
|
103
|
+
pathSegments?: string[];
|
|
99
104
|
collection?: EntityCollection<M> | ResolvedEntityCollection<M>;
|
|
100
105
|
}
|
|
101
106
|
/**
|
|
@@ -175,11 +180,17 @@ export interface DataSource {
|
|
|
175
180
|
* @param entityId
|
|
176
181
|
* @return `true` if there are no other fields besides the given entity
|
|
177
182
|
*/
|
|
178
|
-
checkUniqueField(path: string, name: string, value: any, entityId?: string, collection?: EntityCollection
|
|
183
|
+
checkUniqueField(path: string, name: string, value: any, entityId?: string, collection?: EntityCollection,
|
|
184
|
+
/**
|
|
185
|
+
* Appended last, and optional, so existing implementations and callers keep
|
|
186
|
+
* working unchanged — a function declaring fewer parameters stays assignable.
|
|
187
|
+
* See `pathSegments` on FetchCollectionProps.
|
|
188
|
+
*/
|
|
189
|
+
pathSegments?: string[]): Promise<boolean>;
|
|
179
190
|
/**
|
|
180
191
|
* Generate an id for a new entity
|
|
181
192
|
*/
|
|
182
|
-
generateEntityId(path: string, collection: EntityCollection): string;
|
|
193
|
+
generateEntityId(path: string, collection: EntityCollection, pathSegments?: string[]): string;
|
|
183
194
|
/**
|
|
184
195
|
* Count the number of entities in a collection
|
|
185
196
|
*/
|
|
@@ -202,6 +213,8 @@ export interface DataSource {
|
|
|
202
213
|
}) => Promise<boolean>;
|
|
203
214
|
}
|
|
204
215
|
export type FilterCombinationValidProps = {
|
|
216
|
+
/** `path` split at its real segment boundaries — see `pathSegments` on FetchCollectionProps. */
|
|
217
|
+
pathSegments?: string[];
|
|
205
218
|
path: string;
|
|
206
219
|
collection: EntityCollection<any>;
|
|
207
220
|
filterValues: FilterValues<any>;
|
|
@@ -287,11 +300,11 @@ export interface DataSourceDelegate {
|
|
|
287
300
|
* @param collection
|
|
288
301
|
* @return `true` if there are no other fields besides the given entity
|
|
289
302
|
*/
|
|
290
|
-
checkUniqueField(path: string, name: string, value: any, entityId?: string, collection?: EntityCollection): Promise<boolean>;
|
|
303
|
+
checkUniqueField(path: string, name: string, value: any, entityId?: string, collection?: EntityCollection, pathSegments?: string[]): Promise<boolean>;
|
|
291
304
|
/**
|
|
292
305
|
* Generate an id for a new entity
|
|
293
306
|
*/
|
|
294
|
-
generateEntityId(path: string, collection?: EntityCollection): string;
|
|
307
|
+
generateEntityId(path: string, collection?: EntityCollection, pathSegments?: string[]): string;
|
|
295
308
|
/**
|
|
296
309
|
* Count the number of entities in a collection
|
|
297
310
|
*/
|
package/dist/types/entities.d.ts
CHANGED
|
@@ -17,6 +17,16 @@ export interface Entity<M extends object = any> {
|
|
|
17
17
|
* to the root of the database).
|
|
18
18
|
*/
|
|
19
19
|
path: string;
|
|
20
|
+
/**
|
|
21
|
+
* `path` split at its real segment boundaries, e.g. `["nodes", "node/42", "edges"]`.
|
|
22
|
+
*
|
|
23
|
+
* `path` is a single flattened string, so a parent entity id containing "/" cannot be
|
|
24
|
+
* recovered from it. This keeps each segment whole.
|
|
25
|
+
*
|
|
26
|
+
* Optional, and never derived by splitting `path` — a guess would be wrong in exactly
|
|
27
|
+
* the case the field exists for. Absent means "not known here", not "no slashes".
|
|
28
|
+
*/
|
|
29
|
+
pathSegments?: string[];
|
|
20
30
|
/**
|
|
21
31
|
* Current values
|
|
22
32
|
*/
|
|
@@ -46,7 +56,17 @@ export declare class EntityReference {
|
|
|
46
56
|
* Optional database ID where the entity is stored (if multiple databases are used)
|
|
47
57
|
*/
|
|
48
58
|
readonly databaseId?: string;
|
|
49
|
-
|
|
59
|
+
/**
|
|
60
|
+
* `path` split at its real segment boundaries, e.g. `["nodes", "node/42", "edges"]`.
|
|
61
|
+
*
|
|
62
|
+
* `path` is a single flattened string, so a parent entity id containing "/" cannot be
|
|
63
|
+
* recovered from it. This keeps each segment whole.
|
|
64
|
+
*
|
|
65
|
+
* Optional, and never derived by splitting `path` — a guess would be wrong in exactly
|
|
66
|
+
* the case the field exists for. Absent means "not known here", not "no slashes".
|
|
67
|
+
*/
|
|
68
|
+
readonly pathSegments?: string[];
|
|
69
|
+
constructor(id: string, path: string, databaseId?: string, pathSegments?: string[]);
|
|
50
70
|
get pathWithId(): string;
|
|
51
71
|
get pathWithIdAndDatabase(): string;
|
|
52
72
|
isEntityReference(): boolean;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@firecms/core",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "3.4.0-canary.
|
|
4
|
+
"version": "3.4.0-canary.d20cc85",
|
|
5
5
|
"description": "Awesome Firebase/Firestore-based headless open-source CMS",
|
|
6
6
|
"funding": {
|
|
7
7
|
"url": "https://github.com/sponsors/firecmsco"
|
|
@@ -53,8 +53,8 @@
|
|
|
53
53
|
"@dnd-kit/core": "^6.3.1",
|
|
54
54
|
"@dnd-kit/modifiers": "^9.0.0",
|
|
55
55
|
"@dnd-kit/sortable": "^10.0.0",
|
|
56
|
-
"@firecms/formex": "3.4.0-canary.
|
|
57
|
-
"@firecms/ui": "3.4.0-canary.
|
|
56
|
+
"@firecms/formex": "3.4.0-canary.d20cc85",
|
|
57
|
+
"@firecms/ui": "3.4.0-canary.d20cc85",
|
|
58
58
|
"@floating-ui/dom": "^1.7.4",
|
|
59
59
|
"@radix-ui/react-portal": "^1.1.10",
|
|
60
60
|
"@radix-ui/react-slot": "^1.2.4",
|
|
@@ -89,7 +89,7 @@
|
|
|
89
89
|
"prosemirror-view": "^1.33.8",
|
|
90
90
|
"react-dropzone": "^14.3.8",
|
|
91
91
|
"react-fast-compare": "^3.2.2",
|
|
92
|
-
"react-i18next": "^
|
|
92
|
+
"react-i18next": "^17.0.11",
|
|
93
93
|
"react-image-crop": "^11.0.10",
|
|
94
94
|
"react-markdown": "^9.1.0",
|
|
95
95
|
"react-moveable": "^0.56.0",
|
|
@@ -26,6 +26,8 @@ import { OnCellValueChangeParams } from "../../../common";
|
|
|
26
26
|
interface PopupFormFieldProps<M extends Record<string, any>> {
|
|
27
27
|
customFieldValidator?: CustomFieldValidator;
|
|
28
28
|
path: string;
|
|
29
|
+
/** `path` split at its real segment boundaries; forwarded to the datasource. */
|
|
30
|
+
pathSegments?: string[];
|
|
29
31
|
entityId: string;
|
|
30
32
|
tableKey: string;
|
|
31
33
|
propertyKey?: Extract<keyof M, string>;
|
|
@@ -54,6 +56,7 @@ export function PopupFormFieldLoading<M extends Record<string, any>>({
|
|
|
54
56
|
propertyKey,
|
|
55
57
|
collection: inputCollection,
|
|
56
58
|
path,
|
|
59
|
+
pathSegments,
|
|
57
60
|
cellRect,
|
|
58
61
|
open,
|
|
59
62
|
onClose,
|
|
@@ -66,11 +69,12 @@ export function PopupFormFieldLoading<M extends Record<string, any>>({
|
|
|
66
69
|
if (entityId && inputCollection) {
|
|
67
70
|
dataSource.fetchEntity({
|
|
68
71
|
path,
|
|
72
|
+
pathSegments,
|
|
69
73
|
entityId,
|
|
70
74
|
collection: inputCollection
|
|
71
75
|
}).then(setEntity);
|
|
72
76
|
}
|
|
73
|
-
}, [entityId, inputCollection, dataSource, path]);
|
|
77
|
+
}, [entityId, inputCollection, dataSource, path, pathSegments]);
|
|
74
78
|
|
|
75
79
|
if (!entity) return null;
|
|
76
80
|
return <PopupFormFieldInternal {...{
|
|
@@ -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,
|
|
@@ -789,6 +789,7 @@ export const EntityCollectionView = React.memo(
|
|
|
789
789
|
// EntitiesCount fetches count and updates breadcrumb - no visual rendering needed here
|
|
790
790
|
const countFetcher = <EntitiesCount
|
|
791
791
|
fullPath={fullPath}
|
|
792
|
+
pathSegments={pathSegments}
|
|
792
793
|
collection={collection}
|
|
793
794
|
filter={tableController.filterValues}
|
|
794
795
|
sortBy={tableController.sortBy}
|
|
@@ -933,6 +934,7 @@ export const EntityCollectionView = React.memo(
|
|
|
933
934
|
collection={collection}
|
|
934
935
|
tableController={tableController}
|
|
935
936
|
fullPath={fullPath}
|
|
937
|
+
pathSegments={pathSegments}
|
|
936
938
|
parentCollectionIds={parentCollectionIds}
|
|
937
939
|
columnProperty={selectedKanbanProperty}
|
|
938
940
|
onEntityClick={onEntityClick}
|
|
@@ -1063,6 +1065,7 @@ export const EntityCollectionView = React.memo(
|
|
|
1063
1065
|
|
|
1064
1066
|
{popupCell && <PopupFormField
|
|
1065
1067
|
key={`popup_form_${popupCell?.propertyKey}_${popupCell?.entityId}`}
|
|
1068
|
+
pathSegments={pathSegments}
|
|
1066
1069
|
open={Boolean(popupCell)}
|
|
1067
1070
|
onClose={onPopupClose}
|
|
1068
1071
|
cellRect={popupCell?.cellRect}
|
|
@@ -1118,13 +1121,16 @@ function EntitiesCount({
|
|
|
1118
1121
|
collection,
|
|
1119
1122
|
filter,
|
|
1120
1123
|
sortBy,
|
|
1121
|
-
onCountChange
|
|
1124
|
+
onCountChange,
|
|
1125
|
+
pathSegments
|
|
1122
1126
|
}: {
|
|
1123
1127
|
fullPath: string,
|
|
1124
1128
|
collection: EntityCollection,
|
|
1125
1129
|
filter?: FilterValues<any>,
|
|
1126
1130
|
sortBy?: [string, "asc" | "desc"],
|
|
1127
1131
|
onCountChange?: (count: number) => void,
|
|
1132
|
+
/** `fullPath` split at its real segment boundaries; forwarded to countEntities. */
|
|
1133
|
+
pathSegments?: string[],
|
|
1128
1134
|
}) {
|
|
1129
1135
|
|
|
1130
1136
|
const dataSource = useDataSource(collection);
|
|
@@ -1135,17 +1141,19 @@ function EntitiesCount({
|
|
|
1135
1141
|
const sortByProperty = sortBy ? sortBy[0] : undefined;
|
|
1136
1142
|
const currentSort = sortBy ? sortBy[1] : undefined;
|
|
1137
1143
|
const resolvedPath = useMemo(() => navigation.resolveIdsFrom(fullPath), [fullPath, navigation.resolveIdsFrom]);
|
|
1144
|
+
const resolvedPathSegments = pathSegments;
|
|
1138
1145
|
|
|
1139
1146
|
useEffect(() => {
|
|
1140
1147
|
if (dataSource.countEntities)
|
|
1141
1148
|
dataSource.countEntities({
|
|
1142
1149
|
path: resolvedPath,
|
|
1150
|
+
pathSegments: resolvedPathSegments,
|
|
1143
1151
|
collection,
|
|
1144
1152
|
filter,
|
|
1145
1153
|
orderBy: sortByProperty,
|
|
1146
1154
|
order: currentSort
|
|
1147
1155
|
}).then(setCount).catch(setError);
|
|
1148
|
-
}, [fullPath, dataSource.countEntities, resolvedPath, collection, filter, sortByProperty, currentSort]);
|
|
1156
|
+
}, [fullPath, dataSource.countEntities, resolvedPath, resolvedPathSegments, collection, filter, sortByProperty, currentSort]);
|
|
1149
1157
|
|
|
1150
1158
|
useEffect(() => {
|
|
1151
1159
|
if (onCountChange && count !== undefined) {
|
|
@@ -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,
|
|
@@ -80,6 +83,7 @@ export function useBoardDataController<M extends Record<string, any> = any, COLU
|
|
|
80
83
|
const dataSource = useDataSource(collection);
|
|
81
84
|
const navigation = useNavigationController();
|
|
82
85
|
const resolvedPath = useMemo(() => navigation.resolveIdsFrom(fullPath), [fullPath, navigation.resolveIdsFrom]);
|
|
86
|
+
const resolvedPathSegments = pathSegments;
|
|
83
87
|
|
|
84
88
|
// Stable refs for objects that shouldn't trigger re-subscriptions
|
|
85
89
|
const dataSourceRef = useRef(dataSource);
|
|
@@ -95,11 +99,13 @@ export function useBoardDataController<M extends Record<string, any> = any, COLU
|
|
|
95
99
|
const orderPropertyRef = useRef(orderProperty);
|
|
96
100
|
const searchStringRef = useRef(searchString);
|
|
97
101
|
const resolvedPathRef = useRef(resolvedPath);
|
|
102
|
+
const resolvedPathSegmentsRef = useRef(resolvedPathSegments);
|
|
98
103
|
filterValuesRef.current = filterValues;
|
|
99
104
|
columnPropertyRef.current = columnProperty;
|
|
100
105
|
orderPropertyRef.current = orderProperty;
|
|
101
106
|
searchStringRef.current = searchString;
|
|
102
107
|
resolvedPathRef.current = resolvedPath;
|
|
108
|
+
resolvedPathSegmentsRef.current = resolvedPathSegments;
|
|
103
109
|
|
|
104
110
|
// Track item count per column for pagination
|
|
105
111
|
const [columnItemCounts, setColumnItemCounts] = useState<Record<string, number>>(() => {
|
|
@@ -168,6 +174,7 @@ export function useBoardDataController<M extends Record<string, any> = any, COLU
|
|
|
168
174
|
const currentOrderProperty = orderPropertyRef.current;
|
|
169
175
|
const currentSearchString = searchStringRef.current;
|
|
170
176
|
const currentResolvedPath = resolvedPathRef.current;
|
|
177
|
+
const currentPathSegments = resolvedPathSegmentsRef.current;
|
|
171
178
|
|
|
172
179
|
// Build filter for this column
|
|
173
180
|
const columnFilter: FilterValues<string> = {
|
|
@@ -248,6 +255,7 @@ export function useBoardDataController<M extends Record<string, any> = any, COLU
|
|
|
248
255
|
if (currentDataSource.listenCollection) {
|
|
249
256
|
const unsubscribe = currentDataSource.listenCollection<M>({
|
|
250
257
|
path: currentResolvedPath,
|
|
258
|
+
pathSegments: currentPathSegments,
|
|
251
259
|
collection: currentCollection,
|
|
252
260
|
onUpdate,
|
|
253
261
|
onError,
|
|
@@ -262,6 +270,7 @@ export function useBoardDataController<M extends Record<string, any> = any, COLU
|
|
|
262
270
|
} else {
|
|
263
271
|
currentDataSource.fetchCollection<M>({
|
|
264
272
|
path: currentResolvedPath,
|
|
273
|
+
pathSegments: currentPathSegments,
|
|
265
274
|
collection: currentCollection,
|
|
266
275
|
searchString: currentSearchString,
|
|
267
276
|
filter: columnFilter,
|
|
@@ -297,6 +306,7 @@ export function useBoardDataController<M extends Record<string, any> = any, COLU
|
|
|
297
306
|
const currentColumnProperty = columnPropertyRef.current;
|
|
298
307
|
const currentSearchString = searchStringRef.current;
|
|
299
308
|
const currentResolvedPath = resolvedPathRef.current;
|
|
309
|
+
const currentPathSegments = resolvedPathSegmentsRef.current;
|
|
300
310
|
const currentColumns = columns;
|
|
301
311
|
const currentColumnItemCounts = columnItemCounts;
|
|
302
312
|
|
|
@@ -316,6 +326,7 @@ export function useBoardDataController<M extends Record<string, any> = any, COLU
|
|
|
316
326
|
};
|
|
317
327
|
currentDataSource.countEntities({
|
|
318
328
|
path: currentResolvedPath,
|
|
329
|
+
pathSegments: currentPathSegments,
|
|
319
330
|
collection: currentCollection,
|
|
320
331
|
filter: columnFilter,
|
|
321
332
|
searchString: currentSearchString
|
|
@@ -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,
|
|
@@ -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
|
})))
|
|
@@ -95,8 +95,8 @@ export function useDataSourceTableController<M extends Record<string, any> = any
|
|
|
95
95
|
const navigation = useNavigationController();
|
|
96
96
|
const dataSource = useDataSource(collection);
|
|
97
97
|
const resolvedPath = useMemo(() => navigation.resolveIdsFrom(fullPath), [fullPath, navigation.resolveIdsFrom]);
|
|
98
|
-
|
|
99
|
-
|
|
98
|
+
// Passed through as-is; never derived from `resolvedPath` (see useEntityFetch).
|
|
99
|
+
const resolvedPathSegments = pathSegments;
|
|
100
100
|
|
|
101
101
|
const forceFilter = forceFilterFromProps ?? forceFilterFromCollection;
|
|
102
102
|
const paginationEnabled = collection.pagination === undefined || Boolean(collection.pagination);
|
|
@@ -108,6 +108,7 @@ export function useDataSourceTableController<M extends Record<string, any> = any
|
|
|
108
108
|
return true;
|
|
109
109
|
return dataSource.isFilterCombinationValid({
|
|
110
110
|
path: resolvedPath,
|
|
111
|
+
pathSegments: resolvedPathSegments,
|
|
111
112
|
collection,
|
|
112
113
|
filterValues,
|
|
113
114
|
sortBy
|
|
@@ -234,6 +235,11 @@ export function useDataSourceTableController<M extends Record<string, any> = any
|
|
|
234
235
|
setDataLoading(true);
|
|
235
236
|
|
|
236
237
|
const onEntitiesUpdate = async (entities: Entity<M>[]) => {
|
|
238
|
+
// See useEntityFetch: entities carry the segments they were loaded with, so a
|
|
239
|
+
// reference or a delete derived from a table row stays resolvable.
|
|
240
|
+
if (resolvedPathSegments) {
|
|
241
|
+
entities = entities.map(e => e.pathSegments ? e : { ...e, pathSegments: resolvedPathSegments });
|
|
242
|
+
}
|
|
237
243
|
if (collection.callbacks?.onFetch) {
|
|
238
244
|
try {
|
|
239
245
|
entities = await Promise.all(
|
|
@@ -361,9 +361,11 @@ export function EntityEditViewInner<M extends Record<string, any>>({
|
|
|
361
361
|
const newFullIdPath = fullIdPath && usedEntity ? `${fullIdPath}/${encodeEntityId(usedEntity.id)}/${removeInitialAndTrailingSlashes(subcollectionId)}` : undefined;
|
|
362
362
|
// The subcollection sits under this entity, so its segments are ours plus this
|
|
363
363
|
// entity's id (kept whole, slashes and all) plus the subcollection's own path.
|
|
364
|
-
|
|
364
|
+
// Only extendable when we were given real segments — deriving the parent chain
|
|
365
|
+
// from the flattened `path` would drop the boundary of a slash-bearing parent id.
|
|
366
|
+
const newPathSegments = usedEntity && pathSegments
|
|
365
367
|
? [
|
|
366
|
-
...
|
|
368
|
+
...pathSegments,
|
|
367
369
|
usedEntity.id,
|
|
368
370
|
...removeInitialAndTrailingSlashes(subcollection.path).split("/")
|
|
369
371
|
]
|
|
@@ -432,6 +434,7 @@ export function EntityEditViewInner<M extends Record<string, any>>({
|
|
|
432
434
|
fullIdPath={fullIdPath}
|
|
433
435
|
collection={collection}
|
|
434
436
|
path={path}
|
|
437
|
+
pathSegments={pathSegments}
|
|
435
438
|
entityId={entityId ?? usedEntity?.id}
|
|
436
439
|
onValuesModified={onValuesModified}
|
|
437
440
|
entity={entity}
|
package/src/form/EntityForm.tsx
CHANGED
|
@@ -71,6 +71,11 @@ export type OnUpdateParams = {
|
|
|
71
71
|
|
|
72
72
|
export type EntityFormProps<M extends Record<string, any>> = {
|
|
73
73
|
path: string;
|
|
74
|
+
/**
|
|
75
|
+
* `path` split at its real segment boundaries; forwarded to the datasource for
|
|
76
|
+
* generateEntityId and checkUniqueField. Never derived from `path`.
|
|
77
|
+
*/
|
|
78
|
+
pathSegments?: string[];
|
|
74
79
|
fullIdPath?: string;
|
|
75
80
|
collection: EntityCollection<M>;
|
|
76
81
|
entityId?: string;
|
|
@@ -214,6 +219,7 @@ export function getChanges<T extends object>(source: Partial<T>, comparison: Par
|
|
|
214
219
|
|
|
215
220
|
export function EntityForm<M extends Record<string, any>>({
|
|
216
221
|
path,
|
|
222
|
+
pathSegments,
|
|
217
223
|
fullIdPath,
|
|
218
224
|
entityId: entityIdProp,
|
|
219
225
|
collection,
|
|
@@ -292,7 +298,7 @@ export function EntityForm<M extends Record<string, any>>({
|
|
|
292
298
|
if (mustSetCustomId) {
|
|
293
299
|
return undefined;
|
|
294
300
|
} else {
|
|
295
|
-
return dataSource.generateEntityId(path, collection);
|
|
301
|
+
return dataSource.generateEntityId(path, collection, pathSegments);
|
|
296
302
|
}
|
|
297
303
|
} else {
|
|
298
304
|
return entityIdProp;
|
|
@@ -692,7 +698,7 @@ export function EntityForm<M extends Record<string, any>>({
|
|
|
692
698
|
const uniqueFieldValidator: CustomFieldValidator = useCallback(({
|
|
693
699
|
name,
|
|
694
700
|
value
|
|
695
|
-
}) => dataSource.checkUniqueField(path, name, value, entityId, collection),
|
|
701
|
+
}) => dataSource.checkUniqueField(path, name, value, entityId, collection, pathSegments),
|
|
696
702
|
[dataSource, path, entityId]);
|
|
697
703
|
|
|
698
704
|
const validationSchema = useMemo(() => entityId
|
package/src/hooks/data/delete.ts
CHANGED
|
@@ -81,6 +81,8 @@ export async function deleteEntityWithCallbacks<M extends Record<string, any>, U
|
|
|
81
81
|
}
|
|
82
82
|
}
|
|
83
83
|
return dataSource.deleteEntity({
|
|
84
|
+
// Carried by the entity from the fetch that produced it; undefined if unknown.
|
|
85
|
+
pathSegments: entity.pathSegments,
|
|
84
86
|
entity,
|
|
85
87
|
collection
|
|
86
88
|
}).then(() => {
|
package/src/hooks/data/save.ts
CHANGED
|
@@ -104,7 +104,7 @@ export async function saveEntityWithCallbacks<M extends Record<string, any>, USE
|
|
|
104
104
|
return dataSource.saveEntity({
|
|
105
105
|
collection,
|
|
106
106
|
path: resolvedPath,
|
|
107
|
-
pathSegments
|
|
107
|
+
pathSegments,
|
|
108
108
|
entityId,
|
|
109
109
|
values: updatedValues,
|
|
110
110
|
previousValues,
|
|
@@ -13,6 +13,12 @@ export interface CollectionFetchProps<M extends Record<string, any>> {
|
|
|
13
13
|
* Absolute collection path
|
|
14
14
|
*/
|
|
15
15
|
path: string;
|
|
16
|
+
/**
|
|
17
|
+
* `path` split at its real segment boundaries. Forwarded to the datasource so a parent
|
|
18
|
+
* entity id containing "/" stays unambiguous. Never derived from `path` — see
|
|
19
|
+
* `pathSegments` on the datasource props.
|
|
20
|
+
*/
|
|
21
|
+
pathSegments?: string[];
|
|
16
22
|
|
|
17
23
|
/**
|
|
18
24
|
* collection of the entity displayed by this collection
|
|
@@ -63,6 +69,7 @@ export interface CollectionFetchResult<M extends Record<string, any>> {
|
|
|
63
69
|
export function useCollectionFetch<M extends Record<string, any>, USER extends User>(
|
|
64
70
|
{
|
|
65
71
|
path: inputPath,
|
|
72
|
+
pathSegments,
|
|
66
73
|
collection,
|
|
67
74
|
filterValues,
|
|
68
75
|
sortBy,
|
|
@@ -91,6 +98,10 @@ export function useCollectionFetch<M extends Record<string, any>, USER extends U
|
|
|
91
98
|
setDataLoading(true);
|
|
92
99
|
|
|
93
100
|
const onEntitiesUpdate = async (entities: Entity<M>[]) => {
|
|
101
|
+
// See useEntityFetch: entities carry the segments they were loaded with.
|
|
102
|
+
if (pathSegments) {
|
|
103
|
+
entities = entities.map(e => e.pathSegments ? e : { ...e, pathSegments });
|
|
104
|
+
}
|
|
94
105
|
if (collection.callbacks?.onFetch) {
|
|
95
106
|
try {
|
|
96
107
|
entities = await Promise.all(
|
|
@@ -124,6 +135,7 @@ export function useCollectionFetch<M extends Record<string, any>, USER extends U
|
|
|
124
135
|
if (dataSource.listenCollection) {
|
|
125
136
|
return dataSource.listenCollection<M>({
|
|
126
137
|
path,
|
|
138
|
+
pathSegments,
|
|
127
139
|
collection,
|
|
128
140
|
onUpdate: onEntitiesUpdate,
|
|
129
141
|
onError,
|
|
@@ -137,6 +149,7 @@ export function useCollectionFetch<M extends Record<string, any>, USER extends U
|
|
|
137
149
|
} else {
|
|
138
150
|
dataSource.fetchCollection<M>({
|
|
139
151
|
path,
|
|
152
|
+
pathSegments,
|
|
140
153
|
collection,
|
|
141
154
|
searchString,
|
|
142
155
|
filter: filterValues,
|
|
@@ -55,7 +55,10 @@ export function useEntityFetch<M extends Record<string, any>, USER extends User>
|
|
|
55
55
|
const navigationController = useNavigationController();
|
|
56
56
|
|
|
57
57
|
const path = navigationController.resolveIdsFrom(inputPath);
|
|
58
|
-
|
|
58
|
+
// Never fabricate segments. If the caller did not thread them, pass undefined so a
|
|
59
|
+
// delegate can tell "not provided" from a real answer — splitting `path` here would
|
|
60
|
+
// produce a confidently wrong array for any id containing "/".
|
|
61
|
+
const pathSegments = inputPathSegments;
|
|
59
62
|
|
|
60
63
|
const context: FireCMSContext<USER> = useFireCMSContext();
|
|
61
64
|
|
|
@@ -68,6 +71,12 @@ export function useEntityFetch<M extends Record<string, any>, USER extends User>
|
|
|
68
71
|
setDataLoading(true);
|
|
69
72
|
|
|
70
73
|
const onEntityUpdate = async (updatedEntity: Entity<M> | undefined) => {
|
|
74
|
+
// Attach the segments this entity was loaded with, so anything derived from it
|
|
75
|
+
// later — a reference, a delete — stays resolvable without re-deriving them
|
|
76
|
+
// from the flattened path. Delegates are not required to set this themselves.
|
|
77
|
+
if (updatedEntity && pathSegments && !updatedEntity.pathSegments) {
|
|
78
|
+
updatedEntity = { ...updatedEntity, pathSegments };
|
|
79
|
+
}
|
|
71
80
|
if (collection.callbacks?.onFetch && updatedEntity) {
|
|
72
81
|
try {
|
|
73
82
|
updatedEntity = await collection.callbacks.onFetch({
|
|
@@ -86,6 +86,8 @@ export function resolveNavigationFrom<M extends Record<string, any>, USER extend
|
|
|
86
86
|
}
|
|
87
87
|
return dataSource.fetchEntity({
|
|
88
88
|
path: entry.path,
|
|
89
|
+
// The navigation entry already knows the real segment boundaries.
|
|
90
|
+
pathSegments: entry.pathSegments,
|
|
89
91
|
entityId: entry.entityId,
|
|
90
92
|
collection
|
|
91
93
|
})
|
|
@@ -303,12 +303,15 @@ export function useBuildDataSource({
|
|
|
303
303
|
deleteEntity: useCallback(<M extends Record<string, any>>(
|
|
304
304
|
{
|
|
305
305
|
entity,
|
|
306
|
+
pathSegments,
|
|
306
307
|
collection
|
|
307
308
|
}: DeleteEntityProps<M>
|
|
308
309
|
): Promise<void> => {
|
|
309
310
|
const usedDelegate = collection?.overrides?.dataSourceDelegate ?? delegate;
|
|
310
311
|
return usedDelegate.deleteEntity({
|
|
311
312
|
entity,
|
|
313
|
+
// Falls back to what the entity carries from the fetch that produced it.
|
|
314
|
+
pathSegments: pathSegments ?? entity.pathSegments,
|
|
312
315
|
collection
|
|
313
316
|
});
|
|
314
317
|
}, [delegate.deleteEntity]),
|
|
@@ -328,25 +331,28 @@ export function useBuildDataSource({
|
|
|
328
331
|
name: string,
|
|
329
332
|
value: any,
|
|
330
333
|
entityId?: string,
|
|
331
|
-
collection?: EntityCollection
|
|
334
|
+
collection?: EntityCollection,
|
|
335
|
+
pathSegments?: string[]
|
|
332
336
|
): Promise<boolean> => {
|
|
333
337
|
const usedDelegate = collection?.overrides?.dataSourceDelegate ?? delegate;
|
|
334
|
-
return usedDelegate.checkUniqueField(path, name, value, entityId, collection);
|
|
338
|
+
return usedDelegate.checkUniqueField(path, name, value, entityId, collection, pathSegments);
|
|
335
339
|
}, [delegate.checkUniqueField]),
|
|
336
340
|
|
|
337
|
-
generateEntityId: useCallback((path: string, collection: EntityCollection): string => {
|
|
341
|
+
generateEntityId: useCallback((path: string, collection: EntityCollection, pathSegments?: string[]): string => {
|
|
338
342
|
const usedDelegate = collection?.overrides?.dataSourceDelegate ?? delegate;
|
|
339
|
-
return usedDelegate.generateEntityId(path, collection);
|
|
343
|
+
return usedDelegate.generateEntityId(path, collection, pathSegments);
|
|
340
344
|
}, [delegate.generateEntityId]),
|
|
341
345
|
|
|
342
346
|
countEntities: delegate.countEntities ? async ({
|
|
343
347
|
path,
|
|
348
|
+
pathSegments,
|
|
344
349
|
collection,
|
|
345
350
|
filter,
|
|
346
351
|
order,
|
|
347
352
|
orderBy
|
|
348
353
|
}: {
|
|
349
354
|
path: string,
|
|
355
|
+
pathSegments?: string[],
|
|
350
356
|
collection: EntityCollection<any>,
|
|
351
357
|
filter?: FilterValues<Extract<keyof any, string>>,
|
|
352
358
|
orderBy?: string,
|
|
@@ -355,6 +361,7 @@ export function useBuildDataSource({
|
|
|
355
361
|
const usedDelegate = collection?.overrides?.dataSourceDelegate ?? delegate;
|
|
356
362
|
return usedDelegate.countEntities!({
|
|
357
363
|
path,
|
|
364
|
+
pathSegments,
|
|
358
365
|
filter,
|
|
359
366
|
orderBy,
|
|
360
367
|
order,
|
|
@@ -364,11 +371,13 @@ export function useBuildDataSource({
|
|
|
364
371
|
|
|
365
372
|
isFilterCombinationValid: useCallback(({
|
|
366
373
|
path,
|
|
374
|
+
pathSegments,
|
|
367
375
|
databaseId,
|
|
368
376
|
filterValues,
|
|
369
377
|
sortBy
|
|
370
378
|
}: {
|
|
371
379
|
path: string,
|
|
380
|
+
pathSegments?: string[],
|
|
372
381
|
databaseId?: string,
|
|
373
382
|
filterValues: FilterValues<any>,
|
|
374
383
|
sortBy?: [string, "asc" | "desc"]
|
|
@@ -378,6 +387,7 @@ export function useBuildDataSource({
|
|
|
378
387
|
return delegate.isFilterCombinationValid(
|
|
379
388
|
{
|
|
380
389
|
path,
|
|
390
|
+
pathSegments,
|
|
381
391
|
databaseId,
|
|
382
392
|
filterValues,
|
|
383
393
|
sortBy
|