@firecms/core 3.0.0-beta.13 → 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/ArrayContainer.d.ts +7 -12
- package/dist/components/EntityCollectionTable/EntityCollectionRowActions.d.ts +2 -1
- package/dist/components/EntityPreview.d.ts +4 -2
- 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/components/SelectableTable/SelectableTable.d.ts +1 -1
- package/dist/components/VirtualTable/VirtualTableProps.d.ts +0 -4
- package/dist/components/index.d.ts +1 -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/MapFieldBinding.d.ts +1 -1
- package/dist/form/field_bindings/ReferenceAsStringFieldBinding.d.ts +9 -0
- package/dist/form/field_bindings/StorageUploadFieldBinding.d.ts +3 -9
- package/dist/form/index.d.ts +1 -0
- package/dist/hooks/useBuildNavigationController.d.ts +50 -8
- package/dist/index.es.js +3259 -1649
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +3265 -1658
- package/dist/index.umd.js.map +1 -1
- package/dist/types/analytics.d.ts +1 -1
- package/dist/types/collections.d.ts +28 -4
- package/dist/types/customization_controller.d.ts +8 -0
- package/dist/types/entity_actions.d.ts +46 -6
- package/dist/types/firecms.d.ts +10 -1
- package/dist/types/navigation.d.ts +26 -5
- package/dist/types/plugins.d.ts +32 -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 +8 -0
- package/dist/util/callbacks.d.ts +2 -0
- package/dist/util/icons.d.ts +2 -2
- package/dist/util/index.d.ts +1 -0
- package/dist/util/navigation_from_path.d.ts +4 -0
- package/dist/util/resolutions.d.ts +2 -1
- package/package.json +8 -6
- package/src/components/ArrayContainer.tsx +409 -294
- package/src/components/ClearFilterSortButton.tsx +1 -1
- package/src/components/ConfirmationDialog.tsx +10 -9
- package/src/components/EntityCollectionTable/EntityCollectionRowActions.tsx +6 -0
- package/src/components/EntityCollectionTable/PropertyTableCell.tsx +26 -4
- package/src/components/EntityCollectionTable/internal/CollectionTableToolbar.tsx +3 -3
- package/src/components/EntityCollectionTable/internal/EntityTableCell.tsx +13 -2
- package/src/components/EntityCollectionTable/internal/popup_field/PopupFormField.tsx +53 -27
- package/src/components/EntityCollectionTable/internal/popup_field/useDraggable.tsx +9 -9
- package/src/components/EntityCollectionView/EntityCollectionView.tsx +11 -4
- package/src/components/EntityCollectionView/EntityCollectionViewActions.tsx +3 -2
- package/src/components/EntityPreview.tsx +18 -14
- package/src/components/ErrorView.tsx +1 -1
- package/src/components/FireCMSLogo.tsx +7 -51
- package/src/components/HomePage/DefaultHomePage.tsx +491 -156
- 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 +13 -7
- package/src/components/HomePage/NavigationGroup.tsx +63 -29
- package/src/components/HomePage/RenameGroupDialog.tsx +117 -0
- package/src/components/SelectableTable/SelectableTable.tsx +140 -143
- package/src/components/UnsavedChangesDialog.tsx +6 -2
- package/src/components/VirtualTable/VirtualTable.tsx +8 -30
- package/src/components/VirtualTable/VirtualTableProps.tsx +0 -5
- package/src/components/VirtualTable/fields/VirtualTableInput.tsx +0 -1
- package/src/components/common/default_entity_actions.tsx +25 -9
- package/src/components/common/useDataSourceTableController.tsx +2 -2
- package/src/components/index.tsx +2 -0
- package/src/core/DefaultDrawer.tsx +8 -8
- package/src/core/DrawerNavigationItem.tsx +1 -1
- package/src/core/EntityEditView.tsx +65 -18
- package/src/core/EntityEditViewFormActions.tsx +154 -29
- package/src/core/EntitySidePanel.tsx +19 -21
- package/src/core/FireCMS.tsx +9 -1
- package/src/core/field_configs.tsx +15 -1
- package/src/form/EntityForm.tsx +37 -5
- package/src/form/EntityFormActions.tsx +51 -9
- package/src/form/PropertyFieldBinding.tsx +0 -1
- package/src/form/components/CustomIdField.tsx +3 -1
- package/src/form/components/LabelWithIcon.tsx +1 -1
- package/src/form/components/StorageItemPreview.tsx +1 -1
- package/src/form/components/StorageUploadProgress.tsx +3 -3
- package/src/form/field_bindings/ArrayOfReferencesFieldBinding.tsx +3 -2
- package/src/form/field_bindings/MapFieldBinding.tsx +5 -4
- package/src/form/field_bindings/MarkdownEditorFieldBinding.tsx +4 -2
- package/src/form/field_bindings/MultiSelectFieldBinding.tsx +1 -1
- package/src/form/field_bindings/ReferenceAsStringFieldBinding.tsx +135 -0
- package/src/form/field_bindings/SelectFieldBinding.tsx +2 -1
- package/src/form/field_bindings/StorageUploadFieldBinding.tsx +216 -153
- package/src/form/index.tsx +1 -0
- package/src/hooks/data/save.ts +0 -6
- package/src/hooks/useBuildNavigationController.tsx +299 -96
- package/src/internal/useBuildSideEntityController.tsx +8 -5
- package/src/preview/PropertyPreview.tsx +14 -0
- package/src/preview/components/ImagePreview.tsx +2 -2
- package/src/preview/components/ReferencePreview.tsx +1 -1
- package/src/preview/property_previews/ArrayOfMapsPreview.tsx +1 -1
- package/src/preview/property_previews/MapPropertyPreview.tsx +1 -1
- package/src/preview/property_previews/SkeletonPropertyComponent.tsx +1 -1
- package/src/preview/property_previews/StringPropertyPreview.tsx +1 -1
- package/src/routes/FireCMSRoute.tsx +3 -3
- package/src/types/analytics.ts +3 -0
- package/src/types/collections.ts +32 -4
- package/src/types/customization_controller.tsx +9 -0
- package/src/types/entity_actions.tsx +57 -6
- package/src/types/firecms.tsx +11 -2
- package/src/types/navigation.ts +34 -6
- package/src/types/plugins.tsx +40 -1
- package/src/types/properties.ts +8 -0
- package/src/types/property_config.tsx +1 -0
- package/src/types/side_entity_controller.tsx +10 -5
- package/src/util/callbacks.ts +119 -0
- package/src/util/icons.tsx +22 -7
- package/src/util/index.ts +1 -0
- package/src/util/join_collections.ts +3 -1
- package/src/util/navigation_from_path.ts +15 -5
- package/src/util/navigation_utils.ts +72 -57
- package/src/util/objects.ts +53 -20
- package/src/util/resolutions.ts +13 -1
- package/dist/components/EntityCollectionTable/internal/popup_field/ElementResizeListener.d.ts +0 -5
- package/src/components/EntityCollectionTable/internal/popup_field/ElementResizeListener.tsx +0 -59
|
@@ -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,
|
|
@@ -9,11 +10,13 @@ import {
|
|
|
9
10
|
EntityCollection,
|
|
10
11
|
EntityCollectionsBuilder,
|
|
11
12
|
EntityReference,
|
|
13
|
+
FireCMSPlugin,
|
|
12
14
|
NavigationBlocker,
|
|
13
15
|
NavigationController,
|
|
16
|
+
NavigationEntry,
|
|
17
|
+
NavigationGroupMapping,
|
|
18
|
+
NavigationResult,
|
|
14
19
|
PermissionsBuilder,
|
|
15
|
-
TopNavigationEntry,
|
|
16
|
-
TopNavigationResult,
|
|
17
20
|
User,
|
|
18
21
|
UserConfigurationPersistence
|
|
19
22
|
} from "../types";
|
|
@@ -27,35 +30,77 @@ import {
|
|
|
27
30
|
resolvePermissions
|
|
28
31
|
} from "../util";
|
|
29
32
|
import { getParentReferencesFromPath } from "../util/parent_references_from_path";
|
|
30
|
-
import { useBlocker, useNavigate } from "react-router-dom";
|
|
31
33
|
|
|
32
34
|
const DEFAULT_BASE_PATH = "/";
|
|
33
35
|
const DEFAULT_COLLECTION_PATH = "/c";
|
|
34
36
|
|
|
37
|
+
export const NAVIGATION_DEFAULT_GROUP_NAME = "Views";
|
|
38
|
+
export const NAVIGATION_ADMIN_GROUP_NAME = "Admin";
|
|
39
|
+
|
|
35
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
|
+
*/
|
|
36
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
|
+
*/
|
|
37
50
|
baseCollectionPath?: string,
|
|
51
|
+
/**
|
|
52
|
+
* The auth controller used to manage the user authentication and permissions.
|
|
53
|
+
*/
|
|
38
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
|
+
*/
|
|
39
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
|
+
*/
|
|
40
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
|
+
*/
|
|
41
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
|
+
*/
|
|
42
77
|
adminViews?: CMSView[] | CMSViewsBuilder;
|
|
43
|
-
|
|
78
|
+
/**
|
|
79
|
+
* Controller for storing user preferences.
|
|
80
|
+
*/
|
|
44
81
|
userConfigPersistence?: UserConfigurationPersistence;
|
|
82
|
+
/**
|
|
83
|
+
* Delegate for data source operations, used to resolve collections and views.
|
|
84
|
+
*/
|
|
45
85
|
dataSourceDelegate: DataSourceDelegate;
|
|
46
86
|
/**
|
|
47
|
-
*
|
|
48
|
-
* You receive the current collections as a parameter, and you can return
|
|
49
|
-
* a new list of collections.
|
|
50
|
-
* @see {@link joinCollectionLists}
|
|
51
|
-
* @param collections
|
|
87
|
+
* Plugins to be used in the CMS.
|
|
52
88
|
*/
|
|
53
|
-
|
|
54
|
-
|
|
89
|
+
plugins?: FireCMSPlugin[];
|
|
90
|
+
/**
|
|
91
|
+
* Used to define the name of groups and order of the navigation entries.
|
|
92
|
+
*/
|
|
93
|
+
navigationGroupMappings?: NavigationGroupMapping[];
|
|
55
94
|
/**
|
|
56
95
|
* If true, the navigation logic will not be updated until this flag is false
|
|
57
96
|
*/
|
|
58
97
|
disabled?: boolean;
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* @deprecated
|
|
101
|
+
* Use `navigationGroupMappings` instead.
|
|
102
|
+
*/
|
|
103
|
+
viewsOrder?: string[];
|
|
59
104
|
};
|
|
60
105
|
|
|
61
106
|
export function useBuildNavigationController<EC extends EntityCollection, USER extends User>(props: BuildNavigationContextProps<EC, USER>): NavigationController {
|
|
@@ -68,10 +113,11 @@ export function useBuildNavigationController<EC extends EntityCollection, USER e
|
|
|
68
113
|
views: viewsProp,
|
|
69
114
|
adminViews: adminViewsProp,
|
|
70
115
|
viewsOrder,
|
|
116
|
+
plugins,
|
|
71
117
|
userConfigPersistence,
|
|
72
118
|
dataSourceDelegate,
|
|
73
|
-
|
|
74
|
-
|
|
119
|
+
disabled,
|
|
120
|
+
navigationGroupMappings
|
|
75
121
|
} = props;
|
|
76
122
|
|
|
77
123
|
const navigate = useNavigate();
|
|
@@ -79,10 +125,11 @@ export function useBuildNavigationController<EC extends EntityCollection, USER e
|
|
|
79
125
|
const collectionsRef = useRef<EntityCollection[] | undefined>();
|
|
80
126
|
const viewsRef = useRef<CMSView[] | undefined>();
|
|
81
127
|
const adminViewsRef = useRef<CMSView[] | undefined>();
|
|
128
|
+
const navigationEntriesOrderRef = useRef<string[] | undefined>();
|
|
82
129
|
|
|
83
130
|
const [initialised, setInitialised] = useState<boolean>(false);
|
|
84
131
|
|
|
85
|
-
const [topLevelNavigation, setTopLevelNavigation] = useState<
|
|
132
|
+
const [topLevelNavigation, setTopLevelNavigation] = useState<NavigationResult | undefined>(undefined);
|
|
86
133
|
const [navigationLoading, setNavigationLoading] = useState<boolean>(true);
|
|
87
134
|
const [navigationLoadingError, setNavigationLoadingError] = useState<Error | undefined>(undefined);
|
|
88
135
|
|
|
@@ -99,99 +146,155 @@ export function useBuildNavigationController<EC extends EntityCollection, USER e
|
|
|
99
146
|
const buildUrlCollectionPath = useCallback((path: string): string => `${removeInitialAndTrailingSlashes(baseCollectionPath)}/${encodePath(path)}`,
|
|
100
147
|
[baseCollectionPath]);
|
|
101
148
|
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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),
|
|
107
199
|
type: "collection",
|
|
108
200
|
name: collection.name.trim(),
|
|
109
|
-
path:
|
|
201
|
+
path: pathKey,
|
|
110
202
|
collection,
|
|
111
203
|
description: collection.description?.trim(),
|
|
112
|
-
group:
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
:
|
|
141
|
-
|
|
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[])
|
|
142
255
|
];
|
|
143
256
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
}
|
|
149
|
-
if (b.group !== "Views" && b.group !== "Admin" && (a.group === "Views" || a.group === "Admin")) {
|
|
150
|
-
return 1;
|
|
151
|
-
}
|
|
152
|
-
if (a.group === "Admin" && b.group !== "Admin") {
|
|
153
|
-
return 1;
|
|
154
|
-
}
|
|
155
|
-
if (a.group !== "Admin" && b.group === "Admin") {
|
|
156
|
-
return -1;
|
|
157
|
-
}
|
|
158
|
-
if (a.group === "Views" && b.group !== "Views") {
|
|
159
|
-
return -1;
|
|
160
|
-
}
|
|
161
|
-
if (a.group !== "Views" && b.group === "Views") {
|
|
162
|
-
return 1;
|
|
163
|
-
}
|
|
164
|
-
return 0;
|
|
257
|
+
const groupOrderValue = (groupName?: string): number => {
|
|
258
|
+
if (groupName === NAVIGATION_ADMIN_GROUP_NAME) return 1;
|
|
259
|
+
return 0; // Other groups
|
|
260
|
+
};
|
|
165
261
|
|
|
262
|
+
navigationEntries = navigationEntries.sort((a, b) => {
|
|
263
|
+
return groupOrderValue(a.group) - groupOrderValue(b.group);
|
|
166
264
|
});
|
|
167
265
|
|
|
168
|
-
|
|
266
|
+
const usedViewsOrder = viewsOrder ?? navigationEntriesOrder;
|
|
267
|
+
if (usedViewsOrder) {
|
|
169
268
|
navigationEntries = navigationEntries.sort((a, b) => {
|
|
170
|
-
const
|
|
171
|
-
const
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
if (
|
|
176
|
-
return 1;
|
|
177
|
-
}
|
|
178
|
-
if (bIndex === -1) {
|
|
179
|
-
return -1;
|
|
180
|
-
}
|
|
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;
|
|
181
275
|
return aIndex - bIndex;
|
|
182
276
|
});
|
|
183
277
|
}
|
|
184
278
|
|
|
185
|
-
const
|
|
279
|
+
const collectedGroupsFromEntries = navigationEntries
|
|
186
280
|
.map(e => e.group)
|
|
187
|
-
.filter(Boolean)
|
|
188
|
-
|
|
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));
|
|
189
290
|
|
|
190
291
|
return {
|
|
292
|
+
allowDragAndDrop: plugins?.some(plugin => plugin.homePage?.allowDragAndDrop) ?? false,
|
|
191
293
|
navigationEntries,
|
|
192
|
-
groups
|
|
294
|
+
groups: uniqueGroups,
|
|
295
|
+
onNavigationEntriesUpdate: onNavigationEntriesOrderUpdate,
|
|
193
296
|
};
|
|
194
|
-
}, [buildCMSUrlPath, buildUrlCollectionPath]);
|
|
297
|
+
}, [navigationGroupMappings, buildCMSUrlPath, buildUrlCollectionPath, pluginGroups, onNavigationEntriesOrderUpdate]);
|
|
195
298
|
|
|
196
299
|
const refreshNavigation = useCallback(async () => {
|
|
197
300
|
|
|
@@ -203,12 +306,14 @@ export function useBuildNavigationController<EC extends EntityCollection, USER e
|
|
|
203
306
|
try {
|
|
204
307
|
|
|
205
308
|
const [resolvedCollections = [], resolvedViews, resolvedAdminViews = []] = await Promise.all([
|
|
206
|
-
resolveCollections(collectionsProp, collectionPermissions, authController, dataSourceDelegate,
|
|
309
|
+
resolveCollections(collectionsProp, collectionPermissions, authController, dataSourceDelegate, plugins),
|
|
207
310
|
resolveCMSViews(viewsProp, authController, dataSourceDelegate),
|
|
208
311
|
resolveCMSViews(adminViewsProp, authController, dataSourceDelegate)
|
|
209
312
|
]
|
|
210
313
|
);
|
|
211
314
|
|
|
315
|
+
const computedTopLevelNav = computeTopNavigation(resolvedCollections, resolvedViews, resolvedAdminViews, viewsOrder);
|
|
316
|
+
|
|
212
317
|
let shouldUpdateTopLevelNav = false;
|
|
213
318
|
if (!areCollectionListsEqual(collectionsRef.current ?? [], resolvedCollections)) {
|
|
214
319
|
collectionsRef.current = resolvedCollections;
|
|
@@ -228,7 +333,12 @@ export function useBuildNavigationController<EC extends EntityCollection, USER e
|
|
|
228
333
|
shouldUpdateTopLevelNav = true;
|
|
229
334
|
}
|
|
230
335
|
|
|
231
|
-
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
|
+
|
|
232
342
|
if (shouldUpdateTopLevelNav && !equal(topLevelNavigation, computedTopLevelNav)) {
|
|
233
343
|
setTopLevelNavigation(computedTopLevelNav);
|
|
234
344
|
}
|
|
@@ -251,7 +361,6 @@ export function useBuildNavigationController<EC extends EntityCollection, USER e
|
|
|
251
361
|
viewsProp,
|
|
252
362
|
adminViewsProp,
|
|
253
363
|
computeTopNavigation,
|
|
254
|
-
injectCollections
|
|
255
364
|
]);
|
|
256
365
|
|
|
257
366
|
useEffect(() => {
|
|
@@ -421,6 +530,7 @@ export function useBuildNavigationController<EC extends EntityCollection, USER e
|
|
|
421
530
|
getParentCollectionIds,
|
|
422
531
|
convertIdsToPaths,
|
|
423
532
|
navigate,
|
|
533
|
+
plugins
|
|
424
534
|
};
|
|
425
535
|
}
|
|
426
536
|
|
|
@@ -447,11 +557,24 @@ function filterOutNotAllowedCollections(resolvedCollections: EntityCollection[],
|
|
|
447
557
|
});
|
|
448
558
|
}
|
|
449
559
|
|
|
560
|
+
function applyPluginModifyCollection(resolvedCollections: EntityCollection[], modifyCollection: (collection: EntityCollection) => EntityCollection) {
|
|
561
|
+
return resolvedCollections.map((collection: EntityCollection): EntityCollection => {
|
|
562
|
+
const modifiedCollection = modifyCollection(collection);
|
|
563
|
+
if (modifiedCollection.subcollections) {
|
|
564
|
+
return {
|
|
565
|
+
...modifiedCollection,
|
|
566
|
+
subcollections: applyPluginModifyCollection(modifiedCollection.subcollections, modifyCollection)
|
|
567
|
+
} satisfies EntityCollection;
|
|
568
|
+
}
|
|
569
|
+
return modifiedCollection;
|
|
570
|
+
});
|
|
571
|
+
}
|
|
572
|
+
|
|
450
573
|
async function resolveCollections(collections: undefined | EntityCollection[] | EntityCollectionsBuilder<any>,
|
|
451
574
|
collectionPermissions: PermissionsBuilder | undefined,
|
|
452
575
|
authController: AuthController,
|
|
453
576
|
dataSource: DataSourceDelegate,
|
|
454
|
-
|
|
577
|
+
plugins: FireCMSPlugin[] | undefined): Promise<EntityCollection[]> {
|
|
455
578
|
let resolvedCollections: EntityCollection[] = [];
|
|
456
579
|
if (typeof collections === "function") {
|
|
457
580
|
resolvedCollections = await collections({
|
|
@@ -463,8 +586,16 @@ async function resolveCollections(collections: undefined | EntityCollection[] |
|
|
|
463
586
|
resolvedCollections = collections;
|
|
464
587
|
}
|
|
465
588
|
|
|
466
|
-
if (
|
|
467
|
-
|
|
589
|
+
if (plugins) {
|
|
590
|
+
for (const plugin of plugins) {
|
|
591
|
+
if (plugin.collection?.modifyCollection) {
|
|
592
|
+
resolvedCollections = applyPluginModifyCollection(resolvedCollections, plugin.collection.modifyCollection);
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
if (plugin.collection?.injectCollections) {
|
|
596
|
+
resolvedCollections = plugin.collection.injectCollections(resolvedCollections ?? []);
|
|
597
|
+
}
|
|
598
|
+
}
|
|
468
599
|
}
|
|
469
600
|
|
|
470
601
|
resolvedCollections = applyPermissionsFunctionIfEmpty(resolvedCollections, collectionPermissions);
|
|
@@ -489,9 +620,9 @@ async function resolveCMSViews(baseViews: CMSView[] | CMSViewsBuilder | undefine
|
|
|
489
620
|
function getGroup(collectionOrView: EntityCollection<any, any> | CMSView) {
|
|
490
621
|
const trimmed = collectionOrView.group?.trim();
|
|
491
622
|
if (!trimmed || trimmed === "") {
|
|
492
|
-
return
|
|
623
|
+
return NAVIGATION_DEFAULT_GROUP_NAME;
|
|
493
624
|
}
|
|
494
|
-
return trimmed ??
|
|
625
|
+
return trimmed ?? NAVIGATION_DEFAULT_GROUP_NAME;
|
|
495
626
|
}
|
|
496
627
|
|
|
497
628
|
function areCollectionListsEqual(a: EntityCollection[], b: EntityCollection[]) {
|
|
@@ -570,3 +701,75 @@ function useCustomBlocker(): NavigationBlocker {
|
|
|
570
701
|
reset: blocker?.reset
|
|
571
702
|
}
|
|
572
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
|
+
}
|
|
@@ -33,7 +33,7 @@ export function getEntityViewWidth(props: EntitySidePanelProps<any>, small: bool
|
|
|
33
33
|
selectedSecondaryForm
|
|
34
34
|
} = resolvedSelectedEntityView(props.collection?.entityViews, customizationController, props.selectedTab);
|
|
35
35
|
|
|
36
|
-
const shouldUseSmallLayout = !props.selectedTab || props.selectedTab === JSON_TAB_VALUE || Boolean(selectedSecondaryForm);
|
|
36
|
+
const shouldUseSmallLayout = !props.selectedTab || props.selectedTab === JSON_TAB_VALUE || props.selectedTab === "__history" || Boolean(selectedSecondaryForm);
|
|
37
37
|
|
|
38
38
|
let resolvedWidth: string | undefined;
|
|
39
39
|
if (props.width) {
|
|
@@ -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}
|
|
@@ -62,7 +62,7 @@ export function ImagePreview({
|
|
|
62
62
|
title="Copy url to clipboard" side={"bottom"}>
|
|
63
63
|
<IconButton
|
|
64
64
|
variant={"filled"}
|
|
65
|
-
size={"
|
|
65
|
+
size={"smallest"}
|
|
66
66
|
onClick={(e) => {
|
|
67
67
|
e.stopPropagation();
|
|
68
68
|
e.preventDefault();
|
|
@@ -81,7 +81,7 @@ export function ImagePreview({
|
|
|
81
81
|
href={url}
|
|
82
82
|
rel="noopener noreferrer"
|
|
83
83
|
target="_blank"
|
|
84
|
-
size={"
|
|
84
|
+
size={"smallest"}
|
|
85
85
|
onClick={(e: any) => e.stopPropagation()}
|
|
86
86
|
>
|
|
87
87
|
<OpenInNewIcon className={"text-surface-700 dark:text-surface-300"}
|
|
@@ -141,7 +141,7 @@ function ReferencePreviewExisting<M extends Record<string, any> = any>({
|
|
|
141
141
|
);
|
|
142
142
|
}
|
|
143
143
|
return <EntityPreview size={size}
|
|
144
|
-
|
|
144
|
+
previewKeys={previewProperties}
|
|
145
145
|
disabled={disabled}
|
|
146
146
|
entity={usedEntity}
|
|
147
147
|
collection={collection}
|
|
@@ -37,7 +37,7 @@ export function ArrayOfMapsPreview({
|
|
|
37
37
|
throw Error(`You need to specify a 'properties' prop (or specify a custom field) in your map property ${propertyKey}`);
|
|
38
38
|
}
|
|
39
39
|
const values = value;
|
|
40
|
-
const previewProperties: string[] | undefined = mapProperty.
|
|
40
|
+
const previewProperties: string[] | undefined = mapProperty.previewKeys;
|
|
41
41
|
|
|
42
42
|
if (!values) return null;
|
|
43
43
|
|