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:
committed by
gsxdsm
parent
4b5c7475f4
commit
658a7e73e5
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user