feat(FN-4378): complete Step 5 — prompt github issue action on task delete

Fusion-Task-Id: FN-4378
Fusion-Task-Lineage: d3867ba8-f852-41e3-9db0-584c0ffc23b1
This commit is contained in:
Fusion
2026-05-13 13:45:59 -07:00
committed by gsxdsm
parent 97b2321f7e
commit 19d7370552
6 changed files with 87 additions and 22 deletions

View File

@@ -1,4 +1,4 @@
import type { Task, TaskDetail, Column as ColumnType, TaskCreateInput } from "@fusion/core";
import type { Task, TaskDetail, Column as ColumnType, TaskCreateInput, GithubIssueAction } from "@fusion/core";
import { COLUMNS, DEFAULT_COLUMN, isColumn } from "@fusion/core";
import { sortTasksForDisplayColumn } from "./taskSorting";
import { Column } from "./Column";
@@ -28,7 +28,7 @@ interface BoardProps {
onRetryTask?: (id: string) => Promise<Task>;
onArchiveTask?: (id: string) => Promise<Task>;
onUnarchiveTask?: (id: string) => Promise<Task>;
onDeleteTask?: (id: string) => Promise<Task>;
onDeleteTask?: (id: string, options?: { removeDependencyReferences?: boolean; githubIssueAction?: GithubIssueAction }) => Promise<Task>;
onArchiveAllDone?: () => Promise<Task[]>;
/** Lazy-load archived tasks. Called the first time the user expands the archived column. */
onLoadArchivedTasks?: () => Promise<void>;

View File

@@ -1,7 +1,7 @@
import { memo, useMemo, useState, useCallback, useEffect, useRef } from "react";
import { useFlashOnIncrease } from "../hooks/useFlashOnIncrease";
import { useConfirm } from "../hooks/useConfirm";
import type { Task, TaskDetail, Column as ColumnType, TaskCreateInput } from "@fusion/core";
import type { Task, TaskDetail, Column as ColumnType, TaskCreateInput, GithubIssueAction } from "@fusion/core";
import { COLUMN_LABELS, COLUMN_DESCRIPTIONS, getErrorMessage } from "@fusion/core";
import { TaskCard } from "./TaskCard";
import { WorktreeGroup } from "./WorktreeGroup";
@@ -38,7 +38,7 @@ interface ColumnProps {
onRetryTask?: (id: string) => Promise<Task>;
onArchiveTask?: (id: string) => Promise<Task>;
onUnarchiveTask?: (id: string) => Promise<Task>;
onDeleteTask?: (id: string) => Promise<Task>;
onDeleteTask?: (id: string, options?: { removeDependencyReferences?: boolean; githubIssueAction?: GithubIssueAction }) => Promise<Task>;
onArchiveAllDone?: () => Promise<Task[]>;
collapsed?: boolean;
onToggleCollapse?: () => void;

View File

@@ -1,7 +1,7 @@
import "./ListView.css";
import { useState, useCallback, useMemo, Fragment, useEffect, useRef } from "react";
import { ArrowUpDown, ArrowUp, ArrowDown, Link, Columns3, EyeOff, Eye, ChevronRight, Zap, Trash2 } from "lucide-react";
import type { Task, TaskDetail, Column, TaskCreateInput, MergeResult } from "@fusion/core";
import type { Task, TaskDetail, Column, TaskCreateInput, MergeResult, GithubIssueAction } from "@fusion/core";
import { COLUMN_LABELS, COLUMNS, DEFAULT_COLUMN, getErrorMessage, isColumn } from "@fusion/core";
import { sortTasksForDisplayColumn } from "./taskSorting";
import { batchUpdateTaskModels, fetchNodes, fetchTaskDetail } from "../api";
@@ -167,7 +167,7 @@ interface ListViewProps {
tasks: Task[];
onMoveTask: (id: string, column: Column, optionsOrPosition?: { preserveProgress?: boolean } | number) => Promise<Task>;
onRetryTask?: (id: string) => Promise<Task>;
onDeleteTask: (id: string, options?: { removeDependencyReferences?: boolean }) => Promise<Task>;
onDeleteTask: (id: string, options?: { removeDependencyReferences?: boolean; githubIssueAction?: GithubIssueAction }) => Promise<Task>;
onMergeTask: (id: string) => Promise<MergeResult>;
onResetTask?: (id: string) => Promise<Task>;
onDuplicateTask?: (id: string) => Promise<Task>;

View File

@@ -1,7 +1,7 @@
import "./TaskCard.css";
import { memo, useCallback, useState, useRef, useEffect, useMemo } from "react";
import { Link, Clock, Layers, Pencil, ChevronDown, Folder, Target, Bot, Trash2, RotateCw, Zap, GitBranch } from "lucide-react";
import type { Task, TaskDetail, Column, PrInfo, IssueInfo, TaskPriority } from "@fusion/core";
import type { Task, TaskDetail, Column, PrInfo, IssueInfo, TaskPriority, GithubIssueAction } from "@fusion/core";
import {
COLUMN_LABELS,
DEFAULT_TASK_PRIORITY,
@@ -271,7 +271,7 @@ interface TaskCardProps {
) => Promise<Task>;
onArchiveTask?: (id: string) => Promise<Task>;
onUnarchiveTask?: (id: string) => Promise<Task>;
onDeleteTask?: (id: string, options?: { removeDependencyReferences?: boolean }) => Promise<Task>;
onDeleteTask?: (id: string, options?: { removeDependencyReferences?: boolean; githubIssueAction?: GithubIssueAction }) => Promise<Task>;
onRetryTask?: (id: string) => Promise<Task>;
onOpenDetailWithTab?: (task: Task | TaskDetail, initialTab: "changes") => void;
/** Project-level stuck task timeout in milliseconds (undefined = disabled) */
@@ -1125,9 +1125,41 @@ function TaskCardComponent({
return;
}
const trackedIssue = task.githubTracking?.enabled === true ? task.githubTracking.issue : undefined;
let githubIssueAction: GithubIssueAction | undefined;
if (trackedIssue?.owner && trackedIssue.repo && trackedIssue.number) {
const issueRef = `${trackedIssue.owner}/${trackedIssue.repo}#${trackedIssue.number}`;
const shouldCloseIssue = await confirm({
title: "Linked GitHub Issue",
message: `Choose what to do with ${issueRef} when deleting ${task.id}.\n\nClose the issue?`,
confirmLabel: "Close Issue",
cancelLabel: "More Options",
});
if (shouldCloseIssue) {
githubIssueAction = "close";
} else {
const shouldDeleteIssue = await confirm({
title: "Delete Linked GitHub Issue",
message: `Delete ${issueRef} on GitHub, or leave it unchanged?`,
confirmLabel: "Delete Issue",
cancelLabel: "Leave Unchanged",
danger: true,
});
githubIssueAction = shouldDeleteIssue ? "delete" : "leave";
}
}
try {
await onDeleteTask(task.id);
addToast(`Deleted ${task.id}`, "success");
if (githubIssueAction) {
await onDeleteTask(task.id, { githubIssueAction });
} else {
await onDeleteTask(task.id);
}
const issueSuffix = trackedIssue?.owner && trackedIssue.repo && trackedIssue.number && githubIssueAction
? ` and ${githubIssueAction === "close" ? "closed" : githubIssueAction === "delete" ? "deleted" : "left"} issue ${trackedIssue.owner}/${trackedIssue.repo}#${trackedIssue.number}`
: "";
addToast(`Deleted ${task.id}${issueSuffix}`, "success");
} catch (err) {
const conflict = extractDependencyDeleteConflict(err);
if (!conflict || conflict.dependentIds.length === 0) {
@@ -1148,13 +1180,13 @@ function TaskCardComponent({
}
try {
await onDeleteTask(task.id, { removeDependencyReferences: true });
await onDeleteTask(task.id, { removeDependencyReferences: true, githubIssueAction });
addToast(`Deleted ${task.id} after removing dependency references`, "success");
} catch (retryErr) {
addToast(`Failed to delete ${task.id}: ${getErrorMessage(retryErr)}`, "error");
}
}
}, [addToast, confirm, onDeleteTask, task.id]);
}, [addToast, confirm, onDeleteTask, task.githubTracking?.enabled, task.githubTracking?.issue, task.id]);
const handleOpenFiles = useCallback((e: React.MouseEvent) => {
e.stopPropagation();

View File

@@ -193,16 +193,16 @@
align-items: flex-start;
}
.detail-provenance-context {
max-width: 20ch;
}
.detail-timestamps {
display: flex;
align-items: center;
flex-wrap: nowrap;
}
.detail-provenance-context {
max-width: 20ch;
}
.detail-timestamp-item {
align-items: baseline;
flex-wrap: nowrap;

View File

@@ -7,7 +7,7 @@ import { useOverlayDismiss } from "../hooks/useOverlayDismiss";
import ReactMarkdown from "react-markdown";
import type { Components } from "react-markdown";
import remarkGfm from "remark-gfm";
import type { Task, TaskDetail, TaskAttachment, Column, MergeResult, Settings, GlobalSettings, AgentLogEntry, Agent, TaskPriority, TaskSourceIssue, WorkflowStepResult } from "@fusion/core";
import type { Task, TaskDetail, TaskAttachment, Column, MergeResult, Settings, GlobalSettings, AgentLogEntry, Agent, TaskPriority, TaskSourceIssue, WorkflowStepResult, GithubIssueAction } from "@fusion/core";
import {
COLUMN_LABELS,
DEFAULT_TASK_PRIORITY,
@@ -267,7 +267,7 @@ export interface TaskDetailModalProps {
onClose: () => void;
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 }) => Promise<Task>;
onDeleteTask: (id: string, options?: { removeDependencyReferences?: boolean; githubIssueAction?: GithubIssueAction }) => Promise<Task>;
onMergeTask: (id: string) => Promise<MergeResult>;
onRetryTask?: (id: string) => Promise<Task>;
onResetTask?: (id: string) => Promise<Task>;
@@ -1317,10 +1317,43 @@ export function TaskDetailContent({
danger: true,
});
if (!shouldDelete) return;
const trackedIssue = task.githubTracking?.enabled === true ? task.githubTracking.issue : undefined;
let githubIssueAction: GithubIssueAction | undefined;
if (trackedIssue?.owner && trackedIssue.repo && trackedIssue.number) {
const issueRef = `${trackedIssue.owner}/${trackedIssue.repo}#${trackedIssue.number}`;
const shouldCloseIssue = await confirm({
title: "Linked GitHub Issue",
message: `Choose what to do with ${issueRef} when deleting ${task.id}.\n\nClose the issue?`,
confirmLabel: "Close Issue",
cancelLabel: "More Options",
});
if (shouldCloseIssue) {
githubIssueAction = "close";
} else {
const shouldDeleteIssue = await confirm({
title: "Delete Linked GitHub Issue",
message: `Delete ${issueRef} on GitHub, or leave it unchanged?`,
confirmLabel: "Delete Issue",
cancelLabel: "Leave Unchanged",
danger: true,
});
githubIssueAction = shouldDeleteIssue ? "delete" : "leave";
}
}
try {
await onDeleteTask(task.id);
if (githubIssueAction) {
await onDeleteTask(task.id, { githubIssueAction });
} else {
await onDeleteTask(task.id);
}
requestClose();
addToast(`Deleted ${task.id}`, "info");
const issueSuffix = trackedIssue?.owner && trackedIssue.repo && trackedIssue.number && githubIssueAction
? ` and ${githubIssueAction === "close" ? "closed" : githubIssueAction === "delete" ? "deleted" : "left"} issue ${trackedIssue.owner}/${trackedIssue.repo}#${trackedIssue.number}`
: "";
addToast(`Deleted ${task.id}${issueSuffix}`, "info");
} catch (err) {
const conflict = extractDependencyDeleteConflict(err);
if (!conflict || conflict.dependentIds.length === 0) {
@@ -1341,14 +1374,14 @@ export function TaskDetailContent({
}
try {
await onDeleteTask(task.id, { removeDependencyReferences: true });
await onDeleteTask(task.id, { removeDependencyReferences: true, githubIssueAction });
requestClose();
addToast(`Deleted ${task.id} after removing dependency references`, "info");
} catch (retryErr) {
addToast(getErrorMessage(retryErr), "error");
}
}
}, [task.id, onDeleteTask, requestClose, addToast, confirm]);
}, [task.githubTracking?.enabled, task.githubTracking?.issue, task.id, onDeleteTask, requestClose, addToast, confirm]);
const handleMerge = useCallback(async () => {
const shouldMerge = await confirm({