From e6a7728087d51ae3afe88b9a7c960c4f960b871c Mon Sep 17 00:00:00 2001 From: Dotta Date: Thu, 20 Aug 2026 15:09:00 +0000 Subject: [PATCH] feat(ui): add dedicated BYO apps page Co-Authored-By: Paperclip --- ui/src/App.test.tsx | 1 + ui/src/App.tsx | 1 + ui/src/pages/apps/AppsConnect.test.tsx | 21 ++- ui/src/pages/apps/AppsConnect.tsx | 171 ++++++++++++++----------- ui/src/pages/apps/Browse.test.tsx | 2 +- ui/src/pages/apps/store-cards.tsx | 2 +- 6 files changed, 116 insertions(+), 82 deletions(-) diff --git a/ui/src/App.test.tsx b/ui/src/App.test.tsx index 27a7e0e8738..851b5593c11 100644 --- a/ui/src/App.test.tsx +++ b/ui/src/App.test.tsx @@ -249,6 +249,7 @@ describe("Apps routes", () => { expect(appSource).toContain('} />'); expect(appSource).toContain('} />'); expect(appSource).toContain('} />'); + expect(appSource).toContain('} />'); expect(appSource).toContain('} />'); expect(appSource).toContain('} />'); expect(appSource).toContain('} />'); diff --git a/ui/src/App.tsx b/ui/src/App.tsx index 34ecfbd66e9..4e134d9244e 100644 --- a/ui/src/App.tsx +++ b/ui/src/App.tsx @@ -135,6 +135,7 @@ function boardRoutes() { } /> } /> } /> + } /> } /> } /> } /> diff --git a/ui/src/pages/apps/AppsConnect.test.tsx b/ui/src/pages/apps/AppsConnect.test.tsx index 54ec7139e14..742665e95be 100644 --- a/ui/src/pages/apps/AppsConnect.test.tsx +++ b/ui/src/pages/apps/AppsConnect.test.tsx @@ -173,13 +173,13 @@ describe("AppsConnect — Connect with a link (M4 frame)", () => { vi.clearAllMocks(); }); - async function render(queryClient?: QueryClient) { + async function render(queryClient?: QueryClient, byoOnly = false) { const root = createRoot(container); const client = queryClient ?? new QueryClient({ defaultOptions: { queries: { retry: false } } }); await act(async () => { root.render( - + , ); }); @@ -188,6 +188,23 @@ describe("AppsConnect — Connect with a link (M4 frame)", () => { return root; } + it("shows only the paste-first connection choices on the BYO page", async () => { + await render(undefined, true); + + const text = container.textContent ?? ""; + expect(text).toContain("Connect your own MCP server"); + expect(text).toContain("More ways to connect"); + expect(text).toContain("Run your own"); + expect(text).toContain("Paste a config"); + expect(text).not.toContain("Search apps…"); + expect(text).not.toContain("Pick the app you want your agents to use."); + expect(text).not.toContain("Zapier"); + + const urlInput = container.querySelector('input[aria-label="MCP server URL"]'); + expect(urlInput).toBeTruthy(); + expect(document.activeElement).toBe(urlInput); + }); + it("an unrecognized URL routes to a frame with the URL, defaulted Name, and a Yes/No toggle", async () => { await render(); await gotoLinkFrame(container, "https://www.example.com/actions"); diff --git a/ui/src/pages/apps/AppsConnect.tsx b/ui/src/pages/apps/AppsConnect.tsx index db5c359566e..8f37f3dc493 100644 --- a/ui/src/pages/apps/AppsConnect.tsx +++ b/ui/src/pages/apps/AppsConnect.tsx @@ -150,7 +150,7 @@ function reusableOAuthConnection( }) ?? null; } -export function AppsConnect() { +export function AppsConnect({ byoOnly = false }: { byoOnly?: boolean } = {}) { const navigate = useNavigate(); const routeParams = useParams<{ appKey?: string }>(); const { selectedCompany, selectedCompanyId } = useCompany(); @@ -163,6 +163,7 @@ export function AppsConnect() { const directOAuthSource = isMcpDirectOAuthConnectSlug(sourceSlug) ? sourceSlug : null; const requestedAppKey = appKey ?? directOAuthSource ?? undefined; const zapierSource = sourceSlug === "zapier"; + const byo = byoOnly || searchParams.get("byo") === "1"; // Prefill arrives from the app page for reconnects; read once so later // wizard navigation doesn't fight the URL. @@ -270,17 +271,17 @@ export function AppsConnect() { setInstallMode("none"); setInstallAgentIds(new Set()); setStep("gallery"); - navigate("/apps/connect?byo=1"); + navigate(byoOnly ? "/apps/byo" : "/apps/connect?byo=1"); }; useEffect(() => { setBreadcrumbs([ { label: selectedCompany?.name ?? "Company", href: "/dashboard" }, { label: "Apps", href: "/apps" }, - { label: "Connect an app" }, + { label: byoOnly ? "Connect your own tool" : "Connect an app" }, ]); return () => setBreadcrumbs([]); - }, [setBreadcrumbs, selectedCompany?.name]); + }, [byoOnly, setBreadcrumbs, selectedCompany?.name]); const galleryQuery = useQuery({ queryKey: queryKeys.apps.gallery(selectedCompanyId ?? "__none__"), @@ -687,30 +688,33 @@ export function AppsConnect() { return (
{step !== "success" && ( - navigate("/apps")} - /> + !(byoOnly && step === "gallery") && ( + navigate("/apps")} + /> + ) )} {step === "gallery" && ( { @@ -1093,6 +1097,7 @@ function GalleryStep({ loading, apps, byo = false, + byoOnly = false, source = null, onPick, onUseLink, @@ -1103,6 +1108,8 @@ function GalleryStep({ apps: AppDefinition[]; /** Entered via the "Connect your own MCP server" card (PAP-12371, Finding C): focus the link path. */ byo?: boolean; + /** Canonical BYO page: keep the URL path and alternate methods, without the app gallery. */ + byoOnly?: boolean; source?: string | null; onPick: (entry: AppDefinition) => void; onUseLink: (link: string) => void; @@ -1114,14 +1121,17 @@ function GalleryStep({ const [linkError, setLinkError] = useState(null); const linkSectionRef = useRef(null); const linkInputRef = useRef(null); + const linkInputSelectedRef = useRef(false); - // Arriving from the BYO card: scroll the "Connect with a link" section into - // view and focus its input so the paste-URL path is the obvious next step. + // Arriving from the BYO card: scroll the URL section into view and select its + // input so the operator can paste immediately. useEffect(() => { - if (!byo || loading) return; - linkSectionRef.current?.scrollIntoView({ block: "center" }); + if (!byo || (loading && !byoOnly) || linkInputSelectedRef.current) return; + if (!byoOnly) linkSectionRef.current?.scrollIntoView?.({ block: "center" }); linkInputRef.current?.focus(); - }, [byo, loading]); + linkInputRef.current?.select(); + linkInputSelectedRef.current = true; + }, [byo, byoOnly, loading]); const filtered = useMemo(() => { const q = search.trim().toLowerCase(); if (!q) return apps; @@ -1141,7 +1151,7 @@ function GalleryStep({ onUseLink(next); }; - if (loading) { + if (loading && !byoOnly) { return (
{Array.from({ length: 8 }).map((_, i) => ( @@ -1153,58 +1163,62 @@ function GalleryStep({ return (
-
- - setSearch(e.target.value)} - placeholder="Search apps…" - className="h-11 pl-9" - /> -
+ {!byoOnly && ( + <> +
+ + setSearch(e.target.value)} + placeholder="Search apps…" + className="h-11 pl-9" + /> +
-
- {filtered.map((app) => { - const copy = appCopyFor(app.slug, app.description); - const methods = getAvailableConnectionMethods(app); - const oauth = methods[0]?.auth === "oauth"; - const oauthBlocked = methods.length === 1 && oauth && !isMcpDirectOAuthConnectSlug(app.slug); - const unavailable = app.availability?.available === false; - return ( - - ); - })} -
+
+ {filtered.map((app) => { + const copy = appCopyFor(app.slug, app.description); + const methods = getAvailableConnectionMethods(app); + const oauth = methods[0]?.auth === "oauth"; + const oauthBlocked = methods.length === 1 && oauth && !isMcpDirectOAuthConnectSlug(app.slug); + const unavailable = app.availability?.available === false; + return ( + + ); + })} +
- {filtered.length === 0 && ( -
No apps match “{search}”.
+ {filtered.length === 0 && ( +
No apps match “{search}”.
+ )} + )}
{ setLinkInput(e.target.value); diff --git a/ui/src/pages/apps/Browse.test.tsx b/ui/src/pages/apps/Browse.test.tsx index bc5977903c4..4337908a69a 100644 --- a/ui/src/pages/apps/Browse.test.tsx +++ b/ui/src/pages/apps/Browse.test.tsx @@ -178,7 +178,7 @@ describe("Browse store door (PAP-13254 door 1)", () => { await act(async () => { byoCard?.dispatchEvent(new MouseEvent("click", { bubbles: true })); }); - expect(navigateMock).toHaveBeenCalledWith("/apps/connect?byo=1"); + expect(navigateMock).toHaveBeenCalledWith("/apps/byo"); }); it("filters the gallery by the search query", async () => { diff --git a/ui/src/pages/apps/store-cards.tsx b/ui/src/pages/apps/store-cards.tsx index 0a500823dd5..a84641053c4 100644 --- a/ui/src/pages/apps/store-cards.tsx +++ b/ui/src/pages/apps/store-cards.tsx @@ -7,7 +7,7 @@ import { appSourceConnectHref } from "./app-connect-policy"; export const POPULAR_KEYS = ["zapier", "github", "slack", "notion", "posthog", "linear"]; /** Deep-link into the Connect wizard's bring-your-own-tool URL flow. */ -export const BYO_CONNECT_HREF = "/apps/connect?byo=1"; +export const BYO_CONNECT_HREF = "/apps/byo"; /** Zapier connects with the complete MCP URL issued by Zapier. */ export const ZAPIER_CONNECT_HREF = "/apps/connect?byo=1&source=zapier";