@firecms/core 3.0.0-rc.2 → 3.0.0-rc.3

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 (46) hide show
  1. package/dist/components/HomePage/HomePageDnD.d.ts +2 -1
  2. package/dist/components/PropertyCollectionView.d.ts +23 -0
  3. package/dist/core/EntityEditView.d.ts +10 -4
  4. package/dist/form/EntityForm.d.ts +5 -2
  5. package/dist/form/components/LocalChangesMenu.d.ts +11 -0
  6. package/dist/form/index.d.ts +2 -1
  7. package/dist/index.es.js +1288 -364
  8. package/dist/index.es.js.map +1 -1
  9. package/dist/index.umd.js +1287 -363
  10. package/dist/index.umd.js.map +1 -1
  11. package/dist/types/collections.d.ts +11 -0
  12. package/dist/types/properties.d.ts +32 -6
  13. package/dist/util/collections.d.ts +1 -0
  14. package/dist/util/entity_cache.d.ts +6 -1
  15. package/dist/util/make_properties_editable.d.ts +1 -2
  16. package/dist/util/objects.d.ts +1 -0
  17. package/dist/util/useStorageUploadController.d.ts +1 -0
  18. package/package.json +6 -6
  19. package/src/components/EntityCollectionTable/EntityCollectionRowActions.tsx +47 -47
  20. package/src/components/EntityCollectionView/EntityCollectionView.tsx +6 -1
  21. package/src/components/EntityView.tsx +29 -40
  22. package/src/components/HomePage/DefaultHomePage.tsx +13 -9
  23. package/src/components/HomePage/HomePageDnD.tsx +140 -38
  24. package/src/components/PropertyCollectionView.tsx +329 -0
  25. package/src/components/SelectableTable/filters/DateTimeFilterField.tsx +2 -1
  26. package/src/components/SelectableTable/filters/StringNumberFilterField.tsx +0 -1
  27. package/src/core/EntityEditView.tsx +27 -14
  28. package/src/core/EntityEditViewFormActions.tsx +33 -18
  29. package/src/core/EntitySidePanel.tsx +9 -3
  30. package/src/form/EntityForm.tsx +173 -42
  31. package/src/form/EntityFormActions.tsx +30 -15
  32. package/src/form/components/ErrorFocus.tsx +22 -29
  33. package/src/form/components/LocalChangesMenu.tsx +144 -0
  34. package/src/form/index.tsx +5 -1
  35. package/src/hooks/useBuildNavigationController.tsx +104 -31
  36. package/src/preview/property_previews/MapPropertyPreview.tsx +2 -2
  37. package/src/preview/property_previews/NumberPropertyPreview.tsx +2 -2
  38. package/src/types/collections.ts +12 -0
  39. package/src/types/properties.ts +35 -6
  40. package/src/util/collections.ts +8 -0
  41. package/src/util/createFormexStub.tsx +4 -0
  42. package/src/util/entity_cache.ts +71 -52
  43. package/src/util/join_collections.ts +3 -3
  44. package/src/util/make_properties_editable.ts +0 -22
  45. package/src/util/objects.ts +40 -2
  46. package/src/util/useStorageUploadController.tsx +71 -34
@@ -307,6 +307,17 @@ export interface EntityCollection<M extends Record<string, any> = any, USER exte
307
307
  * This prop has no effect if the history plugin is not enabled
308
308
  */
309
309
  history?: boolean;
310
+ /**
311
+ * Should local changes be backed up in local storage, to prevent data loss on
312
+ * accidental navigations.
313
+ * - `manual_apply`: When the user navigates back to an entity with local changes,
314
+ * they will be prompted to restore the changes.
315
+ * - `auto_apply`: When the user navigates back to an entity with local changes,
316
+ * the changes will be automatically applied.
317
+ * - `false`: Local changes will not be backed up.
318
+ * Defaults to `manual_apply`.
319
+ */
320
+ localChangesBackup?: "manual_apply" | "auto_apply" | false;
310
321
  }
311
322
  /**
312
323
  * Parameter passed to the `Actions` prop in the collection configuration.
@@ -102,6 +102,10 @@ export interface BaseProperty<T extends CMSType, CustomProps = any> {
102
102
  /**
103
103
  * Should this property be editable. If set to true, the user will be able to modify the property and
104
104
  * save the new config. The saved config will then become the source of truth.
105
+ * Defaults to `true.
106
+ * This props is only useful when you are using the collection editor to modify collection
107
+ * configurations from the CMS itself. You can also use the `editable` prop in the
108
+ * `EntityCollection` interface to disable the edition of all properties in a collection.
105
109
  */
106
110
  editable?: boolean;
107
111
  /**
@@ -617,8 +621,15 @@ export type StorageConfig = {
617
621
  /**
618
622
  * Use client side image compression and resizing
619
623
  * Will only be applied to these MIME types: image/jpeg, image/png and image/webp
624
+ * @deprecated Use `imageResize` instead
620
625
  */
621
- imageCompression?: ImageCompression;
626
+ imageCompression?: ImageResize;
627
+ /**
628
+ * Advanced image resizing and cropping configuration.
629
+ * Applied before upload to optimize storage and bandwidth.
630
+ * Only applies to image MIME types: image/jpeg, image/png, image/webp
631
+ */
632
+ imageResize?: ImageResize;
622
633
  /**
623
634
  * Specific metadata set in your uploaded file.
624
635
  * For the default Firebase implementation, the values passed here are of type
@@ -730,17 +741,32 @@ export type PreviewType = "image" | "video" | "audio" | "file";
730
741
  * @group Entity properties
731
742
  */
732
743
  export type FileType = "image/*" | "video/*" | "audio/*" | "application/*" | "text/*" | "font/*" | string;
733
- export interface ImageCompression {
744
+ export interface ImageResize {
745
+ /**
746
+ * Maximum width in pixels. Image will be scaled down proportionally if wider.
747
+ */
748
+ maxWidth?: number;
734
749
  /**
735
- * New image max height (ratio is preserved)
750
+ * Maximum height in pixels. Image will be scaled down proportionally if taller.
736
751
  */
737
752
  maxHeight?: number;
738
753
  /**
739
- * New image max width (ratio is preserved)
754
+ * Resize mode determines how the image fits within maxWidth/maxHeight bounds.
755
+ * - `contain`: Scale down to fit within bounds, preserving aspect ratio (default)
756
+ * - `cover`: Scale to fill bounds, preserving aspect ratio (may crop)
740
757
  */
741
- maxWidth?: number;
758
+ mode?: 'contain' | 'cover';
759
+ /**
760
+ * Output format for the resized image.
761
+ * - `original`: Keep the original format (default)
762
+ * - `jpeg`: Convert to JPEG
763
+ * - `png`: Convert to PNG
764
+ * - `webp`: Convert to WebP
765
+ */
766
+ format?: 'original' | 'jpeg' | 'png' | 'webp';
742
767
  /**
743
- * A number between 0 and 100. Used for the JPEG compression.(if no compress is needed, just set it to 100)
768
+ * Quality for lossy formats (JPEG, WebP). Number between 0 and 100.
769
+ * Higher is better quality but larger file size. Defaults to 80.
744
770
  */
745
771
  quality?: number;
746
772
  }
@@ -9,3 +9,4 @@ export declare function resolveDefaultSelectedView(defaultSelectedView: string |
9
9
  * @param permissionsBuilder
10
10
  */
11
11
  export declare const applyPermissionsFunctionIfEmpty: (collections: EntityCollection[], permissionsBuilder?: PermissionsBuilder<any, any>) => EntityCollection[];
12
+ export declare function getLocalChangesBackup(collection: EntityCollection): "manual_apply" | "auto_apply";
@@ -4,14 +4,18 @@
4
4
  * @param data - The data to cache and persist.
5
5
  */
6
6
  export declare function saveEntityToCache(path: string, data: object): void;
7
+ export declare function removeEntityFromMemoryCache(path: string): void;
8
+ export declare function saveEntityToMemoryCache(path: string, data: object): void;
9
+ export declare function getEntityFromMemoryCache(path: string): object | undefined;
10
+ export declare function hasEntityInCache(path: string): boolean;
7
11
  /**
8
12
  * Retrieves an entity from the in-memory cache or `localStorage`.
9
13
  * If the entity is not in the cache but exists in `localStorage`, it loads it into the cache.
10
14
  * @param path - The unique path/key for the entity.
15
+ * @param useLocalStorage
11
16
  * @returns The cached entity or `undefined` if not found.
12
17
  */
13
18
  export declare function getEntityFromCache(path: string): object | undefined;
14
- export declare function hasEntityInCache(path: string): boolean;
15
19
  /**
16
20
  * Removes an entity from both the in-memory cache and `localStorage`.
17
21
  * @param path - The unique path/key for the entity to remove.
@@ -21,3 +25,4 @@ export declare function removeEntityFromCache(path: string): void;
21
25
  * Clears the entire in-memory cache and removes all related entities from `localStorage`.
22
26
  */
23
27
  export declare function clearEntityCache(): void;
28
+ export declare function flattenKeys(obj: any, prefix?: string, result?: string[]): string[];
@@ -1,3 +1,2 @@
1
- import { Properties, PropertiesOrBuilders } from "../types";
1
+ import { Properties } from "../types";
2
2
  export declare function makePropertiesEditable(properties: Properties): Properties<any>;
3
- export declare function makePropertiesNonEditable(properties: PropertiesOrBuilders): PropertiesOrBuilders;
@@ -1,5 +1,6 @@
1
1
  export declare const pick: <T>(obj: T, ...args: any[]) => T;
2
2
  export declare function isObject(item: any): any;
3
+ export declare function isPlainObject(obj: any): boolean;
3
4
  export declare function mergeDeep<T extends Record<any, any>, U extends Record<any, any>>(target: T, source: U, ignoreUndefined?: boolean): T & U;
4
5
  export declare function getValueInPath(o: object | undefined, path: string): any;
5
6
  export declare function removeInPath(o: object, path: string): object | undefined;
@@ -31,6 +31,7 @@ export declare function useStorageUploadController<M extends object>({ entityId,
31
31
  fileNameBuilder: (file: File) => Promise<string>;
32
32
  storagePathBuilder: (file: File) => string;
33
33
  onFileUploadComplete: (uploadedPath: string, entry: StorageFieldItem, metadata?: any) => Promise<void>;
34
+ onFileUploadError: (entry: StorageFieldItem) => void;
34
35
  onFilesAdded: (acceptedFiles: File[]) => Promise<void>;
35
36
  multipleFilesSupported: boolean;
36
37
  };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@firecms/core",
3
3
  "type": "module",
4
- "version": "3.0.0-rc.2",
4
+ "version": "3.0.0-rc.3",
5
5
  "description": "Awesome Firebase/Firestore-based headless open-source CMS",
6
6
  "funding": {
7
7
  "url": "https://github.com/sponsors/firecmsco"
@@ -53,11 +53,12 @@
53
53
  "@dnd-kit/core": "^6.3.1",
54
54
  "@dnd-kit/modifiers": "^9.0.0",
55
55
  "@dnd-kit/sortable": "^10.0.0",
56
- "@firecms/editor": "^3.0.0-rc.2",
57
- "@firecms/formex": "^3.0.0-rc.2",
58
- "@firecms/ui": "^3.0.0-rc.2",
56
+ "@firecms/editor": "^3.0.0-rc.3",
57
+ "@firecms/formex": "^3.0.0-rc.3",
58
+ "@firecms/ui": "^3.0.0-rc.3",
59
59
  "@radix-ui/react-portal": "^1.1.9",
60
60
  "clsx": "^2.1.1",
61
+ "compressorjs": "^1.2.1",
61
62
  "date-fns": "^3.6.0",
62
63
  "fuse.js": "^7.1.0",
63
64
  "history": "^5.3.0",
@@ -67,7 +68,6 @@
67
68
  "prism-react-renderer": "^2.4.1",
68
69
  "react-dropzone": "^14.3.8",
69
70
  "react-fast-compare": "^3.2.2",
70
- "react-image-file-resizer": "^0.4.8",
71
71
  "react-transition-group": "^4.4.5",
72
72
  "react-use-measure": "^2.1.7",
73
73
  "react-window": "^1.8.11",
@@ -108,7 +108,7 @@
108
108
  "dist",
109
109
  "src"
110
110
  ],
111
- "gitHead": "6295de2d801a3d2cddaa958fd66cb041dd2c240e",
111
+ "gitHead": "213f4c106eb3e6639eb1ad23c7d94282ef54d444",
112
112
  "publishConfig": {
113
113
  "access": "public"
114
114
  },
@@ -1,20 +1,10 @@
1
1
  import React, { MouseEvent, useCallback } from "react";
2
2
 
3
3
  import { CollectionSize, Entity, EntityAction, EntityCollection, SelectionController } from "../../types";
4
- import {
5
- Checkbox,
6
- Chip,
7
- cls,
8
- EditIcon,
9
- IconButton,
10
- Menu,
11
- MenuItem,
12
- MoreVertIcon,
13
- Skeleton,
14
- Tooltip
15
- } from "@firecms/ui";
4
+ import { Badge, Checkbox, cls, IconButton, Menu, MenuItem, MoreVertIcon, Skeleton, Tooltip } from "@firecms/ui";
16
5
  import { useFireCMSContext, useLargeLayout } from "../../hooks";
17
- import { hasEntityInCache } from "../../util/entity_cache";
6
+ import { getEntityFromCache } from "../../util/entity_cache";
7
+ import { getLocalChangesBackup } from "../../util";
18
8
 
19
9
  /**
20
10
  *
@@ -79,7 +69,9 @@ export const EntityCollectionRowActions = function EntityCollectionRowActions({
79
69
 
80
70
  const collapsedActions = actions.filter(a => a.collapsed || a.collapsed === undefined);
81
71
  const uncollapsedActions = actions.filter(a => a.collapsed === false);
82
- const hasDraft = hasEntityInCache(fullPath + "/" + entity.id);
72
+ const enableLocalChangesBackup = collection ? getLocalChangesBackup(collection) : false;
73
+ const hasDraft = enableLocalChangesBackup ? getEntityFromCache(fullPath + "/" + entity.id) : false;
74
+ const iconSize = largeLayout && (size === "m" || size === "l" || size == "xl") ? "medium" : "small";
83
75
  return (
84
76
  <div
85
77
  className={cls(
@@ -99,37 +91,50 @@ export const EntityCollectionRowActions = function EntityCollectionRowActions({
99
91
  {(hasActions || selectionEnabled) &&
100
92
  <div className="w-34 flex justify-center">
101
93
 
102
- {uncollapsedActions.map((action, index) => (
103
- <Tooltip key={index}
104
- title={action.name}
105
- asChild={true}>
106
- <IconButton
107
- onClick={(event: MouseEvent) => {
108
- event.stopPropagation();
109
- action.onClick({
110
- view: "collection",
111
- entity,
112
- fullPath,
113
- fullIdPath,
114
- collection,
115
- context,
116
- selectionController,
117
- highlightEntity,
118
- unhighlightEntity,
119
- onCollectionChange,
120
- openEntityMode: openEntityMode ?? collection?.openEntityMode
121
- });
122
- }}
123
- size={largeLayout ? "medium" : "small"}>
124
- {action.icon}
125
- </IconButton>
126
- </Tooltip>
127
- ))}
94
+ {uncollapsedActions.map((action, index) => {
95
+ const isEditAction = action.key === "edit";
96
+ const tooltip = isEditAction && hasDraft ? "Local unsaved changes" : action.name;
97
+
98
+ let iconButton = <IconButton
99
+ onClick={(event: MouseEvent) => {
100
+ event.stopPropagation();
101
+ action.onClick({
102
+ view: "collection",
103
+ entity,
104
+ fullPath,
105
+ fullIdPath,
106
+ collection,
107
+ context,
108
+ selectionController,
109
+ highlightEntity,
110
+ unhighlightEntity,
111
+ onCollectionChange,
112
+ openEntityMode: openEntityMode ?? collection?.openEntityMode
113
+ });
114
+ }}
115
+ size={iconSize}>
116
+ {action.icon}
117
+ </IconButton>;
118
+ if (isEditAction && hasDraft) {
119
+ iconButton = (
120
+ <Badge color={"warning"}>
121
+ {iconButton}
122
+ </Badge>
123
+ );
124
+ }
125
+ return (
126
+ <Tooltip key={index}
127
+ title={tooltip}
128
+ asChild={true}>
129
+ {iconButton}
130
+ </Tooltip>
131
+ );
132
+ })}
128
133
 
129
134
  {hasCollapsedActions &&
130
135
  <Menu
131
136
  trigger={<IconButton
132
- size={largeLayout ? "medium" : "small"}>
137
+ size={iconSize}>
133
138
  <MoreVertIcon/>
134
139
  </IconButton>}>
135
140
  {collapsedActions.map((action, index) => (
@@ -161,7 +166,7 @@ export const EntityCollectionRowActions = function EntityCollectionRowActions({
161
166
  {selectionEnabled &&
162
167
  <Tooltip title={`Select ${entity.id}`}>
163
168
  <Checkbox
164
- size={largeLayout ? "medium" : "small"}
169
+ size={iconSize}
165
170
  checked={Boolean(isSelected)}
166
171
  onCheckedChange={onCheckedChange}
167
172
  />
@@ -175,11 +180,6 @@ export const EntityCollectionRowActions = function EntityCollectionRowActions({
175
180
  onClick={(event) => {
176
181
  event.stopPropagation();
177
182
  }}>
178
- {hasDraft && <Tooltip title={"Local unsaved changes"} className={"inline"}>
179
- <Chip colorScheme={"orangeDarker"} className={"p-0.5"}>
180
- <EditIcon size={12}/>
181
- </Chip>
182
- </Tooltip>}
183
183
  <span className="min-w-0 truncate text-center">
184
184
  {entity
185
185
  ? entity.id
@@ -376,6 +376,11 @@ export const EntityCollectionView = React.memo(
376
376
  console.error("Save failure");
377
377
  console.error(e);
378
378
  setError(e);
379
+ },
380
+ onPreSaveHookError: (e: Error) => {
381
+ console.error("Pre-save hook error");
382
+ console.error(e);
383
+ setError(e);
379
384
  }
380
385
  });
381
386
 
@@ -694,7 +699,7 @@ export const EntityCollectionView = React.memo(
694
699
  className="mt-4"
695
700
  >
696
701
  <AddIcon/>
697
- Create your first entity
702
+ Create your first entry
698
703
  </Button>
699
704
  </div>
700
705
  : <Typography variant={"label"}>No results with the applied filter/sort</Typography>
@@ -1,11 +1,11 @@
1
1
  import React, { useMemo } from "react";
2
- import { PropertyPreview } from "../preview";
3
2
  import { Entity, EntityCollection, ResolvedEntityCollection, ResolvedProperties } from "../types";
4
3
  import { resolveCollection } from "../util";
5
- import { cls, defaultBorderMixin, IconButton, OpenInNewIcon } from "@firecms/ui";
4
+ import { cls, defaultBorderMixin, IconButton, OpenInNewIcon, Typography } from "@firecms/ui";
6
5
  import { CustomizationController } from "../types/customization_controller";
7
6
  import { useCustomizationController } from "../hooks/useCustomizationController";
8
7
  import { useAuthController } from "../hooks";
8
+ import { PropertyCollectionView } from "./PropertyCollectionView";
9
9
 
10
10
  /**
11
11
  * @group Components
@@ -40,47 +40,36 @@ export function EntityView<M extends Record<string, any>>(
40
40
 
41
41
  return (
42
42
  <div className={"w-full " + className}>
43
- <div className={"w-full mb-4"}>
44
- <div className={cls(defaultBorderMixin, "flex justify-between py-2 border-b last:border-b-0")}>
45
- <div className="flex items-center w-1/4">
46
- <span className="pl-2 text-sm text-surface-600">Id</span>
43
+ <div className={"w-full mb-4 p-4"}>
44
+
45
+ <div className={`grid grid-cols-12 gap-x-4 py-4 items-start border-b ${defaultBorderMixin}`}>
46
+ <div className="col-span-4 pr-2">
47
+ <Typography variant="caption"
48
+ color={"secondary"}
49
+ component={"span"}
50
+ className="break-words">
51
+ Id
52
+ </Typography>
47
53
  </div>
48
- <div
49
- className="flex-grow p-2 ml-2 w-3/4 text-surface-900 dark:text-white min-h-[56px] flex items-center">
50
- <span className="flex-grow mr-2">{entity.id}</span>
51
- {customizationController?.entityLinkBuilder &&
52
- <a href={customizationController.entityLinkBuilder({ entity })}
53
- rel="noopener noreferrer"
54
- target="_blank">
55
- <IconButton>
56
- <OpenInNewIcon
57
- size={"small"}/>
58
- </IconButton>
59
- </a>}
54
+ <div className="col-span-8">
55
+ <div
56
+ className="flex-grow text-surface-900 dark:text-white flex items-center">
57
+ <span className="flex-grow mr-2">{entity.id}</span>
58
+ {customizationController?.entityLinkBuilder &&
59
+ <a href={customizationController.entityLinkBuilder({ entity })}
60
+ rel="noopener noreferrer"
61
+ target="_blank">
62
+ <IconButton>
63
+ <OpenInNewIcon
64
+ size={"small"}/>
65
+ </IconButton>
66
+ </a>}
67
+ </div>
60
68
  </div>
61
69
  </div>
62
- {Object.entries(properties)
63
- .map(([key, property]) => {
64
- const value = entity.values?.[key];
65
- return (
66
- <div
67
- key={`reference_previews_${key}`}
68
- className={cls(defaultBorderMixin, "flex justify-between py-2 border-b last:border-b-0")}>
69
- <div className="flex items-center w-1/4">
70
- <span className="pl-2 text-sm text-surface-600">{property.name}</span>
71
- </div>
72
- <div
73
- className="flex-grow p-2 ml-2 w-3/4 text-surface-900 dark:text-white min-h-[56px] flex items-center">
74
- <PropertyPreview
75
- propertyKey={key}
76
- value={value}
77
- // entity={entity}
78
- property={property}
79
- size={"medium"}/>
80
- </div>
81
- </div>
82
- )
83
- })}
70
+
71
+ <PropertyCollectionView data={entity.values} properties={properties} size={"medium"}/>
72
+
84
73
  </div>
85
74
  </div>
86
75
  );
@@ -1,8 +1,12 @@
1
1
  import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
2
2
  import Fuse from "fuse.js";
3
3
  import { Container, SearchBar } from "@firecms/ui";
4
- import { useCustomizationController, useFireCMSContext, useNavigationController } from "../../hooks";
5
- import { useCollapsedGroups } from "../../hooks/useCollapsedGroups";
4
+ import {
5
+ useCollapsedGroups,
6
+ useCustomizationController,
7
+ useFireCMSContext,
8
+ useNavigationController
9
+ } from "../../hooks";
6
10
  import {
7
11
  CMSAnalyticsEvent,
8
12
  NavigationEntry,
@@ -143,7 +147,6 @@ export function DefaultHomePage({
143
147
  allProcessed = allProcessed.filter(
144
148
  (g) =>
145
149
  g.entries.length ||
146
- groupOrderFromNavController.includes(g.name) ||
147
150
  (g.name === DEFAULT_GROUP_NAME && hasPluginAdditionalCards)
148
151
  );
149
152
  }
@@ -177,6 +180,7 @@ export function DefaultHomePage({
177
180
  const persistNavigationGroups = (
178
181
  latest: { name: string; entries: NavigationEntry[] }[]
179
182
  ) => {
183
+ // Map ALL groups including "Views"
180
184
  const draggable: NavigationGroupMapping[] = latest.map((g) => ({
181
185
  name: g.name,
182
186
  entries: g.entries.map((e) => e.path)
@@ -221,13 +225,14 @@ export function DefaultHomePage({
221
225
  dialogOpenForGroup,
222
226
  setDialogOpenForGroup,
223
227
  handleRenameGroup,
228
+ handleDialogClose,
224
229
  isHoveringNewGroupDropZone,
225
230
  setIsHoveringNewGroupDropZone
226
231
  } = useHomePageDnd({
227
232
  items,
228
233
  setItems: updateItems,
229
234
  disabled: !allowDragAndDrop || performingSearch,
230
- onPersist: persistNavigationGroups, // ——► persistence here
235
+ onPersist: persistNavigationGroups,
231
236
  onGroupMoved: (g) =>
232
237
  context.analyticsController?.onAnalyticsEvent?.("home_move_group", {
233
238
  name: g
@@ -355,7 +360,7 @@ export function DefaultHomePage({
355
360
  items={containers}
356
361
  strategy={verticalListSortingStrategy}
357
362
  >
358
- {items.map((groupData) => {
363
+ {items.map((groupData, groupIndex) => {
359
364
  const groupKey = groupData.name;
360
365
  const entriesInGroup = groupData.entries;
361
366
 
@@ -378,14 +383,13 @@ export function DefaultHomePage({
378
383
 
379
384
  if (
380
385
  entriesInGroup.length === 0 &&
381
- (AdditionalCards.length === 0 || performingSearch) &&
382
- !groupOrderFromNavController.includes(groupKey)
386
+ (AdditionalCards.length === 0 || performingSearch)
383
387
  )
384
388
  return null;
385
389
 
386
390
  return (
387
391
  <SortableNavigationGroup
388
- key={groupKey}
392
+ key={`group-${groupIndex}`}
389
393
  groupName={groupKey}
390
394
  disabled={dndDisabled}
391
395
  >
@@ -557,7 +561,7 @@ export function DefaultHomePage({
557
561
  existingGroupNames={items
558
562
  .map((g) => g.name)
559
563
  .filter((n) => n !== dialogOpenForGroup)}
560
- onClose={() => setDialogOpenForGroup(null)}
564
+ onClose={handleDialogClose}
561
565
  onRename={(newName) => {
562
566
  handleRenameGroup(dialogOpenForGroup, newName);
563
567
  }}