feat(FN-4996): complete Step 3 — add archive-instead delete flow in task detail modal

Fusion-Task-Id: FN-4996
Fusion-Task-Lineage: 650bb5e4-18f2-41a0-ae46-b9112c4a0d74
This commit is contained in:
Fusion (runfusion.ai)
2026-05-18 05:35:03 -07:00
committed by gsxdsm
parent 4b5c7475f4
commit 658a7e73e5
5 changed files with 77 additions and 12 deletions

View File

@@ -1923,7 +1923,7 @@ function AppInner() {
handleSubtaskTasksCreated,
handleGitHubImport,
}}
taskOperations={{ moveTask, deleteTask, mergeTask, retryTask, resetTask, duplicateTask }}
taskOperations={{ moveTask, deleteTask, mergeTask, archiveTask, retryTask, resetTask, duplicateTask }}
deepLink={{ handleDetailClose }}
settings={{ prAuthAvailable, themeMode, colorTheme, dashboardFontScalePct, setThemeMode, setColorTheme, setDashboardFontScalePct }}
onSettingsClose={handleSettingsClose}

View File

@@ -56,6 +56,7 @@ interface AppModalsProps {
moveTask: (taskId: string, column: Column, optionsOrPosition?: { preserveProgress?: boolean } | number) => Promise<Task>;
deleteTask: (taskId: string, options?: { removeDependencyReferences?: boolean; githubIssueAction?: GithubIssueAction }) => Promise<Task>;
mergeTask: (taskId: string) => Promise<MergeResult>;
archiveTask: (taskId: string) => Promise<Task>;
retryTask: (taskId: string) => Promise<Task>;
resetTask: (taskId: string) => Promise<Task>;
duplicateTask: (taskId: string) => Promise<Task>;
@@ -171,6 +172,7 @@ export function AppModals({
onMoveTask={taskOperations.moveTask}
onDeleteTask={taskOperations.deleteTask}
onMergeTask={taskOperations.mergeTask}
onArchiveTask={taskOperations.archiveTask}
onRetryTask={taskOperations.retryTask}
onResetTask={taskOperations.resetTask}
onDuplicateTask={taskOperations.duplicateTask}

View File

@@ -284,6 +284,7 @@ export interface TaskDetailModalProps {
onOpenDetail: (task: Task | TaskDetail) => void; // For clicking dependencies
onMoveTask: (id: string, column: Column, optionsOrPosition?: { preserveProgress?: boolean } | number) => Promise<Task>;
onDeleteTask: (id: string, options?: { removeDependencyReferences?: boolean; githubIssueAction?: GithubIssueAction }) => Promise<Task>;
onArchiveTask?: (id: string) => Promise<Task>;
onMergeTask: (id: string) => Promise<MergeResult>;
onRetryTask?: (id: string) => Promise<Task>;
onResetTask?: (id: string) => Promise<Task>;
@@ -454,6 +455,7 @@ export function TaskDetailContent({
onOpenDetail,
onMoveTask,
onDeleteTask,
onArchiveTask,
onMergeTask,
onRetryTask,
onResetTask,
@@ -1310,7 +1312,7 @@ export function TaskDetailContent({
const fileInputRef = useRef<HTMLInputElement>(null);
const { nodes } = useNodes();
const { confirm } = useConfirm();
const { confirm, confirmWithChoice } = useConfirm();
const handleUnlinkGithubIssue = useCallback(async () => {
if (!canEdit || !githubTrackedIssue || isSavingGithubTracking) return;
@@ -1401,12 +1403,36 @@ export function TaskDetailContent({
);
const handleDelete = useCallback(async () => {
const shouldDelete = await confirm({
title: "Delete Task",
message: `Delete ${task.id}?`,
danger: true,
});
if (!shouldDelete) return;
if (task.column === "done" && onArchiveTask) {
const deleteChoice = await confirmWithChoice({
title: "Delete Task",
message: `Delete ${task.id}?`,
confirmLabel: "Delete",
cancelLabel: "Cancel",
tertiaryLabel: "Archive Instead",
danger: true,
});
if (deleteChoice === "tertiary") {
try {
await onArchiveTask(task.id);
addToast(`Archived ${task.id}`, "success");
requestClose();
} catch (err) {
addToast(getErrorMessage(err), "error");
}
return;
}
if (deleteChoice !== "primary") {
return;
}
} else {
const shouldDelete = await confirm({
title: "Delete Task",
message: `Delete ${task.id}?`,
danger: true,
});
if (!shouldDelete) return;
}
const trackedIssue = task.githubTracking?.enabled === true ? task.githubTracking.issue : undefined;
let githubIssueAction: GithubIssueAction | undefined;
@@ -1471,7 +1497,7 @@ export function TaskDetailContent({
addToast(getErrorMessage(retryErr), "error");
}
}
}, [task.githubTracking?.enabled, task.githubTracking?.issue, task.id, onDeleteTask, requestClose, addToast, confirm]);
}, [task.column, task.githubTracking?.enabled, task.githubTracking?.issue, task.id, onDeleteTask, onArchiveTask, requestClose, addToast, confirm, confirmWithChoice]);
const handleMerge = useCallback(async () => {
const shouldMerge = await confirm({

View File

@@ -86,9 +86,10 @@ vi.mock("../../hooks/usePluginUiSlots", () => ({
}));
export const mockConfirm = vi.fn();
export const mockConfirmWithChoice = vi.fn();
vi.mock("../../hooks/useConfirm", () => ({
useConfirm: () => ({ confirm: mockConfirm }),
useConfirm: () => ({ confirm: mockConfirm, confirmWithChoice: mockConfirmWithChoice }),
}));
export function makeTask(overrides: Partial<TaskDetail> = {}): TaskDetail {
@@ -140,7 +141,9 @@ export function loadDashboardCss(): string {
export function setupTaskDetailModalHooks(): void {
beforeEach(() => {
mockConfirm.mockReset();
mockConfirmWithChoice.mockReset();
mockConfirm.mockResolvedValue(true);
mockConfirmWithChoice.mockResolvedValue("primary");
clearAuthToken();
localStorage.removeItem("fn.authToken");
});

View File

@@ -1,5 +1,5 @@
import { describe, it, expect } from "vitest";
import { render, screen } from "@testing-library/react";
import { describe, it, expect, vi } from "vitest";
import { render, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import {
makeTask,
@@ -9,6 +9,7 @@ import {
noopMove,
noopOpenDetail,
setupTaskDetailModalHooks,
mockConfirmWithChoice,
} from "./TaskDetailModal.test-helpers";
import { TaskDetailModal } from "../TaskDetailModal";
@@ -87,6 +88,39 @@ describe("TaskDetailModal GitHub tracking CTA", () => {
});
});
describe("TaskDetailModal delete affordance", () => {
it("archives done task when Archive Instead is chosen", async () => {
const user = userEvent.setup();
const onArchiveTask = vi.fn(async () => makeTask({ column: "archived" }));
const onDeleteTask = vi.fn(async () => makeTask());
const onClose = vi.fn();
mockConfirmWithChoice.mockResolvedValueOnce("tertiary");
render(
<TaskDetailModal
task={makeTask({ column: "done" })}
onClose={onClose}
onMoveTask={noopMove}
onDeleteTask={onDeleteTask}
onArchiveTask={onArchiveTask}
onMergeTask={noopMerge}
onOpenDetail={noopOpenDetail}
addToast={noop}
/>,
);
await user.click(screen.getByRole("button", { name: "Actions" }));
await user.click(screen.getByRole("menuitem", { name: "Delete" }));
await waitFor(() => {
expect(mockConfirmWithChoice).toHaveBeenCalledWith(expect.objectContaining({ tertiaryLabel: "Archive Instead" }));
expect(onArchiveTask).toHaveBeenCalledWith("FN-099");
expect(onDeleteTask).not.toHaveBeenCalled();
expect(onClose).toHaveBeenCalled();
});
});
});
describe("TaskDetailModal in-review stall diagnostics", () => {
it("renders diagnostic row and jumps to highlighted activity entry", async () => {
const user = userEvent.setup();