FN-7888: wire onDeleteTask through right-dock Tasks list

Restores the Delete affordance in the right-dock Tasks list, which previously rendered TaskCard hosts without a delete handler so the menu action silently did nothing.
- Thread onDeleteTask prop through DockTaskList into TaskCard
- Pass onDeleteTask through overflowViewRegistry's Tasks view render props
- Wire onDeleteTask from useRightDockController into both DockTaskList call sites
- Add regression coverage for desktop context-menu and mobile pointer-up delete flows in TaskCard, DockTaskList, and RightDock tests
- Add a patch changeset documenting the fix

Files changed:
 .changeset/fn-7888-task-delete.md                  |  7 ++++
 packages/dashboard/app/components/DockTaskList.tsx |  9 ++++-
 .../app/components/__tests__/DockTaskList.test.tsx | 16 +++++++-
 .../app/components/__tests__/RightDock.test.tsx    | 16 +++++++-
 .../app/components/__tests__/TaskCard.test.tsx     | 44 ++++++++++++++++++++++
 .../app/components/overflowViewRegistry.tsx        |  4 +-
 .../app/components/useRightDockController.tsx      |  2 +
 7 files changed, 93 insertions(+), 5 deletions(-)

Fusion-Task-Id: FN-7888

Fusion-Task-Lineage: 22767aba-fb92-4ac3-8348-ee0cea4345c2

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
gsxdsm
2026-07-12 18:13:33 -07:00
parent dabedcf79c
commit ddf2f3d956
7 changed files with 93 additions and 5 deletions

View File

@@ -0,0 +1,7 @@
---
"@runfusion/fusion": patch
---
summary: Restore task deletion from the right-dock Tasks list.
category: fix
dev: Threads the shared delete handler through right-dock task-card hosts and adds regression coverage for delete menu activation.

View File

@@ -1,5 +1,5 @@
import { useCallback, useMemo, useState } from "react";
import type { Task, TaskDetail } from "@fusion/core";
import type { GithubIssueAction, Task, TaskDetail } from "@fusion/core";
import type { ToastType } from "../hooks/useToast";
import { TaskCard } from "./TaskCard";
import "./DockTaskList.css";
@@ -8,6 +8,7 @@ export interface DockTaskListProps {
tasks: Array<Task | TaskDetail>;
projectId?: string;
onOpenTask?: (task: Task | TaskDetail) => void;
onDeleteTask?: (id: string, options?: { removeDependencyReferences?: boolean; removeLineageReferences?: boolean; githubIssueAction?: GithubIssueAction; allowResurrection?: boolean }) => Promise<Task>;
addToast?: (message: string, type?: ToastType) => void;
prAuthAvailable?: boolean;
autoMergeEnabled?: boolean;
@@ -24,6 +25,7 @@ export function DockTaskList({
tasks,
projectId,
onOpenTask,
onDeleteTask,
addToast = () => {},
prAuthAvailable = false,
autoMergeEnabled = false,
@@ -75,6 +77,11 @@ export function DockTaskList({
task={task as Task}
projectId={projectId}
onOpenDetail={handleOpenTask}
/*
FNXC:TaskDeletion 2026-07-12-18:04:
Every task Delete affordance must reach the shared confirm→delete flow. The right-dock Tasks list is a TaskCard host, so it must pass onDeleteTask instead of rendering cards that silently lack/delete-disable the destructive path.
*/
onDeleteTask={onDeleteTask}
addToast={addToast}
disableDrag={true}
prAuthAvailable={prAuthAvailable}

View File

@@ -4,11 +4,12 @@ import { describe, expect, it, vi } from "vitest";
import { DockTaskList } from "../DockTaskList";
vi.mock("../TaskCard", () => ({
TaskCard: ({ task, onOpenDetail, disableDrag }: { task: Task | TaskDetail; onOpenDetail: (task: Task | TaskDetail) => void; disableDrag?: boolean }) => (
TaskCard: ({ task, onOpenDetail, onDeleteTask, disableDrag }: { task: Task | TaskDetail; onOpenDetail: (task: Task | TaskDetail) => void; onDeleteTask?: (id: string) => Promise<Task>; disableDrag?: boolean }) => (
<button
type="button"
data-testid={`mock-task-card-${task.id}`}
data-disable-drag={String(disableDrag)}
data-has-delete={String(Boolean(onDeleteTask))}
onClick={() => onOpenDetail(task)}
>
{task.title ?? task.id}
@@ -23,6 +24,19 @@ DockTaskList must route TaskCard's own open action to the dock snapshot setter.
const makeTask = (id: string, title: string, column: string) => ({ id, title, column }) as Task;
describe("DockTaskList", () => {
/*
FNXC:TaskDeletion 2026-07-12-00:00:
The reported inert delete localized to the right-dock Tasks list host: it rendered TaskCard without onDeleteTask, so that surface could not enter the shared confirm→delete flow while board/list/detail hosts were wired.
*/
it("threads delete into right-dock TaskCards so the delete affordance can enter the shared flow", () => {
const task = makeTask("FN-DELETE", "Delete from right dock", "triage");
const onDeleteTask = vi.fn(async () => task);
render(<DockTaskList tasks={[task]} onOpenTask={vi.fn()} onDeleteTask={onDeleteTask} addToast={vi.fn()} />);
expect(screen.getByTestId("mock-task-card-FN-DELETE")).toHaveAttribute("data-has-delete", "true");
});
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");

View File

@@ -23,8 +23,8 @@ vi.mock("../TaskDetailModal", () => ({
}));
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)}>
TaskCard: ({ task, onOpenDetail, onDeleteTask }: { task: { id: string; title?: string }; onOpenDetail: (task: { id: string; title?: string }) => void; onDeleteTask?: (id: string) => Promise<unknown> }) => (
<button type="button" data-testid={`mock-task-card-${task.id}`} data-has-delete={String(Boolean(onDeleteTask))} onClick={() => onOpenDetail(task)}>
{task.title ?? task.id}
</button>
),
@@ -164,6 +164,18 @@ describe("RightDock", () => {
expect(screen.getByTestId("right-dock-files-view")).toHaveAttribute("data-layout", "two-pane");
});
it("threads delete into the compact Tasks tab cards", () => {
const tasks = [
{ id: "FN-DELETE", title: "Right dock delete", column: "triage" },
];
const onDeleteTask = vi.fn();
render(<TestRightDock open={true} renderProps={{ ...renderProps, tasks, onDeleteTask }} />);
fireEvent.click(screen.getByTestId("right-dock-tab-tasks"));
expect(screen.getByTestId("mock-task-card-FN-DELETE")).toHaveAttribute("data-has-delete", "true");
});
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" },

View File

@@ -1079,6 +1079,50 @@ describe("TaskCard", () => {
});
});
it("runs the delete flow from the desktop task context menu", async () => {
const onDeleteTask = vi.fn(async () => makeTask());
mockConfirm.mockResolvedValueOnce(true);
render(
<TaskCard
task={makeTask({ column: "todo", githubTracking: { enabled: false }, sourceIssue: undefined } as any)}
onOpenDetail={noop}
addToast={noop}
onDeleteTask={onDeleteTask}
/>,
);
fireEvent.contextMenu(document.querySelector(".card")!, { clientX: 24, clientY: 28 });
fireEvent.click(screen.getByRole("menuitem", { name: "Delete" }));
await waitFor(() => {
expect(mockConfirm).toHaveBeenCalledWith(expect.objectContaining({ title: "Delete Task" }));
expect(onDeleteTask).toHaveBeenCalledWith("FN-001");
});
});
it("runs the delete flow from the mobile pointer-up task context menu", async () => {
const onDeleteTask = vi.fn(async () => makeTask());
mockConfirm.mockResolvedValueOnce(true);
render(
<TaskCard
task={makeTask({ column: "todo", githubTracking: { enabled: false }, sourceIssue: undefined } as any)}
onOpenDetail={noop}
addToast={noop}
onDeleteTask={onDeleteTask}
/>,
);
fireEvent.contextMenu(document.querySelector(".card")!, { clientX: 24, clientY: 28 });
fireEvent.pointerUp(screen.getByRole("menuitem", { name: "Delete" }), { pointerType: "touch", pointerId: 7 });
await waitFor(() => {
expect(mockConfirm).toHaveBeenCalledWith(expect.objectContaining({ title: "Delete Task" }));
expect(onDeleteTask).toHaveBeenCalledWith("FN-001");
});
});
it("preserves githubIssueAction on dependency-conflict retry", async () => {
const conflict = new Error("Cannot delete task FN-001: still referenced as a dependency by FN-002.") as Error & { status: number; details: { code: string; dependentIds: string[] } };
conflict.status = 409;

View File

@@ -11,7 +11,7 @@ import {
Monitor,
type LucideProps,
} from "lucide-react";
import type { Task, TaskDetail, WorkflowStep } from "@fusion/core";
import type { GithubIssueAction, Task, TaskDetail, WorkflowStep } from "@fusion/core";
import type { PluginDashboardViewEntry } from "../api";
import type { ToastType } from "../hooks/useToast";
import { buildPluginTaskViewId } from "../plugins/pluginViewRegistry";
@@ -79,6 +79,7 @@ export interface OverflowViewRenderProps {
onOpenSettings?: (section?: string) => void;
onOpenTaskDetail?: (taskId: string) => void;
onOpenTaskInDock?: (task: Task | TaskDetail) => void;
onDeleteTask?: (id: string, options?: { removeDependencyReferences?: boolean; removeLineageReferences?: boolean; githubIssueAction?: GithubIssueAction; allowResurrection?: boolean }) => Promise<Task>;
onOpenDetail?: (task: Task | TaskDetail, initialTab?: DetailTaskTab) => void;
onSendSelectionToTask?: (description: string) => void;
onCreateTaskFromInsight?: (payload: { insightId: string; title: string; description: string }) => Promise<void> | void;
@@ -157,6 +158,7 @@ export const STATIC_OVERFLOW_VIEW_ENTRIES: readonly OverflowViewEntry[] = [
tasks={props.tasks ?? []}
projectId={props.projectId}
onOpenTask={props.onOpenTaskInDock}
onDeleteTask={props.onDeleteTask}
addToast={props.addToast}
prAuthAvailable={false}
autoMergeEnabled={false}

View File

@@ -168,6 +168,7 @@ export function useRightDockController(input: RightDockControllerInput): RightDo
task={task}
projectId={input.projectId}
onOpenDetail={(value: Task | TaskDetail) => input.openDetailTask(value)}
onDeleteTask={input.onDeleteTask}
addToast={input.addToast}
disableDrag={true}
prAuthAvailable={input.prAuthAvailable}
@@ -212,6 +213,7 @@ export function useRightDockController(input: RightDockControllerInput): RightDo
DockTaskList rows must open through the controller's ordinary right-dock task route, not TaskCard's canonical full task modal. Thread one controller-level handler into registry render props so both compact and expanded Tasks lists share popup-setting routing and setting-off dock-detail behavior.
*/
onOpenTaskInDock: openTaskFromDockList,
onDeleteTask: input.onDeleteTask,
onOpenDetail: input.openDetailTask,
onSendSelectionToTask: input.onSendSelectionToTask,
onCreateTaskFromInsight: input.onCreateTaskFromInsight,