@firecms/core 3.4.0-canary.f6a889a → 3.4.0
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/CollectionTableToolbar.d.ts +6 -1
- 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 +8 -0
- 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/ErrorBoundary.d.ts +1 -3
- package/dist/components/ReferenceTable/ReferenceSelectionTable.d.ts +10 -1
- package/dist/components/SelectableTable/filters/filter_text_input.d.ts +30 -0
- package/dist/components/SelectableTable/filters/filter_text_input.test.d.ts +1 -0
- package/dist/components/common/table_url_params.d.ts +24 -0
- package/dist/components/common/useDataSourceTableController.d.ts +9 -1
- package/dist/components/common/useTableSearchHelper.d.ts +9 -1
- package/dist/core/EntityEditView.d.ts +9 -1
- 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/save.d.ts +1 -1
- package/dist/hooks/data/useCollectionFetch.d.ts +7 -1
- package/dist/hooks/data/useEntityFetch.d.ts +9 -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 +1753 -1170
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +1751 -1168
- package/dist/index.umd.js.map +1 -1
- package/dist/types/collections.d.ts +14 -0
- package/dist/types/datasource.d.ts +67 -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/properties.d.ts +17 -1
- package/dist/types/side_entity_controller.d.ts +7 -0
- package/dist/types/translations.d.ts +6 -0
- package/dist/util/__tests__/collections.test.d.ts +1 -0
- package/dist/util/__tests__/urls.test.d.ts +1 -0
- package/dist/util/collections.d.ts +1 -1
- package/dist/util/entity_cache.d.ts +11 -0
- package/dist/util/index.d.ts +1 -0
- package/dist/util/navigation_from_path.d.ts +7 -0
- package/dist/util/navigation_utils.d.ts +87 -3
- 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/dist/util/urls.d.ts +22 -0
- package/package.json +4 -4
- package/src/components/DeleteEntityDialog.tsx +2 -0
- package/src/components/EntityCollectionTable/EntityCollectionRowActions.tsx +2 -2
- package/src/components/EntityCollectionTable/EntityCollectionTable.tsx +1 -0
- package/src/components/EntityCollectionTable/internal/CollectionTableToolbar.tsx +7 -0
- 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 +66 -15
- 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/ErrorBoundary.tsx +12 -18
- package/src/components/ReferenceTable/ReferenceSelectionTable.tsx +19 -2
- package/src/components/SelectableTable/filters/StringNumberFilterField.tsx +33 -7
- package/src/components/SelectableTable/filters/filter_text_input.test.ts +184 -0
- package/src/components/SelectableTable/filters/filter_text_input.ts +71 -0
- package/src/components/common/default_entity_actions.tsx +22 -2
- package/src/components/common/table_url_params.ts +172 -0
- package/src/components/common/useDataSourceTableController.tsx +86 -179
- package/src/components/common/useTableSearchHelper.ts +26 -2
- package/src/contexts/SnackbarProvider.tsx +14 -1
- package/src/core/EntityEditView.tsx +20 -3
- package/src/core/EntityEditViewFormActions.tsx +3 -2
- package/src/core/EntitySidePanel.tsx +8 -4
- package/src/form/EntityForm.tsx +31 -10
- package/src/form/EntityFormActions.tsx +2 -0
- package/src/hooks/data/delete.ts +8 -0
- package/src/hooks/data/save.ts +6 -1
- package/src/hooks/data/useCollectionFetch.tsx +21 -1
- package/src/hooks/data/useEntityFetch.tsx +35 -4
- package/src/hooks/useBuildNavigationController.tsx +23 -8
- package/src/hooks/useResolvedNavigationFrom.tsx +3 -1
- package/src/hooks/useValidateAuthenticator.tsx +4 -0
- 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 +37 -15
- package/src/internal/useBuildSideEntityController.tsx +19 -5
- package/src/locales/de.ts +5 -0
- package/src/locales/en.ts +5 -0
- package/src/locales/es.ts +5 -0
- package/src/locales/fr.ts +5 -0
- package/src/locales/hi.ts +5 -0
- package/src/locales/it.ts +5 -0
- package/src/locales/pl.ts +5 -0
- package/src/locales/pt.ts +5 -0
- package/src/preview/components/ReferencePreview.tsx +7 -3
- package/src/preview/components/StorageThumbnail.tsx +24 -2
- package/src/routes/FireCMSRoute.tsx +11 -3
- package/src/types/collections.ts +16 -0
- package/src/types/datasource.ts +67 -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/properties.ts +17 -0
- package/src/types/side_entity_controller.tsx +7 -0
- package/src/types/translations.ts +6 -0
- package/src/util/__tests__/collections.test.ts +32 -0
- package/src/util/__tests__/urls.test.ts +131 -0
- package/src/util/collections.ts +1 -1
- package/src/util/entities.ts +3 -1
- package/src/util/entity_cache.ts +18 -0
- package/src/util/index.ts +1 -0
- package/src/util/navigation_from_path.ts +25 -2
- package/src/util/navigation_utils.ts +208 -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
- package/src/util/previews.ts +8 -1
- package/src/util/resolutions.ts +8 -0
- package/src/util/urls.ts +52 -0
|
@@ -1,12 +1,25 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import React, { PropsWithChildren } from "react";
|
|
3
|
+
import React, { PropsWithChildren, useEffect, useState } from "react";
|
|
4
4
|
import { SnackbarProvider as NotistackSnackbarProvider } from "notistack";
|
|
5
5
|
|
|
6
6
|
export const SnackbarProvider: React.FC<PropsWithChildren<{}>> = ({ children }) => {
|
|
7
7
|
|
|
8
|
+
// Notistack renders the snackbars inline, right where this provider sits in the
|
|
9
|
+
// tree, unless it is given a `domRoot`. Dialogs and side dialogs, on the other
|
|
10
|
+
// hand, are portalled to the body, so any ancestor of this provider that creates
|
|
11
|
+
// a stacking context (a transform, a filter, an `isolate`, a positioned element
|
|
12
|
+
// with a z-index...) traps the snackbars underneath them. Portalling the
|
|
13
|
+
// snackbars to the body as well puts them in the same stacking context as the
|
|
14
|
+
// dialogs, where their z-index actually wins.
|
|
15
|
+
const [domRoot, setDomRoot] = useState<HTMLElement | undefined>(undefined);
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
setDomRoot(document.body);
|
|
18
|
+
}, []);
|
|
19
|
+
|
|
8
20
|
return (
|
|
9
21
|
<NotistackSnackbarProvider maxSnack={3}
|
|
22
|
+
domRoot={domRoot}
|
|
10
23
|
autoHideDuration={3500}>
|
|
11
24
|
{children}
|
|
12
25
|
</NotistackSnackbarProvider>
|
|
@@ -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>
|
|
@@ -71,6 +74,12 @@ export interface EntityEditViewProps<M extends Record<string, any>> {
|
|
|
71
74
|
* The navigation path to the entity.
|
|
72
75
|
*/
|
|
73
76
|
fullIdPath?: string;
|
|
77
|
+
/**
|
|
78
|
+
* `path` split at its real segment boundaries, e.g. `["nodes", "node/42", "edges"]`.
|
|
79
|
+
* Forwarded to the datasource, and extended for any subcollection rendered inside this
|
|
80
|
+
* view, so a parent entity id containing "/" stays unambiguous.
|
|
81
|
+
*/
|
|
82
|
+
pathSegments?: string[];
|
|
74
83
|
collection: EntityCollection<M>;
|
|
75
84
|
entityId?: string;
|
|
76
85
|
databaseId?: string;
|
|
@@ -101,6 +110,7 @@ export function EntityEditView<M extends Record<string, any>, USER extends User>
|
|
|
101
110
|
dataLoadingError
|
|
102
111
|
} = useEntityFetch<M, USER>({
|
|
103
112
|
path: props.path,
|
|
113
|
+
pathSegments: props.pathSegments,
|
|
104
114
|
entityId: entityId,
|
|
105
115
|
collection: props.collection,
|
|
106
116
|
databaseId: props.databaseId,
|
|
@@ -108,7 +118,7 @@ export function EntityEditView<M extends Record<string, any>, USER extends User>
|
|
|
108
118
|
});
|
|
109
119
|
|
|
110
120
|
const initialDirtyValues = entityId
|
|
111
|
-
? getEntityFromMemoryCache(props.path
|
|
121
|
+
? getEntityFromMemoryCache(entityCacheKey(props.path, entityId))
|
|
112
122
|
: getEntityFromMemoryCache(props.path + "#new");
|
|
113
123
|
|
|
114
124
|
const authController = useAuthController();
|
|
@@ -120,7 +130,7 @@ export function EntityEditView<M extends Record<string, any>, USER extends User>
|
|
|
120
130
|
if (status === "new" || status === "copy") {
|
|
121
131
|
return true;
|
|
122
132
|
} else {
|
|
123
|
-
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;
|
|
124
134
|
}
|
|
125
135
|
}, [authController, entity, status]);
|
|
126
136
|
|
|
@@ -146,6 +156,7 @@ export function EntityEditView<M extends Record<string, any>, USER extends User>
|
|
|
146
156
|
export function EntityEditViewInner<M extends Record<string, any>>({
|
|
147
157
|
path,
|
|
148
158
|
fullIdPath,
|
|
159
|
+
pathSegments,
|
|
149
160
|
entityId,
|
|
150
161
|
selectedTab: selectedTabProp,
|
|
151
162
|
collection,
|
|
@@ -194,6 +205,7 @@ export function EntityEditViewInner<M extends Record<string, any>>({
|
|
|
194
205
|
entityId,
|
|
195
206
|
parentCollectionIds,
|
|
196
207
|
path,
|
|
208
|
+
pathSegments,
|
|
197
209
|
status,
|
|
198
210
|
collection,
|
|
199
211
|
context,
|
|
@@ -351,6 +363,9 @@ export function EntityEditViewInner<M extends Record<string, any>>({
|
|
|
351
363
|
const newFullPath = usedEntity ? `${path}/${usedEntity?.id}/${removeInitialAndTrailingSlashes(subcollection.path)}` : undefined;
|
|
352
364
|
// `newFullIdPath` is fed to buildUrlCollectionPath downstream, so it stays escaped.
|
|
353
365
|
const newFullIdPath = fullIdPath && usedEntity ? `${fullIdPath}/${encodeEntityId(usedEntity.id)}/${removeInitialAndTrailingSlashes(subcollectionId)}` : 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);
|
|
354
369
|
|
|
355
370
|
if (selectedTab !== subcollectionId) return null;
|
|
356
371
|
return (
|
|
@@ -365,6 +380,7 @@ export function EntityEditViewInner<M extends Record<string, any>>({
|
|
|
365
380
|
(usedEntity && newFullPath
|
|
366
381
|
? <EntityCollectionView
|
|
367
382
|
fullPath={newFullPath}
|
|
383
|
+
pathSegments={newPathSegments}
|
|
368
384
|
fullIdPath={newFullIdPath}
|
|
369
385
|
parentCollectionIds={[...parentCollectionIds, collection.id]}
|
|
370
386
|
isSubCollection={true}
|
|
@@ -414,6 +430,7 @@ export function EntityEditViewInner<M extends Record<string, any>>({
|
|
|
414
430
|
fullIdPath={fullIdPath}
|
|
415
431
|
collection={collection}
|
|
416
432
|
path={path}
|
|
433
|
+
pathSegments={pathSegments}
|
|
417
434
|
entityId={entityId ?? usedEntity?.id}
|
|
418
435
|
onValuesModified={onValuesModified}
|
|
419
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
|
|
@@ -25,6 +25,7 @@ export function EntitySidePanel(props: EntitySidePanelProps) {
|
|
|
25
25
|
path,
|
|
26
26
|
entityId,
|
|
27
27
|
fullIdPath,
|
|
28
|
+
pathSegments,
|
|
28
29
|
formProps,
|
|
29
30
|
} = props;
|
|
30
31
|
|
|
@@ -59,6 +60,7 @@ export function EntitySidePanel(props: EntitySidePanelProps) {
|
|
|
59
60
|
if (params.status !== "existing") {
|
|
60
61
|
sideEntityController.replace({
|
|
61
62
|
path: params.path,
|
|
63
|
+
pathSegments: params.pathSegments ?? pathSegments,
|
|
62
64
|
entityId: params.entityId,
|
|
63
65
|
fullIdPath: props.fullIdPath,
|
|
64
66
|
selectedTab: params.selectedTab,
|
|
@@ -75,10 +77,10 @@ export function EntitySidePanel(props: EntitySidePanelProps) {
|
|
|
75
77
|
}
|
|
76
78
|
|
|
77
79
|
const parentCollectionIds = useMemo(() => {
|
|
78
|
-
return navigationController.getParentCollectionIds(path);
|
|
80
|
+
return navigationController.getParentCollectionIds(path, pathSegments);
|
|
79
81
|
}, [navigationController, path]);
|
|
80
82
|
|
|
81
|
-
const collection = props.collection ?? navigationController.getCollection(fullIdPath ?? path);
|
|
83
|
+
const collection = props.collection ?? navigationController.getCollection(fullIdPath ?? path, false, pathSegments);
|
|
82
84
|
|
|
83
85
|
useEffect(() => {
|
|
84
86
|
function beforeunload(e: any) {
|
|
@@ -116,6 +118,7 @@ export function EntitySidePanel(props: EntitySidePanelProps) {
|
|
|
116
118
|
<EntityEditView
|
|
117
119
|
{...props}
|
|
118
120
|
fullIdPath={fullIdPath}
|
|
121
|
+
pathSegments={pathSegments}
|
|
119
122
|
layout={"side_panel"}
|
|
120
123
|
collection={collection as EntityCollection}
|
|
121
124
|
parentCollectionIds={parentCollectionIds}
|
|
@@ -135,7 +138,7 @@ export function EntitySidePanel(props: EntitySidePanelProps) {
|
|
|
135
138
|
className="self-center"
|
|
136
139
|
size={"smallest"}
|
|
137
140
|
onClick={() => {
|
|
138
|
-
const key = (status === "new" || status === "copy") ? path + "#new" : path
|
|
141
|
+
const key = (status === "new" || status === "copy") ? path + "#new" : entityCacheKey(path, entityId);
|
|
139
142
|
saveEntityToMemoryCache(key, values);
|
|
140
143
|
if (entityId)
|
|
141
144
|
navigate(location.pathname + location.search);
|
|
@@ -152,6 +155,7 @@ export function EntitySidePanel(props: EntitySidePanelProps) {
|
|
|
152
155
|
}) => {
|
|
153
156
|
sideEntityController.replace({
|
|
154
157
|
path,
|
|
158
|
+
pathSegments,
|
|
155
159
|
entityId,
|
|
156
160
|
fullIdPath: props.fullIdPath,
|
|
157
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,8 +397,8 @@ 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
|
|
391
|
-
if (controller.dirty) {
|
|
400
|
+
const key = (status === "new" || status === "copy") ? path + "#new" : entityCacheKey(path, entityId);
|
|
401
|
+
if (controller.dirty && localChangesBackup !== false) {
|
|
392
402
|
const touchedValues = removeEmptyContainers(extractTouchedValues(values, controller.touched));
|
|
393
403
|
if (touchedValues && Object.keys(touchedValues).length > 0) {
|
|
394
404
|
saveEntityToCache(key, touchedValues);
|
|
@@ -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
|
@@ -43,6 +43,7 @@ export type SaveEntityWithCallbacksProps<M extends Record<string, any>> =
|
|
|
43
43
|
export async function saveEntityWithCallbacks<M extends Record<string, any>, USER extends User>({
|
|
44
44
|
collection,
|
|
45
45
|
path,
|
|
46
|
+
pathSegments,
|
|
46
47
|
entityId,
|
|
47
48
|
values,
|
|
48
49
|
previousValues,
|
|
@@ -67,7 +68,7 @@ export async function saveEntityWithCallbacks<M extends Record<string, any>, USE
|
|
|
67
68
|
|
|
68
69
|
const customizationController = context.customizationController;
|
|
69
70
|
|
|
70
|
-
const resolvedPath = context.navigation.resolveIdsFrom(path);
|
|
71
|
+
const resolvedPath = context.navigation.resolveIdsFrom(path, pathSegments);
|
|
71
72
|
|
|
72
73
|
const callbacks = collection.callbacks;
|
|
73
74
|
if (callbacks?.onPreSave) {
|
|
@@ -83,6 +84,7 @@ export async function saveEntityWithCallbacks<M extends Record<string, any>, USE
|
|
|
83
84
|
updatedValues = await callbacks.onPreSave({
|
|
84
85
|
collection: resolvedCollection,
|
|
85
86
|
path,
|
|
87
|
+
pathSegments,
|
|
86
88
|
resolvedPath,
|
|
87
89
|
entityId,
|
|
88
90
|
values,
|
|
@@ -103,6 +105,7 @@ export async function saveEntityWithCallbacks<M extends Record<string, any>, USE
|
|
|
103
105
|
return dataSource.saveEntity({
|
|
104
106
|
collection,
|
|
105
107
|
path: resolvedPath,
|
|
108
|
+
pathSegments,
|
|
106
109
|
entityId,
|
|
107
110
|
values: updatedValues,
|
|
108
111
|
previousValues,
|
|
@@ -121,6 +124,7 @@ export async function saveEntityWithCallbacks<M extends Record<string, any>, USE
|
|
|
121
124
|
callbacks.onSaveSuccess({
|
|
122
125
|
collection: resolvedCollection,
|
|
123
126
|
path,
|
|
127
|
+
pathSegments,
|
|
124
128
|
resolvedPath,
|
|
125
129
|
entityId: entity.id,
|
|
126
130
|
values: updatedValues,
|
|
@@ -150,6 +154,7 @@ export async function saveEntityWithCallbacks<M extends Record<string, any>, USE
|
|
|
150
154
|
callbacks.onSaveFailure({
|
|
151
155
|
collection: resolvedCollection,
|
|
152
156
|
path,
|
|
157
|
+
pathSegments,
|
|
153
158
|
resolvedPath,
|
|
154
159
|
entityId,
|
|
155
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,12 +3,21 @@ 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
|
|
9
10
|
*/
|
|
10
11
|
export interface EntityFetchProps<M extends Record<string, any>, USER extends User = User> {
|
|
11
12
|
path: string;
|
|
13
|
+
/**
|
|
14
|
+
* `path` split at its real segment boundaries. Forwarded to the datasource so a parent
|
|
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".
|
|
19
|
+
*/
|
|
20
|
+
pathSegments?: string[];
|
|
12
21
|
entityId?: string;
|
|
13
22
|
databaseId?: string;
|
|
14
23
|
collection: EntityCollection<M, USER>;
|
|
@@ -39,6 +48,7 @@ const CACHE: Record<string, Entity<any> | undefined> = {};
|
|
|
39
48
|
export function useEntityFetch<M extends Record<string, any>, USER extends User>(
|
|
40
49
|
{
|
|
41
50
|
path: inputPath,
|
|
51
|
+
pathSegments: inputPathSegments,
|
|
42
52
|
entityId,
|
|
43
53
|
collection,
|
|
44
54
|
databaseId,
|
|
@@ -48,7 +58,19 @@ export function useEntityFetch<M extends Record<string, any>, USER extends User>
|
|
|
48
58
|
const dataSource = useDataSource(collection);
|
|
49
59
|
const navigationController = useNavigationController();
|
|
50
60
|
|
|
51
|
-
|
|
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);
|
|
52
74
|
|
|
53
75
|
const context: FireCMSContext<USER> = useFireCMSContext();
|
|
54
76
|
|
|
@@ -61,11 +83,18 @@ export function useEntityFetch<M extends Record<string, any>, USER extends User>
|
|
|
61
83
|
setDataLoading(true);
|
|
62
84
|
|
|
63
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
|
+
}
|
|
64
92
|
if (collection.callbacks?.onFetch && updatedEntity) {
|
|
65
93
|
try {
|
|
66
94
|
updatedEntity = await collection.callbacks.onFetch({
|
|
67
95
|
collection,
|
|
68
96
|
path,
|
|
97
|
+
pathSegments,
|
|
69
98
|
entity: updatedEntity,
|
|
70
99
|
context
|
|
71
100
|
});
|
|
@@ -73,7 +102,7 @@ export function useEntityFetch<M extends Record<string, any>, USER extends User>
|
|
|
73
102
|
console.error(e);
|
|
74
103
|
}
|
|
75
104
|
}
|
|
76
|
-
CACHE[
|
|
105
|
+
CACHE[entityCacheKey(path, entityId)] = updatedEntity;
|
|
77
106
|
setEntity(updatedEntity);
|
|
78
107
|
setDataLoading(false);
|
|
79
108
|
setDataLoadingError(undefined);
|
|
@@ -86,8 +115,8 @@ export function useEntityFetch<M extends Record<string, any>, USER extends User>
|
|
|
86
115
|
setDataLoadingError(error);
|
|
87
116
|
};
|
|
88
117
|
|
|
89
|
-
if (entityId && useCache && CACHE[
|
|
90
|
-
setEntity(CACHE[
|
|
118
|
+
if (entityId && useCache && CACHE[entityCacheKey(path, entityId)]) {
|
|
119
|
+
setEntity(CACHE[entityCacheKey(path, entityId)]);
|
|
91
120
|
setDataLoading(false);
|
|
92
121
|
setDataLoadingError(undefined);
|
|
93
122
|
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
@@ -97,6 +126,7 @@ export function useEntityFetch<M extends Record<string, any>, USER extends User>
|
|
|
97
126
|
if (dataSource.listenEntity) {
|
|
98
127
|
return dataSource.listenEntity<M>({
|
|
99
128
|
path,
|
|
129
|
+
pathSegments,
|
|
100
130
|
entityId,
|
|
101
131
|
databaseId,
|
|
102
132
|
collection,
|
|
@@ -106,6 +136,7 @@ export function useEntityFetch<M extends Record<string, any>, USER extends User>
|
|
|
106
136
|
} else {
|
|
107
137
|
dataSource.fetchEntity<M>({
|
|
108
138
|
path,
|
|
139
|
+
pathSegments,
|
|
109
140
|
entityId,
|
|
110
141
|
databaseId,
|
|
111
142
|
collection
|