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:
7
.changeset/fn-7888-task-delete.md
Normal file
7
.changeset/fn-7888-task-delete.md
Normal 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.
|
||||
@@ -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}
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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" },
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user