feat(FN-4996): complete Step 4 — add bulk delete archive-instead option
Fusion-Task-Id: FN-4996 Fusion-Task-Lineage: 650bb5e4-18f2-41a0-ae46-b9112c4a0d74
This commit is contained in:
committed by
gsxdsm
parent
658a7e73e5
commit
239f135994
@@ -278,7 +278,7 @@ export function ListView({
|
||||
const [selectedColumn, setSelectedColumn] = useState<Column | null>(null);
|
||||
const viewportMode = useViewportMode();
|
||||
const isMobile = viewportMode === "mobile";
|
||||
const { confirm } = useConfirm();
|
||||
const { confirm, confirmWithChoice } = useConfirm();
|
||||
|
||||
// Column visibility state - initialize from localStorage or reduced default columns
|
||||
const [visibleColumns, setVisibleColumns] = useState<Set<ListColumn>>(() => readVisibleColumns(projectId));
|
||||
@@ -700,23 +700,58 @@ export function ListView({
|
||||
return;
|
||||
}
|
||||
|
||||
const confirmed = await confirm({
|
||||
title: "Delete Selected Tasks",
|
||||
message: `Delete ${deletableTasks.length} selected task${deletableTasks.length === 1 ? "" : "s"}?`,
|
||||
confirmLabel: "Delete",
|
||||
cancelLabel: "Cancel",
|
||||
danger: true,
|
||||
});
|
||||
const doneTasks = deletableTasks.filter((task) => task.column === "done");
|
||||
const otherTasks = deletableTasks.filter((task) => task.column !== "done");
|
||||
|
||||
if (!confirmed) return;
|
||||
let shouldDeleteAll = false;
|
||||
let shouldArchiveDoneInstead = false;
|
||||
|
||||
if (doneTasks.length > 0 && onArchiveTask) {
|
||||
const choice = await confirmWithChoice({
|
||||
title: "Delete Selected Tasks",
|
||||
message: `Delete ${deletableTasks.length} task${deletableTasks.length === 1 ? "" : "s"}, or archive the ${doneTasks.length} done task${doneTasks.length === 1 ? "" : "s"} and delete the rest?`,
|
||||
confirmLabel: "Delete All",
|
||||
cancelLabel: "Cancel",
|
||||
tertiaryLabel: `Archive ${doneTasks.length} Done`,
|
||||
danger: true,
|
||||
});
|
||||
if (choice === "cancel") return;
|
||||
shouldDeleteAll = choice === "primary";
|
||||
shouldArchiveDoneInstead = choice === "tertiary";
|
||||
} else {
|
||||
const confirmed = await confirm({
|
||||
title: "Delete Selected Tasks",
|
||||
message: `Delete ${deletableTasks.length} selected task${deletableTasks.length === 1 ? "" : "s"}?`,
|
||||
confirmLabel: "Delete",
|
||||
cancelLabel: "Cancel",
|
||||
danger: true,
|
||||
});
|
||||
|
||||
if (!confirmed) return;
|
||||
shouldDeleteAll = true;
|
||||
}
|
||||
|
||||
setIsApplying(true);
|
||||
const deletedIds: string[] = [];
|
||||
const archivedIds: string[] = [];
|
||||
const failedIds: string[] = [];
|
||||
const skippedIds = archivedTasks.map((task) => task.id);
|
||||
|
||||
try {
|
||||
for (const task of deletableTasks) {
|
||||
const tasksToDelete = shouldDeleteAll ? deletableTasks : otherTasks;
|
||||
|
||||
if (shouldArchiveDoneInstead && onArchiveTask) {
|
||||
for (const task of doneTasks) {
|
||||
try {
|
||||
await onArchiveTask(task.id);
|
||||
archivedIds.push(task.id);
|
||||
} catch {
|
||||
failedIds.push(task.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const task of tasksToDelete) {
|
||||
try {
|
||||
await onDeleteTask(task.id);
|
||||
deletedIds.push(task.id);
|
||||
@@ -752,21 +787,25 @@ export function ListView({
|
||||
setIsApplying(false);
|
||||
}
|
||||
|
||||
if (deletedIds.length > 0) {
|
||||
if (deletedIds.length > 0 || archivedIds.length > 0) {
|
||||
setSelectedTaskIds((previous) => {
|
||||
const next = new Set(previous);
|
||||
for (const id of deletedIds) {
|
||||
next.delete(id);
|
||||
}
|
||||
for (const id of archivedIds) {
|
||||
next.delete(id);
|
||||
}
|
||||
return next;
|
||||
});
|
||||
}
|
||||
|
||||
addToast(
|
||||
`Deleted ${deletedIds.length} task${deletedIds.length === 1 ? "" : "s"} · ${skippedIds.length} archived skipped · ${failedIds.length} failed`,
|
||||
failedIds.length > 0 ? "error" : "success",
|
||||
);
|
||||
}, [addToast, confirm, onDeleteTask, selectedTaskIds, tasks]);
|
||||
const summaryMessage = shouldArchiveDoneInstead
|
||||
? `Archived ${archivedIds.length}, deleted ${deletedIds.length}, failed ${failedIds.length}`
|
||||
: `Deleted ${deletedIds.length} task${deletedIds.length === 1 ? "" : "s"} · ${skippedIds.length} archived skipped · ${failedIds.length} failed`;
|
||||
|
||||
addToast(summaryMessage, failedIds.length > 0 ? "error" : "success");
|
||||
}, [addToast, confirm, confirmWithChoice, onArchiveTask, onDeleteTask, selectedTaskIds, tasks]);
|
||||
|
||||
const handleBulkPause = useCallback(async () => {
|
||||
if (selectedTaskIds.size === 0) return;
|
||||
|
||||
@@ -28,8 +28,9 @@ vi.mock("../../api", () => ({
|
||||
import { fetchTaskDetail, batchUpdateTaskModels, fetchNodes } from "../../api";
|
||||
|
||||
const mockConfirm = vi.fn();
|
||||
const mockConfirmWithChoice = vi.fn();
|
||||
vi.mock("../../hooks/useConfirm", () => ({
|
||||
useConfirm: () => ({ confirm: mockConfirm }),
|
||||
useConfirm: () => ({ confirm: mockConfirm, confirmWithChoice: mockConfirmWithChoice }),
|
||||
}));
|
||||
|
||||
const mockAddToast = vi.fn();
|
||||
@@ -178,6 +179,7 @@ describe("ListView", () => {
|
||||
prompt: "# Detail",
|
||||
} as TaskDetail);
|
||||
mockConfirm.mockReset();
|
||||
mockConfirmWithChoice.mockReset();
|
||||
localStorage.clear();
|
||||
ensureMatchMedia();
|
||||
vi.spyOn(window, "matchMedia").mockImplementation((query: string) => ({
|
||||
@@ -2754,6 +2756,33 @@ describe("ListView - Bulk Selection", () => {
|
||||
});
|
||||
|
||||
describe("bulk delete", () => {
|
||||
it("archives done tasks and deletes non-done tasks when tertiary action chosen", async () => {
|
||||
const user = userEvent.setup();
|
||||
const tasks = [
|
||||
createMockTask({ id: "FN-001", column: "done" }),
|
||||
createMockTask({ id: "FN-002", column: "done" }),
|
||||
createMockTask({ id: "FN-003", column: "todo" }),
|
||||
];
|
||||
const onDeleteTask = vi.fn(async () => createMockTask());
|
||||
const onArchiveTask = vi.fn(async () => createMockTask());
|
||||
mockConfirmWithChoice.mockResolvedValueOnce("tertiary");
|
||||
|
||||
renderListView({ tasks, onDeleteTask, onArchiveTask });
|
||||
enterBulkEditMode();
|
||||
await user.click(screen.getByLabelText("Select FN-001"));
|
||||
await user.click(screen.getByLabelText("Select FN-002"));
|
||||
await user.click(screen.getByLabelText("Select FN-003"));
|
||||
await user.click(screen.getByRole("button", { name: /delete selected/i }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(onArchiveTask).toHaveBeenCalledTimes(2);
|
||||
expect(onArchiveTask).toHaveBeenCalledWith("FN-001");
|
||||
expect(onArchiveTask).toHaveBeenCalledWith("FN-002");
|
||||
expect(onDeleteTask).toHaveBeenCalledTimes(1);
|
||||
expect(onDeleteTask).toHaveBeenCalledWith("FN-003");
|
||||
});
|
||||
});
|
||||
|
||||
it("deletes selected tasks and clears selection on success", async () => {
|
||||
const user = userEvent.setup();
|
||||
const tasks = [createMockTask({ id: "FN-001" }), createMockTask({ id: "FN-002" })];
|
||||
|
||||
Reference in New Issue
Block a user