@firecms/core 3.0.0-canary.248 → 3.0.0-canary.249
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/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/field_configs.d.ts +1 -1
- 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 +1726 -778
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +1723 -775
- package/dist/index.umd.js.map +1 -1
- package/dist/types/analytics.d.ts +1 -1
- package/dist/types/collections.d.ts +3 -0
- package/dist/types/navigation.d.ts +20 -4
- package/dist/types/plugins.d.ts +12 -0
- package/dist/types/properties.d.ts +7 -0
- package/dist/types/property_config.d.ts +1 -1
- package/dist/util/icons.d.ts +1 -1
- package/package.json +5 -5
- package/src/components/EntityCollectionTable/PropertyTableCell.tsx +25 -3
- package/src/components/HomePage/DefaultHomePage.tsx +476 -157
- package/src/components/HomePage/FavouritesView.tsx +3 -3
- package/src/components/HomePage/HomePageDnD.tsx +613 -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 +113 -0
- package/src/core/DefaultDrawer.tsx +8 -8
- package/src/core/DrawerNavigationItem.tsx +1 -1
- package/src/core/field_configs.tsx +15 -1
- package/src/form/field_bindings/ReferenceAsStringFieldBinding.tsx +135 -0
- package/src/form/field_bindings/RepeatFieldBinding.tsx +0 -1
- package/src/form/index.tsx +1 -0
- package/src/hooks/useBuildNavigationController.tsx +273 -84
- package/src/preview/PropertyPreview.tsx +14 -0
- package/src/types/analytics.ts +3 -0
- package/src/types/collections.ts +3 -0
- package/src/types/navigation.ts +27 -5
- package/src/types/plugins.tsx +15 -0
- package/src/types/properties.ts +8 -0
- package/src/types/property_config.tsx +1 -0
- package/src/util/icons.tsx +7 -3
|
@@ -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
|
+
}
|
|
@@ -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}
|
package/src/types/analytics.ts
CHANGED
package/src/types/collections.ts
CHANGED
|
@@ -76,6 +76,9 @@ export interface EntityCollection<M extends Record<string, any> = any, USER exte
|
|
|
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
|
|
package/src/types/navigation.ts
CHANGED
|
@@ -36,7 +36,7 @@ export type NavigationController<EC extends EntityCollection = EntityCollection<
|
|
|
36
36
|
* level of the navigation (e.g. in the home page or the navigation
|
|
37
37
|
* drawer)
|
|
38
38
|
*/
|
|
39
|
-
topLevelNavigation?:
|
|
39
|
+
topLevelNavigation?: NavigationResult;
|
|
40
40
|
|
|
41
41
|
/**
|
|
42
42
|
* Is the navigation loading (the configuration persistence has not
|
|
@@ -229,7 +229,22 @@ export interface CMSView {
|
|
|
229
229
|
|
|
230
230
|
}
|
|
231
231
|
|
|
232
|
-
|
|
232
|
+
/**
|
|
233
|
+
* Used to group navigation entries in the main navigation.
|
|
234
|
+
*/
|
|
235
|
+
export interface NavigationGroupMapping {
|
|
236
|
+
/**
|
|
237
|
+
* Name of the group, used to display the group header in the UI
|
|
238
|
+
*/
|
|
239
|
+
name: string;
|
|
240
|
+
/**
|
|
241
|
+
* List of collection ids or view paths that belong to this group.
|
|
242
|
+
*/
|
|
243
|
+
entries: string[];
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
export interface NavigationEntry {
|
|
247
|
+
id: string;
|
|
233
248
|
url: string;
|
|
234
249
|
name: string;
|
|
235
250
|
path: string;
|
|
@@ -240,7 +255,14 @@ export interface TopNavigationEntry {
|
|
|
240
255
|
group: string;
|
|
241
256
|
}
|
|
242
257
|
|
|
243
|
-
export type
|
|
244
|
-
|
|
245
|
-
|
|
258
|
+
export type NavigationResult = {
|
|
259
|
+
|
|
260
|
+
allowDragAndDrop: boolean;
|
|
261
|
+
|
|
262
|
+
navigationEntries: NavigationEntry[],
|
|
263
|
+
|
|
264
|
+
groups: string[],
|
|
265
|
+
|
|
266
|
+
onNavigationEntriesUpdate: (entries: NavigationGroupMapping[]) => void;
|
|
246
267
|
};
|
|
268
|
+
|
package/src/types/plugins.tsx
CHANGED
|
@@ -7,6 +7,7 @@ import { FieldProps, FormContext } from "./fields";
|
|
|
7
7
|
import { CMSType, Property } from "./properties";
|
|
8
8
|
import { EntityStatus } from "./entities";
|
|
9
9
|
import { ResolvedProperty } from "./resolved_entities";
|
|
10
|
+
import { NavigationGroupMapping } from "./navigation";
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
13
|
* Interface used to define plugins for FireCMS.
|
|
@@ -84,6 +85,20 @@ export type FireCMSPlugin<PROPS = any, FORM_PROPS = any, EC extends EntityCollec
|
|
|
84
85
|
children: React.ReactNode;
|
|
85
86
|
}
|
|
86
87
|
|
|
88
|
+
/**
|
|
89
|
+
* Allow reordering with drag and drop of the collections in the home page.
|
|
90
|
+
*/
|
|
91
|
+
allowDragAndDrop?: boolean;
|
|
92
|
+
|
|
93
|
+
navigationEntries?: NavigationGroupMapping[];
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* This method will be called when the entries are updated in the home page.
|
|
97
|
+
* group => navigationEntriesOrder (path)
|
|
98
|
+
* @param entries
|
|
99
|
+
*/
|
|
100
|
+
onNavigationEntriesUpdate?: (entries: NavigationGroupMapping[]) => void;
|
|
101
|
+
|
|
87
102
|
}
|
|
88
103
|
|
|
89
104
|
collectionView?: {
|
package/src/types/properties.ts
CHANGED
|
@@ -413,6 +413,14 @@ export interface StringProperty extends BaseProperty<string> {
|
|
|
413
413
|
* Add an icon to clear the value and set it to `null`. Defaults to `false`
|
|
414
414
|
*/
|
|
415
415
|
clearable?: boolean;
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* You can use this property (a string) to behave as a reference to another
|
|
419
|
+
* collection. The stored value is the ID of the entity in the
|
|
420
|
+
* collection, and the `path` prop is used to
|
|
421
|
+
* define the collection this reference points to.
|
|
422
|
+
*/
|
|
423
|
+
reference?: ReferenceProperty;
|
|
416
424
|
}
|
|
417
425
|
|
|
418
426
|
/**
|
package/src/util/icons.tsx
CHANGED
|
@@ -4,13 +4,17 @@ import { coolIconKeys, Icon, IconColor, iconKeys } from "@firecms/ui";
|
|
|
4
4
|
import { slugify } from "./strings";
|
|
5
5
|
import equal from "react-fast-compare"
|
|
6
6
|
|
|
7
|
-
export function getIcon(iconKey?: string,
|
|
7
|
+
export function getIcon(iconKey?: string,
|
|
8
|
+
className?: string,
|
|
9
|
+
color?: IconColor,
|
|
10
|
+
size?: "smallest" | "small" | "medium" | "large" | number,): React.ReactElement | undefined {
|
|
8
11
|
if (!iconKey) return undefined;
|
|
9
12
|
iconKey = slugify(iconKey);
|
|
10
13
|
if (!(iconKey in iconKeysMap)) {
|
|
11
14
|
return undefined;
|
|
12
15
|
}
|
|
13
|
-
return iconKey in iconKeysMap ?
|
|
16
|
+
return iconKey in iconKeysMap ?
|
|
17
|
+
<Icon iconKey={iconKey} size={size} className={className} color={color}/> : undefined;
|
|
14
18
|
}
|
|
15
19
|
|
|
16
20
|
export type IconViewProps = {
|
|
@@ -34,7 +38,7 @@ export const IconForView = React.memo(
|
|
|
34
38
|
size?: "smallest" | "small" | "medium" | "large" | number,
|
|
35
39
|
}): React.ReactElement {
|
|
36
40
|
if (!collectionOrView) return <></>;
|
|
37
|
-
const icon = getIcon(collectionOrView.icon, className, color);
|
|
41
|
+
const icon = getIcon(collectionOrView.icon, className, color, size);
|
|
38
42
|
if (collectionOrView?.icon && icon)
|
|
39
43
|
return icon;
|
|
40
44
|
|