mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-05 13:29:44 +02:00
fix: stabilize mobile issue detail layout (#1912)
Co-authored-by: Devv <devv@Devvs-Mac-mini.local>
This commit is contained in:
@@ -3,6 +3,13 @@ import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import type { Issue, TimelineEntry } from "@multica/core/types";
|
||||
|
||||
const mockViewport = vi.hoisted(() => ({ isMobile: false }));
|
||||
|
||||
vi.mock("@multica/ui/hooks/use-mobile", () => ({
|
||||
useIsMobile: () => mockViewport.isMobile,
|
||||
}));
|
||||
|
||||
// useWorkspaceId() derives from useCurrentWorkspace (relative import inside
|
||||
// @multica/core/hooks.tsx). vi.mock("@multica/core/paths") only intercepts
|
||||
// the bare-specifier, not the internal relative import. Mock the hooks module
|
||||
@@ -364,6 +371,7 @@ function renderIssueDetail(issueId = "issue-1") {
|
||||
describe("IssueDetail (shared)", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mockViewport.isMobile = false;
|
||||
// Default: issue loads successfully
|
||||
mockApiObj.getIssue.mockResolvedValue(mockIssue);
|
||||
mockApiObj.listTimeline.mockResolvedValue(mockTimeline);
|
||||
@@ -425,6 +433,19 @@ describe("IssueDetail (shared)", () => {
|
||||
expect(screen.getByText("Due date")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("uses a non-resizable layout with the sidebar sheet closed by default on mobile", async () => {
|
||||
mockViewport.isMobile = true;
|
||||
|
||||
renderIssueDetail();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByDisplayValue("Implement authentication")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
expect(screen.queryByTestId("panel-group")).not.toBeInTheDocument();
|
||||
expect(screen.queryByText("Properties")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders Details section with Created by and dates", async () => {
|
||||
renderIssueDetail();
|
||||
|
||||
|
||||
@@ -177,14 +177,15 @@ export function IssueDetail({ issueId, onDelete, onDone, defaultSidebarOpen = tr
|
||||
});
|
||||
const sidebarRef = usePanelRef();
|
||||
const isMobile = useIsMobile();
|
||||
const [sidebarOpen, setSidebarOpen] = useState(defaultSidebarOpen);
|
||||
const [desktopSidebarOpen, setDesktopSidebarOpen] = useState(defaultSidebarOpen);
|
||||
const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (isMobile) {
|
||||
setSidebarOpen(false);
|
||||
sidebarRef.current?.collapse();
|
||||
setMobileSidebarOpen(false);
|
||||
}
|
||||
}, [isMobile]);
|
||||
const sidebarOpen = isMobile ? mobileSidebarOpen : desktopSidebarOpen;
|
||||
const [propertiesOpen, setPropertiesOpen] = useState(true);
|
||||
const [detailsOpen, setDetailsOpen] = useState(true);
|
||||
const [parentIssueOpen, setParentIssueOpen] = useState(true);
|
||||
@@ -307,6 +308,18 @@ export function IssueDetail({ issueId, onDelete, onDone, defaultSidebarOpen = tr
|
||||
const actions = useIssueActions(issue);
|
||||
const handleUpdateField = actions.updateField;
|
||||
|
||||
const handleToggleSidebar = useCallback(() => {
|
||||
if (isMobile) {
|
||||
setMobileSidebarOpen((open) => !open);
|
||||
return;
|
||||
}
|
||||
|
||||
const panel = sidebarRef.current;
|
||||
if (!panel) return;
|
||||
if (panel.isCollapsed()) panel.expand();
|
||||
else panel.collapse();
|
||||
}, [isMobile, sidebarRef]);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex flex-1 min-h-0 flex-col">
|
||||
@@ -488,10 +501,8 @@ export function IssueDetail({ issueId, onDelete, onDone, defaultSidebarOpen = tr
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<ResizablePanelGroup orientation="horizontal" className="flex-1 min-h-0" defaultLayout={defaultLayout} onLayoutChanged={onLayoutChanged}>
|
||||
<ResizablePanel id="content" minSize="50%">
|
||||
<div className="flex h-full flex-col">
|
||||
const detailContent = (
|
||||
<div className="flex h-full min-w-0 flex-1 flex-col">
|
||||
<PageHeader className="gap-2 bg-background text-sm">
|
||||
<div className="flex flex-1 items-center gap-1.5 min-w-0">
|
||||
{workspace && (
|
||||
@@ -572,16 +583,7 @@ export function IssueDetail({ issueId, onDelete, onDone, defaultSidebarOpen = tr
|
||||
variant={sidebarOpen ? "secondary" : "ghost"}
|
||||
size="icon-sm"
|
||||
className={sidebarOpen ? "" : "text-muted-foreground"}
|
||||
onClick={() => {
|
||||
if (isMobile) {
|
||||
setSidebarOpen(!sidebarOpen);
|
||||
} else {
|
||||
const panel = sidebarRef.current;
|
||||
if (!panel) return;
|
||||
if (panel.isCollapsed()) panel.expand();
|
||||
else panel.collapse();
|
||||
}
|
||||
}}
|
||||
onClick={handleToggleSidebar}
|
||||
>
|
||||
<PanelRight />
|
||||
</Button>
|
||||
@@ -1000,9 +1002,27 @@ export function IssueDetail({ issueId, onDelete, onDone, defaultSidebarOpen = tr
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
if (isMobile) {
|
||||
return (
|
||||
<div className="flex flex-1 min-h-0">
|
||||
{detailContent}
|
||||
<Sheet open={mobileSidebarOpen} onOpenChange={setMobileSidebarOpen}>
|
||||
<SheetContent side="right" showCloseButton={false} className="w-[320px] overflow-y-auto p-4">
|
||||
{sidebarContent}
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ResizablePanelGroup orientation="horizontal" className="flex-1 min-h-0" defaultLayout={defaultLayout} onLayoutChanged={onLayoutChanged}>
|
||||
<ResizablePanel id="content" minSize="50%">
|
||||
{detailContent}
|
||||
</ResizablePanel>
|
||||
{!isMobile && <ResizableHandle />}
|
||||
{!isMobile && (
|
||||
<ResizableHandle />
|
||||
<ResizablePanel
|
||||
id="sidebar"
|
||||
defaultSize={defaultSidebarOpen ? 320 : 0}
|
||||
@@ -1011,7 +1031,7 @@ export function IssueDetail({ issueId, onDelete, onDone, defaultSidebarOpen = tr
|
||||
collapsible
|
||||
groupResizeBehavior="preserve-pixel-size"
|
||||
panelRef={sidebarRef}
|
||||
onResize={(size) => setSidebarOpen(size.inPixels > 0)}
|
||||
onResize={(size) => setDesktopSidebarOpen(size.inPixels > 0)}
|
||||
>
|
||||
<div className="overflow-y-auto border-l h-full">
|
||||
<div className="p-4">
|
||||
@@ -1019,14 +1039,6 @@ export function IssueDetail({ issueId, onDelete, onDone, defaultSidebarOpen = tr
|
||||
</div>
|
||||
</div>
|
||||
</ResizablePanel>
|
||||
)}
|
||||
{isMobile && (
|
||||
<Sheet open={sidebarOpen} onOpenChange={setSidebarOpen}>
|
||||
<SheetContent side="right" showCloseButton={false} className="w-[320px] overflow-y-auto p-4">
|
||||
{sidebarContent}
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
)}
|
||||
</ResizablePanelGroup>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user