FN-7218: default the task sidebar to active work
Default the right-dock task sidebar to active work and make task-detail navigation reversible. - Filter the dock Tasks list to hide done tasks by default while keeping archived tasks out of the compact sidebar. - Add a Show Done toggle, updated empty states, and styling for the compact controls. - Convert the task-detail header arrow into an accessible back button that uses the existing close-detail path. - Document the sidebar behavior, add a patch changeset, and cover the active/default/back-button flows in tests. Files changed: .changeset/fn-7218-task-sidebar-active-list.md | 7 ++ docs/dashboard-guide.md | 5 +- packages/dashboard/app/components/DockTaskList.css | 37 +++++++++ packages/dashboard/app/components/DockTaskList.tsx | 53 ++++++++++--- packages/dashboard/app/components/RightDock.tsx | 17 ++++- .../app/components/__tests__/DockTaskList.test.tsx | 89 ++++++++++++++++++++-- .../app/components/__tests__/RightDock.test.tsx | 69 ++++++++++++----- 7 files changed, 239 insertions(+), 38 deletions(-) Fusion-Task-Id: FN-7218 Fusion-Task-Lineage: ce8fe55e-a89e-4ca6-940e-2ca06c3c26df Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
7
.changeset/fn-7218-task-sidebar-active-list.md
Normal file
7
.changeset/fn-7218-task-sidebar-active-list.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"@runfusion/fusion": patch
|
||||
---
|
||||
|
||||
summary: Show active tasks by default in the right sidebar and fix its task-detail back button.
|
||||
category: fix
|
||||
dev: Filters the right-dock Tasks list to active tasks by default, adds a Show Done toggle, keeps archived tasks hidden, and wires the header back arrow to the existing dock task close path.
|
||||
@@ -56,14 +56,15 @@ If **Settings → Appearance → Open tasks in the right sidebar** is enabled, b
|
||||
|
||||
<!-- FNXC:DashboardNavigationDocs 2026-06-27-00:00: The right dock now hosts Chat as an inline tool panel; keep this user-facing roster aligned with STATIC_OVERFLOW_VIEW_ENTRIES so users know Chat can also pop out from the dock. -->
|
||||
<!-- FNXC:RightDockTasks 2026-06-28-19:55: The dock task-detail overlay is now anchored to the first-class Tasks tool tab. Document that Tasks is a dock-only auxiliary surface with a last-viewed detail/list fallback, not a new primary navigation destination. -->
|
||||
The dock toolbar has built-in inline tool panels for **Tasks**, **Files**, **Chat**, **Activity Log**, **Git Manager**, **Dev Server** when enabled, **Secrets**, **Todos** when enabled, and **Pull Requests**. These tools render in embedded mode inside the dock instead of opening fixed popup overlays; **Files** opens by default and is the fallback when browser storage points at a removed dock key. The **Tasks** tab shows the last task opened in the dock; when no dock task is active, it shows a compact clickable task list, and the task-detail back button returns to that list. Inline dock views have an expand button that opens the same view in a resizable modal for more room. The right-dock **Files** viewer and its expanded pop-out match the Files modal for browser-previewable file types: image, video/movie, audio, and PDF selections render as native browser previews, while editable text files keep the editor and save flow. Plugin overflow views may add additional right-dock tool tabs, except plugin destinations that explicitly belong in the left sidebar.
|
||||
<!-- FNXC:RightDockTasks 2026-06-28-21:08: The right-dock Tasks list is an active-work queue by default: completed tasks require the local Show Done toggle, archived tasks stay hidden, and both task-detail back affordances return to this list. -->
|
||||
The dock toolbar has built-in inline tool panels for **Tasks**, **Files**, **Chat**, **Activity Log**, **Git Manager**, **Dev Server** when enabled, **Secrets**, **Todos** when enabled, and **Pull Requests**. These tools render in embedded mode inside the dock instead of opening fixed popup overlays; **Files** opens by default and is the fallback when browser storage points at a removed dock key. The **Tasks** tab shows the last task opened in the dock; when no dock task is active, it shows a compact clickable active-task list. Use **Show Done** to include completed tasks in that compact list; archived tasks stay hidden there. Either task-detail back button returns to the Tasks list. Inline dock views have an expand button that opens the same view in a resizable modal for more room. The right-dock **Files** viewer and its expanded pop-out match the Files modal for browser-previewable file types: image, video/movie, audio, and PDF selections render as native browser previews, while editable text files keep the editor and save flow. Plugin overflow views may add additional right-dock tool tabs, except plugin destinations that explicitly belong in the left sidebar.
|
||||
|
||||
Use the desktop/tablet right dock this way:
|
||||
|
||||
1. Open a project screen with **Right Dock Panel** enabled.
|
||||
Expected outcome: the dock appears on the far right with **Files** selected unless a valid previous dock view is stored.
|
||||
2. Select **Tasks**, **Chat**, **Activity Log**, **Git Manager**, **Files**, or another available tool in the dock toolbar.
|
||||
Expected outcome: the selected tool renders inline inside the dock body and the toolbar tab becomes active; **Tasks** either restores the last-viewed dock task or shows the compact task list.
|
||||
Expected outcome: the selected tool renders inline inside the dock body and the toolbar tab becomes active; **Tasks** either restores the last-viewed dock task or shows the compact active-task list with optional **Show Done** completed-task visibility.
|
||||
3. Drag the dock's left-edge resize handle, or focus the separator and use the arrow keys.
|
||||
Expected outcome: the dock width changes within its min/max bounds and is saved for future reloads.
|
||||
4. Select the dock expand action.
|
||||
|
||||
@@ -8,6 +8,17 @@
|
||||
padding: var(--space-sm);
|
||||
}
|
||||
|
||||
.dock-task-list__controls {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
justify-content: flex-end;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.dock-task-list__toggle-done {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.dock-task-list__row {
|
||||
min-width: 0;
|
||||
}
|
||||
@@ -20,6 +31,18 @@
|
||||
padding: var(--space-lg);
|
||||
}
|
||||
|
||||
.dock-task-list--empty .dock-task-list__controls {
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.dock-task-list__empty {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: var(--space-xs);
|
||||
}
|
||||
|
||||
.dock-task-list__empty-title,
|
||||
.dock-task-list__empty-copy {
|
||||
margin: 0;
|
||||
@@ -34,3 +57,17 @@
|
||||
max-width: calc(var(--space-xl) * 12);
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.dock-task-list {
|
||||
padding: var(--space-sm);
|
||||
}
|
||||
|
||||
.dock-task-list__controls {
|
||||
justify-content: stretch;
|
||||
}
|
||||
|
||||
.dock-task-list__toggle-done {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useCallback } from "react";
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
import type { Task, TaskDetail } from "@fusion/core";
|
||||
import type { ToastType } from "../hooks/useToast";
|
||||
import { TaskCard } from "./TaskCard";
|
||||
@@ -16,6 +16,9 @@ export interface DockTaskListProps {
|
||||
/*
|
||||
FNXC:RightDockTasks 2026-06-28-16:50:
|
||||
The Tasks tab empty state is a real compact task list, not a blank placeholder. TaskCard's own open callback is routed directly to `onOpenTask` so clicking the card opens the dock Tasks detail with the back button; no wrapper click handler competes with TaskCard or the full-panel detail modal.
|
||||
|
||||
FNXC:RightDockTasks 2026-06-28-18:25:
|
||||
The compact right-dock Tasks list is an active-work queue by default. It hides completed work until the local Show Done toggle is enabled and never renders archived tasks, including in the expanded dock modal that reuses this component.
|
||||
*/
|
||||
export function DockTaskList({
|
||||
tasks,
|
||||
@@ -25,23 +28,49 @@ export function DockTaskList({
|
||||
prAuthAvailable = false,
|
||||
autoMergeEnabled = false,
|
||||
}: DockTaskListProps) {
|
||||
const [showDone, setShowDone] = useState(false);
|
||||
|
||||
const handleOpenTask = useCallback((task: Task | TaskDetail) => {
|
||||
onOpenTask?.(task);
|
||||
}, [onOpenTask]);
|
||||
|
||||
if (tasks.length === 0) {
|
||||
return (
|
||||
<div className="dock-task-list dock-task-list--empty" data-testid="dock-task-list">
|
||||
<p className="dock-task-list__empty-title">No tasks yet</p>
|
||||
<p className="dock-task-list__empty-copy">Tasks you create or import will appear here for quick right-sidebar review.</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
const doneTasks = useMemo(() => tasks.filter((task) => task.column === "done"), [tasks]);
|
||||
const visibleTasks = useMemo(() => tasks.filter((task) => {
|
||||
if (task.column === "archived") return false;
|
||||
if (task.column === "done") return showDone;
|
||||
return true;
|
||||
}), [showDone, tasks]);
|
||||
const hasDoneTasks = doneTasks.length > 0;
|
||||
const isEmpty = visibleTasks.length === 0;
|
||||
const emptyTitle = tasks.length === 0 ? "No tasks yet" : "No active tasks";
|
||||
const emptyCopy = tasks.length === 0
|
||||
? "Tasks you create or import will appear here for quick right-sidebar review."
|
||||
: hasDoneTasks
|
||||
? "Completed tasks are hidden until you choose Show Done. Archived tasks stay out of this compact sidebar."
|
||||
: "Archived tasks stay out of this compact sidebar. Active tasks will appear here when work is available.";
|
||||
const toggleLabel = showDone ? "Hide Done" : "Show Done";
|
||||
|
||||
return (
|
||||
<div className="dock-task-list" data-testid="dock-task-list">
|
||||
{tasks.map((task) => (
|
||||
<div key={task.id} className="dock-task-list__row" data-testid={`dock-task-list-row-${task.id}`}>
|
||||
<div className={`dock-task-list${isEmpty ? " dock-task-list--empty" : ""}`} data-testid="dock-task-list">
|
||||
{hasDoneTasks ? (
|
||||
<div className="dock-task-list__controls">
|
||||
<button
|
||||
type="button"
|
||||
className="btn dock-task-list__toggle-done"
|
||||
aria-pressed={showDone}
|
||||
onClick={() => setShowDone((current) => !current)}
|
||||
>
|
||||
{toggleLabel}
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
{isEmpty ? (
|
||||
<div className="dock-task-list__empty" data-testid="dock-task-list-empty">
|
||||
<p className="dock-task-list__empty-title">{emptyTitle}</p>
|
||||
<p className="dock-task-list__empty-copy">{emptyCopy}</p>
|
||||
</div>
|
||||
) : visibleTasks.map((task, index) => (
|
||||
<div key={`${task.id}-${index}`} className="dock-task-list__row" data-testid={`dock-task-list-row-${task.id}`}>
|
||||
<TaskCard
|
||||
task={task as Task}
|
||||
projectId={projectId}
|
||||
|
||||
@@ -346,7 +346,22 @@ export function RightDock({
|
||||
{open ? (
|
||||
<>
|
||||
<div className="right-dock__header">
|
||||
{showingDockTask ? <ArrowLeft size={16} /> : <SelectedIcon size={16} />}
|
||||
{showingDockTask ? (
|
||||
/*
|
||||
FNXC:RightDockTasks 2026-06-28-18:31:
|
||||
The task-detail header arrow is a real back button and shares the same close path as the top-right return affordance, so either visible control returns the dock to the Tasks list without leaving an inert icon or stale detail shell.
|
||||
*/
|
||||
<button
|
||||
type="button"
|
||||
className="btn-icon right-dock__header-back"
|
||||
aria-label={closeDockTaskLabel}
|
||||
title={closeDockTaskLabel}
|
||||
data-testid="right-dock-header-back-task"
|
||||
onClick={onCloseDockTask}
|
||||
>
|
||||
<ArrowLeft size={16} />
|
||||
</button>
|
||||
) : <SelectedIcon size={16} />}
|
||||
<div className="right-dock__title" role="heading" aria-level={3}>{showingDockTask ? t("rightDock.taskDetailTitle", "Task detail") : selectedEntry.label}</div>
|
||||
</div>
|
||||
<div className="right-dock__body" role="tabpanel" aria-label={showingDockTask ? t("rightDock.taskDetailTitle", "Task detail") : selectedEntry.label} data-testid="right-dock-body">
|
||||
|
||||
@@ -20,10 +20,12 @@ vi.mock("../TaskCard", () => ({
|
||||
FNXC:RightDockTasks 2026-06-28-17:15:
|
||||
DockTaskList must route TaskCard's own open action to the dock snapshot setter. This explicitly guards against a nested row/card handler split where the card opens the full detail modal while the wrapper also opens the dock detail.
|
||||
*/
|
||||
const makeTask = (id: string, title: string, column: string) => ({ id, title, column }) as Task;
|
||||
|
||||
describe("DockTaskList", () => {
|
||||
it("renders populated task rows and routes TaskCard opens to onOpenTask", () => {
|
||||
const first = { id: "FN-1", title: "First task", column: "todo" } as Task;
|
||||
const second = { id: "FN-2", title: "Second task", column: "in-progress" } as Task;
|
||||
it("renders populated active task rows and routes TaskCard opens to onOpenTask", () => {
|
||||
const first = makeTask("FN-1", "First task", "todo");
|
||||
const second = makeTask("FN-2", "Second task", "in-progress");
|
||||
const onOpenTask = vi.fn();
|
||||
|
||||
render(<DockTaskList tasks={[first, second]} onOpenTask={onOpenTask} addToast={vi.fn()} />);
|
||||
@@ -38,11 +40,88 @@ describe("DockTaskList", () => {
|
||||
expect(onOpenTask).toHaveBeenCalledWith(second);
|
||||
});
|
||||
|
||||
it("renders a friendly empty message and no task rows when there are no tasks", () => {
|
||||
render(<DockTaskList tasks={[]} onOpenTask={vi.fn()} addToast={vi.fn()} />);
|
||||
/*
|
||||
FNXC:RightDockTasks 2026-06-28-18:38:
|
||||
The right-dock Tasks list is active-by-default: done tasks are opt-in via Show Done, archived tasks never appear, and the incoming active/done order is preserved when completed work is shown.
|
||||
*/
|
||||
it("hides done and archived tasks by default, then toggles done tasks without showing archived rows", () => {
|
||||
const active = makeTask("FN-ACTIVE", "Active task", "todo");
|
||||
const done = makeTask("FN-DONE", "Done task", "done");
|
||||
const laterActive = makeTask("FN-LATER", "Later active task", "in-progress");
|
||||
const archived = makeTask("FN-ARCHIVED", "Archived task", "archived");
|
||||
const onOpenTask = vi.fn();
|
||||
|
||||
render(<DockTaskList tasks={[active, done, laterActive, archived]} onOpenTask={onOpenTask} addToast={vi.fn()} />);
|
||||
|
||||
expect(screen.getByTestId("dock-task-list-row-FN-ACTIVE")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("dock-task-list-row-FN-LATER")).toBeInTheDocument();
|
||||
expect(screen.queryByTestId("dock-task-list-row-FN-DONE")).toBeNull();
|
||||
expect(screen.queryByTestId("dock-task-list-row-FN-ARCHIVED")).toBeNull();
|
||||
|
||||
const showDone = screen.getByRole("button", { name: "Show Done" });
|
||||
expect(showDone).toHaveAttribute("aria-pressed", "false");
|
||||
fireEvent.click(showDone);
|
||||
|
||||
expect(screen.getByRole("button", { name: "Hide Done" })).toHaveAttribute("aria-pressed", "true");
|
||||
expect(screen.queryByTestId("dock-task-list-row-FN-ARCHIVED")).toBeNull();
|
||||
expect(screen.getAllByTestId(/dock-task-list-row-/).map((row) => row.getAttribute("data-testid"))).toEqual([
|
||||
"dock-task-list-row-FN-ACTIVE",
|
||||
"dock-task-list-row-FN-DONE",
|
||||
"dock-task-list-row-FN-LATER",
|
||||
]);
|
||||
|
||||
fireEvent.click(screen.getByTestId("mock-task-card-FN-DONE"));
|
||||
expect(onOpenTask).toHaveBeenCalledWith(done);
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: "Hide Done" }));
|
||||
expect(screen.queryByTestId("dock-task-list-row-FN-DONE")).toBeNull();
|
||||
expect(screen.queryByTestId("dock-task-list-row-FN-ARCHIVED")).toBeNull();
|
||||
});
|
||||
|
||||
/*
|
||||
FNXC:RightDockTasks 2026-06-28-18:42:
|
||||
Empty right-dock task states must distinguish a truly empty list from a list whose only rows are completed or archived, so the compact panel never renders blank and the Show Done affordance remains reachable when completed rows exist.
|
||||
*/
|
||||
it("renders distinct empty states for no tasks, only done tasks, and only archived tasks", () => {
|
||||
const { rerender } = render(<DockTaskList tasks={[]} onOpenTask={vi.fn()} addToast={vi.fn()} />);
|
||||
|
||||
expect(screen.getByTestId("dock-task-list")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("dock-task-list-empty")).toBeInTheDocument();
|
||||
expect(screen.getByText("No tasks yet")).toBeInTheDocument();
|
||||
expect(screen.queryByRole("button", { name: /done/i })).toBeNull();
|
||||
expect(screen.queryByTestId(/dock-task-list-row-/)).toBeNull();
|
||||
|
||||
rerender(<DockTaskList tasks={[makeTask("FN-DONE", "Done only", "done")]} onOpenTask={vi.fn()} addToast={vi.fn()} />);
|
||||
expect(screen.getByText("No active tasks")).toBeInTheDocument();
|
||||
expect(screen.getByRole("button", { name: "Show Done" })).toBeInTheDocument();
|
||||
expect(screen.getByText(/Archived tasks stay out/i)).toBeInTheDocument();
|
||||
expect(screen.queryByTestId("dock-task-list-row-FN-DONE")).toBeNull();
|
||||
|
||||
rerender(<DockTaskList tasks={[makeTask("FN-ARCHIVED", "Archived only", "archived")]} onOpenTask={vi.fn()} addToast={vi.fn()} />);
|
||||
expect(screen.getByText("No active tasks")).toBeInTheDocument();
|
||||
expect(screen.queryByRole("button", { name: /done/i })).toBeNull();
|
||||
expect(screen.getByText(/Archived tasks stay out/i)).toBeInTheDocument();
|
||||
expect(screen.queryByTestId("dock-task-list-row-FN-ARCHIVED")).toBeNull();
|
||||
});
|
||||
|
||||
it("renders duplicate task ids as distinct rows without duplicate React key warnings", () => {
|
||||
const duplicateFirst = makeTask("FN-DUP", "Duplicate first", "todo");
|
||||
const duplicateSecond = makeTask("FN-DUP", "Duplicate second", "in-progress");
|
||||
const consoleError = vi.spyOn(console, "error").mockImplementation(() => {});
|
||||
const onOpenTask = vi.fn();
|
||||
|
||||
try {
|
||||
render(<DockTaskList tasks={[duplicateFirst, duplicateSecond]} onOpenTask={onOpenTask} addToast={vi.fn()} />);
|
||||
|
||||
expect(screen.getAllByTestId("dock-task-list-row-FN-DUP")).toHaveLength(2);
|
||||
expect(screen.getByText("Duplicate first")).toBeInTheDocument();
|
||||
expect(screen.getByText("Duplicate second")).toBeInTheDocument();
|
||||
expect(consoleError.mock.calls.some((call) => String(call[0]).includes("Encountered two children with the same key"))).toBe(false);
|
||||
|
||||
fireEvent.click(screen.getByText("Duplicate second"));
|
||||
expect(onOpenTask).toHaveBeenCalledWith(duplicateSecond);
|
||||
} finally {
|
||||
consoleError.mockRestore();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -20,15 +20,11 @@ vi.mock("../TaskDetailModal", () => ({
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock("../DockTaskList", () => ({
|
||||
DockTaskList: ({ tasks = [], onOpenTask }: { tasks?: Array<{ id: string; title?: string }>; onOpenTask?: (task: { id: string; title?: string }) => void }) => (
|
||||
<div data-testid="dock-task-list">
|
||||
{tasks.length === 0 ? <span>No tasks yet</span> : tasks.map((task) => (
|
||||
<button key={task.id} type="button" data-testid={`dock-task-list-row-${task.id}`} onClick={() => onOpenTask?.(task)}>
|
||||
{task.title ?? task.id}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
vi.mock("../TaskCard", () => ({
|
||||
TaskCard: ({ task, onOpenDetail }: { task: { id: string; title?: string }; onOpenDetail: (task: { id: string; title?: string }) => void }) => (
|
||||
<button type="button" data-testid={`mock-task-card-${task.id}`} onClick={() => onOpenDetail(task)}>
|
||||
{task.title ?? task.id}
|
||||
</button>
|
||||
),
|
||||
}));
|
||||
|
||||
@@ -149,16 +145,26 @@ describe("RightDock", () => {
|
||||
expect(screen.getByTestId("right-dock-files-view")).toHaveAttribute("data-layout", "two-pane");
|
||||
});
|
||||
|
||||
it("renders the Tasks tab list at both narrow and wide dock widths", () => {
|
||||
const { unmount } = render(<TestRightDock open={true} renderProps={{ ...renderProps, tasks: [] }} />);
|
||||
it("renders the filtered Tasks tab list at both narrow and wide dock widths", () => {
|
||||
const tasks = [
|
||||
{ id: "FN-ACTIVE", title: "Active dock task", column: "todo" },
|
||||
{ id: "FN-DONE", title: "Done dock task", column: "done" },
|
||||
{ id: "FN-ARCHIVED", title: "Archived dock task", column: "archived" },
|
||||
];
|
||||
const { unmount } = render(<TestRightDock open={true} renderProps={{ ...renderProps, tasks }} />);
|
||||
fireEvent.click(screen.getByTestId("right-dock-tab-tasks"));
|
||||
expect(screen.getByTestId("dock-task-list")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("dock-task-list-row-FN-ACTIVE")).toBeInTheDocument();
|
||||
expect(screen.queryByTestId("dock-task-list-row-FN-DONE")).toBeNull();
|
||||
expect(screen.queryByTestId("dock-task-list-row-FN-ARCHIVED")).toBeNull();
|
||||
unmount();
|
||||
|
||||
window.localStorage.setItem(RIGHT_DOCK_WIDTH_STORAGE_KEY, "900");
|
||||
render(<TestRightDock open={true} renderProps={{ ...renderProps, tasks: [] }} />);
|
||||
render(<TestRightDock open={true} renderProps={{ ...renderProps, tasks }} />);
|
||||
fireEvent.click(screen.getByTestId("right-dock-tab-tasks"));
|
||||
expect(screen.getByTestId("dock-task-list")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("dock-task-list-row-FN-ACTIVE")).toBeInTheDocument();
|
||||
expect(screen.queryByTestId("dock-task-list-row-FN-ARCHIVED")).toBeNull();
|
||||
});
|
||||
|
||||
it("falls back to Files when storage points at a removed right-dock view", () => {
|
||||
@@ -184,7 +190,14 @@ describe("RightDock", () => {
|
||||
expect(screen.queryByTestId("right-dock-collapse-toggle")).toBeNull();
|
||||
});
|
||||
|
||||
it("anchors dock task detail to Tasks and returns to the task list from the close affordance", () => {
|
||||
/*
|
||||
FNXC:RightDockTasks 2026-06-28-18:48:
|
||||
Dock task detail exposes two visible return affordances. The header back button and toolbar return button must share the same close callback and leave only the Tasks list mounted after the controller clears the dock snapshot.
|
||||
*/
|
||||
it.each([
|
||||
["right-dock-close-task"],
|
||||
["right-dock-header-back-task"],
|
||||
])("anchors dock task detail to Tasks and returns to the task list from %s", (buttonTestId) => {
|
||||
const onCloseDockTask = vi.fn();
|
||||
const { rerender } = render(
|
||||
<TestRightDock
|
||||
@@ -199,14 +212,20 @@ describe("RightDock", () => {
|
||||
expect(screen.getByTestId("right-dock-tab-tasks")).toHaveAttribute("aria-selected", "true");
|
||||
expect(screen.getByTestId("right-dock-body")).toHaveTextContent("Sidebar task");
|
||||
expect(screen.queryByTestId("right-dock-files-view")).toBeNull();
|
||||
expect(screen.getAllByTestId("right-dock-header-back-task")).toHaveLength(1);
|
||||
expect(screen.getByTestId("right-dock-header-back-task")).toHaveAttribute("aria-label", screen.getByTestId("right-dock-close-task").getAttribute("aria-label"));
|
||||
expect(screen.getByText("Task detail")).toBeInTheDocument();
|
||||
|
||||
fireEvent.click(screen.getByTestId("right-dock-close-task"));
|
||||
fireEvent.click(screen.getByTestId(buttonTestId));
|
||||
expect(onCloseDockTask).toHaveBeenCalledTimes(1);
|
||||
|
||||
rerender(<TestRightDock open={true} renderProps={{ ...renderProps, tasks: [] }} dockTask={null} dockTaskContent={null} onCloseDockTask={onCloseDockTask} />);
|
||||
expect(screen.getByTestId("right-dock-tab-tasks")).toHaveAttribute("aria-selected", "true");
|
||||
expect(screen.getByTestId("dock-task-list")).toBeInTheDocument();
|
||||
expect(screen.queryByTestId("dock-task-detail")).toBeNull();
|
||||
expect(screen.queryByTestId("right-dock-header-back-task")).toBeNull();
|
||||
expect(screen.queryByTestId("right-dock-close-task")).toBeNull();
|
||||
expect(screen.queryByText("Task detail")).toBeNull();
|
||||
expect(screen.queryByTestId("right-dock-files-view")).toBeNull();
|
||||
});
|
||||
|
||||
@@ -279,7 +298,7 @@ describe("RightDock", () => {
|
||||
|
||||
fireEvent.click(screen.getByTestId("right-dock-tab-tasks"));
|
||||
expect(screen.getByTestId("dock-task-list")).toBeInTheDocument();
|
||||
fireEvent.click(screen.getByTestId("dock-task-list-row-FN-1"));
|
||||
fireEvent.click(screen.getByTestId("mock-task-card-FN-1"));
|
||||
expect(openDetailTask).not.toHaveBeenCalled();
|
||||
expect(screen.getByTestId("dock-task-detail")).toHaveTextContent("First task");
|
||||
|
||||
@@ -605,20 +624,34 @@ describe("RightDock", () => {
|
||||
focusButton.remove();
|
||||
});
|
||||
|
||||
it("renders the Tasks list in the expanded modal and routes row clicks back to the dock", () => {
|
||||
/*
|
||||
FNXC:RightDockTasks 2026-06-28-18:54:
|
||||
The expanded Tasks modal is a registry-rendered DockTaskList surface, not a separate task renderer, so it inherits active-by-default filtering and the archived-never-shown contract while preserving dock row routing.
|
||||
*/
|
||||
it("renders the filtered Tasks list in the expanded modal and routes row clicks back to the dock", () => {
|
||||
const onOpenTaskInDock = vi.fn();
|
||||
const task = { id: "FN-EXPAND", title: "Expanded task", column: "todo" };
|
||||
const doneTask = { id: "FN-EXPAND-DONE", title: "Expanded done task", column: "done" };
|
||||
const archivedTask = { id: "FN-EXPAND-ARCHIVED", title: "Expanded archived task", column: "archived" };
|
||||
render(
|
||||
<RightDockExpandModal
|
||||
viewKey="tasks"
|
||||
renderProps={{ ...renderProps, tasks: [task], onOpenTaskInDock }}
|
||||
renderProps={{ ...renderProps, tasks: [task, doneTask, archivedTask], onOpenTaskInDock }}
|
||||
onClose={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByTestId("right-dock-expand-modal")).toHaveAttribute("aria-label", "Tasks expanded");
|
||||
expect(screen.getByTestId("dock-task-list")).toBeInTheDocument();
|
||||
fireEvent.click(screen.getByTestId("dock-task-list-row-FN-EXPAND"));
|
||||
expect(screen.getByTestId("dock-task-list-row-FN-EXPAND")).toBeInTheDocument();
|
||||
expect(screen.queryByTestId("dock-task-list-row-FN-EXPAND-DONE")).toBeNull();
|
||||
expect(screen.queryByTestId("dock-task-list-row-FN-EXPAND-ARCHIVED")).toBeNull();
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: "Show Done" }));
|
||||
expect(screen.getByTestId("dock-task-list-row-FN-EXPAND-DONE")).toBeInTheDocument();
|
||||
expect(screen.queryByTestId("dock-task-list-row-FN-EXPAND-ARCHIVED")).toBeNull();
|
||||
|
||||
fireEvent.click(screen.getByTestId("mock-task-card-FN-EXPAND"));
|
||||
expect(onOpenTaskInDock).toHaveBeenCalledWith(task);
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user