@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.
- package/dist/components/HomePage/HomePageDnD.d.ts +2 -1
- package/dist/components/PropertyCollectionView.d.ts +23 -0
- package/dist/components/UserDisplay.d.ts +7 -0
- package/dist/components/VirtualTable/fields/VirtualTableUserSelect.d.ts +12 -0
- package/dist/contexts/InternalUserManagementContext.d.ts +3 -0
- package/dist/core/EntityEditView.d.ts +10 -4
- package/dist/core/FireCMS.d.ts +0 -1
- package/dist/core/field_configs.d.ts +1 -1
- package/dist/form/EntityForm.d.ts +5 -2
- package/dist/form/components/LocalChangesMenu.d.ts +11 -0
- package/dist/form/field_bindings/UserSelectFieldBinding.d.ts +12 -0
- package/dist/form/index.d.ts +2 -1
- package/dist/hooks/index.d.ts +2 -0
- package/dist/hooks/useCollapsedGroups.d.ts +9 -0
- package/dist/hooks/useInternalUserManagementController.d.ts +12 -0
- package/dist/index.es.js +1983 -650
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +1981 -648
- package/dist/index.umd.js.map +1 -1
- package/dist/preview/components/UserPreview.d.ts +8 -0
- package/dist/preview/index.d.ts +1 -0
- package/dist/types/collections.d.ts +13 -0
- package/dist/types/entities.d.ts +5 -1
- package/dist/types/firecms.d.ts +15 -0
- package/dist/types/firecms_context.d.ts +16 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/internal_user_management.d.ts +20 -0
- package/dist/types/plugins.d.ts +2 -0
- package/dist/types/properties.d.ts +41 -6
- package/dist/types/property_config.d.ts +1 -1
- package/dist/types/user.d.ts +1 -1
- package/dist/util/collections.d.ts +1 -0
- package/dist/util/entity_cache.d.ts +6 -1
- package/dist/util/make_properties_editable.d.ts +1 -2
- package/dist/util/objects.d.ts +1 -0
- package/dist/util/useStorageUploadController.d.ts +1 -0
- package/package.json +6 -6
- package/src/components/EntityCollectionTable/EntityCollectionRowActions.tsx +47 -47
- package/src/components/EntityCollectionTable/PropertyTableCell.tsx +12 -0
- package/src/components/EntityCollectionView/EntityCollectionView.tsx +6 -1
- package/src/components/EntityView.tsx +29 -40
- package/src/components/ErrorView.tsx +1 -1
- package/src/components/HomePage/DefaultHomePage.tsx +21 -34
- package/src/components/HomePage/HomePageDnD.tsx +143 -83
- package/src/components/HomePage/RenameGroupDialog.tsx +9 -3
- package/src/components/PropertyCollectionView.tsx +329 -0
- package/src/components/PropertyConfigBadge.tsx +2 -2
- package/src/components/SelectableTable/filters/DateTimeFilterField.tsx +2 -1
- package/src/components/SelectableTable/filters/StringNumberFilterField.tsx +1 -2
- package/src/components/UserDisplay.tsx +55 -0
- package/src/components/VirtualTable/fields/VirtualTableUserSelect.tsx +99 -0
- package/src/components/common/useColumnsIds.tsx +1 -8
- package/src/contexts/InternalUserManagementContext.tsx +4 -0
- package/src/core/EntityEditView.tsx +27 -14
- package/src/core/EntityEditViewFormActions.tsx +33 -18
- package/src/core/EntitySidePanel.tsx +9 -3
- package/src/core/FireCMS.tsx +22 -13
- package/src/core/field_configs.tsx +15 -1
- package/src/form/EntityForm.tsx +173 -42
- package/src/form/EntityFormActions.tsx +30 -15
- package/src/form/PropertyFieldBinding.tsx +4 -0
- package/src/form/components/ErrorFocus.tsx +22 -29
- package/src/form/components/LocalChangesMenu.tsx +144 -0
- package/src/form/field_bindings/UserSelectFieldBinding.tsx +94 -0
- package/src/form/index.tsx +5 -1
- package/src/hooks/index.tsx +3 -0
- package/src/hooks/useBrowserTitleAndIcon.tsx +1 -1
- package/src/hooks/useBuildNavigationController.tsx +104 -31
- package/src/hooks/useCollapsedGroups.ts +64 -0
- package/src/hooks/useFireCMSContext.tsx +6 -2
- package/src/hooks/useInternalUserManagementController.tsx +16 -0
- package/src/preview/PropertyPreview.tsx +8 -0
- package/src/preview/components/ReferencePreview.tsx +4 -2
- package/src/preview/components/UserPreview.tsx +27 -0
- package/src/preview/index.ts +1 -0
- package/src/preview/property_previews/ArrayPropertyPreview.tsx +1 -1
- package/src/preview/property_previews/MapPropertyPreview.tsx +2 -2
- package/src/preview/property_previews/NumberPropertyPreview.tsx +2 -2
- package/src/types/collections.ts +14 -0
- package/src/types/entities.ts +7 -1
- package/src/types/firecms.tsx +16 -0
- package/src/types/firecms_context.tsx +17 -0
- package/src/types/index.ts +1 -0
- package/src/types/internal_user_management.ts +24 -0
- package/src/types/plugins.tsx +3 -0
- package/src/types/properties.ts +45 -6
- package/src/types/property_config.tsx +1 -0
- package/src/types/user.ts +1 -1
- package/src/util/collections.ts +8 -0
- package/src/util/createFormexStub.tsx +4 -0
- package/src/util/entities.ts +1 -1
- package/src/util/entity_cache.ts +72 -53
- package/src/util/join_collections.ts +3 -3
- package/src/util/make_properties_editable.ts +0 -22
- package/src/util/objects.ts +40 -2
- 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
|
-
|
|
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 {
|
|
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
|
-
|
|
198
|
-
const
|
|
199
|
-
|
|
200
|
-
|
|
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 =
|
|
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,
|
|
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={
|
|
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={
|
|
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
|
|
172
|
-
setItems
|
|
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
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
505
|
-
|
|
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
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
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
|
-
|
|
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, {
|
|
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({
|
|
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}
|
|
95
|
+
<Dialog open={open}>
|
|
90
96
|
<DialogTitle>Rename Group</DialogTitle>
|
|
91
97
|
<DialogContent>
|
|
92
98
|
<TextField
|