Files
fusion/packages/dashboard/app/hooks/modalPersistence.ts
gsxdsm 511bcaf56d FN-7657: persist GitHub issue import modal state across close/reopen
Retains the GitHub Import Tasks modal's provider/tab/filter/selection state so returning to Import Tasks doesn't reset the user's in-progress import setup.

- GitHubImportModal now persists provider, active tab, label filter, remote, and issue selection per project via a new modalPersistence hook, restoring them on remount instead of always defaulting.
- Added packages/dashboard/app/hooks/modalPersistence.ts to encapsulate the persisted-state read/write logic backed by projectStorage.
- projectStorage.ts gains the `kb-dashboard-github-import-state` storage key.
- Falls back to the existing default-remote auto-detect behavior when no persisted state exists.
- Added extensive test coverage in GitHubImportModal.test.tsx for the new persistence behavior.
- Updated docs/dashboard-guide.md to describe the retained state.
- Added a patch changeset for @runfusion/fusion.

Files changed:
 .changeset/fn-7657-github-import-state-retained.md |   7 +
 docs/dashboard-guide.md                            |   2 +
 .../dashboard/app/components/GitHubImportModal.tsx | 204 +++++++++++++--
 .../__tests__/GitHubImportModal.test.tsx           | 282 +++++++++++++++++++++
 packages/dashboard/app/hooks/modalPersistence.ts   |  72 ++++++
 packages/dashboard/app/utils/projectStorage.ts     |   1 +
 6 files changed, 547 insertions(+), 21 deletions(-)

Fusion-Task-Id: FN-7657
Fusion-Task-Lineage: c8086340-368c-4efd-a12a-4cddeeb0aa26
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-08 00:14:52 -07:00

121 lines
5.0 KiB
TypeScript

import { getScopedItem, removeScopedItem, setScopedItem } from "../utils/projectStorage";
// Storage keys — each modal type has independent storage
export const STORED_PLANNING_KEY = "kb-planning-last-description";
export const STORED_SUBTASK_KEY = "kb-subtask-last-description";
export const STORED_MISSION_KEY = "kb-mission-last-goal";
export const STORED_GITHUB_IMPORT_KEY = "kb-dashboard-github-import-state";
// Planning persistence
export function savePlanningDescription(description: string, projectId?: string): void {
setScopedItem(STORED_PLANNING_KEY, description, projectId);
}
export function getPlanningDescription(projectId?: string): string {
return getScopedItem(STORED_PLANNING_KEY, projectId) || "";
}
export function clearPlanningDescription(projectId?: string): void {
removeScopedItem(STORED_PLANNING_KEY, projectId);
}
// Subtask persistence
export function saveSubtaskDescription(description: string, projectId?: string): void {
setScopedItem(STORED_SUBTASK_KEY, description, projectId);
}
export function getSubtaskDescription(projectId?: string): string {
return getScopedItem(STORED_SUBTASK_KEY, projectId) || "";
}
export function clearSubtaskDescription(projectId?: string): void {
removeScopedItem(STORED_SUBTASK_KEY, projectId);
}
// Mission persistence
export function saveMissionGoal(goal: string, projectId?: string): void {
setScopedItem(STORED_MISSION_KEY, goal, projectId);
}
export function getMissionGoal(projectId?: string): string {
return getScopedItem(STORED_MISSION_KEY, projectId) || "";
}
export function clearMissionGoal(projectId?: string): void {
removeScopedItem(STORED_MISSION_KEY, projectId);
}
// GitHub/GitLab import persistence
/*
FNXC:GitHubImport 2026-07-07-00:00:
The embedded Import Tasks view (`GitHubImportModal` rendered with `presentation="embedded"`, constant `isOpen={true}`) fully
unmounts when the user navigates to another main-content view and remounts from scratch on return, so its "reset state on
open" effect previously wiped provider/tab/filter/remote/selection every time. Persist ONLY the cheap, restorable fields
listed below (never the fetched issues/pulls/gitlab lists, loading flags, or detail caches — those re-derive via the
existing auto-load) per-project so returning to the view resumes where the user left off. First-time opens with no
persisted value must keep the existing default-remote auto-detect behavior untouched.
*/
export interface GitHubImportPersistedState {
provider: "github" | "gitlab";
activeTab: "issues" | "pulls";
labels: string;
selectedRemoteName: string;
owner: string;
repo: string;
gitlabResource: "project_issue" | "group_issue" | "merge_request";
gitlabProject: string;
gitlabGroup: string;
selectedIssueNumber: number | null;
selectedPullNumber: number | null;
selectedGitlabKey: string | null;
}
export function saveGitHubImportState(state: GitHubImportPersistedState, projectId?: string): void {
try {
setScopedItem(STORED_GITHUB_IMPORT_KEY, JSON.stringify(state), projectId);
} catch {
// Best-effort persistence; ignore storage failures (e.g. quota, disabled storage).
}
}
/**
* Reads and defensively re-shapes the persisted GitHub/GitLab import state.
* Returns null when nothing is stored, or when the stored value is corrupt/not an object, so callers can fall back
* to the existing reset/default-remote-auto-detect behavior exactly as before. Each field is individually validated
* and defaulted so a partially-corrupt or schema-drifted blob still yields a usable (if partial) restore.
*/
export function getGitHubImportState(projectId?: string): GitHubImportPersistedState | null {
try {
const raw = getScopedItem(STORED_GITHUB_IMPORT_KEY, projectId);
if (!raw) return null;
const parsed: unknown = JSON.parse(raw);
if (!parsed || typeof parsed !== "object") return null;
const p = parsed as Record<string, unknown>;
return {
provider: p.provider === "gitlab" ? "gitlab" : "github",
activeTab: p.activeTab === "pulls" ? "pulls" : "issues",
labels: typeof p.labels === "string" ? p.labels : "",
selectedRemoteName: typeof p.selectedRemoteName === "string" ? p.selectedRemoteName : "",
owner: typeof p.owner === "string" ? p.owner : "",
repo: typeof p.repo === "string" ? p.repo : "",
gitlabResource:
p.gitlabResource === "group_issue" || p.gitlabResource === "merge_request" ? p.gitlabResource : "project_issue",
gitlabProject: typeof p.gitlabProject === "string" ? p.gitlabProject : "",
gitlabGroup: typeof p.gitlabGroup === "string" ? p.gitlabGroup : "",
selectedIssueNumber: typeof p.selectedIssueNumber === "number" ? p.selectedIssueNumber : null,
selectedPullNumber: typeof p.selectedPullNumber === "number" ? p.selectedPullNumber : null,
selectedGitlabKey: typeof p.selectedGitlabKey === "string" ? p.selectedGitlabKey : null,
};
} catch {
return null;
}
}
export function clearGitHubImportState(projectId?: string): void {
removeScopedItem(STORED_GITHUB_IMPORT_KEY, projectId);
}