fix(connections): restore unified Google Workspace in catalog

Show Google Workspace for new accounts and retain access to saved per-service connections without changing credentials or permissions.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-10-09 11:49:29 -05:00
1 parent b6b5341e0a
commit cbb3b579b4
2 files changed
+29 -28

No files matched your search

+19 -13
View File
@@ -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();
+10 -15
View File
@@ -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(