@firecms/core 3.0.0-rc.1 → 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 (96) hide show
  1. package/dist/components/HomePage/HomePageDnD.d.ts +2 -1
  2. package/dist/components/PropertyCollectionView.d.ts +23 -0
  3. package/dist/components/UserDisplay.d.ts +7 -0
  4. package/dist/components/VirtualTable/fields/VirtualTableUserSelect.d.ts +12 -0
  5. package/dist/contexts/InternalUserManagementContext.d.ts +3 -0
  6. package/dist/core/EntityEditView.d.ts +10 -4
  7. package/dist/core/FireCMS.d.ts +0 -1
  8. package/dist/core/field_configs.d.ts +1 -1
  9. package/dist/form/EntityForm.d.ts +5 -2
  10. package/dist/form/components/LocalChangesMenu.d.ts +11 -0
  11. package/dist/form/field_bindings/UserSelectFieldBinding.d.ts +12 -0
  12. package/dist/form/index.d.ts +2 -1
  13. package/dist/hooks/index.d.ts +2 -0
  14. package/dist/hooks/useCollapsedGroups.d.ts +9 -0
  15. package/dist/hooks/useInternalUserManagementController.d.ts +12 -0
  16. package/dist/index.es.js +1983 -650
  17. package/dist/index.es.js.map +1 -1
  18. package/dist/index.umd.js +1981 -648
  19. package/dist/index.umd.js.map +1 -1
  20. package/dist/preview/components/UserPreview.d.ts +8 -0
  21. package/dist/preview/index.d.ts +1 -0
  22. package/dist/types/collections.d.ts +13 -0
  23. package/dist/types/entities.d.ts +5 -1
  24. package/dist/types/firecms.d.ts +15 -0
  25. package/dist/types/firecms_context.d.ts +16 -0
  26. package/dist/types/index.d.ts +1 -0
  27. package/dist/types/internal_user_management.d.ts +20 -0
  28. package/dist/types/plugins.d.ts +2 -0
  29. package/dist/types/properties.d.ts +41 -6
  30. package/dist/types/property_config.d.ts +1 -1
  31. package/dist/types/user.d.ts +1 -1
  32. package/dist/util/collections.d.ts +1 -0
  33. package/dist/util/entity_cache.d.ts +6 -1
  34. package/dist/util/make_properties_editable.d.ts +1 -2
  35. package/dist/util/objects.d.ts +1 -0
  36. package/dist/util/useStorageUploadController.d.ts +1 -0
  37. package/package.json +6 -6
  38. package/src/components/EntityCollectionTable/EntityCollectionRowActions.tsx +47 -47
  39. package/src/components/EntityCollectionTable/PropertyTableCell.tsx +12 -0
  40. package/src/components/EntityCollectionView/EntityCollectionView.tsx +6 -1
  41. package/src/components/EntityView.tsx +29 -40
  42. package/src/components/ErrorView.tsx +1 -1
  43. package/src/components/HomePage/DefaultHomePage.tsx +21 -34
  44. package/src/components/HomePage/HomePageDnD.tsx +143 -83
  45. package/src/components/HomePage/RenameGroupDialog.tsx +9 -3
  46. package/src/components/PropertyCollectionView.tsx +329 -0
  47. package/src/components/PropertyConfigBadge.tsx +2 -2
  48. package/src/components/SelectableTable/filters/DateTimeFilterField.tsx +2 -1
  49. package/src/components/SelectableTable/filters/StringNumberFilterField.tsx +1 -2
  50. package/src/components/UserDisplay.tsx +55 -0
  51. package/src/components/VirtualTable/fields/VirtualTableUserSelect.tsx +99 -0
  52. package/src/components/common/useColumnsIds.tsx +1 -8
  53. package/src/contexts/InternalUserManagementContext.tsx +4 -0
  54. package/src/core/EntityEditView.tsx +27 -14
  55. package/src/core/EntityEditViewFormActions.tsx +33 -18
  56. package/src/core/EntitySidePanel.tsx +9 -3
  57. package/src/core/FireCMS.tsx +22 -13
  58. package/src/core/field_configs.tsx +15 -1
  59. package/src/form/EntityForm.tsx +173 -42
  60. package/src/form/EntityFormActions.tsx +30 -15
  61. package/src/form/PropertyFieldBinding.tsx +4 -0
  62. package/src/form/components/ErrorFocus.tsx +22 -29
  63. package/src/form/components/LocalChangesMenu.tsx +144 -0
  64. package/src/form/field_bindings/UserSelectFieldBinding.tsx +94 -0
  65. package/src/form/index.tsx +5 -1
  66. package/src/hooks/index.tsx +3 -0
  67. package/src/hooks/useBrowserTitleAndIcon.tsx +1 -1
  68. package/src/hooks/useBuildNavigationController.tsx +104 -31
  69. package/src/hooks/useCollapsedGroups.ts +64 -0
  70. package/src/hooks/useFireCMSContext.tsx +6 -2
  71. package/src/hooks/useInternalUserManagementController.tsx +16 -0
  72. package/src/preview/PropertyPreview.tsx +8 -0
  73. package/src/preview/components/ReferencePreview.tsx +4 -2
  74. package/src/preview/components/UserPreview.tsx +27 -0
  75. package/src/preview/index.ts +1 -0
  76. package/src/preview/property_previews/ArrayPropertyPreview.tsx +1 -1
  77. package/src/preview/property_previews/MapPropertyPreview.tsx +2 -2
  78. package/src/preview/property_previews/NumberPropertyPreview.tsx +2 -2
  79. package/src/types/collections.ts +14 -0
  80. package/src/types/entities.ts +7 -1
  81. package/src/types/firecms.tsx +16 -0
  82. package/src/types/firecms_context.tsx +17 -0
  83. package/src/types/index.ts +1 -0
  84. package/src/types/internal_user_management.ts +24 -0
  85. package/src/types/plugins.tsx +3 -0
  86. package/src/types/properties.ts +45 -6
  87. package/src/types/property_config.tsx +1 -0
  88. package/src/types/user.ts +1 -1
  89. package/src/util/collections.ts +8 -0
  90. package/src/util/createFormexStub.tsx +4 -0
  91. package/src/util/entities.ts +1 -1
  92. package/src/util/entity_cache.ts +72 -53
  93. package/src/util/join_collections.ts +3 -3
  94. package/src/util/make_properties_editable.ts +0 -22
  95. package/src/util/objects.ts +40 -2
  96. package/src/util/useStorageUploadController.tsx +71 -34
@@ -26,7 +26,7 @@ export function ErrorView({
26
26
  tooltip
27
27
  }: ErrorViewProps): React.ReactElement {
28
28
  const component = error instanceof Error ? error.message : error;
29
- // console.warn("ErrorView", error)
29
+ console.warn("ErrorView", JSON.stringify(error))
30
30
 
31
31
  const body = (
32
32
  <div
@@ -1,7 +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";
4
+ import {
5
+ useCollapsedGroups,
6
+ useCustomizationController,
7
+ useFireCMSContext,
8
+ useNavigationController
9
+ } from "../../hooks";
5
10
  import {
6
11
  CMSAnalyticsEvent,
7
12
  NavigationEntry,
@@ -142,7 +147,6 @@ export function DefaultHomePage({
142
147
  allProcessed = allProcessed.filter(
143
148
  (g) =>
144
149
  g.entries.length ||
145
- groupOrderFromNavController.includes(g.name) ||
146
150
  (g.name === DEFAULT_GROUP_NAME && hasPluginAdditionalCards)
147
151
  );
148
152
  }
@@ -176,6 +180,7 @@ export function DefaultHomePage({
176
180
  const persistNavigationGroups = (
177
181
  latest: { name: string; entries: NavigationEntry[] }[]
178
182
  ) => {
183
+ // Map ALL groups including "Views"
179
184
  const draggable: NavigationGroupMapping[] = latest.map((g) => ({
180
185
  name: g.name,
181
186
  entries: g.entries.map((e) => e.path)
@@ -194,14 +199,13 @@ export function DefaultHomePage({
194
199
  onNavigationEntriesUpdate(all);
195
200
  };
196
201
 
197
- const [collapsedGroups, setCollapsedGroups] = useState<Record<string, boolean>>({});
198
- const isGroupCollapsed = useCallback((name: string) => {
199
- return !!collapsedGroups[name];
200
- }, [collapsedGroups]);
202
+ // Use custom hook for collapsed groups with localStorage persistence
203
+ const groupNames = useMemo(() => [
204
+ ...items.map(item => item.name),
205
+ ...(adminGroupData ? [adminGroupData.name] : [])
206
+ ], [items, adminGroupData]);
201
207
 
202
- const toggleGroupCollapsed = useCallback((name: string) => {
203
- setCollapsedGroups(prev => ({ ...prev, [name]: !prev[name] }));
204
- }, []);
208
+ const { isGroupCollapsed, toggleGroupCollapsed } = useCollapsedGroups(groupNames);
205
209
 
206
210
 
207
211
  const {
@@ -221,37 +225,22 @@ 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
234
239
  }),
235
- onCardMovedBetweenGroups: (card) => {
236
- // Find which group the card was moved to and expand it if collapsed
237
- // Check both regular groups and admin group
238
- let targetGroup = items.find(group =>
239
- group.entries.some(entry => entry.url === card.url)
240
- );
241
-
242
- // Also check admin group if not found in regular groups
243
- if (!targetGroup && adminGroupData?.entries.some(entry => entry.url === card.url)) {
244
- targetGroup = adminGroupData;
245
- }
246
-
247
- if (targetGroup && isGroupCollapsed(targetGroup.name)) {
248
- toggleGroupCollapsed(targetGroup.name);
249
- }
250
-
240
+ onCardMovedBetweenGroups: (card) =>
251
241
  context.analyticsController?.onAnalyticsEvent?.("home_move_card", {
252
242
  id: card.id
253
- });
254
- },
243
+ }),
255
244
  onNewGroupDrop: () =>
256
245
  context.analyticsController?.onAnalyticsEvent?.(
257
246
  "home_drop_new_group"
@@ -371,7 +360,7 @@ export function DefaultHomePage({
371
360
  items={containers}
372
361
  strategy={verticalListSortingStrategy}
373
362
  >
374
- {items.map((groupData) => {
363
+ {items.map((groupData, groupIndex) => {
375
364
  const groupKey = groupData.name;
376
365
  const entriesInGroup = groupData.entries;
377
366
 
@@ -394,14 +383,13 @@ export function DefaultHomePage({
394
383
 
395
384
  if (
396
385
  entriesInGroup.length === 0 &&
397
- (AdditionalCards.length === 0 || performingSearch) &&
398
- !groupOrderFromNavController.includes(groupKey)
386
+ (AdditionalCards.length === 0 || performingSearch)
399
387
  )
400
388
  return null;
401
389
 
402
390
  return (
403
391
  <SortableNavigationGroup
404
- key={groupKey}
392
+ key={`group-${groupIndex}`}
405
393
  groupName={groupKey}
406
394
  disabled={dndDisabled}
407
395
  >
@@ -573,10 +561,9 @@ export function DefaultHomePage({
573
561
  existingGroupNames={items
574
562
  .map((g) => g.name)
575
563
  .filter((n) => n !== dialogOpenForGroup)}
576
- onClose={() => setDialogOpenForGroup(null)}
564
+ onClose={handleDialogClose}
577
565
  onRename={(newName) => {
578
566
  handleRenameGroup(dialogOpenForGroup, newName);
579
- setDialogOpenForGroup(null);
580
567
  }}
581
568
  />
582
569
  )}
@@ -163,13 +163,9 @@ export function SortableNavigationGroup({
163
163
  );
164
164
  }
165
165
 
166
- /* ─────────────────────────────────────────────────────────── */
167
- /* Main DnD hook */
168
-
169
- /* ─────────────────────────────────────────────────────────── */
170
166
  export function useHomePageDnd({
171
- items: dndItems,
172
- setItems: setDndItems,
167
+ items,
168
+ setItems,
173
169
  disabled,
174
170
  onCardMovedBetweenGroups,
175
171
  onGroupMoved,
@@ -191,6 +187,9 @@ export function useHomePageDnd({
191
187
  onPersist?: (latest: { name: string; entries: NavigationEntry[] }[]) => void;
192
188
  }) {
193
189
  /* ---------------- local state ---------------- */
190
+ const dndItems = items;
191
+ const setDndItems = setItems;
192
+
194
193
  const [activeId, setActiveId] = useState<UniqueIdentifier | null>(null);
195
194
  const [activeIsGroup, setActiveIsGroup] = useState(false);
196
195
  const [currentDraggingGroupId, setCurrentDraggingGroupId] =
@@ -200,6 +199,15 @@ export function useHomePageDnd({
200
199
  const [dialogOpenForGroup, setDialogOpenForGroup] = useState<string | null>(null);
201
200
  const [isHoveringNewGroupDropZone, setIsHoveringNewGroupDropZone] =
202
201
  useState(false);
202
+ const [pendingNewGroupName, setPendingNewGroupName] = useState<string | null>(null);
203
+ const [stateBeforeNewGroup, setStateBeforeNewGroup] = useState<
204
+ { name: string; entries: NavigationEntry[] }[] | null
205
+ >(null);
206
+
207
+ /* store the original state before any drag modifications */
208
+ const preDragItemsRef = useRef<
209
+ { name: string; entries: NavigationEntry[] }[] | null
210
+ >(null);
203
211
 
204
212
  /* store interim state for cross-group moves */
205
213
  const interimItemsRef = useRef<
@@ -243,38 +251,11 @@ export function useHomePageDnd({
243
251
  const collisionDetection: CollisionDetection = useCallback(
244
252
  (args) => {
245
253
  if (disabled || !activeId) return [];
246
-
247
254
  if (activeIsGroup) {
248
255
  const groups = args.droppableContainers.filter((c) =>
249
256
  dndItems.some((g) => g.name === c.id)
250
257
  );
251
258
  if (!groups.length) return [];
252
-
253
- // Special handling for dropping at the very beginning (first position)
254
- if (groups.length > 0) {
255
- const firstGroup = groups[0];
256
- const firstGroupRect = firstGroup.rect.current;
257
- const { x, y } = args.pointerCoordinates || { x: 0, y: 0 };
258
-
259
- // If pointer is above the first group's top edge, treat it as dropping at position 0
260
- if (firstGroupRect && y < firstGroupRect.top + 20) {
261
- // Return the first group as target, but we'll handle this specially in onDragEnd
262
- return [{ id: firstGroup.id, data: { insertBefore: true } }];
263
- }
264
- }
265
-
266
- // Use closestCorners for better collision detection with collapsed groups
267
- // This provides more precise drop zones between groups
268
- const cornersResult = closestCorners({
269
- ...args,
270
- droppableContainers: groups
271
- });
272
-
273
- if (cornersResult.length) {
274
- return cornersResult;
275
- }
276
-
277
- // Fallback to closestCenter if corners detection fails
278
259
  return closestCenter({
279
260
  ...args,
280
261
  droppableContainers: groups
@@ -348,6 +329,9 @@ export function useHomePageDnd({
348
329
  setDndKitActiveNode(active);
349
330
  if (disabled) return;
350
331
 
332
+ // Capture the original state before any drag modifications
333
+ preDragItemsRef.current = cloneItemsForDnd(dndItems);
334
+
351
335
  const isGroup = dndItems.some((g) => g.name === active.id);
352
336
  if (!active.data.current) active.data.current = {};
353
337
  active.data.current.type = isGroup ? "group" : "item";
@@ -376,18 +360,35 @@ export function useHomePageDnd({
376
360
 
377
361
  if (overCont && activeCont !== overCont) {
378
362
  recentlyMovedToNewContainer.current = true;
379
- const newState = cloneItemsForDnd(dndItems);
380
- const srcIdx = newState.findIndex((g) => g.name === activeCont);
381
- const tgtIdx = newState.findIndex((g) => g.name === overCont);
382
- if (srcIdx === -1 || tgtIdx === -1) return;
383
- const src = newState[srcIdx];
384
- const tgt = newState[tgtIdx];
385
- const idxInSrc = src.entries.findIndex((e) => e.url === activeIdNow);
386
- if (idxInSrc === -1) return;
387
- const [moved] = src.entries.splice(idxInSrc, 1);
388
- tgt.entries.push(moved);
389
- interimItemsRef.current = newState;
390
- setDndItems(newState);
363
+ lastOverId.current = overIdNow;
364
+
365
+ // Update state for visual feedback during drag
366
+ setDndItems((current) => {
367
+ const newState = cloneItemsForDnd(current);
368
+ const srcIdx = newState.findIndex((g) => g.name === activeCont);
369
+ const tgtIdx = newState.findIndex((g) => g.name === overCont);
370
+ if (srcIdx === -1 || tgtIdx === -1) return current;
371
+ const src = newState[srcIdx];
372
+ const tgt = newState[tgtIdx];
373
+ const idxInSrc = src.entries.findIndex((e) => e.url === activeIdNow);
374
+ if (idxInSrc === -1) return current;
375
+ const [moved] = src.entries.splice(idxInSrc, 1);
376
+
377
+ // Calculate insertion position - SAME logic as handleDragEnd
378
+ const overIsContainer = overIdNow === overCont;
379
+ if (overIsContainer) {
380
+ tgt.entries.push(moved);
381
+ } else {
382
+ const overIdx = tgt.entries.findIndex((e) => e.url === overIdNow);
383
+ if (overIdx !== -1) {
384
+ tgt.entries.splice(overIdx, 0, moved);
385
+ } else {
386
+ tgt.entries.push(moved);
387
+ }
388
+ }
389
+
390
+ return newState;
391
+ });
391
392
  } else if (activeCont === overCont) {
392
393
  recentlyMovedToNewContainer.current = false;
393
394
  }
@@ -407,21 +408,7 @@ export function useHomePageDnd({
407
408
 
408
409
  /* ─── group reorder ─── */
409
410
  if (activeIsGroup) {
410
- // Check if we're dropping above the first group (insertBefore flag)
411
- const insertBefore = over.data?.current?.insertBefore;
412
-
413
- if (insertBefore && activeIdNow !== overIdNow) {
414
- // Move to first position (before the target group)
415
- const from = dndItems.findIndex((g) => g.name === activeIdNow);
416
- if (from !== -1 && from !== 0) {
417
- const newState = arrayMove(dndItems, from, 0);
418
- setDndItems(newState);
419
- onPersist?.(newState);
420
- onGroupMoved?.(activeIdNow as string, from, 0);
421
- }
422
- }
423
- // Handle dropping on another group (normal case)
424
- else if (
411
+ if (
425
412
  activeIdNow !== overIdNow &&
426
413
  dndItems.some((g) => g.name === overIdNow)
427
414
  ) {
@@ -437,11 +424,26 @@ export function useHomePageDnd({
437
424
  }
438
425
  /* ─── card move ─── */
439
426
  else {
440
- const activeCont = findDndContainer(activeIdNow);
427
+ // CRITICAL: Find source container from ORIGINAL pre-drag state, not current (potentially stale) dndItems
428
+ const findContainerInState = (id: string, state: { name: string; entries: NavigationEntry[] }[]): string | undefined => {
429
+ const group = state.find((g) => g.name === id);
430
+ if (group) return group.name;
431
+ for (const g of state) {
432
+ if (g.entries.some((e) => e.url === id)) return g.name;
433
+ }
434
+ return undefined;
435
+ };
436
+
437
+ const sourceState = preDragItemsRef.current || dndItems;
438
+ const activeCont = findContainerInState(activeIdNow as string, sourceState);
439
+ const overCont = findDndContainer(overIdNow);
441
440
 
442
441
  /* drop on new-group zone */
443
442
  if (overIdNow === "new-group-drop-zone") {
444
443
  if (activeCont) {
444
+ // Save current state before making changes
445
+ setStateBeforeNewGroup(cloneItemsForDnd(dndItems));
446
+
445
447
  const newState = cloneItemsForDnd(dndItems);
446
448
  const srcIdx = newState.findIndex((g) => g.name === activeCont);
447
449
  if (srcIdx !== -1) {
@@ -462,8 +464,10 @@ export function useHomePageDnd({
462
464
  name: tentative,
463
465
  entries: [dragged]
464
466
  });
467
+
468
+ // Update local state but DON'T persist yet
465
469
  setDndItems(newState);
466
- onPersist?.(newState);
470
+ setPendingNewGroupName(tentative);
467
471
  setDialogOpenForGroup(tentative);
468
472
  onNewGroupDrop?.();
469
473
  }
@@ -500,18 +504,59 @@ export function useHomePageDnd({
500
504
  onPersist?.(newState);
501
505
  }
502
506
  }
503
- } else if (
504
- recentlyMovedToNewContainer.current &&
505
- interimItemsRef.current
506
- ) {
507
- onPersist?.(interimItemsRef.current);
508
- }
507
+ } else if (overCont && activeCont !== overCont) {
508
+ // Card moved between different groups - use CLEAN pre-drag state
509
+ const finalState = cloneItemsForDnd(sourceState);
509
510
 
510
- onCardMovedBetweenGroups?.(
511
- dndItems
512
- .flatMap((g) => g.entries)
513
- .find((e) => e.url === activeIdNow)!
514
- );
511
+ // Find target container from clean state too
512
+ const finalOverId = lastOverId.current || overIdNow;
513
+ const cleanOverCont = findContainerInState(finalOverId as string, sourceState) || overCont;
514
+
515
+ const srcIdx = finalState.findIndex((g) => g.name === activeCont);
516
+ const tgtIdx = finalState.findIndex((g) => g.name === cleanOverCont);
517
+
518
+ if (srcIdx !== -1 && tgtIdx !== -1) {
519
+ const src = finalState[srcIdx];
520
+ const tgt = finalState[tgtIdx];
521
+ const idxInSrc = src.entries.findIndex((e) => e.url === activeIdNow);
522
+
523
+ if (idxInSrc !== -1) {
524
+ // Remove from source
525
+ const [moved] = src.entries.splice(idxInSrc, 1);
526
+
527
+ // Calculate insertion position in target
528
+ const overIsContainer = finalOverId === cleanOverCont;
529
+ if (overIsContainer) {
530
+ tgt.entries.push(moved);
531
+ } else {
532
+ const overIdx = tgt.entries.findIndex((e) => e.url === finalOverId);
533
+ if (overIdx !== -1) {
534
+ tgt.entries.splice(overIdx, 0, moved);
535
+ } else {
536
+ tgt.entries.push(moved);
537
+ }
538
+ }
539
+
540
+ // Remove empty source group if needed
541
+ if (src.entries.length === 0) {
542
+ finalState.splice(srcIdx, 1);
543
+ }
544
+
545
+ setDndItems(finalState);
546
+ onPersist?.(finalState);
547
+
548
+ onCardMovedBetweenGroups?.(moved);
549
+ }
550
+ }
551
+ } else if (recentlyMovedToNewContainer.current) {
552
+ // This shouldn't happen but log it for debugging
553
+ console.error("Move between containers detected but conditions not met", {
554
+ activeCont,
555
+ overCont,
556
+ activeIdNow,
557
+ overIdNow
558
+ });
559
+ }
515
560
  }
516
561
  }
517
562
 
@@ -527,9 +572,7 @@ export function useHomePageDnd({
527
572
  recentlyMovedToNewContainer.current = false;
528
573
  };
529
574
 
530
- const handleDragCancel = () => {
531
- resetDragState();
532
- };
575
+ const handleDragCancel = () => resetDragState();
533
576
 
534
577
  /* ---------------- group rename ---------------- */
535
578
  const handleRenameGroup = (oldName: string, newName: string) => {
@@ -544,9 +587,30 @@ export function useHomePageDnd({
544
587
  ...updated[idx],
545
588
  name: newName
546
589
  };
547
- onPersist?.(updated); // <- ensure rename is saved
590
+
591
+ // Persist after successful rename
592
+ onPersist?.(updated);
548
593
  return updated;
549
594
  });
595
+
596
+ // Clear all pending state
597
+ setPendingNewGroupName(null);
598
+ setStateBeforeNewGroup(null);
599
+ setDialogOpenForGroup(null);
600
+ };
601
+
602
+ /* Handle dialog close without renaming */
603
+ const handleDialogClose = () => {
604
+ // If there's a pending new group that wasn't renamed, restore previous state
605
+ if (pendingNewGroupName && dialogOpenForGroup === pendingNewGroupName && stateBeforeNewGroup) {
606
+ // Restore the state from before the new group was created
607
+ setDndItems(stateBeforeNewGroup);
608
+ }
609
+
610
+ // Clear all pending state
611
+ setPendingNewGroupName(null);
612
+ setStateBeforeNewGroup(null);
613
+ setDialogOpenForGroup(null);
550
614
  };
551
615
 
552
616
  /* ---------------- public API ---------------- */
@@ -577,15 +641,12 @@ export function useHomePageDnd({
577
641
  dialogOpenForGroup,
578
642
  setDialogOpenForGroup,
579
643
  handleRenameGroup,
644
+ handleDialogClose,
580
645
  isHoveringNewGroupDropZone,
581
646
  setIsHoveringNewGroupDropZone
582
647
  };
583
648
  }
584
649
 
585
- /* ─────────────────────────────────────────────────────────── */
586
- /* New-group drop-zone component */
587
-
588
- /* ─────────────────────────────────────────────────────────── */
589
650
  export function NewGroupDropZone({
590
651
  disabled,
591
652
  setIsHovering
@@ -630,8 +691,7 @@ export function NewGroupDropZone({
630
691
  isOver
631
692
  ? "bg-surface-accent-100 dark:bg-surface-accent-800 border-surface-300 dark:border-surface-600"
632
693
  : "bg-surface-50 dark:bg-surface-900 border-surface-200 dark:border-surface-700"
633
- )}
634
- >
694
+ )}>
635
695
  <div className="text-center p-4">
636
696
  <span className="block font-medium text-sm">
637
697
  Drop here to create a new group
@@ -1,4 +1,4 @@
1
- import React, { useState, useEffect, useRef } from "react";
1
+ import React, { useEffect, useRef, useState } from "react";
2
2
  import { Button, Dialog, DialogActions, DialogContent, DialogTitle, TextField } from "@firecms/ui";
3
3
 
4
4
  interface RenameGroupDialogProps {
@@ -9,7 +9,13 @@ interface RenameGroupDialogProps {
9
9
  onRename: (newName: string) => void;
10
10
  }
11
11
 
12
- export function RenameGroupDialog({ open, initialName, existingGroupNames, onClose, onRename }: RenameGroupDialogProps) {
12
+ export function RenameGroupDialog({
13
+ open,
14
+ initialName,
15
+ existingGroupNames,
16
+ onClose,
17
+ onRename
18
+ }: RenameGroupDialogProps) {
13
19
  const [name, setName] = useState(initialName);
14
20
  const [error, setError] = useState<string | null>(null);
15
21
  const inputRef = useRef<HTMLInputElement | HTMLTextAreaElement | null>(null); // Create a ref for the input
@@ -86,7 +92,7 @@ export function RenameGroupDialog({ open, initialName, existingGroupNames, onClo
86
92
  if (!open) return null;
87
93
 
88
94
  return (
89
- <Dialog open={open} onOpenChange={onClose}>
95
+ <Dialog open={open}>
90
96
  <DialogTitle>Rename Group</DialogTitle>
91
97
  <DialogContent>
92
98
  <TextField