@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,45 +1,41 @@
1
- import React, { useCallback, useEffect, useRef, useState } from "react";
2
-
1
+ import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
2
+ import Fuse from "fuse.js";
3
+ import { Container, SearchBar } from "@firecms/ui";
3
4
  import { useCustomizationController, useFireCMSContext, useNavigationController } from "../../hooks";
4
5
  import {
5
6
  CMSAnalyticsEvent,
7
+ NavigationEntry,
8
+ NavigationGroupMapping,
6
9
  PluginGenericProps,
7
- PluginHomePageAdditionalCardsProps,
8
- TopNavigationEntry
10
+ PluginHomePageAdditionalCardsProps
9
11
  } from "../../types";
10
-
11
12
  import { toArray } from "../../util/arrays";
12
- import { NavigationGroup } from "./NavigationGroup";
13
- import { NavigationCardBinding } from "./NavigationCardBinding";
14
-
15
- import Fuse from "fuse.js"
16
-
17
- import { Container, SearchBar } from "@firecms/ui";
18
13
  import { FavouritesView } from "./FavouritesView";
19
14
  import { useRestoreScroll } from "../../internal/useRestoreScroll";
15
+ import { NavigationGroup } from "./NavigationGroup";
16
+ import {
17
+ NavigationGroupDroppable,
18
+ NewGroupDropZone,
19
+ SortableNavigationCard,
20
+ SortableNavigationGroup,
21
+ useHomePageDnd
22
+ } from "./HomePageDnD";
23
+ import { DndContext, DragOverlay, MeasuringStrategy } from "@dnd-kit/core";
24
+ import { NavigationCardBinding } from "./NavigationCardBinding";
25
+ import { SortableContext, verticalListSortingStrategy } from "@dnd-kit/sortable";
26
+ import { restrictToVerticalAxis, restrictToWindowEdges } from "@dnd-kit/modifiers";
27
+ import { RenameGroupDialog } from "./RenameGroupDialog";
20
28
 
21
- /**
22
- * Default entry view for the CMS. This component renders navigation cards
23
- * for each collection defined in the navigation.
29
+ export const DEFAULT_GROUP_NAME = "Views";
30
+ export const ADMIN_GROUP_NAME = "Admin";
24
31
 
25
- * @group Components
26
- */
27
32
  export function DefaultHomePage({
28
33
  additionalActions,
29
34
  additionalChildrenStart,
30
35
  additionalChildrenEnd
31
36
  }: {
32
- /**
33
- * Additional actions to be rendered in the home page, close to the search bar.
34
- */
35
37
  additionalActions?: React.ReactNode;
36
- /**
37
- * Additional children to be rendered in the beginning of the home page.
38
- */
39
38
  additionalChildrenStart?: React.ReactNode;
40
- /**
41
- * Additional children to be rendered at the end of the home page.
42
- */
43
39
  additionalChildrenEnd?: React.ReactNode;
44
40
  }) {
45
41
 
@@ -47,176 +43,514 @@ export function DefaultHomePage({
47
43
  const customizationController = useCustomizationController();
48
44
  const navigationController = useNavigationController();
49
45
 
50
- const fuse = useRef<Fuse<TopNavigationEntry> | null>(null);
51
-
52
46
  if (!navigationController.topLevelNavigation)
53
- throw Error("Navigation not ready in FireCMSHomePage");
54
-
55
- const {
56
- containerRef,
57
- scroll,
58
- direction
59
- } = useRestoreScroll();
47
+ throw Error("Navigation not ready");
60
48
 
61
49
  const {
62
- navigationEntries,
63
- groups
50
+ allowDragAndDrop,
51
+ navigationEntries: rawNavigationEntries,
52
+ groups: groupOrderFromNavController,
53
+ onNavigationEntriesUpdate
64
54
  } = navigationController.topLevelNavigation;
65
55
 
56
+ const fuse = useRef<Fuse<NavigationEntry> | null>(null);
66
57
  const [filteredUrls, setFilteredUrls] = useState<string[] | null>(null);
67
58
  const performingSearch = Boolean(filteredUrls);
68
59
 
69
- const filteredNavigationEntries = filteredUrls
70
- ? filteredUrls.map(url => navigationEntries.find(e => e.url === url)).filter(Boolean) as TopNavigationEntry[]
71
- : navigationEntries;
60
+ // Memoize filtered navigation entries to prevent unnecessary recalculations
61
+ const filteredNavigationEntries = useMemo(() => {
62
+ return filteredUrls
63
+ ? rawNavigationEntries.filter((e) => filteredUrls.includes(e.url))
64
+ : rawNavigationEntries;
65
+ }, [filteredUrls, rawNavigationEntries]);
72
66
 
73
67
  useEffect(() => {
74
- fuse.current = new Fuse(navigationEntries, {
68
+ fuse.current = new Fuse(rawNavigationEntries, {
75
69
  keys: ["name", "description", "group", "path"]
76
70
  });
77
- }, [navigationEntries]);
78
-
79
- const updateSearchResults = useCallback(
80
- (value?: string) => {
81
- if (!value || value === "") {
82
- setFilteredUrls(null);
83
- } else {
84
- const searchResult = fuse.current?.search(value);
85
- if (searchResult) {
86
- setFilteredUrls(searchResult.map((e) => e.item.url));
87
- }
71
+ }, [rawNavigationEntries]);
72
+
73
+ const updateSearch = useCallback((v?: string) => {
74
+ if (!v?.trim()) {
75
+ setFilteredUrls(null);
76
+ return;
77
+ }
78
+ const results = fuse.current?.search(v.trim());
79
+ setFilteredUrls(results ? results.map((x) => x.item.url) : []);
80
+ }, []);
81
+
82
+ /* ───────────────────────────────────────────────────────────────
83
+ Build groups (all items) + isolate Admin
84
+ ─────────────────────────────────────────────────────────────── */
85
+ const [items, setItems] = useState<
86
+ { name: string; entries: NavigationEntry[] }[]
87
+ >([]);
88
+ const [adminGroupData, setAdminGroupData] = useState<{
89
+ name: string;
90
+ entries: NavigationEntry[];
91
+ } | null>(null);
92
+
93
+ // Memoize the processed groups to avoid unnecessary recalculations
94
+ const processedGroups = useMemo(() => {
95
+ const src = filteredNavigationEntries;
96
+ const entriesByGroup: Record<string, NavigationEntry[]> = {};
97
+
98
+ src.forEach((e) => {
99
+ const g =
100
+ e.type === "admin"
101
+ ? ADMIN_GROUP_NAME
102
+ : e.group ?? DEFAULT_GROUP_NAME;
103
+ (entriesByGroup[g] ??= []).push(e);
104
+ });
105
+
106
+ // Check if there are custom actions from plugins that should show in the default group
107
+ const hasPluginAdditionalCards = customizationController.plugins?.some(p => p.homePage?.AdditionalCards);
108
+
109
+ let allProcessed: { name: string; entries: NavigationEntry[] }[];
110
+
111
+ if (performingSearch) {
112
+ const ordered = [
113
+ ...new Set(src.map((e) => e.group ?? DEFAULT_GROUP_NAME))
114
+ ];
115
+ allProcessed = ordered
116
+ .map((name) => ({
117
+ name,
118
+ entries: entriesByGroup[name] || []
119
+ }))
120
+ .filter((g) => g.entries.length);
121
+ } else {
122
+ allProcessed = groupOrderFromNavController.map((g) => ({
123
+ name: g,
124
+ entries: entriesByGroup[g] || []
125
+ }));
126
+ Object.keys(entriesByGroup).forEach((g) => {
127
+ if (!groupOrderFromNavController.includes(g))
128
+ allProcessed.push({
129
+ name: g,
130
+ entries: entriesByGroup[g]
131
+ });
132
+ });
133
+
134
+ // Ensure default group exists if there are plugin additional cards but no collections
135
+ if (hasPluginAdditionalCards && !allProcessed.some(g => g.name === DEFAULT_GROUP_NAME)) {
136
+ allProcessed.push({
137
+ name: DEFAULT_GROUP_NAME,
138
+ entries: []
139
+ });
88
140
  }
89
- }, []);
90
141
 
91
- const filteredGroups = filteredUrls ? filteredNavigationEntries.map(entry => entry.group) : [];
92
- const allGroups: Array<string | undefined> = filteredUrls ? filteredGroups.filter((group, index) => filteredGroups.indexOf(group) === index) : [...groups];
93
- if (filteredNavigationEntries.filter(e => !e.group).length > 0 || filteredNavigationEntries.length === 0) {
94
- allGroups.push(undefined);
95
- }
142
+ allProcessed = allProcessed.filter(
143
+ (g) =>
144
+ g.entries.length ||
145
+ groupOrderFromNavController.includes(g.name) ||
146
+ (g.name === DEFAULT_GROUP_NAME && hasPluginAdditionalCards)
147
+ );
148
+ }
149
+
150
+ const admin = allProcessed.find((g) => g.name === ADMIN_GROUP_NAME);
151
+ return {
152
+ adminGroupData: admin || null,
153
+ items: allProcessed.filter((g) => g.name !== ADMIN_GROUP_NAME)
154
+ };
155
+ }, [filteredNavigationEntries, performingSearch, groupOrderFromNavController, customizationController.plugins]);
156
+
157
+ // Update state only when processedGroups actually changes
158
+ useEffect(() => {
159
+ setAdminGroupData(processedGroups.adminGroupData);
160
+ setItems(processedGroups.items);
161
+ }, [processedGroups]);
162
+
163
+ /* ───────────────────────────────────────────────────────────────
164
+ Local update vs. persistence helpers
165
+ ─────────────────────────────────────────────────────────────── */
166
+ const updateItems = (
167
+ updater:
168
+ | { name: string; entries: NavigationEntry[] }[]
169
+ | ((
170
+ prev: { name: string; entries: NavigationEntry[] }[]
171
+ ) => { name: string; entries: NavigationEntry[] }[])
172
+ ) => {
173
+ setItems(updater); // local only
174
+ };
175
+
176
+ const persistNavigationGroups = (
177
+ latest: { name: string; entries: NavigationEntry[] }[]
178
+ ) => {
179
+ const draggable: NavigationGroupMapping[] = latest.map((g) => ({
180
+ name: g.name,
181
+ entries: g.entries.map((e) => e.path)
182
+ }));
183
+
184
+ const all: NavigationGroupMapping[] = adminGroupData
185
+ ? [
186
+ ...draggable,
187
+ {
188
+ name: adminGroupData.name,
189
+ entries: adminGroupData.entries.map((e) => e.path)
190
+ }
191
+ ]
192
+ : draggable;
193
+
194
+ onNavigationEntriesUpdate(all);
195
+ };
96
196
 
197
+ /* ─────────────────────────────────────────────────────���─────────
198
+ Hook for DnD
199
+ ───�����────────────────────────────────────────────────────────── */
200
+ const {
201
+ sensors,
202
+ collisionDetection,
203
+ onDragStart,
204
+ onDragOver,
205
+ onDragEnd,
206
+ dropAnimation,
207
+ activeItemForOverlay,
208
+ activeGroupData,
209
+ draggingGroupId,
210
+ containers,
211
+ dndKitActiveNode,
212
+ onDragCancel,
213
+ isDraggingCardOnly,
214
+ dialogOpenForGroup,
215
+ setDialogOpenForGroup,
216
+ handleRenameGroup,
217
+ isHoveringNewGroupDropZone,
218
+ setIsHoveringNewGroupDropZone
219
+ } = useHomePageDnd({
220
+ items,
221
+ setItems: updateItems,
222
+ disabled: !allowDragAndDrop || performingSearch,
223
+ onPersist: persistNavigationGroups, // ——► persistence here
224
+ onGroupMoved: (g) =>
225
+ context.analyticsController?.onAnalyticsEvent?.("home_move_group", {
226
+ name: g
227
+ }),
228
+ onCardMovedBetweenGroups: (card) =>
229
+ context.analyticsController?.onAnalyticsEvent?.("home_move_card", {
230
+ id: card.id
231
+ }),
232
+ onNewGroupDrop: () =>
233
+ context.analyticsController?.onAnalyticsEvent?.(
234
+ "home_drop_new_group"
235
+ )
236
+ });
237
+
238
+ const {
239
+ containerRef,
240
+ direction
241
+ } = useRestoreScroll();
242
+
243
+ const dndDisabled = !allowDragAndDrop || performingSearch;
244
+
245
+ const dndModifiers = dndKitActiveNode?.data.current?.type === "group"
246
+ ? [restrictToVerticalAxis, restrictToWindowEdges]
247
+ : [restrictToWindowEdges];
248
+
249
+ /* ───────────────────────────────────────────────────────────────
250
+ Plugin extras
251
+ ─────────────────────────────────────────────────────────────── */
97
252
  let additionalPluginChildrenStart: React.ReactNode | undefined;
98
253
  let additionalPluginChildrenEnd: React.ReactNode | undefined;
99
254
  let additionalPluginSections: React.ReactNode | undefined;
255
+
100
256
  if (customizationController.plugins) {
101
- const sectionProps: PluginGenericProps = {
102
- context
103
- };
104
- additionalPluginSections = <>
105
- {customizationController.plugins.filter(plugin => plugin.homePage?.includeSection)
106
- .map((plugin, i) => {
107
- const section = plugin.homePage!.includeSection!(sectionProps)
108
- return (
109
- <NavigationGroup
110
- group={section.title}
111
- key={`plugin_section_${plugin.key}`}>
112
- {section.children}
113
- </NavigationGroup>
114
- );
115
- })}
116
- </>;
117
-
118
- additionalPluginChildrenStart = <div className={"flex flex-col gap-2"}>
119
- {customizationController.plugins.filter(plugin => plugin.homePage?.additionalChildrenStart)
120
- .map((plugin, i) => {
121
- return <div key={`plugin_children_start_${i}`}>{plugin.homePage!.additionalChildrenStart}</div>;
122
- })}
123
- </div>;
124
-
125
- additionalPluginChildrenEnd = <div className={"flex flex-col gap-2"}>
126
- {customizationController.plugins.filter(plugin => plugin.homePage?.additionalChildrenEnd)
127
- .map((plugin, i) => {
128
- return <div key={`plugin_children_start_${i}`}>{plugin.homePage!.additionalChildrenEnd}</div>;
129
- })}
130
- </div>;
257
+ const sectionProps: PluginGenericProps = { context };
258
+
259
+ additionalPluginSections = (
260
+ <>
261
+ {customizationController.plugins
262
+ .filter((p) => p.homePage?.includeSection)
263
+ .map((plugin) => {
264
+ const section = plugin.homePage!.includeSection!(
265
+ sectionProps
266
+ );
267
+ return (
268
+ <NavigationGroup
269
+ group={section.title}
270
+ key={`plugin_section_${plugin.key}`}
271
+ >
272
+ {section.children}
273
+ </NavigationGroup>
274
+ );
275
+ })}
276
+ </>
277
+ );
278
+
279
+ additionalPluginChildrenStart = (
280
+ <div className="flex flex-col gap-2">
281
+ {customizationController.plugins
282
+ .filter((p) => p.homePage?.additionalChildrenStart)
283
+ .map((plugin, i) => (
284
+ <div key={`plugin_children_start_${i}`}>
285
+ {plugin.homePage!.additionalChildrenStart}
286
+ </div>
287
+ ))}
288
+ </div>
289
+ );
290
+
291
+ additionalPluginChildrenEnd = (
292
+ <div className="flex flex-col gap-2">
293
+ {customizationController.plugins
294
+ .filter((p) => p.homePage?.additionalChildrenEnd)
295
+ .map((plugin, i) => (
296
+ <div key={`plugin_children_end_${i}`}>
297
+ {plugin.homePage!.additionalChildrenEnd}
298
+ </div>
299
+ ))}
300
+ </div>
301
+ );
131
302
  }
132
303
 
304
+ /* ───────────────────────────────────────────────────────────────
305
+ Render
306
+ ─────────���───────────────────────────────────────────────────── */
133
307
  return (
134
- <div id="home_page"
135
- ref={containerRef}
136
- className="py-2 overflow-auto h-full w-full">
137
- <Container maxWidth={"6xl"}>
308
+ <div ref={containerRef} className="py-2 overflow-auto h-full w-full">
309
+ <Container maxWidth="6xl">
310
+ {/* search & actions */}
138
311
  <div
139
312
  className="w-full sticky py-4 transition-all duration-400 ease-in-out top-0 z-10 flex flex-row gap-4"
140
- style={{ top: direction === "down" ? -84 : 0 }}>
141
- <SearchBar onTextSearch={updateSearchResults}
142
- placeholder={"Search collections"}
143
- large={false}
144
- autoFocus={true}
145
- innerClassName={"w-full"}
146
- className={"w-full flex-grow"}/>
313
+ style={{ top: direction === "down" ? -84 : 0 }}
314
+ >
315
+ <SearchBar
316
+ onTextSearch={updateSearch}
317
+ placeholder="Search collections"
318
+ autoFocus
319
+ innerClassName="w-full"
320
+ className="w-full flex-grow"
321
+ />
147
322
  {additionalActions}
148
323
  </div>
149
324
 
150
325
  <FavouritesView hidden={performingSearch}/>
151
326
 
152
327
  {additionalChildrenStart}
153
-
154
328
  {additionalPluginChildrenStart}
155
329
 
156
- {allGroups.map((group, index) => {
157
-
158
- const AdditionalCards: React.ComponentType<PluginHomePageAdditionalCardsProps>[] = [];
159
- const actionProps: PluginHomePageAdditionalCardsProps = {
160
- group,
161
- context
162
- };
163
-
164
- if (customizationController.plugins) {
165
- customizationController.plugins.forEach(plugin => {
166
- if (plugin.homePage?.AdditionalCards) {
167
- AdditionalCards.push(...toArray(plugin.homePage?.AdditionalCards));
168
- }
169
- });
170
- }
171
-
172
- const thisGroupCollections = filteredNavigationEntries
173
- .filter((entry) => entry.group === group || (!entry.group && group === undefined));
174
- if (thisGroupCollections.length === 0 && (AdditionalCards.length === 0 || performingSearch))
175
- return null;
176
- return (
177
- <NavigationGroup
178
- group={group}
179
- key={`plugin_section_${group}`}>
180
-
181
- <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
182
- {thisGroupCollections.map((entry) => (
183
- <div key={`nav_${entry.group}_${entry.path}_${entry.name}`} className="col-span-1">
184
- <NavigationCardBinding
185
- {...entry}
186
- onClick={() => {
187
- let event: CMSAnalyticsEvent;
188
- if (entry.type === "collection") {
189
- event = "home_navigate_to_collection";
190
- } else if (entry.type === "view") {
191
- event = "home_navigate_to_view";
192
- } else if (entry.type === "admin") {
193
- event = "home_navigate_to_admin_view";
194
- } else {
195
- event = "unmapped_event";
196
- }
197
- context.analyticsController?.onAnalyticsEvent?.(event, { path: entry.path });
198
- }}
199
- />
200
- </div>
201
- ))}
202
- {group?.toLowerCase() !== "admin" && AdditionalCards &&
203
- AdditionalCards.map((AdditionalCard, i) => (
204
- <div key={`nav_${group}_add_${i}`}>
205
- <AdditionalCard {...actionProps} />
206
- </div>
207
- ))}
330
+ {/* ─────── DND context ─────── */}
331
+ <DndContext
332
+ sensors={sensors}
333
+ collisionDetection={collisionDetection}
334
+ measuring={{
335
+ droppable: {
336
+ strategy: MeasuringStrategy.Always,
337
+ frequency: 500
338
+ }
339
+ }}
340
+ onDragStart={onDragStart}
341
+ onDragOver={onDragOver}
342
+ onDragEnd={onDragEnd}
343
+ onDragCancel={onDragCancel}
344
+ modifiers={dndModifiers}
345
+ >
346
+ <SortableContext
347
+ key={JSON.stringify(containers)}
348
+ items={containers}
349
+ strategy={verticalListSortingStrategy}
350
+ >
351
+ {items.map((groupData) => {
352
+ const groupKey = groupData.name;
353
+ const entriesInGroup = groupData.entries;
354
+
355
+ const AdditionalCards: React.ComponentType<PluginHomePageAdditionalCardsProps>[] =
356
+ [];
357
+ customizationController.plugins?.forEach((p) => {
358
+ if (p.homePage?.AdditionalCards)
359
+ AdditionalCards.push(
360
+ ...toArray(p.homePage.AdditionalCards)
361
+ );
362
+ });
363
+
364
+ const actionProps: PluginHomePageAdditionalCardsProps = {
365
+ group:
366
+ groupKey === DEFAULT_GROUP_NAME
367
+ ? undefined
368
+ : groupKey,
369
+ context
370
+ };
371
+
372
+ if (
373
+ entriesInGroup.length === 0 &&
374
+ (AdditionalCards.length === 0 || performingSearch) &&
375
+ !groupOrderFromNavController.includes(groupKey)
376
+ )
377
+ return null;
378
+
379
+ return (
380
+ <SortableNavigationGroup
381
+ key={groupKey}
382
+ groupName={groupKey}
383
+ disabled={dndDisabled}
384
+ >
385
+ <NavigationGroup
386
+ group={
387
+ groupKey === DEFAULT_GROUP_NAME
388
+ ? undefined
389
+ : groupKey
390
+ }
391
+ minimised={
392
+ draggingGroupId === groupKey &&
393
+ !isDraggingCardOnly
394
+ }
395
+ isPotentialCardDropTarget={
396
+ isDraggingCardOnly
397
+ }
398
+ dndDisabled={dndDisabled}
399
+ onEditGroup={() => {
400
+ if (dndDisabled) return;
401
+ setDialogOpenForGroup(groupKey);
402
+ }}
403
+ >
404
+ <NavigationGroupDroppable
405
+ id={groupKey}
406
+ itemIds={entriesInGroup.map(
407
+ (e) => e.url
408
+ )}
409
+ isPotentialCardDropTarget={
410
+ isDraggingCardOnly
411
+ }
412
+ >
413
+ <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 ">
414
+ {entriesInGroup.map(
415
+ (entry) => (
416
+ <SortableNavigationCard
417
+ key={entry.url}
418
+ entry={entry}
419
+ onClick={() => {
420
+ let event: CMSAnalyticsEvent =
421
+ "unmapped_event";
422
+ if (
423
+ entry.type ===
424
+ "collection"
425
+ )
426
+ event =
427
+ "home_navigate_to_collection";
428
+ else if (
429
+ entry.type ===
430
+ "view"
431
+ )
432
+ event =
433
+ "home_navigate_to_view";
434
+ else if (
435
+ entry.type ===
436
+ "admin"
437
+ )
438
+ event =
439
+ "home_navigate_to_admin_view";
440
+
441
+ context.analyticsController?.onAnalyticsEvent?.(
442
+ event,
443
+ {
444
+ path: entry.path
445
+ }
446
+ );
447
+ }}
448
+ />
449
+ )
450
+ )}
451
+ {!performingSearch &&
452
+ groupKey.toLowerCase() !==
453
+ ADMIN_GROUP_NAME.toLowerCase() &&
454
+ AdditionalCards.map(
455
+ (C, i) => (
456
+ <C
457
+ key={`extra_${groupKey}_${i}`}
458
+ {...actionProps}
459
+ />
460
+ )
461
+ )}
462
+ </div>
463
+ </NavigationGroupDroppable>
464
+ </NavigationGroup>
465
+ </SortableNavigationGroup>
466
+ );
467
+ })}
468
+ </SortableContext>
469
+
470
+ <NewGroupDropZone
471
+ disabled={dndDisabled}
472
+ setIsHovering={setIsHoveringNewGroupDropZone}
473
+ />
474
+
475
+ <DragOverlay adjustScale={false} dropAnimation={dropAnimation}>
476
+ {activeGroupData &&
477
+ draggingGroupId === activeGroupData.name ? (
478
+ <div
479
+ className="rounded-lg bg-transparent"
480
+ style={{
481
+ padding: 0,
482
+ margin: 0
483
+ }}
484
+ >
485
+ <NavigationGroup
486
+ group={
487
+ activeGroupData.name ===
488
+ DEFAULT_GROUP_NAME
489
+ ? undefined
490
+ : activeGroupData.name
491
+ }
492
+ isPreview={false}
493
+ minimised
494
+ />
208
495
  </div>
209
- </NavigationGroup>
210
- );
211
- })}
496
+ ) : activeItemForOverlay ? (
497
+ <NavigationCardBinding
498
+ {...activeItemForOverlay}
499
+ shrink={isHoveringNewGroupDropZone}
500
+ />
501
+ ) : null}
502
+ </DragOverlay>
503
+ </DndContext>
504
+
505
+ {!performingSearch && adminGroupData && (
506
+ <NavigationGroup group={adminGroupData.name}>
507
+ <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 ">
508
+ {adminGroupData.entries.map((entry) => (
509
+ <NavigationCardBinding
510
+ key={entry.url}
511
+ {...entry}
512
+ onClick={() => {
513
+ let event: CMSAnalyticsEvent =
514
+ "unmapped_event";
515
+ if (entry.type === "collection")
516
+ event =
517
+ "home_navigate_to_collection";
518
+ else if (entry.type === "view")
519
+ event = "home_navigate_to_view";
520
+ else if (entry.type === "admin")
521
+ event =
522
+ "home_navigate_to_admin_view";
523
+
524
+ context.analyticsController?.onAnalyticsEvent?.(
525
+ event,
526
+ { path: entry.path }
527
+ );
528
+ }}
529
+ />
530
+ ))}
531
+ </div>
532
+ </NavigationGroup>
533
+ )}
212
534
 
213
535
  {additionalPluginSections}
214
-
215
536
  {additionalPluginChildrenEnd}
216
-
217
537
  {additionalChildrenEnd}
218
-
219
538
  </Container>
539
+
540
+ {dialogOpenForGroup && (
541
+ <RenameGroupDialog
542
+ open
543
+ initialName={dialogOpenForGroup}
544
+ existingGroupNames={items
545
+ .map((g) => g.name)
546
+ .filter((n) => n !== dialogOpenForGroup)}
547
+ onClose={() => setDialogOpenForGroup(null)}
548
+ onRename={(newName) => {
549
+ handleRenameGroup(dialogOpenForGroup, newName);
550
+ setDialogOpenForGroup(null);
551
+ }}
552
+ />
553
+ )}
220
554
  </div>
221
555
  );
222
556
  }