@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.
Files changed (124) hide show
  1. package/dist/components/ArrayContainer.d.ts +7 -12
  2. package/dist/components/EntityCollectionTable/EntityCollectionRowActions.d.ts +2 -1
  3. package/dist/components/EntityPreview.d.ts +4 -2
  4. package/dist/components/HomePage/DefaultHomePage.d.ts +2 -15
  5. package/dist/components/HomePage/HomePageDnD.d.ts +76 -0
  6. package/dist/components/HomePage/NavigationCard.d.ts +3 -1
  7. package/dist/components/HomePage/NavigationCardBinding.d.ts +3 -2
  8. package/dist/components/HomePage/NavigationGroup.d.ts +7 -1
  9. package/dist/components/HomePage/RenameGroupDialog.d.ts +9 -0
  10. package/dist/components/SelectableTable/SelectableTable.d.ts +1 -1
  11. package/dist/components/VirtualTable/VirtualTableProps.d.ts +0 -4
  12. package/dist/components/index.d.ts +1 -0
  13. package/dist/core/EntityEditView.d.ts +3 -0
  14. package/dist/core/EntityEditViewFormActions.d.ts +1 -1
  15. package/dist/core/field_configs.d.ts +1 -1
  16. package/dist/form/EntityForm.d.ts +2 -1
  17. package/dist/form/EntityFormActions.d.ts +6 -2
  18. package/dist/form/field_bindings/MapFieldBinding.d.ts +1 -1
  19. package/dist/form/field_bindings/ReferenceAsStringFieldBinding.d.ts +9 -0
  20. package/dist/form/field_bindings/StorageUploadFieldBinding.d.ts +3 -9
  21. package/dist/form/index.d.ts +1 -0
  22. package/dist/hooks/useBuildNavigationController.d.ts +50 -8
  23. package/dist/index.es.js +3259 -1649
  24. package/dist/index.es.js.map +1 -1
  25. package/dist/index.umd.js +3265 -1658
  26. package/dist/index.umd.js.map +1 -1
  27. package/dist/types/analytics.d.ts +1 -1
  28. package/dist/types/collections.d.ts +28 -4
  29. package/dist/types/customization_controller.d.ts +8 -0
  30. package/dist/types/entity_actions.d.ts +46 -6
  31. package/dist/types/firecms.d.ts +10 -1
  32. package/dist/types/navigation.d.ts +26 -5
  33. package/dist/types/plugins.d.ts +32 -1
  34. package/dist/types/properties.d.ts +7 -0
  35. package/dist/types/property_config.d.ts +1 -1
  36. package/dist/types/side_entity_controller.d.ts +8 -0
  37. package/dist/util/callbacks.d.ts +2 -0
  38. package/dist/util/icons.d.ts +2 -2
  39. package/dist/util/index.d.ts +1 -0
  40. package/dist/util/navigation_from_path.d.ts +4 -0
  41. package/dist/util/resolutions.d.ts +2 -1
  42. package/package.json +8 -6
  43. package/src/components/ArrayContainer.tsx +409 -294
  44. package/src/components/ClearFilterSortButton.tsx +1 -1
  45. package/src/components/ConfirmationDialog.tsx +10 -9
  46. package/src/components/EntityCollectionTable/EntityCollectionRowActions.tsx +6 -0
  47. package/src/components/EntityCollectionTable/PropertyTableCell.tsx +26 -4
  48. package/src/components/EntityCollectionTable/internal/CollectionTableToolbar.tsx +3 -3
  49. package/src/components/EntityCollectionTable/internal/EntityTableCell.tsx +13 -2
  50. package/src/components/EntityCollectionTable/internal/popup_field/PopupFormField.tsx +53 -27
  51. package/src/components/EntityCollectionTable/internal/popup_field/useDraggable.tsx +9 -9
  52. package/src/components/EntityCollectionView/EntityCollectionView.tsx +11 -4
  53. package/src/components/EntityCollectionView/EntityCollectionViewActions.tsx +3 -2
  54. package/src/components/EntityPreview.tsx +18 -14
  55. package/src/components/ErrorView.tsx +1 -1
  56. package/src/components/FireCMSLogo.tsx +7 -51
  57. package/src/components/HomePage/DefaultHomePage.tsx +491 -156
  58. package/src/components/HomePage/FavouritesView.tsx +3 -3
  59. package/src/components/HomePage/HomePageDnD.tsx +599 -0
  60. package/src/components/HomePage/NavigationCard.tsx +47 -38
  61. package/src/components/HomePage/NavigationCardBinding.tsx +13 -7
  62. package/src/components/HomePage/NavigationGroup.tsx +63 -29
  63. package/src/components/HomePage/RenameGroupDialog.tsx +117 -0
  64. package/src/components/SelectableTable/SelectableTable.tsx +140 -143
  65. package/src/components/UnsavedChangesDialog.tsx +6 -2
  66. package/src/components/VirtualTable/VirtualTable.tsx +8 -30
  67. package/src/components/VirtualTable/VirtualTableProps.tsx +0 -5
  68. package/src/components/VirtualTable/fields/VirtualTableInput.tsx +0 -1
  69. package/src/components/common/default_entity_actions.tsx +25 -9
  70. package/src/components/common/useDataSourceTableController.tsx +2 -2
  71. package/src/components/index.tsx +2 -0
  72. package/src/core/DefaultDrawer.tsx +8 -8
  73. package/src/core/DrawerNavigationItem.tsx +1 -1
  74. package/src/core/EntityEditView.tsx +65 -18
  75. package/src/core/EntityEditViewFormActions.tsx +154 -29
  76. package/src/core/EntitySidePanel.tsx +19 -21
  77. package/src/core/FireCMS.tsx +9 -1
  78. package/src/core/field_configs.tsx +15 -1
  79. package/src/form/EntityForm.tsx +37 -5
  80. package/src/form/EntityFormActions.tsx +51 -9
  81. package/src/form/PropertyFieldBinding.tsx +0 -1
  82. package/src/form/components/CustomIdField.tsx +3 -1
  83. package/src/form/components/LabelWithIcon.tsx +1 -1
  84. package/src/form/components/StorageItemPreview.tsx +1 -1
  85. package/src/form/components/StorageUploadProgress.tsx +3 -3
  86. package/src/form/field_bindings/ArrayOfReferencesFieldBinding.tsx +3 -2
  87. package/src/form/field_bindings/MapFieldBinding.tsx +5 -4
  88. package/src/form/field_bindings/MarkdownEditorFieldBinding.tsx +4 -2
  89. package/src/form/field_bindings/MultiSelectFieldBinding.tsx +1 -1
  90. package/src/form/field_bindings/ReferenceAsStringFieldBinding.tsx +135 -0
  91. package/src/form/field_bindings/SelectFieldBinding.tsx +2 -1
  92. package/src/form/field_bindings/StorageUploadFieldBinding.tsx +216 -153
  93. package/src/form/index.tsx +1 -0
  94. package/src/hooks/data/save.ts +0 -6
  95. package/src/hooks/useBuildNavigationController.tsx +299 -96
  96. package/src/internal/useBuildSideEntityController.tsx +8 -5
  97. package/src/preview/PropertyPreview.tsx +14 -0
  98. package/src/preview/components/ImagePreview.tsx +2 -2
  99. package/src/preview/components/ReferencePreview.tsx +1 -1
  100. package/src/preview/property_previews/ArrayOfMapsPreview.tsx +1 -1
  101. package/src/preview/property_previews/MapPropertyPreview.tsx +1 -1
  102. package/src/preview/property_previews/SkeletonPropertyComponent.tsx +1 -1
  103. package/src/preview/property_previews/StringPropertyPreview.tsx +1 -1
  104. package/src/routes/FireCMSRoute.tsx +3 -3
  105. package/src/types/analytics.ts +3 -0
  106. package/src/types/collections.ts +32 -4
  107. package/src/types/customization_controller.tsx +9 -0
  108. package/src/types/entity_actions.tsx +57 -6
  109. package/src/types/firecms.tsx +11 -2
  110. package/src/types/navigation.ts +34 -6
  111. package/src/types/plugins.tsx +40 -1
  112. package/src/types/properties.ts +8 -0
  113. package/src/types/property_config.tsx +1 -0
  114. package/src/types/side_entity_controller.tsx +10 -5
  115. package/src/util/callbacks.ts +119 -0
  116. package/src/util/icons.tsx +22 -7
  117. package/src/util/index.ts +1 -0
  118. package/src/util/join_collections.ts +3 -1
  119. package/src/util/navigation_from_path.ts +15 -5
  120. package/src/util/navigation_utils.ts +72 -57
  121. package/src/util/objects.ts +53 -20
  122. package/src/util/resolutions.ts +13 -1
  123. package/dist/components/EntityCollectionTable/internal/popup_field/ElementResizeListener.d.ts +0 -5
  124. 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
- viewsOrder?: string[];
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
- * Use this method to inject collections to the CMS.
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
- injectCollections?: (collections: EntityCollection[]) => EntityCollection[];
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
- injectCollections,
74
- disabled
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<TopNavigationResult | undefined>(undefined);
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 computeTopNavigation = useCallback((collections: EntityCollection[], views: CMSView[], adminViews: CMSView[], viewsOrder?: string[]): TopNavigationResult => {
103
- let navigationEntries: TopNavigationEntry[] = [
104
- ...(collections ?? []).map(collection => (!collection.hideFromNavigation
105
- ? ({
106
- url: buildUrlCollectionPath(collection.id ?? collection.path),
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: collection.id ?? collection.path,
201
+ path: pathKey,
110
202
  collection,
111
203
  description: collection.description?.trim(),
112
- group: getGroup(collection)
113
- } satisfies TopNavigationEntry)
114
- : undefined))
115
- .filter(Boolean) as TopNavigationEntry[],
116
- ...(views ?? []).map(view =>
117
- !view.hideFromNavigation
118
- ? ({
119
- url: buildCMSUrlPath(Array.isArray(view.path) ? view.path[0] : view.path),
120
- name: view.name.trim(),
121
- type: "view",
122
- path: view.path,
123
- view,
124
- description: view.description?.trim(),
125
- group: getGroup(view)
126
- } satisfies TopNavigationEntry)
127
- : undefined)
128
- .filter(Boolean) as TopNavigationEntry[],
129
- ...(adminViews ?? []).map(view =>
130
- !view.hideFromNavigation
131
- ? ({
132
- url: buildCMSUrlPath(Array.isArray(view.path) ? view.path[0] : view.path),
133
- name: view.name.trim(),
134
- type: "admin",
135
- path: view.path,
136
- view,
137
- description: view.description?.trim(),
138
- group: "Admin"
139
- } satisfies TopNavigationEntry)
140
- : undefined)
141
- .filter(Boolean) as TopNavigationEntry[]
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
- // Sort by group, entries with group "Admin" will go last, and second to last will be the group "Views"
145
- navigationEntries = navigationEntries.sort((a, b) => {
146
- if (a.group !== "Views" && a.group !== "Admin" && (b.group === "Views" || b.group === "Admin")) {
147
- return -1;
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
- if (viewsOrder) {
266
+ const usedViewsOrder = viewsOrder ?? navigationEntriesOrder;
267
+ if (usedViewsOrder) {
169
268
  navigationEntries = navigationEntries.sort((a, b) => {
170
- const aIndex = viewsOrder.indexOf(a.path);
171
- const bIndex = viewsOrder.indexOf(b.path);
172
- if (aIndex === -1 && bIndex === -1) {
173
- return 0;
174
- }
175
- if (aIndex === -1) {
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 groups: string[] = Object.values(navigationEntries)
279
+ const collectedGroupsFromEntries = navigationEntries
186
280
  .map(e => e.group)
187
- .filter(Boolean)
188
- .filter((value, index, array) => array.indexOf(value) === index) as string[];
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, injectCollections),
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 computedTopLevelNav = computeTopNavigation(resolvedCollections, resolvedViews, resolvedAdminViews, viewsOrder);
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
- injectCollections?: (collections: EntityCollection[]) => EntityCollection[]): Promise<EntityCollection[]> {
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 (injectCollections) {
467
- resolvedCollections = injectCollections(resolvedCollections ?? []);
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 "Views";
623
+ return NAVIGATION_DEFAULT_GROUP_NAME;
493
624
  }
494
- return trimmed ?? "Views";
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
- // fullIdPath: navigationEntry.path,
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
- // fullIdPath: lastCollectionPath,
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 newPath = props.entityId
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: newPath,
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={"small"}
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={"small"}
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
- previewProperties={previewProperties}
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.previewProperties;
40
+ const previewProperties: string[] | undefined = mapProperty.previewKeys;
41
41
 
42
42
  if (!values) return null;
43
43