@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
|
@@ -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
|
-
|
|
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);
|
|
@@ -108,6 +113,7 @@ export function useDataSourceTableController<M extends Record<string, any> = any
|
|
|
108
113
|
return true;
|
|
109
114
|
return dataSource.isFilterCombinationValid({
|
|
110
115
|
path: resolvedPath,
|
|
116
|
+
pathSegments: resolvedPathSegments,
|
|
111
117
|
collection,
|
|
112
118
|
filterValues,
|
|
113
119
|
sortBy
|
|
@@ -234,6 +240,11 @@ export function useDataSourceTableController<M extends Record<string, any> = any
|
|
|
234
240
|
setDataLoading(true);
|
|
235
241
|
|
|
236
242
|
const onEntitiesUpdate = async (entities: Entity<M>[]) => {
|
|
243
|
+
// See useEntityFetch: entities carry the segments they were loaded with, so a
|
|
244
|
+
// reference or a delete derived from a table row stays resolvable.
|
|
245
|
+
if (resolvedPathSegments) {
|
|
246
|
+
entities = entities.map(e => e.pathSegments ? e : { ...e, pathSegments: resolvedPathSegments });
|
|
247
|
+
}
|
|
237
248
|
if (collection.callbacks?.onFetch) {
|
|
238
249
|
try {
|
|
239
250
|
entities = await Promise.all(
|
|
@@ -241,6 +252,7 @@ export function useDataSourceTableController<M extends Record<string, any> = any
|
|
|
241
252
|
collection.callbacks!.onFetch!({
|
|
242
253
|
collection,
|
|
243
254
|
path: resolvedPath,
|
|
255
|
+
pathSegments: resolvedPathSegments,
|
|
244
256
|
entity,
|
|
245
257
|
context
|
|
246
258
|
})));
|
|
@@ -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,15 +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
|
-
//
|
|
363
|
-
//
|
|
364
|
-
const newPathSegments = usedEntity
|
|
365
|
-
? [
|
|
366
|
-
...(pathSegments ?? path.split("/")),
|
|
367
|
-
usedEntity.id,
|
|
368
|
-
...removeInitialAndTrailingSlashes(subcollection.path).split("/")
|
|
369
|
-
]
|
|
370
|
-
: undefined;
|
|
366
|
+
// Only extendable when we were given real segments — deriving the parent chain
|
|
367
|
+
// from the flattened `path` would drop the boundary of a slash-bearing parent id.
|
|
368
|
+
const newPathSegments = buildSubcollectionPathSegments(pathSegments, usedEntity?.id, subcollection.path);
|
|
371
369
|
|
|
372
370
|
if (selectedTab !== subcollectionId) return null;
|
|
373
371
|
return (
|
|
@@ -432,6 +430,7 @@ export function EntityEditViewInner<M extends Record<string, any>>({
|
|
|
432
430
|
fullIdPath={fullIdPath}
|
|
433
431
|
collection={collection}
|
|
434
432
|
path={path}
|
|
433
|
+
pathSegments={pathSegments}
|
|
435
434
|
entityId={entityId ?? usedEntity?.id}
|
|
436
435
|
onValuesModified={onValuesModified}
|
|
437
436
|
entity={entity}
|
|
@@ -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);
|
|
@@ -8,7 +8,7 @@ import { EntityEditView, OnUpdateParams } from "./EntityEditView";
|
|
|
8
8
|
import { useSideDialogContext } from "./SideDialogs";
|
|
9
9
|
import { CloseIcon, IconButton, OpenInFullIcon } from "@firecms/ui";
|
|
10
10
|
import { useLocation, useNavigate } from "react-router-dom";
|
|
11
|
-
import { saveEntityToMemoryCache } from "../util/entity_cache";
|
|
11
|
+
import { entityCacheKey, saveEntityToMemoryCache } from "../util/entity_cache";
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
14
|
* This is the component in charge of rendering the side dialog used
|
|
@@ -60,6 +60,7 @@ export function EntitySidePanel(props: EntitySidePanelProps) {
|
|
|
60
60
|
if (params.status !== "existing") {
|
|
61
61
|
sideEntityController.replace({
|
|
62
62
|
path: params.path,
|
|
63
|
+
pathSegments: params.pathSegments ?? pathSegments,
|
|
63
64
|
entityId: params.entityId,
|
|
64
65
|
fullIdPath: props.fullIdPath,
|
|
65
66
|
selectedTab: params.selectedTab,
|
|
@@ -76,10 +77,10 @@ export function EntitySidePanel(props: EntitySidePanelProps) {
|
|
|
76
77
|
}
|
|
77
78
|
|
|
78
79
|
const parentCollectionIds = useMemo(() => {
|
|
79
|
-
return navigationController.getParentCollectionIds(path);
|
|
80
|
+
return navigationController.getParentCollectionIds(path, pathSegments);
|
|
80
81
|
}, [navigationController, path]);
|
|
81
82
|
|
|
82
|
-
const collection = props.collection ?? navigationController.getCollection(fullIdPath ?? path);
|
|
83
|
+
const collection = props.collection ?? navigationController.getCollection(fullIdPath ?? path, false, pathSegments);
|
|
83
84
|
|
|
84
85
|
useEffect(() => {
|
|
85
86
|
function beforeunload(e: any) {
|
|
@@ -137,7 +138,7 @@ export function EntitySidePanel(props: EntitySidePanelProps) {
|
|
|
137
138
|
className="self-center"
|
|
138
139
|
size={"smallest"}
|
|
139
140
|
onClick={() => {
|
|
140
|
-
const key = (status === "new" || status === "copy") ? path + "#new" : path
|
|
141
|
+
const key = (status === "new" || status === "copy") ? path + "#new" : entityCacheKey(path, entityId);
|
|
141
142
|
saveEntityToMemoryCache(key, values);
|
|
142
143
|
if (entityId)
|
|
143
144
|
navigate(location.pathname + location.search);
|
|
@@ -154,6 +155,7 @@ export function EntitySidePanel(props: EntitySidePanelProps) {
|
|
|
154
155
|
}) => {
|
|
155
156
|
sideEntityController.replace({
|
|
156
157
|
path,
|
|
158
|
+
pathSegments,
|
|
157
159
|
entityId,
|
|
158
160
|
fullIdPath: props.fullIdPath,
|
|
159
161
|
selectedTab,
|
package/src/form/EntityForm.tsx
CHANGED
|
@@ -48,6 +48,7 @@ import { useAnalyticsController } from "../hooks/useAnalyticsController";
|
|
|
48
48
|
import { FormEntry, FormLayout, LabelWithIconAndTooltip, PropertyFieldBinding } from "../form";
|
|
49
49
|
import { ValidationError } from "yup";
|
|
50
50
|
import {
|
|
51
|
+
entityCacheKey,
|
|
51
52
|
flattenKeys,
|
|
52
53
|
getEntityFromCache,
|
|
53
54
|
removeEntityFromCache,
|
|
@@ -64,6 +65,8 @@ export type OnUpdateParams = {
|
|
|
64
65
|
entity: Entity<any>,
|
|
65
66
|
status: EntityStatus,
|
|
66
67
|
path: string,
|
|
68
|
+
/** `path` split at its real segment boundaries, when known. Never derived from `path`. */
|
|
69
|
+
pathSegments?: string[],
|
|
67
70
|
entityId?: string;
|
|
68
71
|
selectedTab?: string;
|
|
69
72
|
collection: EntityCollection<any>
|
|
@@ -71,6 +74,11 @@ export type OnUpdateParams = {
|
|
|
71
74
|
|
|
72
75
|
export type EntityFormProps<M extends Record<string, any>> = {
|
|
73
76
|
path: string;
|
|
77
|
+
/**
|
|
78
|
+
* `path` split at its real segment boundaries; forwarded to the datasource for
|
|
79
|
+
* generateEntityId and checkUniqueField. Never derived from `path`.
|
|
80
|
+
*/
|
|
81
|
+
pathSegments?: string[];
|
|
74
82
|
fullIdPath?: string;
|
|
75
83
|
collection: EntityCollection<M>;
|
|
76
84
|
entityId?: string;
|
|
@@ -214,6 +222,7 @@ export function getChanges<T extends object>(source: Partial<T>, comparison: Par
|
|
|
214
222
|
|
|
215
223
|
export function EntityForm<M extends Record<string, any>>({
|
|
216
224
|
path,
|
|
225
|
+
pathSegments,
|
|
217
226
|
fullIdPath,
|
|
218
227
|
entityId: entityIdProp,
|
|
219
228
|
collection,
|
|
@@ -270,6 +279,7 @@ export function EntityForm<M extends Record<string, any>>({
|
|
|
270
279
|
entityId: entityIdProp,
|
|
271
280
|
collection,
|
|
272
281
|
path,
|
|
282
|
+
pathSegments,
|
|
273
283
|
values: valuesToBeSaved
|
|
274
284
|
});
|
|
275
285
|
}, false, 2000);
|
|
@@ -292,7 +302,7 @@ export function EntityForm<M extends Record<string, any>>({
|
|
|
292
302
|
if (mustSetCustomId) {
|
|
293
303
|
return undefined;
|
|
294
304
|
} else {
|
|
295
|
-
return dataSource.generateEntityId(path, collection);
|
|
305
|
+
return dataSource.generateEntityId(path, collection, pathSegments);
|
|
296
306
|
}
|
|
297
307
|
} else {
|
|
298
308
|
return entityIdProp;
|
|
@@ -308,7 +318,7 @@ export function EntityForm<M extends Record<string, any>>({
|
|
|
308
318
|
const baseInitialValues = useMemo(() => getInitialEntityValues(authController, collection, path, status, entity, customizationController.propertyConfigs), [authController, collection, path, status, entity, customizationController.propertyConfigs]);
|
|
309
319
|
|
|
310
320
|
const localChangesDataRaw = useMemo(() => entityId
|
|
311
|
-
? getEntityFromCache(path
|
|
321
|
+
? getEntityFromCache(entityCacheKey(path, entityId))
|
|
312
322
|
: getEntityFromCache(path + "#new"), [entityId, path]);
|
|
313
323
|
|
|
314
324
|
const [localChangesCleared, setLocalChangesCleared] = useState<boolean>(false);
|
|
@@ -387,7 +397,7 @@ export function EntityForm<M extends Record<string, any>>({
|
|
|
387
397
|
onValuesModified?.(false, initialValues as M);
|
|
388
398
|
},
|
|
389
399
|
onValuesChangeDeferred: (values: M, controller: FormexController<M>) => {
|
|
390
|
-
const key = (status === "new" || status === "copy") ? path + "#new" : path
|
|
400
|
+
const key = (status === "new" || status === "copy") ? path + "#new" : entityCacheKey(path, entityId);
|
|
391
401
|
if (controller.dirty && localChangesBackup !== false) {
|
|
392
402
|
const touchedValues = removeEmptyContainers(extractTouchedValues(values, controller.touched));
|
|
393
403
|
if (touchedValues && Object.keys(touchedValues).length > 0) {
|
|
@@ -468,8 +478,8 @@ export function EntityForm<M extends Record<string, any>>({
|
|
|
468
478
|
removeEntityFromMemoryCache(path + "#new");
|
|
469
479
|
removeEntityFromCache(path + "#new");
|
|
470
480
|
} else {
|
|
471
|
-
removeEntityFromMemoryCache(path
|
|
472
|
-
removeEntityFromCache(path
|
|
481
|
+
removeEntityFromMemoryCache(entityCacheKey(path, entityId));
|
|
482
|
+
removeEntityFromCache(entityCacheKey(path, entityId));
|
|
473
483
|
}
|
|
474
484
|
}
|
|
475
485
|
|
|
@@ -491,6 +501,7 @@ export function EntityForm<M extends Record<string, any>>({
|
|
|
491
501
|
entity: updatedEntity,
|
|
492
502
|
status,
|
|
493
503
|
path,
|
|
504
|
+
pathSegments,
|
|
494
505
|
entityId: updatedEntity.id,
|
|
495
506
|
collection
|
|
496
507
|
});
|
|
@@ -511,16 +522,20 @@ export function EntityForm<M extends Record<string, any>>({
|
|
|
511
522
|
previousValues,
|
|
512
523
|
entityId,
|
|
513
524
|
collection,
|
|
514
|
-
path
|
|
525
|
+
path,
|
|
526
|
+
pathSegments
|
|
515
527
|
}: {
|
|
516
528
|
collection: EntityCollection<M>,
|
|
517
529
|
path: string,
|
|
530
|
+
/** `path` split at its real segment boundaries, travelling with the `path` above. */
|
|
531
|
+
pathSegments?: string[],
|
|
518
532
|
entityId: string | undefined,
|
|
519
533
|
values: M,
|
|
520
534
|
previousValues?: M,
|
|
521
535
|
}) => {
|
|
522
536
|
return saveEntityWithCallbacks({
|
|
523
537
|
path,
|
|
538
|
+
pathSegments,
|
|
524
539
|
entityId,
|
|
525
540
|
values,
|
|
526
541
|
previousValues,
|
|
@@ -538,6 +553,7 @@ export function EntityForm<M extends Record<string, any>>({
|
|
|
538
553
|
type EntityFormSaveParams<M extends Record<string, any>> = {
|
|
539
554
|
collection: ResolvedEntityCollection<M>,
|
|
540
555
|
path: string,
|
|
556
|
+
pathSegments?: string[],
|
|
541
557
|
entityId: string | undefined,
|
|
542
558
|
values: EntityValues<M>,
|
|
543
559
|
previousValues?: EntityValues<M>,
|
|
@@ -547,6 +563,7 @@ export function EntityForm<M extends Record<string, any>>({
|
|
|
547
563
|
const onSaveEntityRequest = async ({
|
|
548
564
|
collection,
|
|
549
565
|
path,
|
|
566
|
+
pathSegments,
|
|
550
567
|
entityId,
|
|
551
568
|
values,
|
|
552
569
|
previousValues,
|
|
@@ -560,6 +577,7 @@ export function EntityForm<M extends Record<string, any>>({
|
|
|
560
577
|
return saveEntity({
|
|
561
578
|
collection,
|
|
562
579
|
path,
|
|
580
|
+
pathSegments,
|
|
563
581
|
entityId,
|
|
564
582
|
values,
|
|
565
583
|
previousValues
|
|
@@ -573,6 +591,7 @@ export function EntityForm<M extends Record<string, any>>({
|
|
|
573
591
|
return onSaveEntityRequest({
|
|
574
592
|
collection: resolvedCollection,
|
|
575
593
|
path,
|
|
594
|
+
pathSegments,
|
|
576
595
|
entityId,
|
|
577
596
|
values,
|
|
578
597
|
previousValues: entity?.values,
|
|
@@ -623,13 +642,14 @@ export function EntityForm<M extends Record<string, any>>({
|
|
|
623
642
|
const plugins = customizationController.plugins;
|
|
624
643
|
|
|
625
644
|
const actionsDisabled = disabled || formex.isSubmitting || (status === "existing" && !formex.dirty) || Boolean(disabledProp);
|
|
626
|
-
const parentCollectionIds = navigationController.getParentCollectionIds(path);
|
|
645
|
+
const parentCollectionIds = navigationController.getParentCollectionIds(path, pathSegments);
|
|
627
646
|
|
|
628
647
|
if (plugins && collection) {
|
|
629
648
|
const actionProps: PluginFormActionProps = {
|
|
630
649
|
entityId,
|
|
631
650
|
parentCollectionIds,
|
|
632
651
|
path,
|
|
652
|
+
pathSegments,
|
|
633
653
|
status,
|
|
634
654
|
collection,
|
|
635
655
|
context,
|
|
@@ -692,7 +712,7 @@ export function EntityForm<M extends Record<string, any>>({
|
|
|
692
712
|
const uniqueFieldValidator: CustomFieldValidator = useCallback(({
|
|
693
713
|
name,
|
|
694
714
|
value
|
|
695
|
-
}) => dataSource.checkUniqueField(path, name, value, entityId, collection),
|
|
715
|
+
}) => dataSource.checkUniqueField(path, name, value, entityId, collection, pathSegments),
|
|
696
716
|
[dataSource, path, entityId]);
|
|
697
717
|
|
|
698
718
|
const validationSchema = useMemo(() => entityId
|
|
@@ -890,6 +910,7 @@ export function EntityForm<M extends Record<string, any>>({
|
|
|
890
910
|
const dialogActions = <EntityFormActionsComponent
|
|
891
911
|
collection={resolvedCollection}
|
|
892
912
|
path={path}
|
|
913
|
+
pathSegments={pathSegments}
|
|
893
914
|
fullPath={path}
|
|
894
915
|
fullIdPath={fullIdPath}
|
|
895
916
|
entity={entity}
|
|
@@ -924,7 +945,7 @@ export function EntityForm<M extends Record<string, any>>({
|
|
|
924
945
|
|
|
925
946
|
{manualApplyLocalChanges && hasLocalChanges &&
|
|
926
947
|
<LocalChangesMenu
|
|
927
|
-
cacheKey={status === "new" || status === "copy" ? path + "#new" : path
|
|
948
|
+
cacheKey={status === "new" || status === "copy" ? path + "#new" : entityCacheKey(path, entityId)}
|
|
928
949
|
properties={resolvedCollection.properties}
|
|
929
950
|
cachedData={localChangesDataRaw as Partial<M>}
|
|
930
951
|
formex={formex}
|
|
@@ -26,6 +26,8 @@ export interface EntityFormActionsProps {
|
|
|
26
26
|
fullIdPath?: string;
|
|
27
27
|
collection: ResolvedEntityCollection;
|
|
28
28
|
path: string;
|
|
29
|
+
/** `path` split at its real segment boundaries, when known. Never derived from `path`. */
|
|
30
|
+
pathSegments?: string[];
|
|
29
31
|
entity?: Entity;
|
|
30
32
|
layout: "bottom" | "side";
|
|
31
33
|
savingError?: Error;
|
package/src/hooks/data/delete.ts
CHANGED
|
@@ -46,6 +46,7 @@ export type DeleteEntityWithCallbacksProps<M extends Record<string, any>, USER e
|
|
|
46
46
|
export async function deleteEntityWithCallbacks<M extends Record<string, any>, USER extends User>({
|
|
47
47
|
dataSource,
|
|
48
48
|
entity,
|
|
49
|
+
pathSegments,
|
|
49
50
|
collection,
|
|
50
51
|
callbacks,
|
|
51
52
|
onDeleteSuccess,
|
|
@@ -62,11 +63,17 @@ export async function deleteEntityWithCallbacks<M extends Record<string, any>, U
|
|
|
62
63
|
|
|
63
64
|
console.debug("Deleting entity", entity.path, entity.id);
|
|
64
65
|
|
|
66
|
+
// An explicit `pathSegments` wins, then the ones the entity was loaded with. The prop
|
|
67
|
+
// used to be accepted and silently ignored, which is the same class of bug this field
|
|
68
|
+
// exists to prevent.
|
|
69
|
+
const resolvedPathSegments = pathSegments ?? entity.pathSegments;
|
|
70
|
+
|
|
65
71
|
const entityDeleteProps: EntityOnDeleteProps<M, any> = {
|
|
66
72
|
entity,
|
|
67
73
|
collection,
|
|
68
74
|
entityId: entity.id,
|
|
69
75
|
path: entity.path,
|
|
76
|
+
pathSegments: resolvedPathSegments,
|
|
70
77
|
context
|
|
71
78
|
};
|
|
72
79
|
|
|
@@ -81,6 +88,7 @@ export async function deleteEntityWithCallbacks<M extends Record<string, any>, U
|
|
|
81
88
|
}
|
|
82
89
|
}
|
|
83
90
|
return dataSource.deleteEntity({
|
|
91
|
+
pathSegments: resolvedPathSegments,
|
|
84
92
|
entity,
|
|
85
93
|
collection
|
|
86
94
|
}).then(() => {
|
package/src/hooks/data/save.ts
CHANGED
|
@@ -68,7 +68,7 @@ export async function saveEntityWithCallbacks<M extends Record<string, any>, USE
|
|
|
68
68
|
|
|
69
69
|
const customizationController = context.customizationController;
|
|
70
70
|
|
|
71
|
-
const resolvedPath = context.navigation.resolveIdsFrom(path);
|
|
71
|
+
const resolvedPath = context.navigation.resolveIdsFrom(path, pathSegments);
|
|
72
72
|
|
|
73
73
|
const callbacks = collection.callbacks;
|
|
74
74
|
if (callbacks?.onPreSave) {
|
|
@@ -84,6 +84,7 @@ export async function saveEntityWithCallbacks<M extends Record<string, any>, USE
|
|
|
84
84
|
updatedValues = await callbacks.onPreSave({
|
|
85
85
|
collection: resolvedCollection,
|
|
86
86
|
path,
|
|
87
|
+
pathSegments,
|
|
87
88
|
resolvedPath,
|
|
88
89
|
entityId,
|
|
89
90
|
values,
|
|
@@ -104,7 +105,7 @@ export async function saveEntityWithCallbacks<M extends Record<string, any>, USE
|
|
|
104
105
|
return dataSource.saveEntity({
|
|
105
106
|
collection,
|
|
106
107
|
path: resolvedPath,
|
|
107
|
-
pathSegments
|
|
108
|
+
pathSegments,
|
|
108
109
|
entityId,
|
|
109
110
|
values: updatedValues,
|
|
110
111
|
previousValues,
|
|
@@ -123,6 +124,7 @@ export async function saveEntityWithCallbacks<M extends Record<string, any>, USE
|
|
|
123
124
|
callbacks.onSaveSuccess({
|
|
124
125
|
collection: resolvedCollection,
|
|
125
126
|
path,
|
|
127
|
+
pathSegments,
|
|
126
128
|
resolvedPath,
|
|
127
129
|
entityId: entity.id,
|
|
128
130
|
values: updatedValues,
|
|
@@ -152,6 +154,7 @@ export async function saveEntityWithCallbacks<M extends Record<string, any>, USE
|
|
|
152
154
|
callbacks.onSaveFailure({
|
|
153
155
|
collection: resolvedCollection,
|
|
154
156
|
path,
|
|
157
|
+
pathSegments,
|
|
155
158
|
resolvedPath,
|
|
156
159
|
entityId,
|
|
157
160
|
values: updatedValues,
|
|
@@ -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: inputPathSegments,
|
|
66
73
|
collection,
|
|
67
74
|
filterValues,
|
|
68
75
|
sortBy,
|
|
@@ -73,7 +80,13 @@ export function useCollectionFetch<M extends Record<string, any>, USER extends U
|
|
|
73
80
|
const dataSource = useDataSource(collection);
|
|
74
81
|
const navigationController = useNavigationController();
|
|
75
82
|
|
|
76
|
-
|
|
83
|
+
// Resolved together so the two representations always describe the same chain.
|
|
84
|
+
// Segments are resolved, never derived: only collection ids are swapped for their
|
|
85
|
+
// real paths, and entity ids are carried through whole (see useEntityFetch).
|
|
86
|
+
const pathSegments = inputPathSegments
|
|
87
|
+
? (navigationController.resolveSegmentsFrom?.(inputPathSegments) ?? inputPathSegments)
|
|
88
|
+
: undefined;
|
|
89
|
+
const path = navigationController.resolveIdsFrom(inputPath, inputPathSegments);
|
|
77
90
|
|
|
78
91
|
const sortByProperty = sortBy ? sortBy[0] : undefined;
|
|
79
92
|
const currentSort = sortBy ? sortBy[1] : undefined;
|
|
@@ -91,6 +104,10 @@ export function useCollectionFetch<M extends Record<string, any>, USER extends U
|
|
|
91
104
|
setDataLoading(true);
|
|
92
105
|
|
|
93
106
|
const onEntitiesUpdate = async (entities: Entity<M>[]) => {
|
|
107
|
+
// See useEntityFetch: entities carry the segments they were loaded with.
|
|
108
|
+
if (pathSegments) {
|
|
109
|
+
entities = entities.map(e => e.pathSegments ? e : { ...e, pathSegments });
|
|
110
|
+
}
|
|
94
111
|
if (collection.callbacks?.onFetch) {
|
|
95
112
|
try {
|
|
96
113
|
entities = await Promise.all(
|
|
@@ -98,6 +115,7 @@ export function useCollectionFetch<M extends Record<string, any>, USER extends U
|
|
|
98
115
|
collection.callbacks!.onFetch!({
|
|
99
116
|
collection,
|
|
100
117
|
path,
|
|
118
|
+
pathSegments,
|
|
101
119
|
entity,
|
|
102
120
|
context
|
|
103
121
|
})));
|
|
@@ -124,6 +142,7 @@ export function useCollectionFetch<M extends Record<string, any>, USER extends U
|
|
|
124
142
|
if (dataSource.listenCollection) {
|
|
125
143
|
return dataSource.listenCollection<M>({
|
|
126
144
|
path,
|
|
145
|
+
pathSegments,
|
|
127
146
|
collection,
|
|
128
147
|
onUpdate: onEntitiesUpdate,
|
|
129
148
|
onError,
|
|
@@ -137,6 +156,7 @@ export function useCollectionFetch<M extends Record<string, any>, USER extends U
|
|
|
137
156
|
} else {
|
|
138
157
|
dataSource.fetchCollection<M>({
|
|
139
158
|
path,
|
|
159
|
+
pathSegments,
|
|
140
160
|
collection,
|
|
141
161
|
searchString,
|
|
142
162
|
filter: filterValues,
|
|
@@ -3,6 +3,7 @@ import { Entity, EntityCollection, FireCMSContext, User } from "../../types";
|
|
|
3
3
|
import { useDataSource } from "./useDataSource";
|
|
4
4
|
import { useNavigationController } from "../useNavigationController";
|
|
5
5
|
import { useFireCMSContext } from "../useFireCMSContext";
|
|
6
|
+
import { entityCacheKey } from "../../util/entity_cache";
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* @group Hooks and utilities
|
|
@@ -11,7 +12,10 @@ export interface EntityFetchProps<M extends Record<string, any>, USER extends Us
|
|
|
11
12
|
path: string;
|
|
12
13
|
/**
|
|
13
14
|
* `path` split at its real segment boundaries. Forwarded to the datasource so a parent
|
|
14
|
-
* entity id containing "/" stays unambiguous.
|
|
15
|
+
* entity id containing "/" stays unambiguous.
|
|
16
|
+
*
|
|
17
|
+
* Optional, and never derived by splitting `path` — a guess would be wrong in exactly
|
|
18
|
+
* the case the field exists for. Absent means "not known here", not "no slashes".
|
|
15
19
|
*/
|
|
16
20
|
pathSegments?: string[];
|
|
17
21
|
entityId?: string;
|
|
@@ -54,8 +58,19 @@ export function useEntityFetch<M extends Record<string, any>, USER extends User>
|
|
|
54
58
|
const dataSource = useDataSource(collection);
|
|
55
59
|
const navigationController = useNavigationController();
|
|
56
60
|
|
|
57
|
-
|
|
58
|
-
|
|
61
|
+
// Never fabricate segments. If the caller did not thread them, pass undefined so a
|
|
62
|
+
// delegate can tell "not provided" from a real answer — splitting `path` here would
|
|
63
|
+
// produce a confidently wrong array for any id containing "/".
|
|
64
|
+
//
|
|
65
|
+
// When they were threaded, both representations are resolved from them together, so
|
|
66
|
+
// `path` cannot end up describing a different chain than `pathSegments`.
|
|
67
|
+
// `resolveSegmentsFrom` is optional on the controller: a host app may be supplying one
|
|
68
|
+
// built against an earlier version. Falling back to the segments as given is what
|
|
69
|
+
// happened before it existed — they are used unresolved, never invented.
|
|
70
|
+
const pathSegments = inputPathSegments
|
|
71
|
+
? (navigationController.resolveSegmentsFrom?.(inputPathSegments) ?? inputPathSegments)
|
|
72
|
+
: undefined;
|
|
73
|
+
const path = navigationController.resolveIdsFrom(inputPath, inputPathSegments);
|
|
59
74
|
|
|
60
75
|
const context: FireCMSContext<USER> = useFireCMSContext();
|
|
61
76
|
|
|
@@ -68,11 +83,18 @@ export function useEntityFetch<M extends Record<string, any>, USER extends User>
|
|
|
68
83
|
setDataLoading(true);
|
|
69
84
|
|
|
70
85
|
const onEntityUpdate = async (updatedEntity: Entity<M> | undefined) => {
|
|
86
|
+
// Attach the segments this entity was loaded with, so anything derived from it
|
|
87
|
+
// later — a reference, a delete — stays resolvable without re-deriving them
|
|
88
|
+
// from the flattened path. Delegates are not required to set this themselves.
|
|
89
|
+
if (updatedEntity && pathSegments && !updatedEntity.pathSegments) {
|
|
90
|
+
updatedEntity = { ...updatedEntity, pathSegments };
|
|
91
|
+
}
|
|
71
92
|
if (collection.callbacks?.onFetch && updatedEntity) {
|
|
72
93
|
try {
|
|
73
94
|
updatedEntity = await collection.callbacks.onFetch({
|
|
74
95
|
collection,
|
|
75
96
|
path,
|
|
97
|
+
pathSegments,
|
|
76
98
|
entity: updatedEntity,
|
|
77
99
|
context
|
|
78
100
|
});
|
|
@@ -80,7 +102,7 @@ export function useEntityFetch<M extends Record<string, any>, USER extends User>
|
|
|
80
102
|
console.error(e);
|
|
81
103
|
}
|
|
82
104
|
}
|
|
83
|
-
CACHE[
|
|
105
|
+
CACHE[entityCacheKey(path, entityId)] = updatedEntity;
|
|
84
106
|
setEntity(updatedEntity);
|
|
85
107
|
setDataLoading(false);
|
|
86
108
|
setDataLoadingError(undefined);
|
|
@@ -93,8 +115,8 @@ export function useEntityFetch<M extends Record<string, any>, USER extends User>
|
|
|
93
115
|
setDataLoadingError(error);
|
|
94
116
|
};
|
|
95
117
|
|
|
96
|
-
if (entityId && useCache && CACHE[
|
|
97
|
-
setEntity(CACHE[
|
|
118
|
+
if (entityId && useCache && CACHE[entityCacheKey(path, entityId)]) {
|
|
119
|
+
setEntity(CACHE[entityCacheKey(path, entityId)]);
|
|
98
120
|
setDataLoading(false);
|
|
99
121
|
setDataLoadingError(undefined);
|
|
100
122
|
// eslint-disable-next-line @typescript-eslint/no-empty-function
|