Skip to content

Commit 9bf9ec6

Browse files
committed
fix(onboarding): validates upstream repo existence on manual add
- adds GET /repos/{owner}/{repo} check on manual upstream entry - 404 shows "Repository not found", disables input during check - enables "Finish Setup" when upstream repos selected (no org repos) - updates button repo count to include upstream repos
1 parent 62a46a3 commit 9bf9ec6

File tree

3 files changed

+63
-8
lines changed

3 files changed

+63
-8
lines changed

src/app/components/onboarding/OnboardingWizard.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -127,12 +127,12 @@ export default function OnboardingWizard() {
127127
<button
128128
type="button"
129129
onClick={handleFinish}
130-
disabled={selectedRepos().length === 0}
130+
disabled={selectedRepos().length === 0 && upstreamRepos().length === 0}
131131
class="btn btn-primary"
132132
>
133-
{selectedRepos().length === 0
133+
{selectedRepos().length + upstreamRepos().length === 0
134134
? "Finish Setup"
135-
: `Finish Setup (${selectedRepos().length} ${selectedRepos().length === 1 ? "repo" : "repos"})`}
135+
: `Finish Setup (${selectedRepos().length + upstreamRepos().length} ${selectedRepos().length + upstreamRepos().length === 1 ? "repo" : "repos"})`}
136136
</button>
137137
</div>
138138
</Show>

src/app/components/onboarding/RepoSelector.tsx

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ export default function RepoSelector(props: RepoSelectorProps) {
4545
const [discoveringUpstream, setDiscoveringUpstream] = createSignal(false);
4646
const [discoveryCapped, setDiscoveryCapped] = createSignal(false);
4747
const [manualEntry, setManualEntry] = createSignal("");
48+
const [validatingManual, setValidatingManual] = createSignal(false);
4849
const [manualEntryError, setManualEntryError] = createSignal<string | null>(null);
4950

5051
// Initialize org states and fetch repos on mount / when selectedOrgs change
@@ -320,7 +321,7 @@ export default function RepoSelector(props: RepoSelectorProps) {
320321
}
321322
}
322323

323-
function handleManualAdd() {
324+
async function handleManualAdd() {
324325
const raw = manualEntry().trim();
325326
if (!raw) return;
326327
if (!VALID_REPO_NAME.test(raw)) {
@@ -344,14 +345,38 @@ export default function RepoSelector(props: RepoSelectorProps) {
344345
return;
345346
}
346347

348+
const client = getClient();
349+
if (!client) {
350+
setManualEntryError("Not connected — try again");
351+
return;
352+
}
353+
354+
setValidatingManual(true);
355+
setManualEntryError(null);
356+
try {
357+
await client.request("GET /repos/{owner}/{repo}", { owner, repo: name });
358+
} catch (err) {
359+
const status = typeof err === "object" && err !== null && "status" in err
360+
? (err as { status: number }).status
361+
: null;
362+
if (status === 404) {
363+
setManualEntryError("Repository not found");
364+
} else {
365+
setManualEntryError("Could not verify repository — try again");
366+
}
367+
return;
368+
} finally {
369+
setValidatingManual(false);
370+
}
371+
347372
const newRepo: RepoRef = { owner, name, fullName };
348373
props.onUpstreamChange?.([...(props.upstreamRepos ?? []), newRepo]);
349374
setManualEntry("");
350375
setManualEntryError(null);
351376
}
352377

353378
function handleManualKeyDown(e: KeyboardEvent) {
354-
if (e.key === "Enter") handleManualAdd();
379+
if (e.key === "Enter") void handleManualAdd();
355380
}
356381

357382
// Manually-added upstream repos not in the discovered list
@@ -554,15 +579,17 @@ export default function RepoSelector(props: RepoSelectorProps) {
554579
setManualEntryError(null);
555580
}}
556581
onKeyDown={handleManualKeyDown}
582+
disabled={validatingManual()}
557583
class="input input-sm flex-1"
558584
aria-label="Add upstream repo manually"
559585
/>
560586
<button
561587
type="button"
562-
onClick={handleManualAdd}
588+
onClick={() => void handleManualAdd()}
589+
disabled={validatingManual()}
563590
class="btn btn-sm btn-outline"
564591
>
565-
Add
592+
{validatingManual() ? "Checking..." : "Add"}
566593
</button>
567594
</div>
568595
<Show when={manualEntryError()}>

tests/components/onboarding/RepoSelector.test.tsx

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@ import userEvent from "@testing-library/user-event";
44
import type { RepoRef, RepoEntry } from "../../../src/app/services/api";
55

66
// Mock getClient before importing component
7+
const mockRequest = vi.fn().mockResolvedValue({ data: {} });
78
vi.mock("../../../src/app/services/github", () => ({
8-
getClient: () => ({}),
9+
getClient: () => ({ request: mockRequest }),
910
}));
1011

1112
vi.mock("../../../src/app/stores/auth", () => ({
@@ -379,6 +380,7 @@ describe("RepoSelector — upstream discovery", () => {
379380
vi.restoreAllMocks();
380381
vi.mocked(api.fetchRepos).mockResolvedValue(myorgRepos);
381382
vi.mocked(api.discoverUpstreamRepos).mockResolvedValue([]);
383+
mockRequest.mockReset().mockResolvedValue({ data: {} });
382384
});
383385

384386
it("does not call discoverUpstreamRepos when showUpstreamDiscovery is false (default)", async () => {
@@ -661,4 +663,30 @@ describe("RepoSelector — upstream discovery", () => {
661663

662664
screen.getByText(/already discovered/i);
663665
});
666+
667+
it("manual entry: shows error when repo does not exist (404)", async () => {
668+
const user = userEvent.setup();
669+
mockRequest.mockRejectedValue(Object.assign(new Error("Not Found"), { status: 404 }));
670+
671+
render(() => (
672+
<RepoSelector
673+
selectedOrgs={["myorg"]}
674+
selected={[]}
675+
onChange={vi.fn()}
676+
showUpstreamDiscovery={true}
677+
upstreamRepos={[]}
678+
onUpstreamChange={vi.fn()}
679+
/>
680+
));
681+
682+
await waitFor(() => screen.getByText("Upstream Repositories"));
683+
684+
const input = screen.getByRole("textbox", { name: /add upstream repo/i });
685+
await user.type(input, "nonexistent-org/no-repo");
686+
await user.click(screen.getByRole("button", { name: /^Add$/ }));
687+
688+
await waitFor(() => {
689+
screen.getByText(/repository not found/i);
690+
});
691+
});
664692
});

0 commit comments

Comments
 (0)