FN-8110: add failed-check fix task action

Add an in-context recovery path for failed GitHub PR checks.

- Add a per-failed-check Create fix task action with progress and result feedback.
- Compose fix-task context from the selected PR, repository, branches, check status, and details URL.
- Cover the GitHub import behavior and document the feature release.

Files changed: .changeset/fn-8110-pr-failed-check-fix-task.md     |   7 +
 docs/dashboard-guide.md                            |   2 +-
 .../dashboard/app/components/GitHubImportModal.css |  23 ++++
 .../dashboard/app/components/GitHubImportModal.tsx |  91 ++++++++++++-
 .../__tests__/GitHubImportModal.test.tsx           | 148 +++++++++++++++++++++
 5 files changed, 269 insertions(+), 2 deletions(-)

Fusion-Task-Id: FN-8110

Fusion-Task-Lineage: 0f69a4e2-d950-4a9f-925e-bd56fb6bf45d

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
gsxdsm
2026-07-16 11:16:06 -07:00
parent 504c702262
commit 68583b5c68
5 changed files with 269 additions and 2 deletions

View File

@@ -0,0 +1,7 @@
---
"@runfusion/fusion": minor
---
summary: Add a Create fix task button on failed PR checks in the GitHub import preview.
category: feature
dev: Reuses createTask with an auto-composed PR and check context prompt (FN-8110).

View File

@@ -312,7 +312,7 @@ Use Import Tasks on desktop/tablet:
3. Stay on **Issues** or switch to **Pull Requests**, then optionally enter issue label filters before loading results. 3. Stay on **Issues** or switch to **Pull Requests**, then optionally enter issue label filters before loading results.
Expected outcome: the list pane shows matching open issues or pull requests and marks entries that already exist on the board. Use **Hide imported** beside the imported count to remove those unavailable rows from the current Issues, Pull Requests, or GitLab list; turning it off restores the greyed **Imported** rows. After a successful GitHub or GitLab import, the source row is marked **Imported** and made unavailable immediately, without waiting for the board list to refresh. Expected outcome: the list pane shows matching open issues or pull requests and marks entries that already exist on the board. Use **Hide imported** beside the imported count to remove those unavailable rows from the current Issues, Pull Requests, or GitLab list; turning it off restores the greyed **Imported** rows. After a successful GitHub or GitLab import, the source row is marked **Imported** and made unavailable immediately, without waiting for the board list to refresh.
4. Select an issue or pull request row. 4. Select an issue or pull request row.
Expected outcome: the full-width candidate list stays visible while its title, source link, body, labels or PR metadata, and import controls open in a draggable and resizable detail window. On mobile, that detail is a full-screen sheet. When selected title/body content is in another language, the detail offers **Translate**, **Show original** / **Show translation**, and **Dismiss**; translation is display-only. Expected outcome: the full-width candidate list stays visible while its title, source link, body, labels or PR metadata, and import controls open in a draggable and resizable detail window. On mobile, that detail is a full-screen sheet. When selected title/body content is in another language, the detail offers **Translate**, **Show original** / **Show translation**, and **Dismiss**; translation is display-only. A pull request preview also shows its checks; each failed check has a **Create fix task** action that creates a new task prefilled with the repository, PR, branches, check status, and check-details link.
5. Select the import action in the detail window. 5. Select the import action in the detail window.
Expected outcome: Fusion creates a task (or review task for a pull request), preserves GitHub provenance/tracking metadata, closes the detail window, and returns to the list. Expected outcome: Fusion creates a task (or review task for a pull request), preserves GitHub provenance/tracking metadata, closes the detail window, and returns to the list.

View File

@@ -1154,6 +1154,18 @@ Checks + Comments sections live below the PR body in the scrollable preview pane
color: var(--text); color: var(--text);
text-decoration: none; text-decoration: none;
word-break: break-word; word-break: break-word;
min-width: 0;
flex: 1 1 auto;
}
/*
FNXC:GitHubImport 2026-07-16-17:00:
FN-8110 keeps a failed check's repair action attached to its own row without introducing a new button visual system.
The shared btn primitives own colors and control treatment; this rule only reserves row layout and mobile-safe wrapping.
*/
.github-import-pr-check-fix-task {
flex: 0 0 auto;
margin-inline-start: auto;
} }
a.github-import-pr-check-name:hover { a.github-import-pr-check-name:hover {
@@ -1628,6 +1640,17 @@ Import Tasks embedded header now adopts the canonical ViewHeader chrome — edge
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
.github-import-pr-check-row {
flex-wrap: wrap;
align-items: center;
}
.github-import-pr-check-fix-task {
width: 100%;
justify-content: center;
margin-inline-start: 0;
}
/* /*
FNXC:GitHubImport 2026-07-15-18:20: FNXC:GitHubImport 2026-07-15-18:20:
This is the breakpoint where the detail FloatingWindow becomes a full-screen sheet. The panel's var(--space-lg) inset already reads as a conventional mobile gutter, so only the actions change: they grow to a 40px touch target because the desktop 30px pair is a mouse-sized hit area and Import is the sheet's primary action. This is the breakpoint where the detail FloatingWindow becomes a full-screen sheet. The panel's var(--space-lg) inset already reads as a conventional mobile gutter, so only the actions change: they grow to a 40px touch target because the desktop 30px pair is a mouse-sized hit area and Import is the sheet's primary action.

View File

@@ -18,6 +18,7 @@ import {
apiImportGitLabMergeRequest, apiImportGitLabMergeRequest,
fetchSettings, fetchSettings,
fetchGitRemotes, fetchGitRemotes,
createTask,
type GitHubIssue, type GitHubIssue,
type GitHubPull, type GitHubPull,
type GitHubPullDetail, type GitHubPullDetail,
@@ -302,6 +303,34 @@ notice tells the operator to refine by label rather than pretending the list is
const ISSUES_FETCH_CAP = 300; const ISSUES_FETCH_CAP = 300;
const ISSUES_PAGE_SIZE = 30; const ISSUES_PAGE_SIZE = 30;
/**
* FNXC:GitHubImport 2026-07-16-17:00:
* FN-8110 lets an operator create a repair task from a failed PR check without retyping its context.
* Keep this prompt composition pure so every check row carries its repository, PR, branch, status, and details-link evidence.
*/
export function buildCheckFixTaskPrompt(
pull: GitHubPull,
check: GitHubPullDetail["checks"][number],
repoSlug: string,
): { title: string; description: string } {
const indicator = check.conclusion ?? check.status ?? "unknown";
const details = check.detailsUrl ? `\nCheck details: ${check.detailsUrl}` : "";
return {
title: `Fix failing check "${check.name}" on PR #${pull.number}`,
description: [
"Fix the failing GitHub pull request check described below.",
"",
`Repository: ${repoSlug}`,
`Pull request: #${pull.number} — ${pull.title}`,
`PR URL: ${pull.html_url}`,
`Branches: ${pull.headBranch} → ${pull.baseBranch}`,
`Failing check: ${check.name}`,
`Check status: ${indicator}${details}`,
].join("\n"),
};
}
export function GitHubImportModal({ isOpen, onClose, onImport, tasks, projectId, presentation = "modal" }: GitHubImportModalProps) { export function GitHubImportModal({ isOpen, onClose, onImport, tasks, projectId, presentation = "modal" }: GitHubImportModalProps) {
const { isEmbedded, scrollLockEnabled, resizePersistEnabled, escapeEnabled } = useEmbeddedPresentation(presentation); const { isEmbedded, scrollLockEnabled, resizePersistEnabled, escapeEnabled } = useEmbeddedPresentation(presentation);
useMobileScrollLock(isOpen && scrollLockEnabled); useMobileScrollLock(isOpen && scrollLockEnabled);
@@ -438,6 +467,10 @@ export function GitHubImportModal({ isOpen, onClose, onImport, tasks, projectId,
const [closingIssue, setClosingIssue] = useState(false); const [closingIssue, setClosingIssue] = useState(false);
const [closeToast, setCloseToast] = useState<{ type: "success" | "error"; message: string } | null>(null); const [closeToast, setCloseToast] = useState<{ type: "success" | "error"; message: string } | null>(null);
const closeToastTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null); const closeToastTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
// FNXC:GitHubImport 2026-07-16-17:00: Track check-task submission by name + row index so duplicate GitHub check names never disable each other's controls.
const [creatingCheckFixTaskRows, setCreatingCheckFixTaskRows] = useState<Set<string>>(new Set());
const [checkFixTaskToast, setCheckFixTaskToast] = useState<{ type: "success" | "error"; message: string } | null>(null);
const checkFixTaskToastTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [isIssuesEmptyState, setIsIssuesEmptyState] = useState(false); const [isIssuesEmptyState, setIsIssuesEmptyState] = useState(false);
@@ -1120,6 +1153,7 @@ export function GitHubImportModal({ isOpen, onClose, onImport, tasks, projectId,
// FNXC:GitHubImport 2026-06-23-03:15: Clear the transient close toast timer on unmount. // FNXC:GitHubImport 2026-06-23-03:15: Clear the transient close toast timer on unmount.
useEffect(() => () => { useEffect(() => () => {
if (closeToastTimerRef.current) clearTimeout(closeToastTimerRef.current); if (closeToastTimerRef.current) clearTimeout(closeToastTimerRef.current);
if (checkFixTaskToastTimerRef.current) clearTimeout(checkFixTaskToastTimerRef.current);
}, []); }, []);
/* /*
@@ -1150,6 +1184,36 @@ export function GitHubImportModal({ isOpen, onClose, onImport, tasks, projectId,
const selectedIssue = issues.find((i) => i.number === selectedIssueNumber); const selectedIssue = issues.find((i) => i.number === selectedIssueNumber);
const selectedPull = pulls.find((p) => p.number === selectedPullNumber); const selectedPull = pulls.find((p) => p.number === selectedPullNumber);
const handleCreateCheckFixTask = useCallback(async (
check: GitHubPullDetail["checks"][number],
rowKey: string,
) => {
if (!selectedPull || !owner.trim() || !repo.trim()) return;
setCreatingCheckFixTaskRows((current) => new Set(current).add(rowKey));
if (checkFixTaskToastTimerRef.current) clearTimeout(checkFixTaskToastTimerRef.current);
setCheckFixTaskToast(null);
try {
const task = await createTask(buildCheckFixTaskPrompt(selectedPull, check, `${owner.trim()}/${repo.trim()}`), projectId);
onImport(task);
setCheckFixTaskToast({ type: "success", message: t("git.checkFixTaskCreated", "Fix task created") });
} catch (err: unknown) {
setCheckFixTaskToast({
type: "error",
message: getErrorMessage(err) || t("git.failedToCreateCheckFixTask", "Failed to create fix task"),
});
} finally {
setCreatingCheckFixTaskRows((current) => {
const next = new Set(current);
next.delete(rowKey);
return next;
});
checkFixTaskToastTimerRef.current = setTimeout(() => setCheckFixTaskToast(null), 4000);
}
}, [onImport, owner, projectId, repo, selectedPull, t]);
/* /*
FNXC:GitHubImport 2026-06-23-03:15: FNXC:GitHubImport 2026-06-23-03:15:
An issue counts as closed if the upstream state is closed OR we closed it locally this session. Only OPEN issues show the Close button. An issue counts as closed if the upstream state is closed OR we closed it locally this session. Only OPEN issues show the Close button.
@@ -1882,14 +1946,30 @@ export function GitHubImportModal({ isOpen, onClose, onImport, tasks, projectId,
: indicator === "neutral" || indicator === "skipped" : indicator === "neutral" || indicator === "skipped"
? "neutral" ? "neutral"
: "pending"; : "pending";
// FNXC:GitHubImport 2026-07-16-18:00: GitHub can return duplicate check names, and a user can switch PRs before a create finishes; include the PR number so only the originating row is disabled.
const rowKey = `${selectedPull.number}:${check.name}-${idx}`;
const creatingFixTask = creatingCheckFixTaskRows.has(rowKey);
return ( return (
<li key={`${check.name}-${idx}`} className="github-import-pr-check-row"> <li key={rowKey} className="github-import-pr-check-row">
<span className={`github-import-pr-check-pill github-import-pr-check-pill--${variant}`}>{indicator || "pending"}</span> <span className={`github-import-pr-check-pill github-import-pr-check-pill--${variant}`}>{indicator || "pending"}</span>
{check.detailsUrl ? ( {check.detailsUrl ? (
<a className="github-import-pr-check-name" href={check.detailsUrl} target="_blank" rel="noopener noreferrer">{check.name}</a> <a className="github-import-pr-check-name" href={check.detailsUrl} target="_blank" rel="noopener noreferrer">{check.name}</a>
) : ( ) : (
<span className="github-import-pr-check-name">{check.name}</span> <span className="github-import-pr-check-name">{check.name}</span>
)} )}
{variant === "failure" && (
<button
type="button"
className="btn btn-secondary btn-sm github-import-pr-check-fix-task"
data-testid="github-import-pr-check-fix-task"
aria-label={t("git.createCheckFixTaskAria", "Create fix task for {{checkName}}", { checkName: check.name })}
disabled={creatingFixTask}
onClick={() => handleCreateCheckFixTask(check, rowKey)}
>
{creatingFixTask && <Loader2 size={14} className="spin" aria-hidden="true" />}
{t("git.createCheckFixTask", "Create fix task")}
</button>
)}
</li> </li>
); );
})} })}
@@ -1897,6 +1977,15 @@ export function GitHubImportModal({ isOpen, onClose, onImport, tasks, projectId,
) : ( ) : (
<div className="preview-detail-empty" data-testid="github-import-pr-checks-empty">{t("git.noChecks", "No checks")}</div> <div className="preview-detail-empty" data-testid="github-import-pr-checks-empty">{t("git.noChecks", "No checks")}</div>
)} )}
{checkFixTaskToast && (
<div
className={`github-import-close-toast github-import-close-toast--${checkFixTaskToast.type}`}
role="status"
data-testid="github-import-pr-check-fix-task-toast"
>
{checkFixTaskToast.message}
</div>
)}
</div> </div>
<CommentsThread <CommentsThread
comments={pullDetail?.comments ?? []} comments={pullDetail?.comments ?? []}

View File

@@ -17,6 +17,7 @@ import {
apiImportGitLabMergeRequest, apiImportGitLabMergeRequest,
fetchSettings, fetchSettings,
fetchGitRemotes, fetchGitRemotes,
createTask,
translateImportContent, translateImportContent,
} from "../../api"; } from "../../api";
import type { Task } from "@fusion/core"; import type { Task } from "@fusion/core";
@@ -44,6 +45,7 @@ vi.mock("../../api", async (importOriginal) => {
apiImportGitLabMergeRequest: vi.fn(), apiImportGitLabMergeRequest: vi.fn(),
fetchSettings: vi.fn(), fetchSettings: vi.fn(),
fetchGitRemotes: vi.fn(), fetchGitRemotes: vi.fn(),
createTask: vi.fn(),
translateImportContent: vi.fn(), translateImportContent: vi.fn(),
}; };
}); });
@@ -175,6 +177,8 @@ describe("GitHubImportModal", () => {
vi.mocked(apiImportGitLabGroupIssue).mockReset(); vi.mocked(apiImportGitLabGroupIssue).mockReset();
vi.mocked(apiImportGitLabMergeRequest).mockReset(); vi.mocked(apiImportGitLabMergeRequest).mockReset();
vi.mocked(fetchSettings).mockReset(); vi.mocked(fetchSettings).mockReset();
vi.mocked(createTask).mockReset();
vi.mocked(createTask).mockResolvedValue(mockTask);
vi.mocked(fetchSettings).mockResolvedValue({ gitlabEnabled: true } as never); vi.mocked(fetchSettings).mockResolvedValue({ gitlabEnabled: true } as never);
// Set default mock for apiFetchGitHubIssues to return empty array (prevents undefined issues state) // Set default mock for apiFetchGitHubIssues to return empty array (prevents undefined issues state)
vi.mocked(apiFetchGitHubIssues).mockResolvedValue([]); vi.mocked(apiFetchGitHubIssues).mockResolvedValue([]);
@@ -192,6 +196,150 @@ describe("GitHubImportModal", () => {
onImport.mockReset(); onImport.mockReset();
}); });
/*
* FNXC:GitHubImport 2026-07-16-17:00:
* FN-8110's per-check repair action is exercised through the same selected-PR detail path in both modal and embedded presentations.
* The helper deliberately controls only the API seam so the tests cover the real row rendering and task callback behavior.
*/
const renderSelectedPullChecks = async (
checks: Array<{ name: string; status: string; conclusion?: string; detailsUrl?: string }>,
presentation: "modal" | "embedded" = "modal",
detailRequest: Promise<{ comments: []; checks: Array<{ name: string; status: string; conclusion?: string; detailsUrl?: string }> }> = Promise.resolve({ comments: [], checks }),
) => {
const pull = {
number: 81,
title: "Fixable PR",
body: "PR body",
html_url: "https://github.com/owner/repo/pull/81",
headBranch: "broken-build",
baseBranch: "main",
};
vi.mocked(fetchGitRemotes).mockResolvedValueOnce([{ name: "origin", owner: "owner", repo: "repo", url: "" }]);
vi.mocked(apiFetchGitHubPulls).mockResolvedValueOnce([pull]);
vi.mocked(apiFetchGitHubPullDetail).mockReturnValueOnce(detailRequest as never);
render(<GitHubImportModal isOpen onClose={onClose} onImport={onImport} tasks={[]} projectId="project-1" presentation={presentation} />);
fireEvent.click(await screen.findByRole("tab", { name: /Pull Requests/i }));
await screen.findByText("Fixable PR");
fireEvent.click(screen.getByRole("button", { name: /Select pull request #81/i }));
};
describe("failed PR check fix tasks", () => {
const checkVariants = [
{ name: "failure", status: "completed", conclusion: "failure", detailsUrl: "https://github.com/owner/repo/runs/1" },
{ name: "success", status: "completed", conclusion: "success" },
{ name: "pending", status: "in_progress" },
{ name: "neutral", status: "completed", conclusion: "skipped" },
];
it.each(["modal", "embedded"] as const)("creates a task from the failed row in %s presentation", async (presentation) => {
const createdTask = { ...mockTask, id: `FN-${presentation}` };
vi.mocked(createTask).mockResolvedValueOnce(createdTask);
await renderSelectedPullChecks(checkVariants, presentation);
const buttons = await screen.findAllByTestId("github-import-pr-check-fix-task");
expect(buttons).toHaveLength(1);
expect(buttons[0]).toHaveAccessibleName("Create fix task for failure");
fireEvent.click(buttons[0]);
await waitFor(() => {
expect(createTask).toHaveBeenCalledWith(expect.objectContaining({
title: 'Fix failing check "failure" on PR #81',
description: expect.stringContaining("Repository: owner/repo"),
}), "project-1");
});
const [{ description }] = vi.mocked(createTask).mock.calls[0];
expect(description).toContain("https://github.com/owner/repo/pull/81");
expect(description).toContain("broken-build → main");
expect(description).toContain("Failing check: failure");
expect(description).toContain("https://github.com/owner/repo/runs/1");
expect(await screen.findByTestId("github-import-pr-check-fix-task-toast")).toHaveTextContent("Fix task created");
expect(onImport).toHaveBeenCalledWith(createdTask);
});
it("renders the affordance only for failed variants and keeps mobile wrapping source coverage", async () => {
await renderSelectedPullChecks(checkVariants);
expect(await screen.findAllByTestId("github-import-pr-check-fix-task")).toHaveLength(1);
expect(screen.getByRole("button", { name: "Create fix task for failure" })).toBeTruthy();
expect(screen.queryByRole("button", { name: "Create fix task for success" })).toBeNull();
expect(screen.queryByRole("button", { name: "Create fix task for pending" })).toBeNull();
expect(screen.queryByRole("button", { name: "Create fix task for neutral" })).toBeNull();
const source = readFileSync(resolve(__dirname, "../GitHubImportModal.css"), "utf8");
expect(source).toMatch(/@media \(max-width: 768px\)[\s\S]*\.github-import-pr-check-row\s*\{[\s\S]*flex-wrap: wrap;/);
expect(source).toMatch(/@media \(max-width: 768px\)[\s\S]*\.github-import-pr-check-fix-task\s*\{[\s\S]*width: 100%;/);
});
it("keeps duplicate failed check rows independently actionable while creation is in flight", async () => {
let resolveFirstCreate!: (task: Task) => void;
vi.mocked(createTask)
.mockImplementationOnce(() => new Promise<Task>((resolve) => { resolveFirstCreate = resolve; }))
.mockResolvedValueOnce({ ...mockTask, id: "FN-second" });
await renderSelectedPullChecks([
{ name: "duplicate", status: "completed", conclusion: "failure" },
{ name: "duplicate", status: "completed", conclusion: "failure" },
]);
const [first, second] = await screen.findAllByTestId("github-import-pr-check-fix-task");
fireEvent.click(first);
await waitFor(() => expect(first).toBeDisabled());
expect(first.querySelector(".spin")).toBeTruthy();
expect(second).not.toBeDisabled();
fireEvent.click(second);
expect(createTask).toHaveBeenCalledTimes(2);
await act(async () => { resolveFirstCreate(mockTask); });
});
it("surfaces create errors inline and permits retry", async () => {
vi.mocked(createTask)
.mockRejectedValueOnce(new Error("Task service unavailable"))
.mockResolvedValueOnce({ ...mockTask, id: "FN-retry" });
await renderSelectedPullChecks([{ name: "lint", status: "completed", conclusion: "failure" }]);
const button = await screen.findByTestId("github-import-pr-check-fix-task");
fireEvent.click(button);
expect(await screen.findByTestId("github-import-pr-check-fix-task-toast")).toHaveTextContent("Task service unavailable");
expect(button).not.toBeDisabled();
expect(onImport).not.toHaveBeenCalled();
fireEvent.click(button);
await waitFor(() => expect(onImport).toHaveBeenCalledWith(expect.objectContaining({ id: "FN-retry" })));
expect(createTask).toHaveBeenCalledTimes(2);
});
it("emits no fix action while PR detail is loading, errored, empty, or has no failures", async () => {
let resolveDetail!: (detail: { comments: []; checks: [] }) => void;
const loadingDetail = new Promise<{ comments: []; checks: [] }>((resolve) => { resolveDetail = resolve; });
await renderSelectedPullChecks([], "modal", loadingDetail);
expect(await screen.findByTestId("github-import-pr-checks-loading")).toBeTruthy();
expect(screen.queryByTestId("github-import-pr-check-fix-task")).toBeNull();
await act(async () => { resolveDetail({ comments: [], checks: [] }); });
expect(await screen.findByTestId("github-import-pr-checks-empty")).toBeTruthy();
expect(screen.queryByTestId("github-import-pr-check-fix-task")).toBeNull();
});
it("emits no fix action for PR detail errors or populated non-failing checks", async () => {
vi.mocked(apiFetchGitHubPullDetail).mockRejectedValueOnce(new Error("Checks unavailable"));
vi.mocked(fetchGitRemotes).mockResolvedValueOnce([{ name: "origin", owner: "owner", repo: "repo", url: "" }]);
vi.mocked(apiFetchGitHubPulls).mockResolvedValueOnce([{
number: 82, title: "No failures", body: "", html_url: "https://github.com/owner/repo/pull/82", headBranch: "feature", baseBranch: "main",
}]);
render(<GitHubImportModal isOpen onClose={onClose} onImport={onImport} tasks={[]} />);
fireEvent.click(await screen.findByRole("tab", { name: /Pull Requests/i }));
await screen.findByText("No failures");
fireEvent.click(screen.getByRole("button", { name: /Select pull request #82/i }));
expect(await screen.findByTestId("github-import-pr-checks-error")).toHaveTextContent("Checks unavailable");
expect(screen.queryByTestId("github-import-pr-check-fix-task")).toBeNull();
});
it("emits no fix action for populated successful, pending, and neutral checks", async () => {
await renderSelectedPullChecks(checkVariants.slice(1));
expect(await screen.findByTestId("github-import-pr-checks")).toBeTruthy();
expect(screen.queryByTestId("github-import-pr-check-fix-task")).toBeNull();
});
});
it("renders when isOpen is true", async () => { it("renders when isOpen is true", async () => {
vi.mocked(fetchGitRemotes).mockResolvedValueOnce([]); vi.mocked(fetchGitRemotes).mockResolvedValueOnce([]);
render(<GitHubImportModal isOpen={true} onClose={onClose} onImport={onImport} tasks={[]} />); render(<GitHubImportModal isOpen={true} onClose={onClose} onImport={onImport} tasks={[]} />);