From 7c6f8b3bb0833bf889878abb4d77153284de40a4 Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Tue, 9 Jun 2026 00:32:23 -0700 Subject: [PATCH] FN-6044: fix GitHub import mobile preview scrolling Keep GitHub import previews fully readable and scrollable on mobile while preserving desktop truncation. - allow the active mobile preview pane to size and scroll correctly within the modal - show full issue and pull request bodies on mobile while keeping desktop previews truncated - add regression coverage for mobile scrolling, no-description fallbacks, and mobile/desktop body rendering Files changed: packages/dashboard/app/components/GitHubImportModal.css | 18 +++ packages/dashboard/app/components/GitHubImportModal.tsx | 18 ++- packages/dashboard/app/components/__tests__/GitHubImportModal.test.tsx | 160 +++++++++++++++++++++ 3 files changed, 190 insertions(+), 6 deletions(-) Fusion-Task-Id: FN-6044 Fusion-Task-Lineage: 43b6e686-a34f-4fec-9929-b4d28c12ac7a --- .../app/components/GitHubImportModal.css | 18 ++ .../app/components/GitHubImportModal.tsx | 18 +- .../__tests__/GitHubImportModal.test.tsx | 160 ++++++++++++++++++ 3 files changed, 190 insertions(+), 6 deletions(-) diff --git a/packages/dashboard/app/components/GitHubImportModal.css b/packages/dashboard/app/components/GitHubImportModal.css index 588e3e7610..5b671b3dec 100644 --- a/packages/dashboard/app/components/GitHubImportModal.css +++ b/packages/dashboard/app/components/GitHubImportModal.css @@ -777,6 +777,9 @@ } .github-import-modal__body { + flex: 1; + min-height: 0; + overflow: hidden; padding: var(--space-md); gap: var(--space-md); } @@ -844,6 +847,12 @@ max-height: 50vh; } + .github-import-workspace { + flex: 1; + min-height: 0; + overflow: hidden; + } + /* Mobile pane visibility - show one at a time */ .github-import-list-pane.mobile { display: none; @@ -864,7 +873,16 @@ .github-import-preview-pane.mobile.active { display: flex; flex: 1; + min-height: 0; max-height: none; + overflow: hidden; + } + + .github-import-preview-pane.mobile.active .github-import-pane-content { + flex: 1; + min-height: 0; + overflow-y: auto; + overscroll-behavior: contain; } /* Back button styles */ diff --git a/packages/dashboard/app/components/GitHubImportModal.tsx b/packages/dashboard/app/components/GitHubImportModal.tsx index 11528586cc..3ef7233dd4 100644 --- a/packages/dashboard/app/components/GitHubImportModal.tsx +++ b/packages/dashboard/app/components/GitHubImportModal.tsx @@ -39,6 +39,16 @@ function clampListPaneWidth(width: number) { return Math.max(GITHUB_IMPORT_LIST_PANE_MIN_WIDTH, Math.min(GITHUB_IMPORT_LIST_PANE_MAX_WIDTH, width)); } +function formatPreviewBody(body: string | null | undefined, isMobile: boolean) { + if (!body) { + return null; + } + if (isMobile) { + return body; + } + return body.slice(0, 200) + (body.length > 200 ? "…" : ""); +} + export function GitHubImportModal({ isOpen, onClose, onImport, tasks, projectId }: GitHubImportModalProps) { useMobileScrollLock(isOpen); const { t } = useTranslation("app"); @@ -797,9 +807,7 @@ export function GitHubImportModal({ isOpen, onClose, onImport, tasks, projectId
{t("git.previewIssueMeta", "Issue #{{number}}", { number: selectedIssue.number })}
{selectedIssue.title}
- {selectedIssue.body - ? selectedIssue.body.slice(0, 200) + (selectedIssue.body.length > 200 ? "…" : "") - : t("git.noDescription", "(no description)")} + {formatPreviewBody(selectedIssue.body, isMobile) || t("git.noDescription", "(no description)")}
) : activeTab === "issues" ? ( @@ -820,9 +828,7 @@ export function GitHubImportModal({ isOpen, onClose, onImport, tasks, projectId {t("git.branchLabel", "Branch:")} {selectedPull.headBranch} → {selectedPull.baseBranch}
- {selectedPull.body - ? selectedPull.body.slice(0, 200) + (selectedPull.body.length > 200 ? "…" : "") - : t("git.noDescription", "(no description)")} + {formatPreviewBody(selectedPull.body, isMobile) || t("git.noDescription", "(no description)")}
) : activeTab === "pulls" ? ( diff --git a/packages/dashboard/app/components/__tests__/GitHubImportModal.test.tsx b/packages/dashboard/app/components/__tests__/GitHubImportModal.test.tsx index 78c9dec90a..d8d501e5db 100644 --- a/packages/dashboard/app/components/__tests__/GitHubImportModal.test.tsx +++ b/packages/dashboard/app/components/__tests__/GitHubImportModal.test.tsx @@ -83,6 +83,12 @@ describe("GitHubImportModal", () => { expect(source).toContain("Some hardcoded colors below"); }); + it("keeps mobile preview content vertically scrollable inside the active pane", () => { + const source = readFileSync(resolve(__dirname, "../GitHubImportModal.css"), "utf8"); + expect(source).toContain(".github-import-preview-pane.mobile.active {\n display: flex;\n flex: 1;\n min-height: 0;\n max-height: none;\n overflow: hidden;"); + expect(source).toContain(".github-import-preview-pane.mobile.active .github-import-pane-content {\n flex: 1;\n min-height: 0;\n overflow-y: auto;\n overscroll-behavior: contain;"); + }); + beforeEach(() => { vi.clearAllMocks(); vi.mocked(fetchGitRemotes).mockReset(); @@ -183,6 +189,30 @@ describe("GitHubImportModal", () => { expect(screen.queryByTestId("github-import-preview-empty")).toBeNull(); }); + it("preserves the no-description fallback for empty and null issue bodies", async () => { + const issues = [ + { number: 1, title: "Empty Issue", body: "", html_url: "https://github.com/owner/repo/issues/1", labels: [] }, + { number: 2, title: "Null Issue", body: null, html_url: "https://github.com/owner/repo/issues/2", labels: [] }, + ]; + vi.mocked(fetchGitRemotes).mockResolvedValueOnce(singleRemote); + vi.mocked(apiFetchGitHubIssues).mockResolvedValueOnce(issues); + + render(); + + await waitFor(() => { + expect(screen.getByText("Empty Issue")).toBeTruthy(); + expect(screen.getByText("Null Issue")).toBeTruthy(); + }); + + fireEvent.click(screen.getByRole("radio", { name: /Select issue #1/i })); + let previewCard = await screen.findByTestId("github-import-preview-card"); + expect(within(previewCard).getByText("(no description)")).toBeTruthy(); + + fireEvent.click(screen.getByRole("radio", { name: /Select issue #2/i })); + previewCard = await screen.findByTestId("github-import-preview-card"); + expect(within(previewCard).getByText("(no description)")).toBeTruthy(); + }); + it("has optional labels input with filter placeholder", async () => { vi.mocked(fetchGitRemotes).mockResolvedValueOnce(singleRemote); render(); @@ -662,6 +692,136 @@ describe("GitHubImportModal", () => { expect(previewPane.classList.contains("active")).toBe(false); }); + it("renders long selected issue body in full on mobile without a truncation ellipsis", async () => { + Object.defineProperty(window, "innerWidth", { + writable: true, + configurable: true, + value: 480, + }); + + const beyondPreviousCutoff = "visible body text after the old cutoff"; + const longBody = `${"A".repeat(210)} ${beyondPreviousCutoff}`; + const issues = [ + { number: 1, title: "Long Issue", body: longBody, html_url: "https://github.com/owner/repo/issues/1", labels: [] }, + ]; + vi.mocked(fetchGitRemotes).mockResolvedValueOnce(singleRemote); + vi.mocked(apiFetchGitHubIssues).mockResolvedValueOnce(issues); + + render(); + + await waitFor(() => { + expect(screen.getByText("Long Issue")).toBeTruthy(); + }); + + fireEvent.click(screen.getByRole("radio", { name: /Select issue #1/i })); + + const previewPane = screen.getByTestId("github-import-preview-pane"); + await waitFor(() => { + expect(previewPane.classList.contains("active")).toBe(true); + }); + + const previewCard = await screen.findByTestId("github-import-preview-card"); + expect(within(previewCard).getByText((content) => content.includes(beyondPreviousCutoff))).toBeTruthy(); + expect(previewCard.textContent).toContain(longBody); + expect(previewCard.textContent).not.toContain(`${"A".repeat(200)}…`); + }); + + it("renders long selected pull request body in full on mobile without a truncation ellipsis", async () => { + Object.defineProperty(window, "innerWidth", { + writable: true, + configurable: true, + value: 480, + }); + + const beyondPreviousCutoff = "visible pull request body text after the old cutoff"; + const longBody = `${"P".repeat(210)} ${beyondPreviousCutoff}`; + const pulls = [ + { number: 1, title: "Long PR", body: longBody, html_url: "https://github.com/owner/repo/pull/1", headBranch: "feature", baseBranch: "main" }, + ]; + vi.mocked(fetchGitRemotes).mockResolvedValueOnce(singleRemote); + vi.mocked(apiFetchGitHubPulls).mockResolvedValueOnce(pulls); + + render(); + + fireEvent.click(await screen.findByRole("tab", { name: /Pull Requests/i })); + + await waitFor(() => { + expect(screen.getByText("Long PR")).toBeTruthy(); + }); + + fireEvent.click(screen.getByRole("radio", { name: /Select pull request #1/i })); + + const previewPane = screen.getByTestId("github-import-preview-pane"); + await waitFor(() => { + expect(previewPane.classList.contains("active")).toBe(true); + }); + + const previewCard = await screen.findByTestId("github-import-preview-card"); + expect(within(previewCard).getByText((content) => content.includes(beyondPreviousCutoff))).toBeTruthy(); + expect(previewCard.textContent).toContain(longBody); + expect(previewCard.textContent).not.toContain(`${"P".repeat(200)}…`); + }); + + it("truncates long selected issue body on desktop", async () => { + Object.defineProperty(window, "innerWidth", { + writable: true, + configurable: true, + value: 1200, + }); + + const beyondDesktopCutoff = "desktop issue text after the cutoff"; + const longBody = `${"I".repeat(210)} ${beyondDesktopCutoff}`; + const issues = [ + { number: 1, title: "Long Desktop Issue", body: longBody, html_url: "https://github.com/owner/repo/issues/1", labels: [] }, + ]; + vi.mocked(fetchGitRemotes).mockResolvedValueOnce(singleRemote); + vi.mocked(apiFetchGitHubIssues).mockResolvedValueOnce(issues); + + render(); + + await waitFor(() => { + expect(screen.getByText("Long Desktop Issue")).toBeTruthy(); + }); + + fireEvent.click(screen.getByRole("radio", { name: /Select issue #1/i })); + + const previewCard = await screen.findByTestId("github-import-preview-card"); + expect(previewCard.textContent).toContain(`${"I".repeat(200)}…`); + expect(previewCard.textContent).not.toContain(beyondDesktopCutoff); + expect(previewCard.textContent).not.toContain(longBody); + }); + + it("truncates long selected pull request body on desktop", async () => { + Object.defineProperty(window, "innerWidth", { + writable: true, + configurable: true, + value: 1200, + }); + + const beyondDesktopCutoff = "desktop pull request text after the cutoff"; + const longBody = `${"R".repeat(210)} ${beyondDesktopCutoff}`; + const pulls = [ + { number: 1, title: "Long Desktop PR", body: longBody, html_url: "https://github.com/owner/repo/pull/1", headBranch: "feature", baseBranch: "main" }, + ]; + vi.mocked(fetchGitRemotes).mockResolvedValueOnce(singleRemote); + vi.mocked(apiFetchGitHubPulls).mockResolvedValueOnce(pulls); + + render(); + + fireEvent.click(await screen.findByRole("tab", { name: /Pull Requests/i })); + + await waitFor(() => { + expect(screen.getByText("Long Desktop PR")).toBeTruthy(); + }); + + fireEvent.click(screen.getByRole("radio", { name: /Select pull request #1/i })); + + const previewCard = await screen.findByTestId("github-import-preview-card"); + expect(previewCard.textContent).toContain(`${"R".repeat(200)}…`); + expect(previewCard.textContent).not.toContain(beyondDesktopCutoff); + expect(previewCard.textContent).not.toContain(longBody); + }); + it("returns to list view on mobile after successful import", async () => { Object.defineProperty(window, "innerWidth", { writable: true,