feat(FN-5049): complete Step 1 — add PR create modal scroll body
Fusion-Task-Id: FN-5049 Fusion-Task-Lineage: d4b3872d-00ff-466a-bb0d-893c5d90962c
This commit is contained in:
committed by
gsxdsm
parent
be6279f656
commit
b4d6b85692
@@ -1,7 +1,16 @@
|
||||
.pr-create-modal {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.pr-create-modal__body {
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
flex-direction: column;
|
||||
gap: var(--space-lg);
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
padding: var(--space-lg) var(--modal-padding);
|
||||
}
|
||||
|
||||
.pr-create-modal__loading {
|
||||
@@ -100,18 +109,25 @@
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--space-sm);
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.pr-create-modal__chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--space-xs);
|
||||
min-width: 0;
|
||||
padding: var(--space-xs) var(--space-sm);
|
||||
border-radius: var(--radius-pill);
|
||||
border: var(--btn-border-width) solid var(--border);
|
||||
background: color-mix(in srgb, var(--card) 88%, transparent);
|
||||
}
|
||||
|
||||
.pr-create-modal__chip-label {
|
||||
min-width: 0;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.pr-create-modal__chip--colored {
|
||||
background: color-mix(in srgb, var(--pr-chip-label-color, var(--card)) 25%, transparent);
|
||||
}
|
||||
@@ -128,6 +144,7 @@
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--space-sm);
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.pr-create-modal__option-item {
|
||||
@@ -167,6 +184,12 @@
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.pr-create-modal__commit-row > *,
|
||||
.pr-create-modal__file-row > * {
|
||||
min-width: 0;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.pr-create-modal .form-error {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -198,7 +221,7 @@
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.pr-create-modal {
|
||||
.pr-create-modal__body {
|
||||
gap: var(--space-md);
|
||||
}
|
||||
|
||||
@@ -211,6 +234,28 @@
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.pr-create-modal__draft {
|
||||
align-self: start;
|
||||
}
|
||||
|
||||
.pr-create-modal__preflight-row {
|
||||
grid-template-columns: auto 1fr;
|
||||
}
|
||||
|
||||
.pr-create-modal__preflight-row .status-dot {
|
||||
grid-column: 1;
|
||||
grid-row: 1;
|
||||
}
|
||||
|
||||
.pr-create-modal__preflight-row svg {
|
||||
grid-column: 2;
|
||||
grid-row: 1;
|
||||
}
|
||||
|
||||
.pr-create-modal__preflight-row > div {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.pr-create-modal__inline-actions {
|
||||
width: 100%;
|
||||
flex-wrap: wrap;
|
||||
|
||||
@@ -77,7 +77,7 @@ function OptionChips<T extends { login?: string; name?: string; color?: string }
|
||||
className={`pr-create-modal__chip${hasColor ? " pr-create-modal__chip--colored" : ""}`}
|
||||
style={chipStyle}
|
||||
>
|
||||
{getLabel(item)}
|
||||
<span className="pr-create-modal__chip-label">{getLabel(item)}</span>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-icon pr-create-modal__chip-remove"
|
||||
@@ -336,9 +336,10 @@ export function PrCreateModal({
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{loading ? <div className="pr-create-modal__loading">Loading PR metadata…</div> : (
|
||||
<>
|
||||
<section className="pr-create-modal__section">
|
||||
<div className="pr-create-modal__body">
|
||||
{loading ? <div className="pr-create-modal__loading">Loading PR metadata…</div> : (
|
||||
<>
|
||||
<section className="pr-create-modal__section">
|
||||
<h3 className="pr-create-modal__section-title">Pre-flight checks</h3>
|
||||
<div className="pr-create-modal__preflight">
|
||||
{checks.map((check) => (
|
||||
@@ -441,20 +442,21 @@ export function PrCreateModal({
|
||||
</div>
|
||||
</details>
|
||||
|
||||
{error && (
|
||||
<div className="form-error pr-error" role="alert">
|
||||
<p>{error}</p>
|
||||
{lastGhError?.hint ? <p className="pr-error__hint">{lastGhError.hint}</p> : null}
|
||||
<div className="pr-error__actions">
|
||||
{lastGhError?.action?.kind === "shell" ? <p>Action: run <code>{lastGhError.action.command}</code></p> : null}
|
||||
{lastGhError?.action?.kind === "open" ? <p>Action: open <a href={lastGhError.action.url} target="_blank" rel="noreferrer">docs</a></p> : null}
|
||||
{lastGhError?.retryable ? <button type="button" className="btn btn-sm pr-error__retry" onClick={() => void submit()}>Retry</button> : null}
|
||||
<button type="button" className="btn btn-sm pr-error__dismiss" onClick={() => { setLastGhError(null); setError(null); }} aria-label="Dismiss PR error">×</button>
|
||||
{error && (
|
||||
<div className="form-error pr-error" role="alert">
|
||||
<p>{error}</p>
|
||||
{lastGhError?.hint ? <p className="pr-error__hint">{lastGhError.hint}</p> : null}
|
||||
<div className="pr-error__actions">
|
||||
{lastGhError?.action?.kind === "shell" ? <p>Action: run <code>{lastGhError.action.command}</code></p> : null}
|
||||
{lastGhError?.action?.kind === "open" ? <p>Action: open <a href={lastGhError.action.url} target="_blank" rel="noreferrer">docs</a></p> : null}
|
||||
{lastGhError?.retryable ? <button type="button" className="btn btn-sm pr-error__retry" onClick={() => void submit()}>Retry</button> : null}
|
||||
<button type="button" className="btn btn-sm pr-error__dismiss" onClick={() => { setLastGhError(null); setError(null); }} aria-label="Dismiss PR error">×</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="modal-actions">
|
||||
<button type="button" className="btn" onClick={onClose} disabled={submitting}>Cancel</button>
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
import { render, screen, waitFor } from "@testing-library/react";
|
||||
import { beforeAll, afterAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { PrCreateModal } from "../PrCreateModal";
|
||||
import { loadAllAppCss } from "../../test/cssFixture";
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
generatePrMetadata: vi.fn(),
|
||||
fetchPrPreflight: vi.fn(),
|
||||
fetchPrOptions: vi.fn(),
|
||||
createPr: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("../../api", () => ({
|
||||
generatePrMetadata: mocks.generatePrMetadata,
|
||||
fetchPrPreflight: mocks.fetchPrPreflight,
|
||||
fetchPrOptions: mocks.fetchPrOptions,
|
||||
createPr: mocks.createPr,
|
||||
}));
|
||||
|
||||
describe("PrCreateModal layout", () => {
|
||||
let styleEl: HTMLStyleElement;
|
||||
|
||||
beforeAll(() => {
|
||||
styleEl = document.createElement("style");
|
||||
styleEl.textContent = loadAllAppCss();
|
||||
document.head.appendChild(styleEl);
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
styleEl.remove();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mocks.generatePrMetadata.mockResolvedValue({ title: "AI title", body: "AI body", templateUsed: false });
|
||||
mocks.fetchPrPreflight.mockResolvedValue({
|
||||
branchOnRemote: true,
|
||||
commitsPresent: true,
|
||||
conflictsWithBase: false,
|
||||
ghAuthOk: true,
|
||||
defaultBaseBranch: "main",
|
||||
head: "fusion/FN-5049",
|
||||
commits: [],
|
||||
changedFiles: [],
|
||||
});
|
||||
mocks.fetchPrOptions.mockResolvedValue({
|
||||
baseBranches: ["main"],
|
||||
reviewers: [],
|
||||
assignees: [],
|
||||
labels: [],
|
||||
});
|
||||
});
|
||||
|
||||
it("renders a dedicated scroll body between modal header and actions", async () => {
|
||||
render(<PrCreateModal open taskId="FN-5049" onClose={vi.fn()} onCreated={vi.fn()} addToast={vi.fn()} />);
|
||||
|
||||
await waitFor(() => expect(mocks.generatePrMetadata).toHaveBeenCalled());
|
||||
|
||||
const dialog = screen.getByRole("dialog");
|
||||
const modal = dialog.classList.contains("modal") ? dialog : dialog.closest(".modal");
|
||||
expect(modal).toBeTruthy();
|
||||
|
||||
const header = modal?.querySelector(":scope > .modal-header");
|
||||
const body = modal?.querySelector(":scope > .pr-create-modal__body");
|
||||
const actions = modal?.querySelector(":scope > .modal-actions");
|
||||
|
||||
expect(header).toBeTruthy();
|
||||
expect(body).toBeTruthy();
|
||||
expect(actions).toBeTruthy();
|
||||
expect(actions?.parentElement).toBe(modal);
|
||||
|
||||
const allAutoOverflow = Array.from(modal?.querySelectorAll<HTMLElement>("*") ?? []).filter(
|
||||
(element) => getComputedStyle(element).overflowY === "auto",
|
||||
);
|
||||
expect(allAutoOverflow).toHaveLength(1);
|
||||
expect(allAutoOverflow[0]).toBe(body);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user