From 038ec049752c7356b0814e872cfda085a65eb968 Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Thu, 16 Jul 2026 11:30:17 -0700 Subject: [PATCH] FN-8113: confirm GitHub issue closures Guard GitHub issue closure with a destructive confirmation flow. - Style the Close issue action as dangerous and require confirmation before API calls. - Cover confirmation, cancellation, success, failure, and mobile behavior in modal tests. - Add a patch changeset describing the safer close interaction. Files changed: .changeset/fn-8113-github-close-confirm.md | 7 ++ .../dashboard/app/components/GitHubImportModal.tsx | 22 ++++-- .../__tests__/GitHubImportModal.test.tsx | 91 +++++++++++++++++++++- 3 files changed, 113 insertions(+), 7 deletions(-) Fusion-Task-Id: FN-8113 Fusion-Task-Lineage: 80e4e1a3-b779-473d-9658-4231bd58d7e6 Co-authored-by: Fusion (runfusion.ai) --- .changeset/fn-8113-github-close-confirm.md | 7 ++ .../app/components/GitHubImportModal.tsx | 22 ++++- .../__tests__/GitHubImportModal.test.tsx | 93 ++++++++++++++++++- 3 files changed, 114 insertions(+), 8 deletions(-) create mode 100644 .changeset/fn-8113-github-close-confirm.md diff --git a/.changeset/fn-8113-github-close-confirm.md b/.changeset/fn-8113-github-close-confirm.md new file mode 100644 index 0000000000..5ea75b3691 --- /dev/null +++ b/.changeset/fn-8113-github-close-confirm.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": patch +--- + +summary: GitHub import "Close issue" button is now red and asks for confirmation before closing. +category: fix +dev: GitHubImportModal.handleCloseIssue gated behind useConfirm({ danger: true }); button uses btn-danger. diff --git a/packages/dashboard/app/components/GitHubImportModal.tsx b/packages/dashboard/app/components/GitHubImportModal.tsx index da3686812d..ae508fd23e 100644 --- a/packages/dashboard/app/components/GitHubImportModal.tsx +++ b/packages/dashboard/app/components/GitHubImportModal.tsx @@ -38,6 +38,7 @@ import type { TFunction } from "i18next"; import { useModalResizePersist } from "../hooks/useModalResizePersist"; import { useMobileScrollLock } from "../hooks/useMobileScrollLock"; import { useOverlayDismiss } from "../hooks/useOverlayDismiss"; +import { useConfirm } from "../hooks/useConfirm"; import { useEmbeddedPresentation, type ModalPresentation } from "../hooks/useEmbeddedPresentation"; import { getGitHubImportState, saveGitHubImportState } from "../hooks/modalPersistence"; import { FloatingWindow } from "./FloatingWindow"; @@ -335,6 +336,7 @@ export function GitHubImportModal({ isOpen, onClose, onImport, tasks, projectId, const { isEmbedded, scrollLockEnabled, resizePersistEnabled, escapeEnabled } = useEmbeddedPresentation(presentation); useMobileScrollLock(isOpen && scrollLockEnabled); const { t, i18n } = useTranslation("app"); + const { confirm } = useConfirm(); /* FNXC:GitHubImportTranslate 2026-07-14-12:00: Translation target is the active dashboard locale (i18n.resolvedLanguage). When content is another language, the preview offers Translate / Show original / Dismiss. @@ -1157,17 +1159,27 @@ export function GitHubImportModal({ isOpen, onClose, onImport, tasks, projectId, }, []); /* - FNXC:GitHubImport 2026-07-15-17:10: - Closing an issue keeps its FloatingWindow open through the success toast so the confirmation is visible and the locally closed state can replace the Close action. The full-width list remains behind the draggable/resizable detail window. + FNXC:GitHubImport 2026-07-16-20:00: + Closing permanently mutates the upstream GitHub issue, so its danger styling and confirmation gate must precede every local state mutation and API call. Keep the detail FloatingWindow open through the success toast so local closed state can replace the action. */ const handleCloseIssue = useCallback(async () => { if (selectedIssueNumber === null || !owner.trim() || !repo.trim()) return; const issueNumber = selectedIssueNumber; + const repository = `${owner.trim()}/${repo.trim()}`; + const shouldClose = await confirm({ + danger: true, + title: t("git.closeIssueConfirmTitle", "Close issue #{{number}}?", { number: issueNumber }), + message: t("git.closeIssueConfirmMessage", "This closes {{repo}}#{{number}} on GitHub. This cannot be undone from here.", { repo: repository, number: issueNumber }), + confirmLabel: t("git.closeIssue", "Close issue"), + cancelLabel: t("common.cancel", "Cancel"), + }); + if (!shouldClose) return; + setClosingIssue(true); if (closeToastTimerRef.current) clearTimeout(closeToastTimerRef.current); setCloseToast(null); try { - await apiCloseGitHubIssue(`${owner.trim()}/${repo.trim()}`, issueNumber); + await apiCloseGitHubIssue(repository, issueNumber); setClosedIssueNumbers((prev) => { const next = new Set(prev); next.add(issueNumber); @@ -1180,7 +1192,7 @@ export function GitHubImportModal({ isOpen, onClose, onImport, tasks, projectId, setClosingIssue(false); closeToastTimerRef.current = setTimeout(() => setCloseToast(null), 4000); } - }, [selectedIssueNumber, owner, repo, t]); + }, [selectedIssueNumber, owner, repo, t, confirm]); const selectedIssue = issues.find((i) => i.number === selectedIssueNumber); const selectedPull = pulls.find((p) => p.number === selectedPullNumber); @@ -2021,7 +2033,7 @@ export function GitHubImportModal({ isOpen, onClose, onImport, tasks, projectId,
{activeTab === "issues" && selectedIssue && !selectedIssueClosed && (