@firecms/core 3.0.0-canary.248 → 3.0.0-canary.249

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/dist/components/HomePage/DefaultHomePage.d.ts +2 -15
  2. package/dist/components/HomePage/HomePageDnD.d.ts +76 -0
  3. package/dist/components/HomePage/NavigationCard.d.ts +3 -1
  4. package/dist/components/HomePage/NavigationCardBinding.d.ts +3 -2
  5. package/dist/components/HomePage/NavigationGroup.d.ts +7 -1
  6. package/dist/components/HomePage/RenameGroupDialog.d.ts +9 -0
  7. package/dist/core/field_configs.d.ts +1 -1
  8. package/dist/form/field_bindings/ReferenceAsStringFieldBinding.d.ts +9 -0
  9. package/dist/form/index.d.ts +1 -0
  10. package/dist/hooks/useBuildNavigationController.d.ts +51 -2
  11. package/dist/index.es.js +1726 -778
  12. package/dist/index.es.js.map +1 -1
  13. package/dist/index.umd.js +1723 -775
  14. package/dist/index.umd.js.map +1 -1
  15. package/dist/types/analytics.d.ts +1 -1
  16. package/dist/types/collections.d.ts +3 -0
  17. package/dist/types/navigation.d.ts +20 -4
  18. package/dist/types/plugins.d.ts +12 -0
  19. package/dist/types/properties.d.ts +7 -0
  20. package/dist/types/property_config.d.ts +1 -1
  21. package/dist/util/icons.d.ts +1 -1
  22. package/package.json +5 -5
  23. package/src/components/EntityCollectionTable/PropertyTableCell.tsx +25 -3
  24. package/src/components/HomePage/DefaultHomePage.tsx +476 -157
  25. package/src/components/HomePage/FavouritesView.tsx +3 -3
  26. package/src/components/HomePage/HomePageDnD.tsx +613 -0
  27. package/src/components/HomePage/NavigationCard.tsx +47 -38
  28. package/src/components/HomePage/NavigationCardBinding.tsx +10 -6
  29. package/src/components/HomePage/NavigationGroup.tsx +63 -29
  30. package/src/components/HomePage/RenameGroupDialog.tsx +113 -0
  31. package/src/core/DefaultDrawer.tsx +8 -8
  32. package/src/core/DrawerNavigationItem.tsx +1 -1
  33. package/src/core/field_configs.tsx +15 -1
  34. package/src/form/field_bindings/ReferenceAsStringFieldBinding.tsx +135 -0
  35. package/src/form/field_bindings/RepeatFieldBinding.tsx +0 -1
  36. package/src/form/index.tsx +1 -0
  37. package/src/hooks/useBuildNavigationController.tsx +273 -84
  38. package/src/preview/PropertyPreview.tsx +14 -0
  39. package/src/types/analytics.ts +3 -0
  40. package/src/types/collections.ts +3 -0
  41. package/src/types/navigation.ts +27 -5
  42. package/src/types/plugins.tsx +15 -0
  43. package/src/types/properties.ts +8 -0
  44. package/src/types/property_config.tsx +1 -0
  45. package/src/util/icons.tsx +7 -3
@@ -1,5 +1,6 @@
1
1
  import { useCallback, useEffect, useRef, useState } from "react";
2
2
  import equal from "react-fast-compare"
3
+ import { useBlocker, useNavigate } from "react-router-dom";
3
4
 
4
5
  import {
5
6
  AuthController,
@@ -12,9 +13,10 @@ import {
12
13
  FireCMSPlugin,
13
14
  NavigationBlocker,
14
15
  NavigationController,
16
+ NavigationEntry,
17
+ NavigationGroupMapping,
18
+ NavigationResult,
15
19
  PermissionsBuilder,
16
- TopNavigationEntry,
17
- TopNavigationResult,
18
20
  User,
19
21
  UserConfigurationPersistence
20
22
  } from "../types";
@@ -28,27 +30,77 @@ import {
28
30
  resolvePermissions
29
31
  } from "../util";
30
32
  import { getParentReferencesFromPath } from "../util/parent_references_from_path";
31
- import { useBlocker, useNavigate } from "react-router-dom";
32
33
 
33
34
  const DEFAULT_BASE_PATH = "/";
34
35
  const DEFAULT_COLLECTION_PATH = "/c";
35
36
 
37
+ export const NAVIGATION_DEFAULT_GROUP_NAME = "Views";
38
+ export const NAVIGATION_ADMIN_GROUP_NAME = "Admin";
39
+
36
40
  export type BuildNavigationContextProps<EC extends EntityCollection, USER extends User> = {
41
+ /**
42
+ * Base path for the CMS, used to build the all the URLs.
43
+ * Defaults to "/".
44
+ */
37
45
  basePath?: string,
46
+ /**
47
+ * Base path for the collections, used to build the collection URLs.
48
+ * Defaults to "c" (e.g. "/c/products").
49
+ */
38
50
  baseCollectionPath?: string,
51
+ /**
52
+ * The auth controller used to manage the user authentication and permissions.
53
+ */
39
54
  authController: AuthController<USER>;
55
+ /**
56
+ * The collections to be used in the CMS.
57
+ * This can be a static array of collections or a function that returns a promise
58
+ * resolving to an array of collections.
59
+ */
40
60
  collections?: EC[] | EntityCollectionsBuilder<EC>;
61
+ /**
62
+ * Optional permissions builder to be applied to the collections.
63
+ * If not provided, the permissions will be resolved from the collection configuration.
64
+ */
41
65
  collectionPermissions?: PermissionsBuilder;
66
+ /**
67
+ * Custom views to be added to the CMS, these will be available in the main navigation.
68
+ * This can be a static array of views or a function that returns a promise
69
+ * resolving to an array of views.
70
+ */
42
71
  views?: CMSView[] | CMSViewsBuilder;
72
+ /**
73
+ * Custom views to be added to the CMS admin navigation.
74
+ * This can be a static array of views or a function that returns a promise
75
+ * resolving to an array of views.
76
+ */
43
77
  adminViews?: CMSView[] | CMSViewsBuilder;
44
- 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
+ }
@@ -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}
@@ -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
 
@@ -76,6 +76,9 @@ export interface EntityCollection<M extends Record<string, any> = any, USER exte
76
76
  * Optional field used to group top level navigation entries under a~
77
77
  * navigation view. If you set this value in a subcollection it has no
78
78
  * effect.
79
+ * @deprecated This prop is deprecated and will be removed in the future.
80
+ * You can apply grouping by using the `navigationGroupMappings` prop in the
81
+ * {@link useBuildNavigationController} hook instead.
79
82
  */
80
83
  group?: string;
81
84
 
@@ -36,7 +36,7 @@ export type NavigationController<EC extends EntityCollection = EntityCollection<
36
36
  * level of the navigation (e.g. in the home page or the navigation
37
37
  * drawer)
38
38
  */
39
- topLevelNavigation?: TopNavigationResult;
39
+ topLevelNavigation?: NavigationResult;
40
40
 
41
41
  /**
42
42
  * Is the navigation loading (the configuration persistence has not
@@ -229,7 +229,22 @@ export interface CMSView {
229
229
 
230
230
  }
231
231
 
232
- export interface TopNavigationEntry {
232
+ /**
233
+ * Used to group navigation entries in the main navigation.
234
+ */
235
+ export interface NavigationGroupMapping {
236
+ /**
237
+ * Name of the group, used to display the group header in the UI
238
+ */
239
+ name: string;
240
+ /**
241
+ * List of collection ids or view paths that belong to this group.
242
+ */
243
+ entries: string[];
244
+ }
245
+
246
+ export interface NavigationEntry {
247
+ id: string;
233
248
  url: string;
234
249
  name: string;
235
250
  path: string;
@@ -240,7 +255,14 @@ export interface TopNavigationEntry {
240
255
  group: string;
241
256
  }
242
257
 
243
- export type TopNavigationResult = {
244
- navigationEntries: TopNavigationEntry[],
245
- groups: string[]
258
+ export type NavigationResult = {
259
+
260
+ allowDragAndDrop: boolean;
261
+
262
+ navigationEntries: NavigationEntry[],
263
+
264
+ groups: string[],
265
+
266
+ onNavigationEntriesUpdate: (entries: NavigationGroupMapping[]) => void;
246
267
  };
268
+
@@ -7,6 +7,7 @@ import { FieldProps, FormContext } from "./fields";
7
7
  import { CMSType, Property } from "./properties";
8
8
  import { EntityStatus } from "./entities";
9
9
  import { ResolvedProperty } from "./resolved_entities";
10
+ import { NavigationGroupMapping } from "./navigation";
10
11
 
11
12
  /**
12
13
  * Interface used to define plugins for FireCMS.
@@ -84,6 +85,20 @@ export type FireCMSPlugin<PROPS = any, FORM_PROPS = any, EC extends EntityCollec
84
85
  children: React.ReactNode;
85
86
  }
86
87
 
88
+ /**
89
+ * Allow reordering with drag and drop of the collections in the home page.
90
+ */
91
+ allowDragAndDrop?: boolean;
92
+
93
+ navigationEntries?: NavigationGroupMapping[];
94
+
95
+ /**
96
+ * This method will be called when the entries are updated in the home page.
97
+ * group => navigationEntriesOrder (path)
98
+ * @param entries
99
+ */
100
+ onNavigationEntriesUpdate?: (entries: NavigationGroupMapping[]) => void;
101
+
87
102
  }
88
103
 
89
104
  collectionView?: {
@@ -413,6 +413,14 @@ export interface StringProperty extends BaseProperty<string> {
413
413
  * Add an icon to clear the value and set it to `null`. Defaults to `false`
414
414
  */
415
415
  clearable?: boolean;
416
+
417
+ /**
418
+ * You can use this property (a string) to behave as a reference to another
419
+ * collection. The stored value is the ID of the entity in the
420
+ * collection, and the `path` prop is used to
421
+ * define the collection this reference points to.
422
+ */
423
+ reference?: ReferenceProperty;
416
424
  }
417
425
 
418
426
  /**
@@ -62,6 +62,7 @@ export type PropertyConfigId =
62
62
  "group" |
63
63
  "key_value" |
64
64
  "reference" |
65
+ "reference_as_string" |
65
66
  "multi_references" |
66
67
  "switch" |
67
68
  "date_time" |
@@ -4,13 +4,17 @@ import { coolIconKeys, Icon, IconColor, iconKeys } from "@firecms/ui";
4
4
  import { slugify } from "./strings";
5
5
  import equal from "react-fast-compare"
6
6
 
7
- export function getIcon(iconKey?: string, className?: string, color?:IconColor): React.ReactElement | undefined {
7
+ export function getIcon(iconKey?: string,
8
+ className?: string,
9
+ color?: IconColor,
10
+ size?: "smallest" | "small" | "medium" | "large" | number,): React.ReactElement | undefined {
8
11
  if (!iconKey) return undefined;
9
12
  iconKey = slugify(iconKey);
10
13
  if (!(iconKey in iconKeysMap)) {
11
14
  return undefined;
12
15
  }
13
- return iconKey in iconKeysMap ? <Icon iconKey={iconKey} size={"medium"} className={className} color={color}/> : undefined;
16
+ return iconKey in iconKeysMap ?
17
+ <Icon iconKey={iconKey} size={size} className={className} color={color}/> : undefined;
14
18
  }
15
19
 
16
20
  export type IconViewProps = {
@@ -34,7 +38,7 @@ export const IconForView = React.memo(
34
38
  size?: "smallest" | "small" | "medium" | "large" | number,
35
39
  }): React.ReactElement {
36
40
  if (!collectionOrView) return <></>;
37
- const icon = getIcon(collectionOrView.icon, className, color);
41
+ const icon = getIcon(collectionOrView.icon, className, color, size);
38
42
  if (collectionOrView?.icon && icon)
39
43
  return icon;
40
44