diff --git a/apps/fabro-web/app/install-api.test.ts b/apps/fabro-web/app/install-api.test.ts index 2fbf09518..bf24b9140 100644 --- a/apps/fabro-web/app/install-api.test.ts +++ b/apps/fabro-web/app/install-api.test.ts @@ -1,6 +1,6 @@ import { describe, expect, test } from "bun:test"; -import { buildInstallGithubAppOwner, readInstallError } from "./install-api"; +import { readInstallError } from "./install-api"; describe("readInstallError", () => { test("prefers the structured install error payload", async () => { @@ -31,15 +31,3 @@ describe("readInstallError", () => { }); }); -describe("buildInstallGithubAppOwner", () => { - test("uses the personal tag for personal app installs", () => { - expect(buildInstallGithubAppOwner("personal", "")).toEqual({ kind: "personal" }); - }); - - test("trims organization slugs and tags them with `org`", () => { - expect(buildInstallGithubAppOwner("org", " acme ")).toEqual({ - kind: "org", - slug: "acme", - }); - }); -}); diff --git a/apps/fabro-web/app/install-api.ts b/apps/fabro-web/app/install-api.ts index a04a723dc..7a7a56a5f 100644 --- a/apps/fabro-web/app/install-api.ts +++ b/apps/fabro-web/app/install-api.ts @@ -64,15 +64,6 @@ export async function readInstallError( return `${fallback} (${response.status})`; } -export function buildInstallGithubAppOwner( - ownerKind: "personal" | "org", - organizationSlug: string, -): InstallGithubAppOwner { - return ownerKind === "org" - ? { kind: "org", slug: organizationSlug.trim() } - : { kind: "personal" }; -} - export async function getInstallSession(token: string): Promise { const response = await installFetch("/install/session", token); if (!response.ok) { diff --git a/apps/fabro-web/app/install-app.tsx b/apps/fabro-web/app/install-app.tsx index 12bc7fefc..6c390ede2 100644 --- a/apps/fabro-web/app/install-app.tsx +++ b/apps/fabro-web/app/install-app.tsx @@ -7,7 +7,6 @@ import { type InstallGithubAppOwner, type InstallLlmProviderInput, type InstallSessionResponse, - buildInstallGithubAppOwner, createInstallGithubAppManifest, finishInstall, getInstallSession, @@ -41,6 +40,20 @@ type FinishState = InstallFinishResponse | null; type GithubStrategy = "token" | "app"; type GithubOwnerKind = "personal" | "org"; +type SessionState = + | { status: "idle" } + | { status: "loading" } + | { status: "error"; message: string } + | { status: "ready"; data: InstallSessionResponse }; + +type TokenForm = { token: string; username: string }; + +type AppForm = { + owner: InstallGithubAppOwner; + appName: string; + allowedUsername: string; +}; + type ProviderSelection = Record< string, { @@ -54,22 +67,20 @@ export default function InstallApp() { const [installToken, setInstallToken] = useState(() => readStoredInstallToken(), ); - const [session, setSession] = useState(null); - const [loadingSession, setLoadingSession] = useState(false); - const [sessionError, setSessionError] = useState(null); + const [sessionState, setSessionState] = useState({ status: "idle" }); + const session = sessionState.status === "ready" ? sessionState.data : null; const [manualToken, setManualToken] = useState(""); const [llmSelection, setLlmSelection] = useState(() => defaultProviderSelection(), ); const [canonicalUrl, setCanonicalUrl] = useState(""); const [githubStrategy, setGithubStrategy] = useState("token"); - const [githubToken, setGithubToken] = useState(""); - const [githubUsername, setGithubUsername] = useState(""); - const [githubOwnerKind, setGithubOwnerKind] = - useState("personal"); - const [githubOrganization, setGithubOrganization] = useState(""); - const [githubAppName, setGithubAppName] = useState("Fabro"); - const [githubAllowedUsername, setGithubAllowedUsername] = useState(""); + const [tokenForm, setTokenForm] = useState({ token: "", username: "" }); + const [appForm, setAppForm] = useState({ + owner: { kind: "personal" }, + appName: "Fabro", + allowedUsername: "", + }); const [saveError, setSaveError] = useState(null); const [submitting, setSubmitting] = useState(false); const [finishState, setFinishState] = useState(null); @@ -98,17 +109,16 @@ export default function InstallApp() { useEffect(() => { if (!installToken) { - setSession(null); + setSessionState({ status: "idle" }); return; } let cancelled = false; - setLoadingSession(true); - setSessionError(null); + setSessionState({ status: "loading" }); getInstallSession(installToken) .then((nextSession) => { if (cancelled) return; - setSession(nextSession); + setSessionState({ status: "ready", data: nextSession }); setCanonicalUrl((current) => current || nextSession.server?.canonical_url || nextSession.prefill.canonical_url, ); @@ -117,28 +127,25 @@ export default function InstallApp() { ); if (nextSession.github?.strategy === "app") { setGithubStrategy("app"); - const owner = nextSession.github.owner ?? { kind: "personal" }; - if (owner.kind === "org") { - setGithubOwnerKind("org"); - setGithubOrganization(owner.slug); - } else { - setGithubOwnerKind("personal"); - setGithubOrganization(""); - } - setGithubAppName(nextSession.github.app_name || "Fabro"); - setGithubAllowedUsername(nextSession.github.allowed_username || ""); + setAppForm({ + owner: nextSession.github.owner ?? { kind: "personal" }, + appName: nextSession.github.app_name || "Fabro", + allowedUsername: nextSession.github.allowed_username || "", + }); } else if (nextSession.github?.strategy === "token") { setGithubStrategy("token"); - setGithubUsername(nextSession.github.username || ""); + setTokenForm((current) => ({ + ...current, + username: nextSession.github?.username || current.username, + })); } }) .catch((error) => { if (cancelled) return; - setSession(null); - setSessionError(error instanceof Error ? error.message : "Install session failed"); - }) - .finally(() => { - if (!cancelled) setLoadingSession(false); + setSessionState({ + status: "error", + message: error instanceof Error ? error.message : "Install session failed", + }); }); return () => { @@ -210,6 +217,9 @@ export default function InstallApp() { const completedSteps = new Set(session?.completed_steps ?? []); + const sessionError = + sessionState.status === "error" ? sessionState.message : null; + if (!installToken) { return ( { const nextToken = manualToken.trim(); if (!nextToken) { - setSessionError("Paste the install token from the server logs."); + setSessionState({ + status: "error", + message: "Paste the install token from the server logs.", + }); return; } persistInstallToken(nextToken); setInstallToken(nextToken); - setSessionError(null); + setSessionState({ status: "idle" }); }} /> ); } - if (loadingSession && !session) { + if (sessionState.status === "loading") { return ( @@ -240,7 +253,7 @@ export default function InstallApp() { ); } - if (sessionError && !session) { + if (sessionState.status === "error") { return ( - + {githubStrategy === "token" ? ( <>