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:
@@ -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 { COLUMNS, DEFAULT_COLUMN, isColumn } from "@fusion/core";
|
||||||
import { sortTasksForDisplayColumn } from "./taskSorting";
|
import { sortTasksForDisplayColumn } from "./taskSorting";
|
||||||
import { Column } from "./Column";
|
import { Column } from "./Column";
|
||||||
@@ -28,7 +28,7 @@ interface BoardProps {
|
|||||||
onRetryTask?: (id: string) => Promise<Task>;
|
onRetryTask?: (id: string) => Promise<Task>;
|
||||||
onArchiveTask?: (id: string) => Promise<Task>;
|
onArchiveTask?: (id: string) => Promise<Task>;
|
||||||
onUnarchiveTask?: (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[]>;
|
onArchiveAllDone?: () => Promise<Task[]>;
|
||||||
/** Lazy-load archived tasks. Called the first time the user expands the archived column. */
|
/** Lazy-load archived tasks. Called the first time the user expands the archived column. */
|
||||||
onLoadArchivedTasks?: () => Promise<void>;
|
onLoadArchivedTasks?: () => Promise<void>;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { memo, useMemo, useState, useCallback, useEffect, useRef } from "react";
|
import { memo, useMemo, useState, useCallback, useEffect, useRef } from "react";
|
||||||
import { useFlashOnIncrease } from "../hooks/useFlashOnIncrease";
|
import { useFlashOnIncrease } from "../hooks/useFlashOnIncrease";
|
||||||
import { useConfirm } from "../hooks/useConfirm";
|
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 { COLUMN_LABELS, COLUMN_DESCRIPTIONS, getErrorMessage } from "@fusion/core";
|
||||||
import { TaskCard } from "./TaskCard";
|
import { TaskCard } from "./TaskCard";
|
||||||
import { WorktreeGroup } from "./WorktreeGroup";
|
import { WorktreeGroup } from "./WorktreeGroup";
|
||||||
@@ -38,7 +38,7 @@ interface ColumnProps {
|
|||||||
onRetryTask?: (id: string) => Promise<Task>;
|
onRetryTask?: (id: string) => Promise<Task>;
|
||||||
onArchiveTask?: (id: string) => Promise<Task>;
|
onArchiveTask?: (id: string) => Promise<Task>;
|
||||||
onUnarchiveTask?: (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[]>;
|
onArchiveAllDone?: () => Promise<Task[]>;
|
||||||
collapsed?: boolean;
|
collapsed?: boolean;
|
||||||
onToggleCollapse?: () => void;
|
onToggleCollapse?: () => void;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import "./ListView.css";
|
import "./ListView.css";
|
||||||
import { useState, useCallback, useMemo, Fragment, useEffect, useRef } from "react";
|
import { useState, useCallback, useMemo, Fragment, useEffect, useRef } from "react";
|
||||||
import { ArrowUpDown, ArrowUp, ArrowDown, Link, Columns3, EyeOff, Eye, ChevronRight, Zap, Trash2 } from "lucide-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 { COLUMN_LABELS, COLUMNS, DEFAULT_COLUMN, getErrorMessage, isColumn } from "@fusion/core";
|
||||||
import { sortTasksForDisplayColumn } from "./taskSorting";
|
import { sortTasksForDisplayColumn } from "./taskSorting";
|
||||||
import { batchUpdateTaskModels, fetchNodes, fetchTaskDetail } from "../api";
|
import { batchUpdateTaskModels, fetchNodes, fetchTaskDetail } from "../api";
|
||||||
@@ -167,7 +167,7 @@ interface ListViewProps {
|
|||||||
tasks: Task[];
|
tasks: Task[];
|
||||||
onMoveTask: (id: string, column: Column, optionsOrPosition?: { preserveProgress?: boolean } | number) => Promise<Task>;
|
onMoveTask: (id: string, column: Column, optionsOrPosition?: { preserveProgress?: boolean } | number) => Promise<Task>;
|
||||||
onRetryTask?: (id: string) => 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>;
|
onMergeTask: (id: string) => Promise<MergeResult>;
|
||||||
onResetTask?: (id: string) => Promise<Task>;
|
onResetTask?: (id: string) => Promise<Task>;
|
||||||
onDuplicateTask?: (id: string) => Promise<Task>;
|
onDuplicateTask?: (id: string) => Promise<Task>;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import "./TaskCard.css";
|
import "./TaskCard.css";
|
||||||
import { memo, useCallback, useState, useRef, useEffect, useMemo } from "react";
|
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 { 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 {
|
import {
|
||||||
COLUMN_LABELS,
|
COLUMN_LABELS,
|
||||||
DEFAULT_TASK_PRIORITY,
|
DEFAULT_TASK_PRIORITY,
|
||||||
@@ -271,7 +271,7 @@ interface TaskCardProps {
|
|||||||
) => Promise<Task>;
|
) => Promise<Task>;
|
||||||
onArchiveTask?: (id: string) => Promise<Task>;
|
onArchiveTask?: (id: string) => Promise<Task>;
|
||||||
onUnarchiveTask?: (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>;
|
onRetryTask?: (id: string) => Promise<Task>;
|
||||||
onOpenDetailWithTab?: (task: Task | TaskDetail, initialTab: "changes") => void;
|
onOpenDetailWithTab?: (task: Task | TaskDetail, initialTab: "changes") => void;
|
||||||
/** Project-level stuck task timeout in milliseconds (undefined = disabled) */
|
/** Project-level stuck task timeout in milliseconds (undefined = disabled) */
|
||||||
@@ -1125,9 +1125,41 @@ function TaskCardComponent({
|
|||||||
return;
|
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 {
|
try {
|
||||||
await onDeleteTask(task.id);
|
if (githubIssueAction) {
|
||||||
addToast(`Deleted ${task.id}`, "success");
|
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) {
|
} catch (err) {
|
||||||
const conflict = extractDependencyDeleteConflict(err);
|
const conflict = extractDependencyDeleteConflict(err);
|
||||||
if (!conflict || conflict.dependentIds.length === 0) {
|
if (!conflict || conflict.dependentIds.length === 0) {
|
||||||
@@ -1148,13 +1180,13 @@ function TaskCardComponent({
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await onDeleteTask(task.id, { removeDependencyReferences: true });
|
await onDeleteTask(task.id, { removeDependencyReferences: true, githubIssueAction });
|
||||||
addToast(`Deleted ${task.id} after removing dependency references`, "success");
|
addToast(`Deleted ${task.id} after removing dependency references`, "success");
|
||||||
} catch (retryErr) {
|
} catch (retryErr) {
|
||||||
addToast(`Failed to delete ${task.id}: ${getErrorMessage(retryErr)}`, "error");
|
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) => {
|
const handleOpenFiles = useCallback((e: React.MouseEvent) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|||||||
@@ -193,16 +193,16 @@
|
|||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
}
|
}
|
||||||
|
|
||||||
.detail-provenance-context {
|
|
||||||
max-width: 20ch;
|
|
||||||
}
|
|
||||||
|
|
||||||
.detail-timestamps {
|
.detail-timestamps {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
flex-wrap: nowrap;
|
flex-wrap: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.detail-provenance-context {
|
||||||
|
max-width: 20ch;
|
||||||
|
}
|
||||||
|
|
||||||
.detail-timestamp-item {
|
.detail-timestamp-item {
|
||||||
align-items: baseline;
|
align-items: baseline;
|
||||||
flex-wrap: nowrap;
|
flex-wrap: nowrap;
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { useOverlayDismiss } from "../hooks/useOverlayDismiss";
|
|||||||
import ReactMarkdown from "react-markdown";
|
import ReactMarkdown from "react-markdown";
|
||||||
import type { Components } from "react-markdown";
|
import type { Components } from "react-markdown";
|
||||||
import remarkGfm from "remark-gfm";
|
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 {
|
import {
|
||||||
COLUMN_LABELS,
|
COLUMN_LABELS,
|
||||||
DEFAULT_TASK_PRIORITY,
|
DEFAULT_TASK_PRIORITY,
|
||||||
@@ -267,7 +267,7 @@ export interface TaskDetailModalProps {
|
|||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
onOpenDetail: (task: Task | TaskDetail) => void; // For clicking dependencies
|
onOpenDetail: (task: Task | TaskDetail) => void; // For clicking dependencies
|
||||||
onMoveTask: (id: string, column: Column, optionsOrPosition?: { preserveProgress?: boolean } | number) => Promise<Task>;
|
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>;
|
onMergeTask: (id: string) => Promise<MergeResult>;
|
||||||
onRetryTask?: (id: string) => Promise<Task>;
|
onRetryTask?: (id: string) => Promise<Task>;
|
||||||
onResetTask?: (id: string) => Promise<Task>;
|
onResetTask?: (id: string) => Promise<Task>;
|
||||||
@@ -1317,10 +1317,43 @@ export function TaskDetailContent({
|
|||||||
danger: true,
|
danger: true,
|
||||||
});
|
});
|
||||||
if (!shouldDelete) return;
|
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 {
|
try {
|
||||||
await onDeleteTask(task.id);
|
if (githubIssueAction) {
|
||||||
|
await onDeleteTask(task.id, { githubIssueAction });
|
||||||
|
} else {
|
||||||
|
await onDeleteTask(task.id);
|
||||||
|
}
|
||||||
requestClose();
|
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) {
|
} catch (err) {
|
||||||
const conflict = extractDependencyDeleteConflict(err);
|
const conflict = extractDependencyDeleteConflict(err);
|
||||||
if (!conflict || conflict.dependentIds.length === 0) {
|
if (!conflict || conflict.dependentIds.length === 0) {
|
||||||
@@ -1341,14 +1374,14 @@ export function TaskDetailContent({
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await onDeleteTask(task.id, { removeDependencyReferences: true });
|
await onDeleteTask(task.id, { removeDependencyReferences: true, githubIssueAction });
|
||||||
requestClose();
|
requestClose();
|
||||||
addToast(`Deleted ${task.id} after removing dependency references`, "info");
|
addToast(`Deleted ${task.id} after removing dependency references`, "info");
|
||||||
} catch (retryErr) {
|
} catch (retryErr) {
|
||||||
addToast(getErrorMessage(retryErr), "error");
|
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 handleMerge = useCallback(async () => {
|
||||||
const shouldMerge = await confirm({
|
const shouldMerge = await confirm({
|
||||||
|
|||||||
Reference in New Issue
Block a user