@firecms/core 3.0.0-beta.14 → 3.0.0-beta.15
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/HomePage/DefaultHomePage.d.ts +2 -15
- package/dist/components/HomePage/HomePageDnD.d.ts +76 -0
- package/dist/components/HomePage/NavigationCard.d.ts +3 -1
- package/dist/components/HomePage/NavigationCardBinding.d.ts +3 -2
- package/dist/components/HomePage/NavigationGroup.d.ts +7 -1
- package/dist/components/HomePage/RenameGroupDialog.d.ts +9 -0
- package/dist/core/EntityEditView.d.ts +3 -0
- package/dist/core/EntityEditViewFormActions.d.ts +1 -1
- package/dist/core/field_configs.d.ts +1 -1
- package/dist/form/EntityForm.d.ts +2 -1
- package/dist/form/EntityFormActions.d.ts +6 -2
- package/dist/form/field_bindings/ReferenceAsStringFieldBinding.d.ts +9 -0
- package/dist/form/index.d.ts +1 -0
- package/dist/hooks/useBuildNavigationController.d.ts +51 -2
- package/dist/index.es.js +2184 -1052
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +2181 -1049
- package/dist/index.umd.js.map +1 -1
- package/dist/types/analytics.d.ts +1 -1
- package/dist/types/collections.d.ts +8 -2
- package/dist/types/customization_controller.d.ts +8 -0
- package/dist/types/entity_actions.d.ts +46 -6
- package/dist/types/firecms.d.ts +8 -0
- package/dist/types/navigation.d.ts +21 -5
- package/dist/types/plugins.d.ts +20 -1
- package/dist/types/properties.d.ts +7 -0
- package/dist/types/property_config.d.ts +1 -1
- package/dist/types/side_entity_controller.d.ts +4 -0
- package/dist/util/icons.d.ts +2 -2
- package/dist/util/navigation_from_path.d.ts +4 -0
- package/dist/util/resolutions.d.ts +2 -1
- package/package.json +5 -5
- package/src/components/ConfirmationDialog.tsx +1 -0
- package/src/components/EntityCollectionTable/EntityCollectionRowActions.tsx +6 -0
- package/src/components/EntityCollectionTable/PropertyTableCell.tsx +25 -3
- package/src/components/EntityCollectionTable/internal/CollectionTableToolbar.tsx +2 -2
- package/src/components/EntityCollectionView/EntityCollectionView.tsx +7 -4
- package/src/components/EntityCollectionView/EntityCollectionViewActions.tsx +3 -2
- package/src/components/FireCMSLogo.tsx +7 -51
- package/src/components/HomePage/DefaultHomePage.tsx +491 -157
- package/src/components/HomePage/FavouritesView.tsx +3 -3
- package/src/components/HomePage/HomePageDnD.tsx +599 -0
- package/src/components/HomePage/NavigationCard.tsx +47 -38
- package/src/components/HomePage/NavigationCardBinding.tsx +10 -6
- package/src/components/HomePage/NavigationGroup.tsx +63 -29
- package/src/components/HomePage/RenameGroupDialog.tsx +117 -0
- package/src/components/UnsavedChangesDialog.tsx +6 -2
- package/src/components/common/default_entity_actions.tsx +25 -9
- package/src/components/common/useDataSourceTableController.tsx +2 -2
- package/src/core/DefaultDrawer.tsx +8 -8
- package/src/core/DrawerNavigationItem.tsx +1 -1
- package/src/core/EntityEditView.tsx +41 -6
- package/src/core/EntityEditViewFormActions.tsx +154 -29
- package/src/core/EntitySidePanel.tsx +5 -2
- package/src/core/FireCMS.tsx +2 -0
- package/src/core/field_configs.tsx +15 -1
- package/src/form/EntityForm.tsx +36 -4
- package/src/form/EntityFormActions.tsx +51 -9
- package/src/form/components/StorageItemPreview.tsx +1 -1
- package/src/form/components/StorageUploadProgress.tsx +3 -3
- package/src/form/field_bindings/MarkdownEditorFieldBinding.tsx +4 -2
- package/src/form/field_bindings/ReferenceAsStringFieldBinding.tsx +135 -0
- package/src/form/field_bindings/RepeatFieldBinding.tsx +0 -1
- package/src/form/field_bindings/StorageUploadFieldBinding.tsx +12 -17
- package/src/form/index.tsx +1 -0
- package/src/hooks/useBuildNavigationController.tsx +273 -84
- package/src/internal/useBuildSideEntityController.tsx +7 -4
- package/src/preview/PropertyPreview.tsx +14 -0
- package/src/routes/FireCMSRoute.tsx +3 -3
- package/src/types/analytics.ts +3 -0
- package/src/types/collections.ts +8 -2
- package/src/types/customization_controller.tsx +9 -0
- package/src/types/entity_actions.tsx +57 -6
- package/src/types/firecms.tsx +9 -0
- package/src/types/navigation.ts +28 -6
- package/src/types/plugins.tsx +24 -1
- package/src/types/properties.ts +8 -0
- package/src/types/property_config.tsx +1 -0
- package/src/types/side_entity_controller.tsx +5 -0
- package/src/util/icons.tsx +22 -7
- package/src/util/join_collections.ts +3 -1
- package/src/util/navigation_from_path.ts +15 -5
- package/src/util/navigation_utils.ts +2 -2
- package/src/util/resolutions.ts +13 -1
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useCallback, useEffect, useRef, useState } from "react";
|
|
2
2
|
import equal from "react-fast-compare"
|
|
3
|
+
import { useBlocker, useNavigate } from "react-router-dom";
|
|
3
4
|
|
|
4
5
|
import {
|
|
5
6
|
AuthController,
|
|
@@ -12,9 +13,10 @@ import {
|
|
|
12
13
|
FireCMSPlugin,
|
|
13
14
|
NavigationBlocker,
|
|
14
15
|
NavigationController,
|
|
16
|
+
NavigationEntry,
|
|
17
|
+
NavigationGroupMapping,
|
|
18
|
+
NavigationResult,
|
|
15
19
|
PermissionsBuilder,
|
|
16
|
-
TopNavigationEntry,
|
|
17
|
-
TopNavigationResult,
|
|
18
20
|
User,
|
|
19
21
|
UserConfigurationPersistence
|
|
20
22
|
} from "../types";
|
|
@@ -28,27 +30,77 @@ import {
|
|
|
28
30
|
resolvePermissions
|
|
29
31
|
} from "../util";
|
|
30
32
|
import { getParentReferencesFromPath } from "../util/parent_references_from_path";
|
|
31
|
-
import { useBlocker, useNavigate } from "react-router-dom";
|
|
32
33
|
|
|
33
34
|
const DEFAULT_BASE_PATH = "/";
|
|
34
35
|
const DEFAULT_COLLECTION_PATH = "/c";
|
|
35
36
|
|
|
37
|
+
export const NAVIGATION_DEFAULT_GROUP_NAME = "Views";
|
|
38
|
+
export const NAVIGATION_ADMIN_GROUP_NAME = "Admin";
|
|
39
|
+
|
|
36
40
|
export type BuildNavigationContextProps<EC extends EntityCollection, USER extends User> = {
|
|
41
|
+
/**
|
|
42
|
+
* Base path for the CMS, used to build the all the URLs.
|
|
43
|
+
* Defaults to "/".
|
|
44
|
+
*/
|
|
37
45
|
basePath?: string,
|
|
46
|
+
/**
|
|
47
|
+
* Base path for the collections, used to build the collection URLs.
|
|
48
|
+
* Defaults to "c" (e.g. "/c/products").
|
|
49
|
+
*/
|
|
38
50
|
baseCollectionPath?: string,
|
|
51
|
+
/**
|
|
52
|
+
* The auth controller used to manage the user authentication and permissions.
|
|
53
|
+
*/
|
|
39
54
|
authController: AuthController<USER>;
|
|
55
|
+
/**
|
|
56
|
+
* The collections to be used in the CMS.
|
|
57
|
+
* This can be a static array of collections or a function that returns a promise
|
|
58
|
+
* resolving to an array of collections.
|
|
59
|
+
*/
|
|
40
60
|
collections?: EC[] | EntityCollectionsBuilder<EC>;
|
|
61
|
+
/**
|
|
62
|
+
* Optional permissions builder to be applied to the collections.
|
|
63
|
+
* If not provided, the permissions will be resolved from the collection configuration.
|
|
64
|
+
*/
|
|
41
65
|
collectionPermissions?: PermissionsBuilder;
|
|
66
|
+
/**
|
|
67
|
+
* Custom views to be added to the CMS, these will be available in the main navigation.
|
|
68
|
+
* This can be a static array of views or a function that returns a promise
|
|
69
|
+
* resolving to an array of views.
|
|
70
|
+
*/
|
|
42
71
|
views?: CMSView[] | CMSViewsBuilder;
|
|
72
|
+
/**
|
|
73
|
+
* Custom views to be added to the CMS admin navigation.
|
|
74
|
+
* This can be a static array of views or a function that returns a promise
|
|
75
|
+
* resolving to an array of views.
|
|
76
|
+
*/
|
|
43
77
|
adminViews?: CMSView[] | CMSViewsBuilder;
|
|
44
|
-
|
|
78
|
+
/**
|
|
79
|
+
* Controller for storing user preferences.
|
|
80
|
+
*/
|
|
45
81
|
userConfigPersistence?: UserConfigurationPersistence;
|
|
82
|
+
/**
|
|
83
|
+
* Delegate for data source operations, used to resolve collections and views.
|
|
84
|
+
*/
|
|
46
85
|
dataSourceDelegate: DataSourceDelegate;
|
|
86
|
+
/**
|
|
87
|
+
* Plugins to be used in the CMS.
|
|
88
|
+
*/
|
|
47
89
|
plugins?: FireCMSPlugin[];
|
|
90
|
+
/**
|
|
91
|
+
* Used to define the name of groups and order of the navigation entries.
|
|
92
|
+
*/
|
|
93
|
+
navigationGroupMappings?: NavigationGroupMapping[];
|
|
48
94
|
/**
|
|
49
95
|
* If true, the navigation logic will not be updated until this flag is false
|
|
50
96
|
*/
|
|
51
97
|
disabled?: boolean;
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* @deprecated
|
|
101
|
+
* Use `navigationGroupMappings` instead.
|
|
102
|
+
*/
|
|
103
|
+
viewsOrder?: string[];
|
|
52
104
|
};
|
|
53
105
|
|
|
54
106
|
export function useBuildNavigationController<EC extends EntityCollection, USER extends User>(props: BuildNavigationContextProps<EC, USER>): NavigationController {
|
|
@@ -64,7 +116,8 @@ export function useBuildNavigationController<EC extends EntityCollection, USER e
|
|
|
64
116
|
plugins,
|
|
65
117
|
userConfigPersistence,
|
|
66
118
|
dataSourceDelegate,
|
|
67
|
-
disabled
|
|
119
|
+
disabled,
|
|
120
|
+
navigationGroupMappings
|
|
68
121
|
} = props;
|
|
69
122
|
|
|
70
123
|
const navigate = useNavigate();
|
|
@@ -72,10 +125,11 @@ export function useBuildNavigationController<EC extends EntityCollection, USER e
|
|
|
72
125
|
const collectionsRef = useRef<EntityCollection[] | undefined>();
|
|
73
126
|
const viewsRef = useRef<CMSView[] | undefined>();
|
|
74
127
|
const adminViewsRef = useRef<CMSView[] | undefined>();
|
|
128
|
+
const navigationEntriesOrderRef = useRef<string[] | undefined>();
|
|
75
129
|
|
|
76
130
|
const [initialised, setInitialised] = useState<boolean>(false);
|
|
77
131
|
|
|
78
|
-
const [topLevelNavigation, setTopLevelNavigation] = useState<
|
|
132
|
+
const [topLevelNavigation, setTopLevelNavigation] = useState<NavigationResult | undefined>(undefined);
|
|
79
133
|
const [navigationLoading, setNavigationLoading] = useState<boolean>(true);
|
|
80
134
|
const [navigationLoadingError, setNavigationLoadingError] = useState<Error | undefined>(undefined);
|
|
81
135
|
|
|
@@ -92,99 +146,155 @@ export function useBuildNavigationController<EC extends EntityCollection, USER e
|
|
|
92
146
|
const buildUrlCollectionPath = useCallback((path: string): string => `${removeInitialAndTrailingSlashes(baseCollectionPath)}/${encodePath(path)}`,
|
|
93
147
|
[baseCollectionPath]);
|
|
94
148
|
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
149
|
+
const allPluginGroups = plugins?.flatMap(plugin => plugin.homePage?.navigationEntries ? plugin.homePage.navigationEntries.map(e => e.name) : []) ?? [];
|
|
150
|
+
const pluginGroups = [...new Set(allPluginGroups)];
|
|
151
|
+
|
|
152
|
+
const onNavigationEntriesOrderUpdate = useCallback((entries: NavigationGroupMapping[]) => {
|
|
153
|
+
if (!plugins) {
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
// remove all groups that have no entries
|
|
157
|
+
const filteredEntries = entries.filter(entry => entry.entries.length > 0);
|
|
158
|
+
if (plugins.some(plugin => plugin.homePage?.onNavigationEntriesUpdate)) {
|
|
159
|
+
plugins.forEach(plugin => {
|
|
160
|
+
if (plugin.homePage?.onNavigationEntriesUpdate) {
|
|
161
|
+
plugin.homePage.onNavigationEntriesUpdate(filteredEntries);
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
}, [plugins]);
|
|
167
|
+
|
|
168
|
+
const computeTopNavigation = useCallback((collections: EntityCollection[], views: CMSView[], adminViews: CMSView[], viewsOrder?: string[]): NavigationResult => {
|
|
169
|
+
|
|
170
|
+
const finalNavigationGroupMappings: NavigationGroupMapping[] = computeNavigationGroups({
|
|
171
|
+
navigationGroupMappings: navigationGroupMappings,
|
|
172
|
+
collections,
|
|
173
|
+
views,
|
|
174
|
+
plugins: plugins
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
const allPluginNavigationEntries = finalNavigationGroupMappings.map((g) => g.entries).flat() ?? [];
|
|
178
|
+
const navigationEntriesOrder = ([...new Set(allPluginNavigationEntries)]);
|
|
179
|
+
|
|
180
|
+
let navigationEntries: NavigationEntry[] = [
|
|
181
|
+
...(collections ?? []).reduce((acc, collection) => {
|
|
182
|
+
if (collection.hideFromNavigation) return acc;
|
|
183
|
+
|
|
184
|
+
const pathKey = collection.id ?? collection.path;
|
|
185
|
+
let groupName = getGroup(collection); // Initial group
|
|
186
|
+
|
|
187
|
+
if (finalNavigationGroupMappings) {
|
|
188
|
+
for (const pluginGroupDef of finalNavigationGroupMappings) {
|
|
189
|
+
if (pluginGroupDef.entries.includes(pathKey)) {
|
|
190
|
+
groupName = pluginGroupDef.name;
|
|
191
|
+
break;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
acc.push({
|
|
197
|
+
id: `collection:${pathKey}`,
|
|
198
|
+
url: buildUrlCollectionPath(pathKey),
|
|
100
199
|
type: "collection",
|
|
101
200
|
name: collection.name.trim(),
|
|
102
|
-
path:
|
|
201
|
+
path: pathKey,
|
|
103
202
|
collection,
|
|
104
203
|
description: collection.description?.trim(),
|
|
105
|
-
group:
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
:
|
|
134
|
-
|
|
204
|
+
group: groupName ?? NAVIGATION_DEFAULT_GROUP_NAME
|
|
205
|
+
});
|
|
206
|
+
return acc;
|
|
207
|
+
}, [] as NavigationEntry[]),
|
|
208
|
+
|
|
209
|
+
...(views ?? []).reduce((acc, view) => {
|
|
210
|
+
if (view.hideFromNavigation) return acc;
|
|
211
|
+
|
|
212
|
+
const pathKey = Array.isArray(view.path) ? view.path[0] : view.path;
|
|
213
|
+
let groupName = getGroup(view); // Initial group
|
|
214
|
+
|
|
215
|
+
if (finalNavigationGroupMappings) {
|
|
216
|
+
for (const pluginGroupDef of finalNavigationGroupMappings) {
|
|
217
|
+
if (pluginGroupDef.entries.includes(pathKey)) {
|
|
218
|
+
groupName = pluginGroupDef.name;
|
|
219
|
+
break;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
acc.push({
|
|
225
|
+
id: `view:${pathKey}`,
|
|
226
|
+
url: buildCMSUrlPath(pathKey),
|
|
227
|
+
name: view.name.trim(),
|
|
228
|
+
type: "view",
|
|
229
|
+
path: view.path,
|
|
230
|
+
view,
|
|
231
|
+
description: view.description?.trim(),
|
|
232
|
+
group: groupName ?? NAVIGATION_DEFAULT_GROUP_NAME
|
|
233
|
+
});
|
|
234
|
+
return acc;
|
|
235
|
+
}, [] as NavigationEntry[]),
|
|
236
|
+
|
|
237
|
+
...(adminViews ?? []).reduce((acc, view) => {
|
|
238
|
+
if (view.hideFromNavigation) return acc;
|
|
239
|
+
|
|
240
|
+
const pathKey = Array.isArray(view.path) ? view.path[0] : view.path;
|
|
241
|
+
const groupName = NAVIGATION_ADMIN_GROUP_NAME;
|
|
242
|
+
|
|
243
|
+
acc.push({
|
|
244
|
+
id: `admin:${pathKey}`,
|
|
245
|
+
url: buildCMSUrlPath(pathKey),
|
|
246
|
+
name: view.name.trim(),
|
|
247
|
+
type: "admin",
|
|
248
|
+
path: view.path,
|
|
249
|
+
view,
|
|
250
|
+
description: view.description?.trim(),
|
|
251
|
+
group: groupName
|
|
252
|
+
});
|
|
253
|
+
return acc;
|
|
254
|
+
}, [] as NavigationEntry[])
|
|
135
255
|
];
|
|
136
256
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
}
|
|
142
|
-
if (b.group !== "Views" && b.group !== "Admin" && (a.group === "Views" || a.group === "Admin")) {
|
|
143
|
-
return 1;
|
|
144
|
-
}
|
|
145
|
-
if (a.group === "Admin" && b.group !== "Admin") {
|
|
146
|
-
return 1;
|
|
147
|
-
}
|
|
148
|
-
if (a.group !== "Admin" && b.group === "Admin") {
|
|
149
|
-
return -1;
|
|
150
|
-
}
|
|
151
|
-
if (a.group === "Views" && b.group !== "Views") {
|
|
152
|
-
return -1;
|
|
153
|
-
}
|
|
154
|
-
if (a.group !== "Views" && b.group === "Views") {
|
|
155
|
-
return 1;
|
|
156
|
-
}
|
|
157
|
-
return 0;
|
|
257
|
+
const groupOrderValue = (groupName?: string): number => {
|
|
258
|
+
if (groupName === NAVIGATION_ADMIN_GROUP_NAME) return 1;
|
|
259
|
+
return 0; // Other groups
|
|
260
|
+
};
|
|
158
261
|
|
|
262
|
+
navigationEntries = navigationEntries.sort((a, b) => {
|
|
263
|
+
return groupOrderValue(a.group) - groupOrderValue(b.group);
|
|
159
264
|
});
|
|
160
265
|
|
|
161
|
-
|
|
266
|
+
const usedViewsOrder = viewsOrder ?? navigationEntriesOrder;
|
|
267
|
+
if (usedViewsOrder) {
|
|
162
268
|
navigationEntries = navigationEntries.sort((a, b) => {
|
|
163
|
-
const
|
|
164
|
-
const
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
if (
|
|
169
|
-
return 1;
|
|
170
|
-
}
|
|
171
|
-
if (bIndex === -1) {
|
|
172
|
-
return -1;
|
|
173
|
-
}
|
|
269
|
+
const getSortPath = (navEntry: NavigationEntry) => typeof navEntry.path === "string" ? navEntry.path : navEntry.path[0];
|
|
270
|
+
const aIndex = usedViewsOrder.indexOf(getSortPath(a));
|
|
271
|
+
const bIndex = usedViewsOrder.indexOf(getSortPath(b));
|
|
272
|
+
if (aIndex === -1 && bIndex === -1) return 0;
|
|
273
|
+
if (aIndex === -1) return 1;
|
|
274
|
+
if (bIndex === -1) return -1;
|
|
174
275
|
return aIndex - bIndex;
|
|
175
276
|
});
|
|
176
277
|
}
|
|
177
278
|
|
|
178
|
-
const
|
|
279
|
+
const collectedGroupsFromEntries = navigationEntries
|
|
179
280
|
.map(e => e.group)
|
|
180
|
-
.filter(Boolean)
|
|
181
|
-
|
|
281
|
+
.filter(Boolean) as string[];
|
|
282
|
+
|
|
283
|
+
const allDefinedGroups = [
|
|
284
|
+
...(pluginGroups ?? []),
|
|
285
|
+
...collectedGroupsFromEntries
|
|
286
|
+
];
|
|
287
|
+
|
|
288
|
+
const uniqueGroups = [...new Set(allDefinedGroups)]
|
|
289
|
+
.sort((a, b) => groupOrderValue(a) - groupOrderValue(b));
|
|
182
290
|
|
|
183
291
|
return {
|
|
292
|
+
allowDragAndDrop: plugins?.some(plugin => plugin.homePage?.allowDragAndDrop) ?? false,
|
|
184
293
|
navigationEntries,
|
|
185
|
-
groups
|
|
294
|
+
groups: uniqueGroups,
|
|
295
|
+
onNavigationEntriesUpdate: onNavigationEntriesOrderUpdate,
|
|
186
296
|
};
|
|
187
|
-
}, [buildCMSUrlPath, buildUrlCollectionPath]);
|
|
297
|
+
}, [navigationGroupMappings, buildCMSUrlPath, buildUrlCollectionPath, pluginGroups, onNavigationEntriesOrderUpdate]);
|
|
188
298
|
|
|
189
299
|
const refreshNavigation = useCallback(async () => {
|
|
190
300
|
|
|
@@ -202,6 +312,8 @@ export function useBuildNavigationController<EC extends EntityCollection, USER e
|
|
|
202
312
|
]
|
|
203
313
|
);
|
|
204
314
|
|
|
315
|
+
const computedTopLevelNav = computeTopNavigation(resolvedCollections, resolvedViews, resolvedAdminViews, viewsOrder);
|
|
316
|
+
|
|
205
317
|
let shouldUpdateTopLevelNav = false;
|
|
206
318
|
if (!areCollectionListsEqual(collectionsRef.current ?? [], resolvedCollections)) {
|
|
207
319
|
collectionsRef.current = resolvedCollections;
|
|
@@ -221,7 +333,12 @@ export function useBuildNavigationController<EC extends EntityCollection, USER e
|
|
|
221
333
|
shouldUpdateTopLevelNav = true;
|
|
222
334
|
}
|
|
223
335
|
|
|
224
|
-
const
|
|
336
|
+
const navigationEntriesOrder = computedTopLevelNav.navigationEntries.map(e => e.id);
|
|
337
|
+
if (!equal(navigationEntriesOrderRef.current, navigationEntriesOrder)) {
|
|
338
|
+
navigationEntriesOrderRef.current = navigationEntriesOrder;
|
|
339
|
+
shouldUpdateTopLevelNav = true;
|
|
340
|
+
}
|
|
341
|
+
|
|
225
342
|
if (shouldUpdateTopLevelNav && !equal(topLevelNavigation, computedTopLevelNav)) {
|
|
226
343
|
setTopLevelNavigation(computedTopLevelNav);
|
|
227
344
|
}
|
|
@@ -243,7 +360,7 @@ export function useBuildNavigationController<EC extends EntityCollection, USER e
|
|
|
243
360
|
disabled,
|
|
244
361
|
viewsProp,
|
|
245
362
|
adminViewsProp,
|
|
246
|
-
computeTopNavigation
|
|
363
|
+
computeTopNavigation,
|
|
247
364
|
]);
|
|
248
365
|
|
|
249
366
|
useEffect(() => {
|
|
@@ -503,9 +620,9 @@ async function resolveCMSViews(baseViews: CMSView[] | CMSViewsBuilder | undefine
|
|
|
503
620
|
function getGroup(collectionOrView: EntityCollection<any, any> | CMSView) {
|
|
504
621
|
const trimmed = collectionOrView.group?.trim();
|
|
505
622
|
if (!trimmed || trimmed === "") {
|
|
506
|
-
return
|
|
623
|
+
return NAVIGATION_DEFAULT_GROUP_NAME;
|
|
507
624
|
}
|
|
508
|
-
return trimmed ??
|
|
625
|
+
return trimmed ?? NAVIGATION_DEFAULT_GROUP_NAME;
|
|
509
626
|
}
|
|
510
627
|
|
|
511
628
|
function areCollectionListsEqual(a: EntityCollection[], b: EntityCollection[]) {
|
|
@@ -584,3 +701,75 @@ function useCustomBlocker(): NavigationBlocker {
|
|
|
584
701
|
reset: blocker?.reset
|
|
585
702
|
}
|
|
586
703
|
}
|
|
704
|
+
|
|
705
|
+
function computeNavigationGroups({
|
|
706
|
+
navigationGroupMappings,
|
|
707
|
+
collections,
|
|
708
|
+
views,
|
|
709
|
+
plugins
|
|
710
|
+
}: {
|
|
711
|
+
navigationGroupMappings?: NavigationGroupMapping[],
|
|
712
|
+
collections?: EntityCollection[],
|
|
713
|
+
views?: CMSView[],
|
|
714
|
+
plugins?: FireCMSPlugin[]
|
|
715
|
+
}): NavigationGroupMapping[] {
|
|
716
|
+
|
|
717
|
+
let result = navigationGroupMappings;
|
|
718
|
+
|
|
719
|
+
result = plugins ? plugins?.reduce((acc, plugin) => {
|
|
720
|
+
if (plugin.homePage?.navigationEntries) {
|
|
721
|
+
plugin.homePage.navigationEntries.forEach((entry) => {
|
|
722
|
+
const {
|
|
723
|
+
name,
|
|
724
|
+
entries
|
|
725
|
+
} = entry;
|
|
726
|
+
const existingGroup = acc.find(entry => entry.name === name);
|
|
727
|
+
if (existingGroup) {
|
|
728
|
+
existingGroup.entries.push(...entries);
|
|
729
|
+
} else {
|
|
730
|
+
acc.push({
|
|
731
|
+
name,
|
|
732
|
+
entries: [...entries]
|
|
733
|
+
});
|
|
734
|
+
}
|
|
735
|
+
});
|
|
736
|
+
|
|
737
|
+
}
|
|
738
|
+
return acc;
|
|
739
|
+
}, [...(result ?? [])] as NavigationGroupMapping[]) : result;
|
|
740
|
+
|
|
741
|
+
if (!result) {
|
|
742
|
+
// Convert views and collections to navigation group mappings, grouped by their group name
|
|
743
|
+
result = [];
|
|
744
|
+
const groupMap: Record<string, string[]> = {};
|
|
745
|
+
|
|
746
|
+
// Add collections
|
|
747
|
+
(collections ?? []).forEach(collection => {
|
|
748
|
+
const name = getGroup(collection);
|
|
749
|
+
const entry = collection.id ?? collection.path;
|
|
750
|
+
if (!groupMap[name]) groupMap[name] = [];
|
|
751
|
+
groupMap[name].push(entry);
|
|
752
|
+
});
|
|
753
|
+
|
|
754
|
+
// Add views
|
|
755
|
+
(views ?? []).forEach(view => {
|
|
756
|
+
const name = getGroup(view);
|
|
757
|
+
const entry = Array.isArray(view.path) ? view.path[0] : view.path;
|
|
758
|
+
if (!groupMap[name]) groupMap[name] = [];
|
|
759
|
+
groupMap[name].push(entry);
|
|
760
|
+
});
|
|
761
|
+
|
|
762
|
+
// Convert groupMap to initialGroupMappings array
|
|
763
|
+
result = Object.entries(groupMap).map(([name, entries]) => ({
|
|
764
|
+
name,
|
|
765
|
+
entries
|
|
766
|
+
}));
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
// Remove duplicates in entries
|
|
770
|
+
result.forEach(group => {
|
|
771
|
+
group.entries = [...new Set(group.entries)];
|
|
772
|
+
});
|
|
773
|
+
|
|
774
|
+
return result;
|
|
775
|
+
}
|
|
@@ -215,6 +215,7 @@ export const useBuildSideEntityController = (navigation: NavigationController,
|
|
|
215
215
|
|
|
216
216
|
export function buildSidePanelsFromUrl(path: string, collections: EntityCollection[], newFlag: boolean): EntitySidePanelProps<any>[] {
|
|
217
217
|
|
|
218
|
+
|
|
218
219
|
const navigationViewsForPath: NavigationViewInternal<any>[] = getNavigationEntriesFromPath({
|
|
219
220
|
path,
|
|
220
221
|
collections
|
|
@@ -222,18 +223,20 @@ export function buildSidePanelsFromUrl(path: string, collections: EntityCollecti
|
|
|
222
223
|
|
|
223
224
|
let sidePanel: EntitySidePanelProps<any> | undefined = undefined;
|
|
224
225
|
let lastCollectionPath = "";
|
|
226
|
+
let lastCollectionId: string | undefined = undefined;
|
|
225
227
|
for (let i = 0; i < navigationViewsForPath.length; i++) {
|
|
226
228
|
const navigationEntry = navigationViewsForPath[i];
|
|
227
229
|
|
|
228
230
|
if (navigationEntry.type === "collection") {
|
|
229
231
|
lastCollectionPath = navigationEntry.path;
|
|
232
|
+
lastCollectionId = navigationEntry.collection.id;
|
|
230
233
|
}
|
|
231
234
|
|
|
232
235
|
const previousEntry = navigationViewsForPath[i - 1];
|
|
233
236
|
if (navigationEntry.type === "entity") {
|
|
234
237
|
sidePanel = {
|
|
235
238
|
path: navigationEntry.path,
|
|
236
|
-
|
|
239
|
+
fullIdPath: navigationEntry.fullIdPath,
|
|
237
240
|
entityId: navigationEntry.entityId,
|
|
238
241
|
copy: false,
|
|
239
242
|
width: navigationEntry.parentCollection?.sideDialogWidth
|
|
@@ -255,7 +258,7 @@ export function buildSidePanelsFromUrl(path: string, collections: EntityCollecti
|
|
|
255
258
|
if (newFlag) {
|
|
256
259
|
sidePanel = {
|
|
257
260
|
path: lastCollectionPath,
|
|
258
|
-
|
|
261
|
+
fullIdPath: lastCollectionId,
|
|
259
262
|
copy: false
|
|
260
263
|
}
|
|
261
264
|
}
|
|
@@ -273,7 +276,7 @@ const propsToSidePanel = (props: EntitySidePanelProps,
|
|
|
273
276
|
|
|
274
277
|
const collectionPath = removeInitialAndTrailingSlashes(props.path);
|
|
275
278
|
|
|
276
|
-
const
|
|
279
|
+
const urlPath = props.entityId
|
|
277
280
|
? buildUrlCollectionPath(`${collectionPath}/${props.entityId}${props.selectedTab ? "/" + props.selectedTab : ""}#${SIDE_URL_HASH}`)
|
|
278
281
|
: buildUrlCollectionPath(`${collectionPath}#${NEW_URL_HASH}`);
|
|
279
282
|
|
|
@@ -286,7 +289,7 @@ const propsToSidePanel = (props: EntitySidePanelProps,
|
|
|
286
289
|
return {
|
|
287
290
|
key: `${props.path}/${props.entityId}`,
|
|
288
291
|
component: <EntitySidePanel {...resolvedPanelProps}/>,
|
|
289
|
-
urlPath:
|
|
292
|
+
urlPath: urlPath,
|
|
290
293
|
parentUrlPath: buildUrlCollectionPath(collectionPath),
|
|
291
294
|
width: entityViewWidth,
|
|
292
295
|
onClose: props.onClose,
|
|
@@ -97,6 +97,20 @@ export const PropertyPreview = React.memo(function PropertyPreview<T extends CMS
|
|
|
97
97
|
previewType={stringProperty.url}/>;
|
|
98
98
|
} else if (stringProperty.markdown) {
|
|
99
99
|
content = <Markdown source={value} size={"small"}/>;
|
|
100
|
+
} else if (stringProperty.reference) {
|
|
101
|
+
if (typeof stringProperty.reference.path === "string") {
|
|
102
|
+
content = <ReferencePreview
|
|
103
|
+
disabled={!stringProperty.reference.path}
|
|
104
|
+
previewProperties={stringProperty.reference.previewProperties}
|
|
105
|
+
includeId={stringProperty.reference.includeId}
|
|
106
|
+
includeEntityLink={stringProperty.reference.includeEntityLink}
|
|
107
|
+
size={props.size}
|
|
108
|
+
reference={new EntityReference(value, stringProperty.reference.path)}
|
|
109
|
+
/>;
|
|
110
|
+
} else {
|
|
111
|
+
content = <EmptyValue/>;
|
|
112
|
+
}
|
|
113
|
+
|
|
100
114
|
} else {
|
|
101
115
|
content = <StringPropertyPreview {...props}
|
|
102
116
|
property={stringProperty}
|
|
@@ -79,7 +79,8 @@ export function FireCMSRoute() {
|
|
|
79
79
|
key={`collection_view_${collection.id ?? collection.path}`}
|
|
80
80
|
isSubCollection={false}
|
|
81
81
|
parentCollectionIds={[]}
|
|
82
|
-
fullPath={collection.
|
|
82
|
+
fullPath={collection.path}
|
|
83
|
+
fullIdPath={collection.id}
|
|
83
84
|
updateUrl={true}
|
|
84
85
|
{...collection}
|
|
85
86
|
Actions={toArray(collection.Actions)}/>
|
|
@@ -100,7 +101,7 @@ export function FireCMSRoute() {
|
|
|
100
101
|
fullIdPath={collection.id}
|
|
101
102
|
isSubCollection={false}
|
|
102
103
|
parentCollectionIds={[]}
|
|
103
|
-
fullPath={collection.
|
|
104
|
+
fullPath={collection.path}
|
|
104
105
|
updateUrl={true}
|
|
105
106
|
{...collection}
|
|
106
107
|
Actions={toArray(collection.Actions)}/>;
|
|
@@ -200,7 +201,6 @@ function EntityFullScreenRoute({
|
|
|
200
201
|
const collection = isNew ? lastCollectionEntry!.collection : lastEntityEntry!.parentCollection;
|
|
201
202
|
const fullIdPath = isNew ? lastCollectionEntry!.path : lastEntityEntry!.path;
|
|
202
203
|
const collectionPath = navigation.resolveIdsFrom(fullIdPath);
|
|
203
|
-
|
|
204
204
|
return <>
|
|
205
205
|
<EntityEditView
|
|
206
206
|
key={collection.id + "_" + (isNew ? "new" : (isCopy ? entityId + "_copy" : entityId))}
|
package/src/types/analytics.ts
CHANGED
package/src/types/collections.ts
CHANGED
|
@@ -70,12 +70,15 @@ export interface EntityCollection<M extends Record<string, any> = any, USER exte
|
|
|
70
70
|
* e.g. 'account_tree' or 'person'.
|
|
71
71
|
* Find all the icons in https://firecms.co/docs/icons
|
|
72
72
|
*/
|
|
73
|
-
icon?: string;
|
|
73
|
+
icon?: string | React.ReactNode;
|
|
74
74
|
|
|
75
75
|
/**
|
|
76
76
|
* Optional field used to group top level navigation entries under a~
|
|
77
77
|
* navigation view. If you set this value in a subcollection it has no
|
|
78
78
|
* effect.
|
|
79
|
+
* @deprecated This prop is deprecated and will be removed in the future.
|
|
80
|
+
* You can apply grouping by using the `navigationGroupMappings` prop in the
|
|
81
|
+
* {@link useBuildNavigationController} hook instead.
|
|
79
82
|
*/
|
|
80
83
|
group?: string;
|
|
81
84
|
|
|
@@ -193,8 +196,11 @@ export interface EntityCollection<M extends Record<string, any> = any, USER exte
|
|
|
193
196
|
* }
|
|
194
197
|
* }
|
|
195
198
|
* ```
|
|
199
|
+
*
|
|
200
|
+
* You can also pass the action as a string that represents the `key`, in which case it will
|
|
201
|
+
* use the action defined in the main configuration under `entityActions`.
|
|
196
202
|
*/
|
|
197
|
-
entityActions?: EntityAction<M, USER>[];
|
|
203
|
+
entityActions?: (EntityAction<M, USER> | string)[];
|
|
198
204
|
|
|
199
205
|
/**
|
|
200
206
|
* Pass your own selection controller if you want to control selected
|
|
@@ -4,6 +4,7 @@ import { FireCMSPlugin } from "./plugins";
|
|
|
4
4
|
import { EntityCustomView } from "./collections";
|
|
5
5
|
import { Locale } from "./locales";
|
|
6
6
|
import { PropertyConfig } from "./property_config";
|
|
7
|
+
import { EntityAction } from "./entity_actions";
|
|
7
8
|
|
|
8
9
|
export type CustomizationController = {
|
|
9
10
|
|
|
@@ -26,6 +27,14 @@ export type CustomizationController = {
|
|
|
26
27
|
*/
|
|
27
28
|
entityViews?: EntityCustomView[];
|
|
28
29
|
|
|
30
|
+
/**
|
|
31
|
+
* List of actions that can be performed on entities.
|
|
32
|
+
* These actions are displayed in the entity view and in the collection view.
|
|
33
|
+
* You can later reuse these actions in the `entityActions` prop of a collection,
|
|
34
|
+
* by specifying the `key` of the action.
|
|
35
|
+
*/
|
|
36
|
+
entityActions?: EntityAction<any, any>[];
|
|
37
|
+
|
|
29
38
|
/**
|
|
30
39
|
* Format of the dates in the CMS.
|
|
31
40
|
* Defaults to 'MMMM dd, yyyy, HH:mm:ss'
|