Skip to content

Commit 5f50173

Browse files
committed
Collapsing home sections again
1 parent 91dd621 commit 5f50173

7 files changed

Lines changed: 1997 additions & 1161 deletions

File tree

package-lock.json

Lines changed: 1790 additions & 1108 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/firecms_core/src/components/HomePage/DefaultHomePage.tsx

Lines changed: 36 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -194,9 +194,16 @@ export function DefaultHomePage({
194194
onNavigationEntriesUpdate(all);
195195
};
196196

197-
/* ─────────────────────────────────────────────────────���─────────
198-
Hook for DnD
199-
───�����────────────────────────────────────────────────────────── */
197+
const [collapsedGroups, setCollapsedGroups] = useState<Record<string, boolean>>({});
198+
const isGroupCollapsed = useCallback((name: string) => {
199+
return !!collapsedGroups[name];
200+
}, [collapsedGroups]);
201+
202+
const toggleGroupCollapsed = useCallback((name: string) => {
203+
setCollapsedGroups(prev => ({ ...prev, [name]: !prev[name] }));
204+
}, []);
205+
206+
200207
const {
201208
sensors,
202209
collisionDetection,
@@ -225,10 +232,26 @@ export function DefaultHomePage({
225232
context.analyticsController?.onAnalyticsEvent?.("home_move_group", {
226233
name: g
227234
}),
228-
onCardMovedBetweenGroups: (card) =>
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+
229251
context.analyticsController?.onAnalyticsEvent?.("home_move_card", {
230252
id: card.id
231-
}),
253+
});
254+
},
232255
onNewGroupDrop: () =>
233256
context.analyticsController?.onAnalyticsEvent?.(
234257
"home_drop_new_group"
@@ -303,7 +326,7 @@ export function DefaultHomePage({
303326

304327
/* ───────────────────────────────────────────────────────────────
305328
Render
306-
─────────���───────────────────────────────────────────────────── */
329+
────────────────────────────────────────────────────────────── */
307330
return (
308331
<div ref={containerRef} className="py-2 overflow-auto h-full w-full">
309332
<Container maxWidth="6xl">
@@ -400,6 +423,8 @@ export function DefaultHomePage({
400423
if (dndDisabled) return;
401424
setDialogOpenForGroup(groupKey);
402425
}}
426+
collapsed={isGroupCollapsed(groupKey)}
427+
onToggleCollapsed={() => toggleGroupCollapsed(groupKey)}
403428
>
404429
<NavigationGroupDroppable
405430
id={groupKey}
@@ -503,7 +528,11 @@ export function DefaultHomePage({
503528
</DndContext>
504529

505530
{!performingSearch && adminGroupData && (
506-
<NavigationGroup group={adminGroupData.name}>
531+
<NavigationGroup
532+
group={adminGroupData.name}
533+
collapsed={isGroupCollapsed(adminGroupData.name)}
534+
onToggleCollapsed={() => toggleGroupCollapsed(adminGroupData.name)}
535+
>
507536
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 ">
508537
{adminGroupData.entries.map((entry) => (
509538
<NavigationCardBinding

packages/firecms_core/src/components/HomePage/HomePageDnD.tsx

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -243,11 +243,38 @@ export function useHomePageDnd({
243243
const collisionDetection: CollisionDetection = useCallback(
244244
(args) => {
245245
if (disabled || !activeId) return [];
246+
246247
if (activeIsGroup) {
247248
const groups = args.droppableContainers.filter((c) =>
248249
dndItems.some((g) => g.name === c.id)
249250
);
250251
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
251278
return closestCenter({
252279
...args,
253280
droppableContainers: groups
@@ -380,7 +407,21 @@ export function useHomePageDnd({
380407

381408
/* ─── group reorder ─── */
382409
if (activeIsGroup) {
383-
if (
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 (
384425
activeIdNow !== overIdNow &&
385426
dndItems.some((g) => g.name === overIdNow)
386427
) {
@@ -486,7 +527,9 @@ export function useHomePageDnd({
486527
recentlyMovedToNewContainer.current = false;
487528
};
488529

489-
const handleDragCancel = () => resetDragState();
530+
const handleDragCancel = () => {
531+
resetDragState();
532+
};
490533

491534
/* ---------------- group rename ---------------- */
492535
const handleRenameGroup = (oldName: string, newName: string) => {

packages/firecms_core/src/components/HomePage/NavigationGroup.tsx

Lines changed: 121 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,72 +1,152 @@
11
import React, { PropsWithChildren, useState } from "react";
2-
import { cls, EditIcon, IconButton, Typography } from "@firecms/ui";
2+
import { cls, EditIcon, IconButton, Typography, ExpandablePanel } from "@firecms/ui";
33

44
export function NavigationGroup({
55
children,
66
group,
77
minimised,
88
isPreview,
99
isPotentialCardDropTarget,
10-
onEditGroup, // New prop to handle editing
11-
dndDisabled // New prop to disable editing when D&D is off
10+
onEditGroup,
11+
dndDisabled,
12+
collapsed,
13+
onToggleCollapsed
1214
}: PropsWithChildren<{
1315
group: string | undefined,
1416
minimised?: boolean,
1517
isPreview?: boolean,
1618
isPotentialCardDropTarget?: boolean,
17-
onEditGroup?: (groupName: string) => void; // Callback to open dialog
18-
dndDisabled?: boolean; // Added dndDisabled prop
19+
onEditGroup?: (groupName: string) => void;
20+
dndDisabled?: boolean;
21+
collapsed?: boolean;
22+
onToggleCollapsed?: () => void;
1923
}>) {
2024

2125
const [isHovered, setIsHovered] = useState(false);
2226
const currentGroupName = group ?? "Views";
2327

28+
// Show caret only when not in preview and there is a toggle handler
29+
const showCaret = !isPreview && !!onToggleCollapsed;
30+
31+
// Helper for the title content (left side)
32+
const TitleContent = (
33+
<div className={cls("flex items-center", isPreview ? "px-1 py-0.5" : "")}
34+
>
35+
<Typography
36+
variant={isPreview ? "body2" : "caption"}
37+
component={"h2"}
38+
color="secondary"
39+
className={cls(
40+
"p-4 py-2 rounded",
41+
"font-medium uppercase text-sm text-surface-600 dark:text-surface-400"
42+
)}
43+
>
44+
{currentGroupName}
45+
</Typography>
46+
{!isPreview && onEditGroup && !dndDisabled && (
47+
<IconButton
48+
size="smallest"
49+
onClick={(e) => {
50+
e.stopPropagation(); // Prevent toggle on click
51+
onEditGroup(currentGroupName);
52+
}}
53+
className={cls("ml-2 ", isHovered ? "opacity-100" : "opacity-0", "transition-opacity duration-100")}
54+
>
55+
<EditIcon size="smallest"/>
56+
</IconButton>
57+
)}
58+
</div>
59+
);
60+
2461
return (
2562
<div className={cls(
2663
!isPotentialCardDropTarget ? "my-10" : "my-6",
2764
"transition-all duration-200 ease-in-out"
2865
)}
2966
>
30-
<div className={`flex items-center ${isPreview ? "px-1 py-0.5 m-0" : "ml-3.5 mt-6"} `}
67+
{/* Preview: static header + content (no caret / no collapse) */}
68+
{isPreview && (
69+
<>
70+
<div
71+
className={cls(
72+
"flex items-center justify-between w-full",
73+
"p-4 py-2"
74+
)}
75+
onMouseEnter={() => setIsHovered(true)}
76+
onMouseLeave={() => setIsHovered(false)}
77+
>
78+
{TitleContent}
79+
</div>
80+
{children}
81+
</>
82+
)}
3183

32-
onMouseEnter={() => setIsHovered(true)}
33-
onMouseLeave={() => setIsHovered(false)}>
34-
<Typography
35-
variant={isPreview ? "body2" : "caption"}
36-
component={"h2"}
37-
color="secondary"
38-
// Minimal padding and no margin for preview title
39-
className={`${isPreview ? "px-1 py-0.5" : "ml-3.5"} font-medium uppercase text-sm text-surface-600 dark:text-surface-400`}
84+
{/* Interactive collapsible version when a toggle handler is provided */}
85+
{!isPreview && showCaret && (
86+
<ExpandablePanel
87+
invisible
88+
expanded={!collapsed}
89+
onExpandedChange={(open) => {
90+
if (open !== !collapsed) {
91+
onToggleCollapsed?.();
92+
}
93+
}}
94+
className={cls("mt-6")}
95+
titleClassName={cls(
96+
"min-h-0 p-0 border-none",
97+
"rounded-t flex items-center justify-between w-full",
98+
"hover:bg-transparent",
99+
"cursor-pointer select-none"
100+
)}
101+
innerClassName={cls("mt-4", !minimised ? "pt-0" : "")}
102+
title={
103+
<div
104+
onMouseEnter={() => setIsHovered(true)}
105+
onMouseLeave={() => setIsHovered(false)}
106+
className="flex items-center"
107+
>
108+
{TitleContent}
109+
</div>
110+
}
40111
>
41-
{currentGroupName}
42-
</Typography>
43-
{!isPreview && onEditGroup && !dndDisabled && (
44-
<IconButton
45-
size="smallest"
46-
onClick={(e) => {
47-
e.stopPropagation(); // Prevent other click events
48-
onEditGroup(currentGroupName);
49-
}}
50-
className={cls("ml-2 ", isHovered ? "opacity-100" : "opacity-0", "transition-opacity duration-100")}
112+
{minimised ? (
113+
<div className={cls("mt-4 p-8 bg-surface-accent-200 dark:bg-surface-accent-800 rounded-lg")}
114+
style={{ minHeight: "50px" }}>
115+
</div>
116+
) : (
117+
<div className={cls("mt-4", !minimised ? "pt-0" : "")}>
118+
{children}
119+
</div>
120+
)}
121+
</ExpandablePanel>
122+
)}
123+
124+
{/* Non-collapsible (no caret) runtime, keep old behavior */}
125+
{!isPreview && !showCaret && (
126+
<>
127+
<div
128+
className={cls(
129+
"flex items-center justify-between w-full",
130+
"mt-6"
131+
)}
132+
onMouseEnter={() => setIsHovered(true)}
133+
onMouseLeave={() => setIsHovered(false)}
51134
>
52-
<EditIcon size="smallest"/>
53-
</IconButton>
54-
)}
55-
</div>
135+
{TitleContent}
136+
</div>
56137

57-
{isPreview ? (
58-
children
59-
) : minimised ? (
60-
// For minimised view in the main list
61-
<div className={cls("mt-4 p-8 bg-surface-accent-200 dark:bg-surface-accent-800 rounded-lg")}
62-
style={{ minHeight: "50px" }}>
63-
</div>
64-
) : (
65-
// If highlighted, the parent div already has padding, so children (NavigationGroupDroppable) don't need extra margin top as much.
66-
// The inner content of NavigationGroupDroppable will define its own padding if needed when active.
67-
<div className={cls("mt-4", !minimised ? "pt-0" : "")}>
68-
{children}
69-
</div>
138+
{!collapsed && (
139+
minimised ? (
140+
<div className={cls("mt-4 p-8 bg-surface-accent-200 dark:bg-surface-accent-800 rounded-lg")}
141+
style={{ minHeight: "50px" }}>
142+
</div>
143+
) : (
144+
<div className={cls("mt-4", !minimised ? "pt-0" : "")}>
145+
{children}
146+
</div>
147+
)
148+
)}
149+
</>
70150
)}
71151
</div>
72152
);

packages/ui/src/components/DialogTitle.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ export function DialogTitle({
2626

2727
const title = <DialogPrimitive.Title asChild>
2828
<Typography variant={variant}
29-
className={cls({ "mt-8 mx-6": includeMargin }, className)}
29+
className={cls({ "mt-6 mx-6": includeMargin }, className)}
3030
gutterBottom={gutterBottom}
3131
{...props}>
3232
{children}

packages/ui/src/components/ExpandablePanel.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ export function ExpandablePanel({
9494
<Collapsible.Trigger
9595
className={cls(
9696
"rounded-t flex items-center justify-between w-full min-h-[52px]",
97-
"hover:bg-surface-accent-200 hover:bg-opacity-20 dark:hover:bg-surface-800 dark:hover:bg-opacity-20",
97+
"hover:bg-surface-accent-200 hover:bg-opacity-40 dark:hover:bg-surface-800 dark:hover:bg-opacity-40",
9898
invisible ? "border-b px-2" : "p-4",
9999
open ? "py-6" : "py-4",
100100
"transition-all duration-200",

packages/user_management/src/useUserManagementPlugin.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,9 @@ export function IntroWidget({
5858
<Typography>
5959
You have no users or roles defined. You can create default roles and add the current user as admin.
6060
</Typography>
61-
<Button onClick={() => {
61+
<Button
62+
variant={"outlined"}
63+
onClick={() => {
6264
if (!authController.user?.uid) {
6365
throw Error("UsersTable, authController misconfiguration");
6466
}

0 commit comments

Comments
 (0)