@firecms/core 3.0.0-canary.167 → 3.0.0-canary.169
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/EntityCollectionRowActions.d.ts +2 -1
- package/dist/components/EntityCollectionTable/EntityCollectionTable.d.ts +2 -2
- package/dist/components/EntityCollectionTable/EntityCollectionTableProps.d.ts +15 -1
- package/dist/components/EntityCollectionTable/index.d.ts +1 -1
- package/dist/components/EntityCollectionView/EntityCollectionView.d.ts +4 -0
- package/dist/components/SelectableTable/SelectableTable.d.ts +12 -2
- package/dist/components/UnsavedChangesDialog.d.ts +8 -0
- package/dist/components/VirtualTable/VirtualTableProps.d.ts +13 -0
- package/dist/components/common/index.d.ts +1 -1
- package/dist/components/common/{useDataSourceEntityCollectionTableController.d.ts → useDataSourceTableController.d.ts} +10 -2
- package/dist/components/common/useDebouncedCallback.d.ts +1 -0
- package/dist/components/common/useScrollRestoration.d.ts +14 -0
- package/dist/contexts/BreacrumbsContext.d.ts +8 -0
- package/dist/core/EntityEditView.d.ts +25 -7
- package/dist/core/FireCMSRouter.d.ts +4 -0
- package/dist/core/NavigationRoutes.d.ts +0 -1
- package/dist/core/index.d.ts +2 -1
- package/dist/hooks/data/useEntityFetch.d.ts +2 -1
- package/dist/hooks/useBreadcrumbsController.d.ts +26 -0
- package/dist/hooks/useBuildNavigationController.d.ts +0 -1
- package/dist/index.es.js +2344 -1305
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +2344 -1306
- package/dist/index.umd.js.map +1 -1
- package/dist/internal/useUnsavedChangesDialog.d.ts +7 -9
- package/dist/routes/CustomCMSRoute.d.ts +4 -0
- package/dist/routes/FireCMSRoute.d.ts +1 -0
- package/dist/routes/HomePageRoute.d.ts +3 -0
- package/dist/types/collections.d.ts +18 -6
- package/dist/types/datasource.d.ts +2 -2
- package/dist/types/dialogs_controller.d.ts +2 -6
- package/dist/types/entities.d.ts +1 -1
- package/dist/types/entity_actions.d.ts +1 -6
- package/dist/types/navigation.d.ts +28 -12
- package/dist/types/plugins.d.ts +1 -0
- package/dist/types/side_entity_controller.d.ts +1 -1
- package/dist/util/entity_cache.d.ts +23 -0
- package/dist/util/navigation_from_path.d.ts +5 -1
- package/dist/util/navigation_utils.d.ts +12 -1
- package/package.json +5 -5
- package/src/components/CircularProgressCenter.tsx +1 -1
- package/src/components/DeleteEntityDialog.tsx +1 -1
- package/src/components/EntityCollectionTable/EntityCollectionRowActions.tsx +40 -19
- package/src/components/EntityCollectionTable/EntityCollectionTable.tsx +10 -4
- package/src/components/EntityCollectionTable/EntityCollectionTableProps.tsx +18 -1
- package/src/components/EntityCollectionTable/index.tsx +1 -1
- package/src/components/EntityCollectionTable/internal/CollectionTableToolbar.tsx +3 -3
- package/src/components/EntityCollectionView/EntityCollectionView.tsx +79 -38
- package/src/components/NotFoundPage.tsx +2 -2
- package/src/components/ReferenceTable/ReferenceSelectionTable.tsx +9 -5
- package/src/components/SelectableTable/SelectableTable.tsx +18 -3
- package/src/components/UnsavedChangesDialog.tsx +42 -0
- package/src/components/VirtualTable/VirtualTable.tsx +19 -2
- package/src/components/VirtualTable/VirtualTableProps.tsx +15 -0
- package/src/components/VirtualTable/fields/VirtualTableInput.tsx +1 -1
- package/src/components/common/default_entity_actions.tsx +24 -10
- package/src/components/common/index.ts +1 -1
- package/src/components/common/{useDataSourceEntityCollectionTableController.tsx → useDataSourceTableController.tsx} +122 -13
- package/src/components/common/useDebouncedCallback.tsx +20 -0
- package/src/components/common/useScrollRestoration.tsx +68 -0
- package/src/contexts/BreacrumbsContext.tsx +38 -0
- package/src/contexts/DialogsProvider.tsx +0 -1
- package/src/core/DefaultAppBar.tsx +28 -4
- package/src/core/EntityEditView.tsx +457 -296
- package/src/core/EntitySidePanel.tsx +72 -6
- package/src/core/FireCMS.tsx +7 -4
- package/src/core/FireCMSRouter.tsx +17 -0
- package/src/core/NavigationRoutes.tsx +15 -28
- package/src/core/SideDialogs.tsx +10 -5
- package/src/core/index.tsx +4 -2
- package/src/hooks/data/save.ts +1 -1
- package/src/hooks/data/useCollectionFetch.tsx +1 -1
- package/src/hooks/data/useEntityFetch.tsx +5 -1
- package/src/hooks/useBreadcrumbsController.tsx +31 -0
- package/src/hooks/useBuildNavigationController.tsx +59 -21
- package/src/hooks/useLargeLayout.tsx +0 -35
- package/src/hooks/useResolvedNavigationFrom.tsx +4 -6
- package/src/internal/useBuildSideDialogsController.tsx +4 -2
- package/src/internal/useBuildSideEntityController.tsx +157 -77
- package/src/internal/useUnsavedChangesDialog.tsx +126 -92
- package/src/routes/CustomCMSRoute.tsx +21 -0
- package/src/routes/FireCMSRoute.tsx +230 -0
- package/src/routes/HomePageRoute.tsx +17 -0
- package/src/types/collections.ts +19 -6
- package/src/types/datasource.ts +2 -1
- package/src/types/dialogs_controller.tsx +2 -6
- package/src/types/entities.ts +1 -1
- package/src/types/entity_actions.tsx +1 -7
- package/src/types/navigation.ts +35 -16
- package/src/types/plugins.tsx +1 -0
- package/src/types/side_entity_controller.tsx +1 -1
- package/src/util/entity_cache.ts +196 -0
- package/src/util/navigation_from_path.ts +10 -2
- package/src/util/navigation_utils.ts +53 -1
- package/src/util/parent_references_from_path.ts +3 -3
- /package/src/util/{common.tsx → common.ts} +0 -0
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import { Blocker, useBlocker, useLocation } from "react-router";
|
|
2
|
+
import { EntityEditView } from "../core/EntityEditView";
|
|
3
|
+
import { useNavigationController } from "../hooks";
|
|
4
|
+
import { useEffect, useRef, useState } from "react";
|
|
5
|
+
import { useNavigate } from "react-router-dom";
|
|
6
|
+
import {
|
|
7
|
+
getNavigationEntriesFromPath,
|
|
8
|
+
NavigationViewCollectionInternal,
|
|
9
|
+
NavigationViewEntityCustomInternal,
|
|
10
|
+
NavigationViewInternal
|
|
11
|
+
} from "../util/navigation_from_path";
|
|
12
|
+
import { useBreadcrumbsController } from "../hooks/useBreadcrumbsController";
|
|
13
|
+
import { toArray } from "../util/arrays";
|
|
14
|
+
import { EntityCollectionView } from "../components";
|
|
15
|
+
import { UnsavedChangesDialog } from "../components/UnsavedChangesDialog";
|
|
16
|
+
|
|
17
|
+
export function FireCMSRoute() {
|
|
18
|
+
|
|
19
|
+
const location = useLocation();
|
|
20
|
+
const navigation = useNavigationController();
|
|
21
|
+
const breadcrumbs = useBreadcrumbsController();
|
|
22
|
+
|
|
23
|
+
const hash = location.hash;
|
|
24
|
+
const isSidePanel = hash.includes("#side");
|
|
25
|
+
const isNew = hash.includes("#new") || hash.includes("#new_side");
|
|
26
|
+
|
|
27
|
+
const pathname = location.pathname;
|
|
28
|
+
const navigationPath = navigation.urlPathToDataPath(pathname);
|
|
29
|
+
|
|
30
|
+
const navigationEntries = getNavigationEntriesFromPath({
|
|
31
|
+
path: navigationPath,
|
|
32
|
+
collections: navigation.collections ?? []
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
breadcrumbs.set({
|
|
37
|
+
breadcrumbs: navigationEntries.map(entry => {
|
|
38
|
+
if (entry.type === "entity") {
|
|
39
|
+
return ({
|
|
40
|
+
title: entry.entityId,
|
|
41
|
+
url: navigation.buildUrlCollectionPath(entry.fullPath)
|
|
42
|
+
});
|
|
43
|
+
} else if (entry.type === "custom_view") {
|
|
44
|
+
return ({
|
|
45
|
+
title: entry.view.name,
|
|
46
|
+
url: navigation.buildUrlCollectionPath(entry.fullPath)
|
|
47
|
+
});
|
|
48
|
+
} else if (entry.type === "collection") {
|
|
49
|
+
return ({
|
|
50
|
+
title: entry.collection.name,
|
|
51
|
+
url: navigation.buildUrlCollectionPath(entry.fullPath)
|
|
52
|
+
});
|
|
53
|
+
} else {
|
|
54
|
+
throw new Error("Unexpected navigation entry type");
|
|
55
|
+
}
|
|
56
|
+
})
|
|
57
|
+
});
|
|
58
|
+
}, [navigationEntries.map(entry => entry.path).join(",")]);
|
|
59
|
+
|
|
60
|
+
if (isNew) {
|
|
61
|
+
return <EntityFullScreenRoute
|
|
62
|
+
pathname={pathname}
|
|
63
|
+
navigationEntries={navigationEntries}
|
|
64
|
+
isNew={true}
|
|
65
|
+
/>;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (navigationEntries.length === 1 && navigationEntries[0].type === "collection") {
|
|
69
|
+
const collection = navigation.getCollection(navigationEntries[0].path);
|
|
70
|
+
if (!collection)
|
|
71
|
+
return null;
|
|
72
|
+
return <EntityCollectionView
|
|
73
|
+
key={`collection_view_${collection.id ?? collection.path}`}
|
|
74
|
+
isSubCollection={false}
|
|
75
|
+
parentCollectionIds={[]}
|
|
76
|
+
fullPath={collection.id}
|
|
77
|
+
updateUrl={true}
|
|
78
|
+
{...collection}
|
|
79
|
+
Actions={toArray(collection.Actions)}/>
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (isSidePanel) {
|
|
83
|
+
const lastCollectionEntry = navigationEntries.findLast((entry) => entry.type === "collection");
|
|
84
|
+
if (lastCollectionEntry) {
|
|
85
|
+
const collection = navigation.getCollection(lastCollectionEntry.path);
|
|
86
|
+
if (!collection)
|
|
87
|
+
return null;
|
|
88
|
+
return <EntityCollectionView
|
|
89
|
+
key={`collection_view_${collection.id ?? collection.path}`}
|
|
90
|
+
isSubCollection={false}
|
|
91
|
+
parentCollectionIds={[]}
|
|
92
|
+
fullPath={collection.id}
|
|
93
|
+
updateUrl={true}
|
|
94
|
+
{...collection}
|
|
95
|
+
Actions={toArray(collection.Actions)}/>;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return <EntityFullScreenRoute
|
|
100
|
+
pathname={pathname}
|
|
101
|
+
navigationEntries={navigationEntries}
|
|
102
|
+
isNew={isNew}
|
|
103
|
+
/>;
|
|
104
|
+
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function EntityFullScreenRoute({
|
|
108
|
+
pathname,
|
|
109
|
+
navigationEntries,
|
|
110
|
+
isNew
|
|
111
|
+
}: {
|
|
112
|
+
pathname: string;
|
|
113
|
+
navigationEntries: NavigationViewInternal[],
|
|
114
|
+
isNew: boolean
|
|
115
|
+
}) {
|
|
116
|
+
|
|
117
|
+
const navigation = useNavigationController();
|
|
118
|
+
const navigate = useNavigate();
|
|
119
|
+
|
|
120
|
+
const navigationPath = navigation.urlPathToDataPath(pathname);
|
|
121
|
+
|
|
122
|
+
// is navigating away blocked
|
|
123
|
+
const blocked = useRef(false);
|
|
124
|
+
|
|
125
|
+
const lastEntityEntry = navigationEntries.findLast((entry) => entry.type === "entity");
|
|
126
|
+
const navigationEntriesAfterEntity = lastEntityEntry ? navigationEntries.slice(navigationEntries.indexOf(lastEntityEntry) + 1) : [];
|
|
127
|
+
|
|
128
|
+
const lastCustomView = navigationEntriesAfterEntity.findLast(
|
|
129
|
+
(entry) => entry.type === "custom_view" || entry.type === "collection"
|
|
130
|
+
) as NavigationViewCollectionInternal<any> | NavigationViewEntityCustomInternal<any> | undefined;
|
|
131
|
+
|
|
132
|
+
const entityId = lastEntityEntry?.entityId;
|
|
133
|
+
|
|
134
|
+
const urlTab = isNew ? undefined : (lastCustomView && "id" in lastCustomView ? lastCustomView?.id : undefined) ?? lastCustomView?.path;
|
|
135
|
+
const [selectedTab, setSelectedTab] = useState<string | undefined>(urlTab);
|
|
136
|
+
|
|
137
|
+
const parentCollectionIds = navigation.getParentCollectionIds(navigationPath);
|
|
138
|
+
useEffect(() => {
|
|
139
|
+
if (urlTab !== selectedTab) {
|
|
140
|
+
setSelectedTab(urlTab);
|
|
141
|
+
}
|
|
142
|
+
}, [urlTab]);
|
|
143
|
+
|
|
144
|
+
const basePath = !entityId || isNew
|
|
145
|
+
? pathname
|
|
146
|
+
: pathname.substring(0, pathname.lastIndexOf(`/${entityId}`));
|
|
147
|
+
|
|
148
|
+
const entityPath = basePath + `/${entityId}`;
|
|
149
|
+
|
|
150
|
+
let blocker: Blocker | undefined = undefined;
|
|
151
|
+
try {
|
|
152
|
+
blocker = useBlocker(({
|
|
153
|
+
nextLocation
|
|
154
|
+
}) => {
|
|
155
|
+
if (nextLocation.pathname.startsWith(entityPath))
|
|
156
|
+
return false;
|
|
157
|
+
return blocked.current;
|
|
158
|
+
});
|
|
159
|
+
} catch (e) {
|
|
160
|
+
console.warn("Blocker not available, navigation will not be blocked");
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function updateUrl(entityId: string | undefined, newSelectedTab: string | undefined, replace: boolean, path: string, isNew: boolean) {
|
|
164
|
+
|
|
165
|
+
console.log("Updating url", {
|
|
166
|
+
entityId,
|
|
167
|
+
newSelectedTab,
|
|
168
|
+
replace,
|
|
169
|
+
basePath,
|
|
170
|
+
path,
|
|
171
|
+
isNew
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
if (!isNew && (newSelectedTab ?? null) === (selectedTab ?? null)) {
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (isNew) {
|
|
179
|
+
navigate(`${basePath}/${entityId}`, { replace: replace });
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (newSelectedTab) {
|
|
184
|
+
navigate(`${basePath}/${entityId}/${newSelectedTab}`, { replace: replace });
|
|
185
|
+
} else {
|
|
186
|
+
navigate(`${basePath}/${entityId}`, { replace: replace });
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const lastCollectionEntry = navigationEntries.findLast((entry) => entry.type === "collection");
|
|
192
|
+
|
|
193
|
+
if (isNew && !lastCollectionEntry) {
|
|
194
|
+
throw new Error("No collection found in the navigation");
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (!isNew && !lastEntityEntry) {
|
|
198
|
+
throw new Error("No entity found in the navigation");
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const collection = isNew ? lastCollectionEntry!.collection : lastEntityEntry!.parentCollection;
|
|
202
|
+
const collectionPath = navigation.resolveIdsFrom(isNew ? lastCollectionEntry!.path : lastEntityEntry!.path);
|
|
203
|
+
|
|
204
|
+
return <>
|
|
205
|
+
<EntityEditView
|
|
206
|
+
key={collection.id + "_" + (isNew ? "new" : entityId)}
|
|
207
|
+
entityId={isNew ? undefined : entityId}
|
|
208
|
+
collection={collection}
|
|
209
|
+
layout={"full_screen"}
|
|
210
|
+
path={collectionPath}
|
|
211
|
+
selectedTab={selectedTab ?? undefined}
|
|
212
|
+
onValuesModified={(modified) => blocked.current = modified}
|
|
213
|
+
onSaved={(params) => {
|
|
214
|
+
updateUrl(params.entityId, params.selectedTab, true, params.path, isNew);
|
|
215
|
+
}}
|
|
216
|
+
onTabChange={(params) => {
|
|
217
|
+
updateUrl(params.entityId, params.selectedTab, !isNew, params.path, isNew);
|
|
218
|
+
setSelectedTab(params.selectedTab);
|
|
219
|
+
}}
|
|
220
|
+
parentCollectionIds={parentCollectionIds}
|
|
221
|
+
/>
|
|
222
|
+
|
|
223
|
+
<UnsavedChangesDialog
|
|
224
|
+
open={blocker?.state === "blocked"}
|
|
225
|
+
handleOk={() => blocker?.proceed?.()}
|
|
226
|
+
handleCancel={() => blocker?.reset?.()}
|
|
227
|
+
body={"You have unsaved changes in this entity."}/>
|
|
228
|
+
|
|
229
|
+
</>;
|
|
230
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { useBreadcrumbsController } from "../hooks/useBreadcrumbsController";
|
|
2
|
+
import { useEffect } from "react";
|
|
3
|
+
|
|
4
|
+
export function HomePageRoute({ children }: {
|
|
5
|
+
children: React.ReactNode;
|
|
6
|
+
}) {
|
|
7
|
+
|
|
8
|
+
const breadcrumbs = useBreadcrumbsController();
|
|
9
|
+
|
|
10
|
+
useEffect(() => {
|
|
11
|
+
breadcrumbs.set({
|
|
12
|
+
breadcrumbs: []
|
|
13
|
+
});
|
|
14
|
+
}, []);
|
|
15
|
+
|
|
16
|
+
return children;
|
|
17
|
+
}
|
package/src/types/collections.ts
CHANGED
|
@@ -84,6 +84,11 @@ export interface EntityCollection<M extends Record<string, any> = any, USER exte
|
|
|
84
84
|
*/
|
|
85
85
|
properties: PropertiesOrBuilders<M>;
|
|
86
86
|
|
|
87
|
+
/**
|
|
88
|
+
* Default preview properties displayed when this collection is referenced to.
|
|
89
|
+
*/
|
|
90
|
+
previewProperties?: string[];
|
|
91
|
+
|
|
87
92
|
/**
|
|
88
93
|
* Title property of the entity. This is the property that will be used
|
|
89
94
|
* as the title in entity related views and references.
|
|
@@ -92,11 +97,10 @@ export interface EntityCollection<M extends Record<string, any> = any, USER exte
|
|
|
92
97
|
titleProperty?: keyof M;
|
|
93
98
|
|
|
94
99
|
/**
|
|
95
|
-
*
|
|
96
|
-
* Defaults to `
|
|
97
|
-
* Keep in mind that you can also set this prop to individual properties.
|
|
100
|
+
* When editing an entity, you can choose to open the entity in a side dialog
|
|
101
|
+
* or in a full screen dialog. Defaults to `full_screen`.
|
|
98
102
|
*/
|
|
99
|
-
|
|
103
|
+
openEntityMode?: "side_panel" | "full_screen";
|
|
100
104
|
|
|
101
105
|
/**
|
|
102
106
|
* Order in which the properties are displayed.
|
|
@@ -313,9 +317,12 @@ export interface EntityCollection<M extends Record<string, any> = any, USER exte
|
|
|
313
317
|
sideDialogWidth?: number | string;
|
|
314
318
|
|
|
315
319
|
/**
|
|
316
|
-
*
|
|
320
|
+
* Can this collection configuration be edited by the end user.
|
|
321
|
+
* Defaults to `true`.
|
|
322
|
+
* Keep in mind that you can also set this prop to individual properties.
|
|
323
|
+
* This prop has only effect if you are using the collection editor.
|
|
317
324
|
*/
|
|
318
|
-
|
|
325
|
+
editable?: boolean;
|
|
319
326
|
}
|
|
320
327
|
|
|
321
328
|
/**
|
|
@@ -570,6 +577,12 @@ export type EntityTableController<M extends Record<string, any> = any> = {
|
|
|
570
577
|
clearFilter?: () => void;
|
|
571
578
|
itemCount?: number;
|
|
572
579
|
setItemCount?: (itemCount: number) => void;
|
|
580
|
+
initialScroll?: number;
|
|
581
|
+
onScroll?: (props: {
|
|
582
|
+
scrollDirection: "forward" | "backward",
|
|
583
|
+
scrollOffset: number,
|
|
584
|
+
scrollUpdateWasRequested: boolean
|
|
585
|
+
}) => void;
|
|
573
586
|
paginationEnabled?: boolean;
|
|
574
587
|
pageSize?: number;
|
|
575
588
|
checkFilterCombination?: (filterValues: FilterValues<any>,
|
package/src/types/datasource.ts
CHANGED
|
@@ -9,6 +9,7 @@ import { FireCMSContext } from "./firecms_context";
|
|
|
9
9
|
export interface FetchEntityProps<M extends Record<string, any> = any> {
|
|
10
10
|
path: string;
|
|
11
11
|
entityId: string;
|
|
12
|
+
databaseId?: string;
|
|
12
13
|
collection?: EntityCollection<M, any>
|
|
13
14
|
}
|
|
14
15
|
|
|
@@ -18,7 +19,6 @@ export interface FetchEntityProps<M extends Record<string, any> = any> {
|
|
|
18
19
|
export type ListenEntityProps<M extends Record<string, any> = any> =
|
|
19
20
|
FetchEntityProps<M>
|
|
20
21
|
& {
|
|
21
|
-
databaseId?: string;
|
|
22
22
|
onUpdate: (entity: Entity<M>) => void,
|
|
23
23
|
onError?: (error: Error) => void,
|
|
24
24
|
}
|
|
@@ -139,6 +139,7 @@ export interface DataSource {
|
|
|
139
139
|
fetchEntity<M extends Record<string, any> = any>({
|
|
140
140
|
path,
|
|
141
141
|
entityId,
|
|
142
|
+
databaseId,
|
|
142
143
|
collection
|
|
143
144
|
}: FetchEntityProps<M>
|
|
144
145
|
): Promise<Entity<M> | undefined>;
|
|
@@ -22,16 +22,12 @@ export interface DialogsController {
|
|
|
22
22
|
* Props used to open a side dialog
|
|
23
23
|
* @group Hooks and utilities
|
|
24
24
|
*/
|
|
25
|
-
export interface DialogControllerEntryProps
|
|
25
|
+
export interface DialogControllerEntryProps {
|
|
26
26
|
|
|
27
27
|
key: string;
|
|
28
28
|
/**
|
|
29
29
|
* The component type that will be rendered
|
|
30
30
|
*/
|
|
31
|
-
Component: React.ComponentType<{ open: boolean, closeDialog: () => void }
|
|
32
|
-
/**
|
|
33
|
-
* Props to pass to the dialog component
|
|
34
|
-
*/
|
|
35
|
-
props?: T;
|
|
31
|
+
Component: React.ComponentType<{ open: boolean, closeDialog: () => void }>;
|
|
36
32
|
|
|
37
33
|
}
|
package/src/types/entities.ts
CHANGED
|
@@ -8,7 +8,7 @@ export type EntityStatus = "new" | "existing" | "copy";
|
|
|
8
8
|
* Representation of an entity fetched from the datasource
|
|
9
9
|
* @group Models
|
|
10
10
|
*/
|
|
11
|
-
export interface Entity<M extends object> {
|
|
11
|
+
export interface Entity<M extends object = any> {
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
14
|
* ID of the entity
|
|
@@ -4,8 +4,6 @@ import { Entity } from "./entities";
|
|
|
4
4
|
import { EntityCollection, SelectionController } from "./collections";
|
|
5
5
|
import { User } from "./user";
|
|
6
6
|
import { SideEntityController } from "./side_entity_controller";
|
|
7
|
-
import { FormexController } from "@firecms/formex";
|
|
8
|
-
import { FormContext } from "./fields";
|
|
9
7
|
|
|
10
8
|
/**
|
|
11
9
|
* An entity action is a custom action that can be performed on an entity.
|
|
@@ -66,9 +64,5 @@ export type EntityActionClickProps<M extends object, USER extends User = User> =
|
|
|
66
64
|
*/
|
|
67
65
|
sideEntityController?: SideEntityController;
|
|
68
66
|
|
|
69
|
-
|
|
70
|
-
* You can use the form context to manage the state of the form.
|
|
71
|
-
* This is only available if this action in being triggered from a form context.
|
|
72
|
-
*/
|
|
73
|
-
formContext?: FormContext<M>;
|
|
67
|
+
openEntityMode: "side_panel" | "full_screen";
|
|
74
68
|
};
|
package/src/types/navigation.ts
CHANGED
|
@@ -56,8 +56,8 @@ export type NavigationController<EC extends EntityCollection = EntityCollection<
|
|
|
56
56
|
* Get the collection configuration for a given path.
|
|
57
57
|
* The collection is resolved from the given path or alias.
|
|
58
58
|
*/
|
|
59
|
-
getCollection: (pathOrId: string,
|
|
60
|
-
|
|
59
|
+
getCollection: (pathOrId: string, includeUserOverride?: boolean) => EC | undefined;
|
|
60
|
+
|
|
61
61
|
/**
|
|
62
62
|
* Get the collection configuration from its parent ids.
|
|
63
63
|
*/
|
|
@@ -69,12 +69,14 @@ export type NavigationController<EC extends EntityCollection = EntityCollection<
|
|
|
69
69
|
getCollectionFromPaths: (pathSegments: string[]) => EC | undefined;
|
|
70
70
|
|
|
71
71
|
/**
|
|
72
|
-
* Default path under the navigation routes of the CMS will be created
|
|
72
|
+
* Default path under the navigation routes of the CMS will be created.
|
|
73
|
+
* Defaults to '/'. You may want to change this `basepath` to 'admin' for example.
|
|
73
74
|
*/
|
|
74
75
|
basePath: string;
|
|
75
76
|
|
|
76
77
|
/**
|
|
77
|
-
* Default path under the collection routes of the CMS will be created
|
|
78
|
+
* Default path under the collection routes of the CMS will be created.
|
|
79
|
+
* It defaults to '/c'
|
|
78
80
|
*/
|
|
79
81
|
baseCollectionPath: string;
|
|
80
82
|
|
|
@@ -87,16 +89,6 @@ export type NavigationController<EC extends EntityCollection = EntityCollection<
|
|
|
87
89
|
*/
|
|
88
90
|
urlPathToDataPath: (cmsPath: string) => string;
|
|
89
91
|
|
|
90
|
-
/**
|
|
91
|
-
* Convert a collection or entity path to a URL path
|
|
92
|
-
* @param path
|
|
93
|
-
*/
|
|
94
|
-
buildCMSUrlPath: (path: string) => string;
|
|
95
|
-
|
|
96
|
-
buildUrlEditCollectionPath: (props: {
|
|
97
|
-
path: string
|
|
98
|
-
}) => string;
|
|
99
|
-
|
|
100
92
|
/**
|
|
101
93
|
* Base url path for the home screen
|
|
102
94
|
*/
|
|
@@ -117,10 +109,11 @@ export type NavigationController<EC extends EntityCollection = EntityCollection<
|
|
|
117
109
|
buildUrlCollectionPath: (path: string) => string;
|
|
118
110
|
|
|
119
111
|
/**
|
|
120
|
-
* Turn a path with
|
|
112
|
+
* Turn a path with collection ids into a resolved path.
|
|
113
|
+
* The ids (typically used in urls) will be replaced with relative paths (typically used in database paths)
|
|
121
114
|
* @param pathWithAliases
|
|
122
115
|
*/
|
|
123
|
-
|
|
116
|
+
resolveIdsFrom: (pathWithAliases: string) => string;
|
|
124
117
|
|
|
125
118
|
/**
|
|
126
119
|
* Call this method to recalculate the navigation
|
|
@@ -144,8 +137,34 @@ export type NavigationController<EC extends EntityCollection = EntityCollection<
|
|
|
144
137
|
* @param ids
|
|
145
138
|
*/
|
|
146
139
|
convertIdsToPaths: (ids: string[]) => string[];
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* A function to navigate to a specified route or URL.
|
|
143
|
+
*
|
|
144
|
+
* @param {string} to - The target route or URL to navigate to.
|
|
145
|
+
* @param {NavigateOptions} [options] - Optional configuration settings for navigation, such as replace behavior or state data.
|
|
146
|
+
*/
|
|
147
|
+
navigate: (to: string, options?: NavigateOptions) => void;
|
|
148
|
+
|
|
147
149
|
}
|
|
148
150
|
|
|
151
|
+
export interface NavigateOptions {
|
|
152
|
+
replace?: boolean;
|
|
153
|
+
state?: any;
|
|
154
|
+
preventScrollReset?: boolean;
|
|
155
|
+
relative?: "route" | "path";
|
|
156
|
+
flushSync?: boolean;
|
|
157
|
+
viewTransition?: boolean;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// currently not used, in favor of a single blocker in `FireCMSRoute`
|
|
161
|
+
export type NavigationBlocker = {
|
|
162
|
+
updateBlockListener: (path: string, block: boolean, basePath?: string) => () => void;
|
|
163
|
+
isBlocked: (path: string) => boolean;
|
|
164
|
+
proceed?: () => void;
|
|
165
|
+
reset?: () => void;
|
|
166
|
+
};
|
|
167
|
+
|
|
149
168
|
/**
|
|
150
169
|
* Custom additional views created by the developer, added to the main
|
|
151
170
|
* navigation.
|
package/src/types/plugins.tsx
CHANGED
|
@@ -195,6 +195,7 @@ export interface PluginFormActionProps<USER extends User = User, EC extends Enti
|
|
|
195
195
|
formContext?: FormContext<any>;
|
|
196
196
|
context: FireCMSContext<USER>;
|
|
197
197
|
currentEntityId?: string;
|
|
198
|
+
layout: "side_panel" | "full_screen";
|
|
198
199
|
}
|
|
199
200
|
|
|
200
201
|
export type PluginFieldBuilderParams<T extends CMSType = CMSType, M extends Record<string, any> = any, EC extends EntityCollection<M> = EntityCollection<M>> = {
|
|
@@ -28,7 +28,7 @@ export interface EntitySidePanelProps<M extends Record<string, any> = any> {
|
|
|
28
28
|
* Open the entity with a selected sub-collection view. If the panel for this
|
|
29
29
|
* entity was already open, it is replaced.
|
|
30
30
|
*/
|
|
31
|
-
|
|
31
|
+
selectedTab?: string;
|
|
32
32
|
|
|
33
33
|
/**
|
|
34
34
|
* Use this prop to override the width of the form view.
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { EntityReference, GeoPoint, Vector } from "../types";
|
|
2
|
+
|
|
3
|
+
// Define a unique prefix for entity keys in localStorage to avoid key collisions
|
|
4
|
+
const LOCAL_STORAGE_PREFIX = "entity_cache::";
|
|
5
|
+
|
|
6
|
+
// In-memory cache to store entities for quick access
|
|
7
|
+
const entityCache: Map<string, object> = new Map();
|
|
8
|
+
|
|
9
|
+
// Check `localStorage` availability once during initialization
|
|
10
|
+
const isLocalStorageAvailable = typeof localStorage !== "undefined";
|
|
11
|
+
|
|
12
|
+
// Define custom replacer for JSON.stringify
|
|
13
|
+
function customReplacer(key: string, value: any): any {
|
|
14
|
+
// Handle Date objects
|
|
15
|
+
if (value instanceof Date) {
|
|
16
|
+
return { __type: "Date", value: value.toISOString() };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Handle EntityReference
|
|
20
|
+
if (value instanceof EntityReference) {
|
|
21
|
+
return { __type: "EntityReference", id: value.id, path: value.path };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Handle GeoPoint
|
|
25
|
+
if (value instanceof GeoPoint) {
|
|
26
|
+
return { __type: "GeoPoint", latitude: value.latitude, longitude: value.longitude };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Handle Vector
|
|
30
|
+
if (value instanceof Vector) {
|
|
31
|
+
return { __type: "Vector", value: value.value };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return value;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Define custom reviver for JSON.parse
|
|
38
|
+
function customReviver(key: string, value: any): any {
|
|
39
|
+
if (value && typeof value === "object" && "__type" in value) {
|
|
40
|
+
switch (value.__type) {
|
|
41
|
+
case "Date":
|
|
42
|
+
return new Date(value.value);
|
|
43
|
+
case "EntityReference":
|
|
44
|
+
return new EntityReference(value.id, value.path);
|
|
45
|
+
case "GeoPoint":
|
|
46
|
+
return new GeoPoint(value.latitude, value.longitude);
|
|
47
|
+
case "Vector":
|
|
48
|
+
return new Vector(value.value);
|
|
49
|
+
default:
|
|
50
|
+
return value;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return value;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Initialize the in-memory cache by loading entities from `localStorage`
|
|
57
|
+
if (isLocalStorageAvailable) {
|
|
58
|
+
try {
|
|
59
|
+
// Iterate over all keys in localStorage to find those with the specified prefix
|
|
60
|
+
for (let i = 0; i < localStorage.length; i++) {
|
|
61
|
+
const fullKey = localStorage.key(i);
|
|
62
|
+
if (fullKey && fullKey.startsWith(LOCAL_STORAGE_PREFIX)) {
|
|
63
|
+
const path = fullKey.substring(LOCAL_STORAGE_PREFIX.length);
|
|
64
|
+
const entityString = localStorage.getItem(fullKey);
|
|
65
|
+
if (entityString) {
|
|
66
|
+
try {
|
|
67
|
+
const entity: object = JSON.parse(entityString, customReviver);
|
|
68
|
+
entityCache.set(path, entity);
|
|
69
|
+
} catch (parseError) {
|
|
70
|
+
console.error(
|
|
71
|
+
`Failed to parse entity for path "${path}" from localStorage:`,
|
|
72
|
+
parseError
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
} catch (error) {
|
|
79
|
+
console.error("Error accessing localStorage during initialization:", error);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Saves data to the in-memory cache and persists it individually in `localStorage`.
|
|
85
|
+
* @param path - The unique path/key for the data.
|
|
86
|
+
* @param data - The data to cache and persist.
|
|
87
|
+
*/
|
|
88
|
+
export function saveEntityToCache(path: string, data: object): void {
|
|
89
|
+
// Update the in-memory cache
|
|
90
|
+
entityCache.set(path, data);
|
|
91
|
+
|
|
92
|
+
// Persist the data individually in localStorage
|
|
93
|
+
if (isLocalStorageAvailable) {
|
|
94
|
+
try {
|
|
95
|
+
const key = LOCAL_STORAGE_PREFIX + path;
|
|
96
|
+
const entityString = JSON.stringify(data, customReplacer);
|
|
97
|
+
localStorage.setItem(key, entityString);
|
|
98
|
+
} catch (error) {
|
|
99
|
+
console.error(
|
|
100
|
+
`Failed to save entity for path "${path}" to localStorage:`,
|
|
101
|
+
error
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Retrieves an entity from the in-memory cache or `localStorage`.
|
|
109
|
+
* If the entity is not in the cache but exists in `localStorage`, it loads it into the cache.
|
|
110
|
+
* @param path - The unique path/key for the entity.
|
|
111
|
+
* @returns The cached entity or `undefined` if not found.
|
|
112
|
+
*/
|
|
113
|
+
export function getEntityFromCache(path: string): object | undefined {
|
|
114
|
+
|
|
115
|
+
// Attempt to retrieve the entity from the in-memory cache
|
|
116
|
+
if (entityCache.has(path)) {
|
|
117
|
+
return entityCache.get(path);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// If not in the cache, attempt to load it from localStorage
|
|
121
|
+
if (isLocalStorageAvailable) {
|
|
122
|
+
try {
|
|
123
|
+
const key = LOCAL_STORAGE_PREFIX + path;
|
|
124
|
+
const entityString = localStorage.getItem(key);
|
|
125
|
+
if (entityString) {
|
|
126
|
+
const entity: object = JSON.parse(entityString, customReviver);
|
|
127
|
+
entityCache.set(path, entity); // Update the cache
|
|
128
|
+
return entity;
|
|
129
|
+
}
|
|
130
|
+
} catch (error) {
|
|
131
|
+
console.error(
|
|
132
|
+
`Failed to load entity for path "${path}" from localStorage:`,
|
|
133
|
+
error
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Entity not found
|
|
139
|
+
return undefined;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function hasEntityInCache(path: string): boolean {
|
|
143
|
+
return entityCache.has(path);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Removes an entity from both the in-memory cache and `localStorage`.
|
|
148
|
+
* @param path - The unique path/key for the entity to remove.
|
|
149
|
+
*/
|
|
150
|
+
export function removeEntityFromCache(path: string): void {
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
console.log("Removing entity from cache", path);
|
|
154
|
+
|
|
155
|
+
// Remove from the in-memory cache
|
|
156
|
+
entityCache.delete(path);
|
|
157
|
+
|
|
158
|
+
// Remove from localStorage
|
|
159
|
+
if (isLocalStorageAvailable) {
|
|
160
|
+
try {
|
|
161
|
+
const key = LOCAL_STORAGE_PREFIX + path;
|
|
162
|
+
localStorage.removeItem(key);
|
|
163
|
+
} catch (error) {
|
|
164
|
+
console.error(
|
|
165
|
+
`Failed to remove entity for path "${path}" from localStorage:`,
|
|
166
|
+
error
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Clears the entire in-memory cache and removes all related entities from `localStorage`.
|
|
174
|
+
*/
|
|
175
|
+
export function clearEntityCache(): void {
|
|
176
|
+
// Clear the in-memory cache
|
|
177
|
+
entityCache.clear();
|
|
178
|
+
|
|
179
|
+
// Remove all entities with the specified prefix from localStorage
|
|
180
|
+
if (isLocalStorageAvailable) {
|
|
181
|
+
try {
|
|
182
|
+
const keysToRemove: string[] = [];
|
|
183
|
+
for (let i = 0; i < localStorage.length; i++) {
|
|
184
|
+
const fullKey = localStorage.key(i);
|
|
185
|
+
if (fullKey && fullKey.startsWith(LOCAL_STORAGE_PREFIX)) {
|
|
186
|
+
keysToRemove.push(fullKey);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Remove the keys after collecting them to avoid issues while iterating
|
|
191
|
+
keysToRemove.forEach((key) => localStorage.removeItem(key));
|
|
192
|
+
} catch (error) {
|
|
193
|
+
console.error("Failed to clear entity cache from localStorage:", error);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|