@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,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,175 +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
+ };
96
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
+ };
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
- additionalPluginChildrenStart = <div className={"flex flex-col gap-2"}>
118
- {customizationController.plugins.filter(plugin => plugin.homePage?.additionalChildrenStart)
119
- .map((plugin, i) => {
120
- return <div key={`plugin_children_start_${i}`}>{plugin.homePage!.additionalChildrenStart}</div>;
121
- })}
122
- </div>;
123
-
124
- additionalPluginChildrenEnd = <div className={"flex flex-col gap-2"}>
125
- {customizationController.plugins.filter(plugin => plugin.homePage?.additionalChildrenEnd)
126
- .map((plugin, i) => {
127
- return <div key={`plugin_children_start_${i}`}>{plugin.homePage!.additionalChildrenEnd}</div>;
128
- })}
129
- </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
+ );
130
302
  }
131
303
 
304
+ /* ───────────────────────────────────────────────────────────────
305
+ Render
306
+ ─────────���───────────────────────────────────────────────────── */
132
307
  return (
133
- <div id="home_page"
134
- ref={containerRef}
135
- className="py-2 overflow-auto h-full w-full">
136
- <Container maxWidth={"6xl"}>
308
+ <div ref={containerRef} className="py-2 overflow-auto h-full w-full">
309
+ <Container maxWidth="6xl">
310
+ {/* search & actions */}
137
311
  <div
138
312
  className="w-full sticky py-4 transition-all duration-400 ease-in-out top-0 z-10 flex flex-row gap-4"
139
- style={{ top: direction === "down" ? -84 : 0 }}>
140
- <SearchBar onTextSearch={updateSearchResults}
141
- placeholder={"Search collections"}
142
- large={false}
143
- autoFocus={true}
144
- innerClassName={"w-full"}
145
- 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
+ />
146
322
  {additionalActions}
147
323
  </div>
148
324
 
149
325
  <FavouritesView hidden={performingSearch}/>
150
326
 
151
327
  {additionalChildrenStart}
152
-
153
328
  {additionalPluginChildrenStart}
154
329
 
155
- {allGroups.map((group, index) => {
156
-
157
- const AdditionalCards: React.ComponentType<PluginHomePageAdditionalCardsProps>[] = [];
158
- const actionProps: PluginHomePageAdditionalCardsProps = {
159
- group,
160
- context
161
- };
162
-
163
- if (customizationController.plugins) {
164
- customizationController.plugins.forEach(plugin => {
165
- if (plugin.homePage?.AdditionalCards) {
166
- AdditionalCards.push(...toArray(plugin.homePage?.AdditionalCards));
167
- }
168
- });
169
- }
170
-
171
- const thisGroupCollections = filteredNavigationEntries
172
- .filter((entry) => entry.group === group || (!entry.group && group === undefined));
173
- if (thisGroupCollections.length === 0 && (AdditionalCards.length === 0 || performingSearch))
174
- return null;
175
- return (
176
- <NavigationGroup
177
- group={group}
178
- key={`plugin_section_${group}`}>
179
-
180
- <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
181
- {thisGroupCollections.map((entry) => (
182
- <div key={`nav_${entry.group}_${entry.name}`} className="col-span-1">
183
- <NavigationCardBinding
184
- {...entry}
185
- onClick={() => {
186
- let event: CMSAnalyticsEvent;
187
- if (entry.type === "collection") {
188
- event = "home_navigate_to_collection";
189
- } else if (entry.type === "view") {
190
- event = "home_navigate_to_view";
191
- } else if (entry.type === "admin") {
192
- event = "home_navigate_to_admin_view";
193
- } else {
194
- event = "unmapped_event";
195
- }
196
- context.analyticsController?.onAnalyticsEvent?.(event, { path: entry.path });
197
- }}
198
- />
199
- </div>
200
- ))}
201
- {group?.toLowerCase() !== "admin" && AdditionalCards &&
202
- AdditionalCards.map((AdditionalCard, i) => (
203
- <div key={`nav_${group}_add_${i}`}>
204
- <AdditionalCard {...actionProps} />
205
- </div>
206
- ))}
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
+ />
207
495
  </div>
208
- </NavigationGroup>
209
- );
210
- })}
496
+ ) : activeItemForOverlay ? (
497
+ <NavigationCardBinding
498
+ {...activeItemForOverlay}
499
+ shrink={isHoveringNewGroupDropZone}
500
+ />
501
+ ) : null}
502
+ </DragOverlay>
503
+ </DndContext>
211
504
 
212
- {additionalPluginSections}
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";
213
523
 
214
- {additionalPluginChildrenEnd}
524
+ context.analyticsController?.onAnalyticsEvent?.(
525
+ event,
526
+ { path: entry.path }
527
+ );
528
+ }}
529
+ />
530
+ ))}
531
+ </div>
532
+ </NavigationGroup>
533
+ )}
215
534
 
535
+ {additionalPluginSections}
536
+ {additionalPluginChildrenEnd}
216
537
  {additionalChildrenEnd}
217
-
218
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
+ )}
219
554
  </div>
220
555
  );
221
556
  }