@firecms/core 3.0.0-beta.14 → 3.0.0-beta.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. package/dist/components/EntityCollectionTable/EntityCollectionRowActions.d.ts +2 -1
  2. package/dist/components/HomePage/DefaultHomePage.d.ts +2 -15
  3. package/dist/components/HomePage/HomePageDnD.d.ts +76 -0
  4. package/dist/components/HomePage/NavigationCard.d.ts +3 -1
  5. package/dist/components/HomePage/NavigationCardBinding.d.ts +3 -2
  6. package/dist/components/HomePage/NavigationGroup.d.ts +7 -1
  7. package/dist/components/HomePage/RenameGroupDialog.d.ts +9 -0
  8. package/dist/core/EntityEditView.d.ts +3 -0
  9. package/dist/core/EntityEditViewFormActions.d.ts +1 -1
  10. package/dist/core/field_configs.d.ts +1 -1
  11. package/dist/form/EntityForm.d.ts +2 -1
  12. package/dist/form/EntityFormActions.d.ts +6 -2
  13. package/dist/form/field_bindings/ReferenceAsStringFieldBinding.d.ts +9 -0
  14. package/dist/form/index.d.ts +1 -0
  15. package/dist/hooks/useBuildNavigationController.d.ts +51 -2
  16. package/dist/index.es.js +2184 -1052
  17. package/dist/index.es.js.map +1 -1
  18. package/dist/index.umd.js +2181 -1049
  19. package/dist/index.umd.js.map +1 -1
  20. package/dist/types/analytics.d.ts +1 -1
  21. package/dist/types/collections.d.ts +8 -2
  22. package/dist/types/customization_controller.d.ts +8 -0
  23. package/dist/types/entity_actions.d.ts +46 -6
  24. package/dist/types/firecms.d.ts +8 -0
  25. package/dist/types/navigation.d.ts +21 -5
  26. package/dist/types/plugins.d.ts +20 -1
  27. package/dist/types/properties.d.ts +7 -0
  28. package/dist/types/property_config.d.ts +1 -1
  29. package/dist/types/side_entity_controller.d.ts +4 -0
  30. package/dist/util/icons.d.ts +2 -2
  31. package/dist/util/navigation_from_path.d.ts +4 -0
  32. package/dist/util/resolutions.d.ts +2 -1
  33. package/package.json +5 -5
  34. package/src/components/ConfirmationDialog.tsx +1 -0
  35. package/src/components/EntityCollectionTable/EntityCollectionRowActions.tsx +6 -0
  36. package/src/components/EntityCollectionTable/PropertyTableCell.tsx +25 -3
  37. package/src/components/EntityCollectionTable/internal/CollectionTableToolbar.tsx +2 -2
  38. package/src/components/EntityCollectionView/EntityCollectionView.tsx +7 -4
  39. package/src/components/EntityCollectionView/EntityCollectionViewActions.tsx +3 -2
  40. package/src/components/FireCMSLogo.tsx +7 -51
  41. package/src/components/HomePage/DefaultHomePage.tsx +491 -157
  42. package/src/components/HomePage/FavouritesView.tsx +3 -3
  43. package/src/components/HomePage/HomePageDnD.tsx +599 -0
  44. package/src/components/HomePage/NavigationCard.tsx +47 -38
  45. package/src/components/HomePage/NavigationCardBinding.tsx +10 -6
  46. package/src/components/HomePage/NavigationGroup.tsx +63 -29
  47. package/src/components/HomePage/RenameGroupDialog.tsx +117 -0
  48. package/src/components/UnsavedChangesDialog.tsx +6 -2
  49. package/src/components/common/default_entity_actions.tsx +25 -9
  50. package/src/components/common/useDataSourceTableController.tsx +2 -2
  51. package/src/core/DefaultDrawer.tsx +8 -8
  52. package/src/core/DrawerNavigationItem.tsx +1 -1
  53. package/src/core/EntityEditView.tsx +41 -6
  54. package/src/core/EntityEditViewFormActions.tsx +154 -29
  55. package/src/core/EntitySidePanel.tsx +5 -2
  56. package/src/core/FireCMS.tsx +2 -0
  57. package/src/core/field_configs.tsx +15 -1
  58. package/src/form/EntityForm.tsx +36 -4
  59. package/src/form/EntityFormActions.tsx +51 -9
  60. package/src/form/components/StorageItemPreview.tsx +1 -1
  61. package/src/form/components/StorageUploadProgress.tsx +3 -3
  62. package/src/form/field_bindings/MarkdownEditorFieldBinding.tsx +4 -2
  63. package/src/form/field_bindings/ReferenceAsStringFieldBinding.tsx +135 -0
  64. package/src/form/field_bindings/RepeatFieldBinding.tsx +0 -1
  65. package/src/form/field_bindings/StorageUploadFieldBinding.tsx +12 -17
  66. package/src/form/index.tsx +1 -0
  67. package/src/hooks/useBuildNavigationController.tsx +273 -84
  68. package/src/internal/useBuildSideEntityController.tsx +7 -4
  69. package/src/preview/PropertyPreview.tsx +14 -0
  70. package/src/routes/FireCMSRoute.tsx +3 -3
  71. package/src/types/analytics.ts +3 -0
  72. package/src/types/collections.ts +8 -2
  73. package/src/types/customization_controller.tsx +9 -0
  74. package/src/types/entity_actions.tsx +57 -6
  75. package/src/types/firecms.tsx +9 -0
  76. package/src/types/navigation.ts +28 -6
  77. package/src/types/plugins.tsx +24 -1
  78. package/src/types/properties.ts +8 -0
  79. package/src/types/property_config.tsx +1 -0
  80. package/src/types/side_entity_controller.tsx +5 -0
  81. package/src/util/icons.tsx +22 -7
  82. package/src/util/join_collections.ts +3 -1
  83. package/src/util/navigation_from_path.ts +15 -5
  84. package/src/util/navigation_utils.ts +2 -2
  85. package/src/util/resolutions.ts +13 -1
@@ -1,5 +1,6 @@
1
1
  import { useCallback, useEffect, useRef, useState } from "react";
2
2
  import equal from "react-fast-compare"
3
+ import { useBlocker, useNavigate } from "react-router-dom";
3
4
 
4
5
  import {
5
6
  AuthController,
@@ -12,9 +13,10 @@ import {
12
13
  FireCMSPlugin,
13
14
  NavigationBlocker,
14
15
  NavigationController,
16
+ NavigationEntry,
17
+ NavigationGroupMapping,
18
+ NavigationResult,
15
19
  PermissionsBuilder,
16
- TopNavigationEntry,
17
- TopNavigationResult,
18
20
  User,
19
21
  UserConfigurationPersistence
20
22
  } from "../types";
@@ -28,27 +30,77 @@ import {
28
30
  resolvePermissions
29
31
  } from "../util";
30
32
  import { getParentReferencesFromPath } from "../util/parent_references_from_path";
31
- import { useBlocker, useNavigate } from "react-router-dom";
32
33
 
33
34
  const DEFAULT_BASE_PATH = "/";
34
35
  const DEFAULT_COLLECTION_PATH = "/c";
35
36
 
37
+ export const NAVIGATION_DEFAULT_GROUP_NAME = "Views";
38
+ export const NAVIGATION_ADMIN_GROUP_NAME = "Admin";
39
+
36
40
  export type BuildNavigationContextProps<EC extends EntityCollection, USER extends User> = {
41
+ /**
42
+ * Base path for the CMS, used to build the all the URLs.
43
+ * Defaults to "/".
44
+ */
37
45
  basePath?: string,
46
+ /**
47
+ * Base path for the collections, used to build the collection URLs.
48
+ * Defaults to "c" (e.g. "/c/products").
49
+ */
38
50
  baseCollectionPath?: string,
51
+ /**
52
+ * The auth controller used to manage the user authentication and permissions.
53
+ */
39
54
  authController: AuthController<USER>;
55
+ /**
56
+ * The collections to be used in the CMS.
57
+ * This can be a static array of collections or a function that returns a promise
58
+ * resolving to an array of collections.
59
+ */
40
60
  collections?: EC[] | EntityCollectionsBuilder<EC>;
61
+ /**
62
+ * Optional permissions builder to be applied to the collections.
63
+ * If not provided, the permissions will be resolved from the collection configuration.
64
+ */
41
65
  collectionPermissions?: PermissionsBuilder;
66
+ /**
67
+ * Custom views to be added to the CMS, these will be available in the main navigation.
68
+ * This can be a static array of views or a function that returns a promise
69
+ * resolving to an array of views.
70
+ */
42
71
  views?: CMSView[] | CMSViewsBuilder;
72
+ /**
73
+ * Custom views to be added to the CMS admin navigation.
74
+ * This can be a static array of views or a function that returns a promise
75
+ * resolving to an array of views.
76
+ */
43
77
  adminViews?: CMSView[] | CMSViewsBuilder;
44
- viewsOrder?: string[];
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<TopNavigationResult | undefined>(undefined);
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 computeTopNavigation = useCallback((collections: EntityCollection[], views: CMSView[], adminViews: CMSView[], viewsOrder?: string[]): TopNavigationResult => {
96
- let navigationEntries: TopNavigationEntry[] = [
97
- ...(collections ?? []).map(collection => (!collection.hideFromNavigation
98
- ? ({
99
- 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),
100
199
  type: "collection",
101
200
  name: collection.name.trim(),
102
- path: collection.id ?? collection.path,
201
+ path: pathKey,
103
202
  collection,
104
203
  description: collection.description?.trim(),
105
- group: getGroup(collection)
106
- } satisfies TopNavigationEntry)
107
- : undefined))
108
- .filter(Boolean) as TopNavigationEntry[],
109
- ...(views ?? []).map(view =>
110
- !view.hideFromNavigation
111
- ? ({
112
- url: buildCMSUrlPath(Array.isArray(view.path) ? view.path[0] : view.path),
113
- name: view.name.trim(),
114
- type: "view",
115
- path: view.path,
116
- view,
117
- description: view.description?.trim(),
118
- group: getGroup(view)
119
- } satisfies TopNavigationEntry)
120
- : undefined)
121
- .filter(Boolean) as TopNavigationEntry[],
122
- ...(adminViews ?? []).map(view =>
123
- !view.hideFromNavigation
124
- ? ({
125
- url: buildCMSUrlPath(Array.isArray(view.path) ? view.path[0] : view.path),
126
- name: view.name.trim(),
127
- type: "admin",
128
- path: view.path,
129
- view,
130
- description: view.description?.trim(),
131
- group: "Admin"
132
- } satisfies TopNavigationEntry)
133
- : undefined)
134
- .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[])
135
255
  ];
136
256
 
137
- // Sort by group, entries with group "Admin" will go last, and second to last will be the group "Views"
138
- navigationEntries = navigationEntries.sort((a, b) => {
139
- if (a.group !== "Views" && a.group !== "Admin" && (b.group === "Views" || b.group === "Admin")) {
140
- return -1;
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
- if (viewsOrder) {
266
+ const usedViewsOrder = viewsOrder ?? navigationEntriesOrder;
267
+ if (usedViewsOrder) {
162
268
  navigationEntries = navigationEntries.sort((a, b) => {
163
- const aIndex = viewsOrder.indexOf(a.path);
164
- const bIndex = viewsOrder.indexOf(b.path);
165
- if (aIndex === -1 && bIndex === -1) {
166
- return 0;
167
- }
168
- if (aIndex === -1) {
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 groups: string[] = Object.values(navigationEntries)
279
+ const collectedGroupsFromEntries = navigationEntries
179
280
  .map(e => e.group)
180
- .filter(Boolean)
181
- .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));
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 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
+
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 "Views";
623
+ return NAVIGATION_DEFAULT_GROUP_NAME;
507
624
  }
508
- return trimmed ?? "Views";
625
+ return trimmed ?? NAVIGATION_DEFAULT_GROUP_NAME;
509
626
  }
510
627
 
511
628
  function areCollectionListsEqual(a: EntityCollection[], b: EntityCollection[]) {
@@ -584,3 +701,75 @@ function useCustomBlocker(): NavigationBlocker {
584
701
  reset: blocker?.reset
585
702
  }
586
703
  }
704
+
705
+ function computeNavigationGroups({
706
+ navigationGroupMappings,
707
+ collections,
708
+ views,
709
+ plugins
710
+ }: {
711
+ navigationGroupMappings?: NavigationGroupMapping[],
712
+ collections?: EntityCollection[],
713
+ views?: CMSView[],
714
+ plugins?: FireCMSPlugin[]
715
+ }): NavigationGroupMapping[] {
716
+
717
+ let result = navigationGroupMappings;
718
+
719
+ result = plugins ? plugins?.reduce((acc, plugin) => {
720
+ if (plugin.homePage?.navigationEntries) {
721
+ plugin.homePage.navigationEntries.forEach((entry) => {
722
+ const {
723
+ name,
724
+ entries
725
+ } = entry;
726
+ const existingGroup = acc.find(entry => entry.name === name);
727
+ if (existingGroup) {
728
+ existingGroup.entries.push(...entries);
729
+ } else {
730
+ acc.push({
731
+ name,
732
+ entries: [...entries]
733
+ });
734
+ }
735
+ });
736
+
737
+ }
738
+ return acc;
739
+ }, [...(result ?? [])] as NavigationGroupMapping[]) : result;
740
+
741
+ if (!result) {
742
+ // Convert views and collections to navigation group mappings, grouped by their group name
743
+ result = [];
744
+ const groupMap: Record<string, string[]> = {};
745
+
746
+ // Add collections
747
+ (collections ?? []).forEach(collection => {
748
+ const name = getGroup(collection);
749
+ const entry = collection.id ?? collection.path;
750
+ if (!groupMap[name]) groupMap[name] = [];
751
+ groupMap[name].push(entry);
752
+ });
753
+
754
+ // Add views
755
+ (views ?? []).forEach(view => {
756
+ const name = getGroup(view);
757
+ const entry = Array.isArray(view.path) ? view.path[0] : view.path;
758
+ if (!groupMap[name]) groupMap[name] = [];
759
+ groupMap[name].push(entry);
760
+ });
761
+
762
+ // Convert groupMap to initialGroupMappings array
763
+ result = Object.entries(groupMap).map(([name, entries]) => ({
764
+ name,
765
+ entries
766
+ }));
767
+ }
768
+
769
+ // Remove duplicates in entries
770
+ result.forEach(group => {
771
+ group.entries = [...new Set(group.entries)];
772
+ });
773
+
774
+ return result;
775
+ }
@@ -215,6 +215,7 @@ export const useBuildSideEntityController = (navigation: NavigationController,
215
215
 
216
216
  export function buildSidePanelsFromUrl(path: string, collections: EntityCollection[], newFlag: boolean): EntitySidePanelProps<any>[] {
217
217
 
218
+
218
219
  const navigationViewsForPath: NavigationViewInternal<any>[] = getNavigationEntriesFromPath({
219
220
  path,
220
221
  collections
@@ -222,18 +223,20 @@ export function buildSidePanelsFromUrl(path: string, collections: EntityCollecti
222
223
 
223
224
  let sidePanel: EntitySidePanelProps<any> | undefined = undefined;
224
225
  let lastCollectionPath = "";
226
+ let lastCollectionId: string | undefined = undefined;
225
227
  for (let i = 0; i < navigationViewsForPath.length; i++) {
226
228
  const navigationEntry = navigationViewsForPath[i];
227
229
 
228
230
  if (navigationEntry.type === "collection") {
229
231
  lastCollectionPath = navigationEntry.path;
232
+ lastCollectionId = navigationEntry.collection.id;
230
233
  }
231
234
 
232
235
  const previousEntry = navigationViewsForPath[i - 1];
233
236
  if (navigationEntry.type === "entity") {
234
237
  sidePanel = {
235
238
  path: navigationEntry.path,
236
- // 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}
@@ -79,7 +79,8 @@ export function FireCMSRoute() {
79
79
  key={`collection_view_${collection.id ?? collection.path}`}
80
80
  isSubCollection={false}
81
81
  parentCollectionIds={[]}
82
- fullPath={collection.id}
82
+ fullPath={collection.path}
83
+ fullIdPath={collection.id}
83
84
  updateUrl={true}
84
85
  {...collection}
85
86
  Actions={toArray(collection.Actions)}/>
@@ -100,7 +101,7 @@ export function FireCMSRoute() {
100
101
  fullIdPath={collection.id}
101
102
  isSubCollection={false}
102
103
  parentCollectionIds={[]}
103
- fullPath={collection.id}
104
+ fullPath={collection.path}
104
105
  updateUrl={true}
105
106
  {...collection}
106
107
  Actions={toArray(collection.Actions)}/>;
@@ -200,7 +201,6 @@ function EntityFullScreenRoute({
200
201
  const collection = isNew ? lastCollectionEntry!.collection : lastEntityEntry!.parentCollection;
201
202
  const fullIdPath = isNew ? lastCollectionEntry!.path : lastEntityEntry!.path;
202
203
  const collectionPath = navigation.resolveIdsFrom(fullIdPath);
203
-
204
204
  return <>
205
205
  <EntityEditView
206
206
  key={collection.id + "_" + (isNew ? "new" : (isCopy ? entityId + "_copy" : entityId))}
@@ -28,6 +28,9 @@ export type CMSAnalyticsEvent =
28
28
  | "home_navigate_to_view"
29
29
  | "home_navigate_to_admin_view"
30
30
  | "home_favorite_navigate_to_view"
31
+ | "home_move_card"
32
+ | "home_move_group"
33
+ | "home_drop_new_group"
31
34
 
32
35
  | "collection_inline_editing"
33
36
 
@@ -70,12 +70,15 @@ export interface EntityCollection<M extends Record<string, any> = any, USER exte
70
70
  * e.g. 'account_tree' or 'person'.
71
71
  * Find all the icons in https://firecms.co/docs/icons
72
72
  */
73
- icon?: string;
73
+ icon?: string | React.ReactNode;
74
74
 
75
75
  /**
76
76
  * Optional field used to group top level navigation entries under a~
77
77
  * navigation view. If you set this value in a subcollection it has no
78
78
  * effect.
79
+ * @deprecated This prop is deprecated and will be removed in the future.
80
+ * You can apply grouping by using the `navigationGroupMappings` prop in the
81
+ * {@link useBuildNavigationController} hook instead.
79
82
  */
80
83
  group?: string;
81
84
 
@@ -193,8 +196,11 @@ export interface EntityCollection<M extends Record<string, any> = any, USER exte
193
196
  * }
194
197
  * }
195
198
  * ```
199
+ *
200
+ * You can also pass the action as a string that represents the `key`, in which case it will
201
+ * use the action defined in the main configuration under `entityActions`.
196
202
  */
197
- entityActions?: EntityAction<M, USER>[];
203
+ entityActions?: (EntityAction<M, USER> | string)[];
198
204
 
199
205
  /**
200
206
  * Pass your own selection controller if you want to control selected
@@ -4,6 +4,7 @@ import { FireCMSPlugin } from "./plugins";
4
4
  import { EntityCustomView } from "./collections";
5
5
  import { Locale } from "./locales";
6
6
  import { PropertyConfig } from "./property_config";
7
+ import { EntityAction } from "./entity_actions";
7
8
 
8
9
  export type CustomizationController = {
9
10
 
@@ -26,6 +27,14 @@ export type CustomizationController = {
26
27
  */
27
28
  entityViews?: EntityCustomView[];
28
29
 
30
+ /**
31
+ * List of actions that can be performed on entities.
32
+ * These actions are displayed in the entity view and in the collection view.
33
+ * You can later reuse these actions in the `entityActions` prop of a collection,
34
+ * by specifying the `key` of the action.
35
+ */
36
+ entityActions?: EntityAction<any, any>[];
37
+
29
38
  /**
30
39
  * Format of the dates in the CMS.
31
40
  * Defaults to 'MMMM dd, yyyy, HH:mm:ss'