diff --git a/ui/src/pages/apps/Browse.test.tsx b/ui/src/pages/apps/Browse.test.tsx index dd0f1a29885..0801c3c2718 100644 --- a/ui/src/pages/apps/Browse.test.tsx +++ b/ui/src/pages/apps/Browse.test.tsx @@ -813,7 +813,7 @@ describe("Connectors landing page", () => { "google-calendar", "google-chat", "google-people", "google-workspace-search", ]; - it("temporarily hides all Google Workspace catalog rows without changing their definitions", async () => { + it("offers one Google Workspace connector while retaining the individual service definitions", async () => { const definitions = googleSlugs.map((slug) => getAppStoreDefinition(slug)!); listGalleryMock.mockResolvedValue({ apps: [...definitions, getAppStoreDefinition("notion")] }); const client = await renderBrowse(); @@ -821,17 +821,23 @@ describe("Connectors landing page", () => { for (const definition of definitions) { expect(definition.methods.length).toBeGreaterThan(0); expect(getAppStoreDefinition(definition.slug)).toBe(definition); - expect(container.querySelector(`[data-app-slug="${definition.slug}"]`)).toBeNull(); + if (definition.slug === "google-workspace") { + expect(container.querySelectorAll('[data-app-slug="google-workspace"]')).toHaveLength(1); + } else { + expect(container.querySelector(`[data-app-slug="${definition.slug}"]`)).toBeNull(); + } } expect(container.querySelector('[data-app-slug="notion"]')).not.toBeNull(); expect(client.getQueryData(queryKeys.apps.gallery("company-1"))).toEqual({ apps: [...definitions, getAppStoreDefinition("notion")], }); expect(archiveConnectionMock).not.toHaveBeenCalled(); + await clickButton("Connect", container.querySelector('[data-app-slug="google-workspace"]')!); + expect(navigateMock).toHaveBeenCalledWith("/apps/connect?source=google-workspace"); }); it.each(["active", "draft", "disabled"])( - "hides saved Google %s accounts without disabling or removing them", + "shows saved Google %s accounts without changing their access or status", async (status) => { const applications = googleSlugs.map((slug) => application({ id: `app-${slug}`, name: `Saved ${slug}`, applicationKey: `app-gallery:${slug}:one`, @@ -847,8 +853,8 @@ describe("Connectors landing page", () => { const client = await renderBrowse(); for (const slug of googleSlugs) { - expect(container.querySelector(`[data-app-slug="${slug}"]`)).toBeNull(); - expect(container.textContent).not.toContain(`Account for ${slug}`); + expect(container.querySelector(`[data-app-slug="${slug}"]`)).not.toBeNull(); + expect(container.textContent).toContain(`Account for ${slug}`); } expect(client.getQueryData(queryKeys.tools.connections("company-1"))).toEqual({ connections }); expect(client.getQueryData(queryKeys.tools.applications("company-1"))).toEqual({ applications }); @@ -857,7 +863,7 @@ describe("Connectors landing page", () => { ); it.each(["config", "transportConfig"])( - "hides a Google account identified by %s even when its gallery entry is absent", + "shows a Google account identified by %s even when its gallery entry is absent", async (sourceField) => { listGalleryMock.mockResolvedValue({ apps: [] }); listApplicationsMock.mockResolvedValue({ applications: [application({ @@ -869,14 +875,14 @@ describe("Connectors landing page", () => { }), connection()] }); await renderBrowse(); - expect(container.textContent).not.toContain("Saved Google account"); + expect(container.textContent).toContain("Saved Google account"); expect(container.textContent).toContain("Notion"); expect(archiveConnectionMock).not.toHaveBeenCalled(); }, ); it.each(["catalog", "custom"])( - "preserves non-Google accounts in a mixed-provider %s row", + "preserves both Google and non-Google accounts in a mixed-provider %s row", async (rowKind) => { const notion = getAppStoreDefinition("notion")!; listGalleryMock.mockResolvedValue({ apps: rowKind === "catalog" ? [notion] : [] }); @@ -884,7 +890,7 @@ describe("Connectors landing page", () => { ? { name: "My tools", applicationKey: null, metadata: null } : {})] }); const connections = [connection({ - id: "google-account", name: "Hidden Google account", + id: "google-account", name: "Saved Google account", config: { sourceTemplateKey: "google-docs" }, }), connection({ id: "notion-account", name: "Visible Notion account", @@ -894,7 +900,7 @@ describe("Connectors landing page", () => { const client = await renderBrowse(); expect(container.textContent).toContain("Visible Notion account"); - expect(container.textContent).not.toContain("Hidden Google account"); + expect(container.textContent).toContain("Saved Google account"); expect(container.textContent).toContain(rowKind === "catalog" ? "Notion" : "My tools"); expect(client.getQueryData(queryKeys.tools.connections("company-1"))).toEqual({ connections }); expect(archiveConnectionMock).not.toHaveBeenCalled(); @@ -902,7 +908,7 @@ describe("Connectors landing page", () => { ); it.each(["metadata", "applicationKey"])( - "keeps a non-Google custom connector identified by %s when only its Google accounts are hidden", + "keeps a non-Google custom connector identified by %s and its saved Google account", async (sourceField) => { listGalleryMock.mockResolvedValue({ apps: [] }); const savedApplication = application({ @@ -912,14 +918,14 @@ describe("Connectors landing page", () => { }); listApplicationsMock.mockResolvedValue({ applications: [savedApplication] }); const connections = [connection({ - name: "Hidden Google account", config: { sourceTemplateKey: "google-docs" }, + name: "Saved Google account", config: { sourceTemplateKey: "google-docs" }, })]; listConnectionsMock.mockResolvedValue({ connections }); const client = await renderBrowse(); expect(container.querySelector('[data-app-slug="custom-provider"]')).not.toBeNull(); expect(container.textContent).toContain("My custom connector"); - expect(container.textContent).not.toContain("Hidden Google account"); + expect(container.textContent).toContain("Saved Google account"); expect(client.getQueryData(queryKeys.tools.applications("company-1"))).toEqual({ applications: [savedApplication] }); expect(client.getQueryData(queryKeys.tools.connections("company-1"))).toEqual({ connections }); expect(archiveConnectionMock).not.toHaveBeenCalled(); diff --git a/ui/src/pages/apps/Browse.tsx b/ui/src/pages/apps/Browse.tsx index 987dadaa8d8..2b09f66095b 100644 --- a/ui/src/pages/apps/Browse.tsx +++ b/ui/src/pages/apps/Browse.tsx @@ -138,11 +138,12 @@ type ConnectionRemovalTarget = { poolRevision?: number; } & ({ kind: "chat"; provider: ChatProvider } | { kind?: undefined }); -// Temporary, page-only hold until Google OAuth verification is approved. -// Keep definitions, direct setup/management routes, and runtime access intact. -// Remove this filter after approval; reviewer instances stay on their pinned build. -const GOOGLE_CONNECTOR_SLUGS = new Set( - Object.values(GOOGLE_WORKSPACE_CONNECTOR_PROFILES).map((profile) => profile.appSlug), +// Offer the unified connector for new Google accounts. Existing per-service +// connections remain visible and keep their definitions and management routes. +const LEGACY_GOOGLE_CONNECTOR_SLUGS = new Set( + Object.values(GOOGLE_WORKSPACE_CONNECTOR_PROFILES) + .map((profile) => profile.appSlug) + .filter((slug) => slug !== "google-workspace"), ); function chatProviderForSlug(slug: string): ChatProvider | null { @@ -602,15 +603,7 @@ export function Browse({ renderAccountDetails = (connection) => connection.conne const savedAppConnections = connectionsByApplicationId.get(application.id) ?? []; if (applicationSlug === "gateway" && savedAppConnections.length === 0) continue; - let appConnections = savedAppConnections.filter( - (connection) => !GOOGLE_CONNECTOR_SLUGS.has(appConnectionSourceSlug(connection) ?? ""), - ); - // Hide source-only Google rows, but keep independently identified connectors. - if ( - (!applicationSlug || applicationSlug === "link") && - savedAppConnections.length > 0 && - appConnections.length === 0 - ) continue; + let appConnections = savedAppConnections; // One legacy gateway application may contain several API formats. Group // each saved AI account by its actual routing, not the old application slug. const ungroupedCount = appConnections.length; @@ -730,7 +723,9 @@ export function Browse({ renderAccountDetails = (connection) => connection.conne })); return [...rowsBySlug.values(), ...customRows, ...aggregatorRows] - .filter((row) => !GOOGLE_CONNECTOR_SLUGS.has(row.slug) || row.upstreamApps?.some(snapshot => snapshot.accounts.length > 0)) + .filter((row) => !LEGACY_GOOGLE_CONNECTOR_SLUGS.has(row.slug) + || row.connections.length > 0 + || row.upstreamApps?.some(snapshot => snapshot.accounts.length > 0)) .map((row) => ({ ...row, connections: [...row.connections].sort(