feat(connections): gate the built-in Google Workspace connector behind an experimental toggle (#19247)

## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - The Apps catalog lets operators connect external tools, including
Google services.
> - Paperclip has a built-in Google Workspace connector, but Google has
not yet verified Paperclip's Google app.
> - The built-in connector also hides the Composio and Arcade Google
apps, because native Google definitions claim those app names.
> - Users therefore see an unverified connector and cannot find the
working aggregator Google apps.
> - This pull request puts the built-in connector behind a default-off
experimental setting. When the setting is off, the aggregator Google
apps appear.
> - The benefit is that users get working Google connectors now, and the
team can still test the built-in connector on demand.

## Linked Issues or Issue Description

**Problem**
Google has not verified Paperclip's Google app yet. The Apps page still
offers the built-in Google Workspace connector. The built-in Google
definitions also suppress the Composio and Arcade Google apps (Gmail,
Google Calendar, Google Drive, Docs, Sheets, Slides, Chat), so users
cannot find them.

**Proposed solution**
Add the `enableGoogleWorkspaceConnector` experimental setting. The
default is off. When it is off, hide and block the built-in connector,
and show the aggregator Google apps. When it is on, keep the current
behavior.

**Alternatives considered**
Delete the built-in connector until verification is complete. We
rejected this option because the team must continue to test the
connector.

**Additional context**
Composio documents Gmail (63 tools), Google Calendar (50 tools), and
Google Drive toolkits that use Composio-managed OAuth. These toolkits do
not need Paperclip's Google app verification.

## What Changed

- Added the default-off `enableGoogleWorkspaceConnector` experimental
setting to the shared types, validator, feature catalog, server
defaults, and the managed-key list for Paperclip Cloud staging.
- Added `experimentalFeatureKey: "enableGoogleWorkspaceConnector"` to
the `google-workspace` app definition. The existing generic connector
gate then hides the connector in the gallery and blocks its setup,
discovery, execution, and agent suggestions when the setting is off.
- Added a "Google Workspace connector (Paperclip)" toggle card to
**Settings → Experimental**.
- Apps page (`Browse.tsx`): when the setting is off, built-in Google
rows without saved accounts no longer claim aggregator app identities.
The Composio and Arcade Google apps then show as aggregator rows. The
legacy Google slug filter now applies only to native rows, so the
Composio `gmail` row is not dropped.
- Saved per-service Google connections (Gmail, Drive, and others) are
not gated. They stay visible and usable.
- Updated `doc/connections/GOOGLE-WORKSPACE.md`.
- Tests: added tests for the hidden and visible Apps page states, the
settings toggle, and the definition gate. Existing built-in Google
Workspace tests now enable the setting.

## Verification

- `cd packages/shared && npx vitest run` passes (1055 tests).
- `cd server && npx vitest run
src/__tests__/connection-management.integration.test.ts
src/__tests__/tool-access-service.test.ts
src/__tests__/tool-gateway-service.test.ts
src/services/gmail-tool-governance.test.ts
src/services/google-chat-tool-policy.test.ts
src/__tests__/instance-settings-service.test.ts
src/__tests__/health.test.ts
src/__tests__/instance-settings-managed-overlay.test.ts` passes (864
tests).
- `cd ui && npx vitest run src/pages/apps
src/pages/InstanceExperimentalSettings.test.tsx
src/features/connections` passes.
- `tsc --noEmit` passes for `packages/shared`, `server`, and `ui`. The
`ui` run shows errors only in the untouched
`storybook/support/feature-map` files.
- `pnpm check:token-gates` passes.
- Manual check: open **Apps** with the setting off. Search for "Gmail".
The Composio Gmail row appears, and the built-in Google Workspace card
does not. Turn on the setting in **Settings → Experimental**. The
built-in card appears again.

## Risks

- Existing built-in `google-workspace` connections stop running while
the setting is off, because the generic gate blocks execution. This is
intentional, so that the unverified Google app is not used. Turn on the
setting on instances that must keep these connections.
- Saved per-service Google connections are not affected.
- When the setting is on, the behavior does not change.

> For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and
discuss it in `#dev` before opening the PR. Feature PRs that overlap
with planned core work may need to be redirected — check the roadmap
first. See `CONTRIBUTING.md`.

## Model Used

- Claude Opus 5.5 (Anthropic), model ID `claude-opus-5-5`, running in
Claude Code with tool use (shell, file edits, web fetch).

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [ ] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
DottaandClaude Opus 5.5 authored and GitHub committed 2026-11-05 14:37:24 +00:00
1 parent 0c9962a9bc
commit 21640b6d51
16 files changed
+135 -9

No files matched your search

+8
View File
@@ -6,6 +6,14 @@ Docs, Sheets, and Slides), and Calendar. All three start enabled with read and
write access. Users can disable a group or choose read-only access. New setup
does not offer Chat, Contacts/directory, or cross-service Search.
The built-in connector is behind the default-off `enableGoogleWorkspaceConnector`
experimental setting while Paperclip's Google app verification is pending. When
the setting is off, the Apps page hides the built-in connector, and the server
blocks its setup, discovery, and execution. Google apps from MCP aggregators
(for example Composio Gmail, Google Calendar, and Google Drive) then appear in
the catalog instead. Saved per-service Google connections stay visible and usable.
Turn the setting on in **Settings → Experimental** to test the built-in connector.
The combined connection has one credential grant, capability catalog, policy
profile, audit trail and reconnect lifecycle. It requests only the selected
groups' scopes (nine scopes for the default selection), not the legacy full
@@ -1,4 +1,5 @@
{
"experimentalFeatureKey": "enableGoogleWorkspaceConnector",
"schemaVersion": 1,
"slug": "google-workspace",
"name": "Google Workspace",
@@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest";
import { getConnectableAppDefinition } from "./app-definitions.js";
import { isConnectorFeatureEnabled } from "./connector-availability.js";
import { appDefinitionSchema } from "./validators/app-definition.js";
import { instanceExperimentalSettingsSchema } from "./validators/instance.js";
describe("connector availability", () => {
const ordinary = getConnectableAppDefinition("notion")!;
@@ -24,4 +25,15 @@ describe("connector availability", () => {
expect(isConnectorFeatureEnabled(notebook, {})).toBe(false);
expect(isConnectorFeatureEnabled(notebook, { enablePaperclipNotebook: true })).toBe(true);
});
it("gates only the built-in Google Workspace connector, default off, while verification is pending", () => {
const workspace = getConnectableAppDefinition("google-workspace")!;
expect(workspace.experimentalFeatureKey).toBe("enableGoogleWorkspaceConnector");
expect(isConnectorFeatureEnabled(workspace, instanceExperimentalSettingsSchema.parse({}))).toBe(false);
expect(isConnectorFeatureEnabled(workspace, { enableGoogleWorkspaceConnector: true })).toBe(true);
// Saved per-service Google connections keep working; only the unified entry is gated.
for (const slug of ["gmail", "google-drive", "google-calendar", "google-docs", "google-sheets"]) {
expect(getConnectableAppDefinition(slug)?.experimentalFeatureKey).toBeUndefined();
}
});
});
+7
View File
@@ -175,6 +175,13 @@ export const INSTANCE_FEATURE_CATALOG: Record<InstanceFeatureKey, FeatureCatalog
cloudDefault: false,
selfHostedDefault: false,
},
enableGoogleWorkspaceConnector: {
title: "Google Workspace connector (Paperclip)",
description: "Show and allow the built-in Google Workspace connector while its Google app verification is pending. When off, Google apps are offered through Composio and Arcade instead.",
tier: "managed",
cloudDefault: false,
selfHostedDefault: false,
},
enableMcpAggregators: {
title: "MCP aggregators (compatibility)",
description: "Deprecated compatibility key. MCP aggregators are always enabled; stored and managed values are ignored.",
+2
View File
@@ -87,6 +87,8 @@ export interface InstanceExperimentalSettings {
/** Show experimental memory connection setup. Existing connections remain usable. */
enableMemoryConnectors: boolean;
enablePaperclipNotebook: boolean;
/** Show and allow the built-in Google Workspace connector while its Google verification is pending. */
enableGoogleWorkspaceConnector: boolean;
enableCases: boolean;
enableAgentChat: boolean;
/** Reorganized left nav, Inbox-as-Tasks-views, and the chat agent rail + task cards. */
@@ -62,6 +62,7 @@ export const instanceExperimentalSettingsSchema = z.object({
enableMcpAggregators: z.boolean().default(true),
enableMemoryConnectors: z.boolean().default(false),
enablePaperclipNotebook: z.boolean().default(false),
enableGoogleWorkspaceConnector: z.boolean().default(false),
enableCases: z.boolean().default(false),
enableAgentChat: z.boolean().default(false),
enableCombinedInboxTasks: z.boolean().default(false),
@@ -54,7 +54,7 @@ describe("connection management with production default agents", () => {
server = await startRunnerApiTestServer({ authPublicBaseUrl: "https://paperclip.fixture.invalid", humanSessions: true, databaseUrl: process.env.PAPERCLIP_CONNECTION_MANAGEMENT_TEST_DATABASE_URL });
lifecycle = configureAgentLifecycle(server.db, { requiredPluginIds: async () => [], runPlugin: async () => "complete", runHost: async () => "complete" });
vi.stubEnv("PAPERCLIP_SECRETS_MASTER_KEY_FILE", join(server.root, "master.key"));
await instanceSettingsService(server.db).updateExperimental({ enableChatConnectors: true, enableGitHubReviewBots: true });
await instanceSettingsService(server.db).updateExperimental({ enableChatConnectors: true, enableGitHubReviewBots: true, enableGoogleWorkspaceConnector: true });
}, 90_000);
afterAll(async () => { await lifecycle?.stop(); await server?.close(); providerFetch?.mockRestore(); vi.unstubAllEnvs(); vi.unstubAllGlobals(); });
@@ -115,6 +115,7 @@ describe("instance settings service", () => {
enableGitHubReviewBots: false,
enableMemoryConnectors: false,
enablePaperclipNotebook: false,
enableGoogleWorkspaceConnector: false,
enableConferenceRoomChat: false,
enableClassicTaskInterface: false,
enableExternalObjects: false,
@@ -954,14 +954,14 @@ describeEmbeddedPostgres("tool access service", () => {
if (externalDatabaseUrl) {
await applyPendingMigrations(externalDatabaseUrl);
db = createDb(externalDatabaseUrl, { maxConnections: 4 });
await instanceSettingsService(db).updateExperimental({ enableMemoryConnectors: true });
await instanceSettingsService(db).updateExperimental({ enableMemoryConnectors: true, enableGoogleWorkspaceConnector: true });
return;
}
tempDb = await startEmbeddedPostgresTestDatabase(
"paperclip-tool-access-service-",
);
db = createDb(tempDb.connectionString, { maxConnections: 4 });
await instanceSettingsService(db).updateExperimental({ enableMemoryConnectors: true });
await instanceSettingsService(db).updateExperimental({ enableMemoryConnectors: true, enableGoogleWorkspaceConnector: true });
}, 20_000);
afterEach(async () => {
+1
View File
@@ -19,6 +19,7 @@ const CLOUD_STAGING_EXPERIMENTAL_KEYS: ReadonlySet<string> = new Set([
"enableFastResponses",
"enableFirstTaskPlanProposal",
"enableGitHubReviewBots",
"enableGoogleWorkspaceConnector",
"enableGoalsSidebarLink",
"enableGrokBot",
"enableIsolatedWorkspaces",
+2
View File
@@ -265,6 +265,7 @@ export function normalizeExperimentalSettings(raw: unknown): InstanceExperimenta
enableGitHubReviewBots: parsed.data.enableGitHubReviewBots ?? false,
enableMemoryConnectors: parsed.data.enableMemoryConnectors ?? false,
enablePaperclipNotebook: parsed.data.enablePaperclipNotebook ?? false,
enableGoogleWorkspaceConnector: parsed.data.enableGoogleWorkspaceConnector ?? false,
enableCases: parsed.data.enableCases ?? false,
enableAgentChat: parsed.data.enableAgentChat ?? false,
enableCombinedInboxTasks: parsed.data.enableCombinedInboxTasks ?? false,
@@ -322,6 +323,7 @@ export function normalizeExperimentalSettings(raw: unknown): InstanceExperimenta
enableGitHubReviewBots: false,
enableMemoryConnectors: false,
enablePaperclipNotebook: false,
enableGoogleWorkspaceConnector: false,
enableCases: false,
enableAgentChat: false,
enableCombinedInboxTasks: false,
@@ -96,6 +96,7 @@ function defaultExperimentalSettings(): InstanceExperimentalSettingsPayload {
enableChatConnectors: false,
enableGitHubReviewBots: false,
enableMemoryConnectors: false,
enableGoogleWorkspaceConnector: false,
enablePaperclipNotebook: false,
enableCases: false,
enableAgentChat: false,
@@ -245,6 +246,18 @@ describe("InstanceExperimentalSettings — Conference Room Chat card (PAP-11233)
}
});
it("defaults the Paperclip Google Workspace connector off and persists an explicit toggle in both directions", async () => {
await renderPage();
const selector = 'button[aria-label="Toggle Paperclip Google Workspace connector experimental setting"]';
expect(container.querySelector(selector)?.getAttribute("aria-checked")).toBe("false");
for (const enabled of [true, false]) {
await act(() => container.querySelector<HTMLButtonElement>(selector)!.click());
await flushReact();
expect(mockInstanceSettingsApi.updateExperimental).toHaveBeenLastCalledWith({ enableGoogleWorkspaceConnector: enabled });
expect(container.querySelector(selector)?.getAttribute("aria-checked")).toBe(String(enabled));
}
});
it("defaults Combined Inbox + Task List off and persists an explicit toggle in both directions", async () => {
await renderPage();
const selector = 'button[aria-label="Toggle combined inbox and task list experimental setting"]';
@@ -494,6 +494,18 @@ export function InstanceExperimentalSettings() {
ariaLabel="Toggle GitHub review bots experimental setting"
/>
<ExperimentalToggleCard
title="Google Workspace connector (Paperclip)"
description="Show the built-in Google Workspace connector for Gmail, Drive, and Calendar while Google verification is pending."
footnote="Off by default. While off, Google apps are offered through Composio and Arcade instead, and the built-in connector cannot be set up or run."
checked={experimentalQuery.data?.enableGoogleWorkspaceConnector === true}
onCheckedChange={(checked) => toggleMutation.mutate({ enableGoogleWorkspaceConnector: checked })}
disabled={toggleMutation.isPending}
settingKey="enableGoogleWorkspaceConnector"
managed={managedKeys.enableGoogleWorkspaceConnector}
ariaLabel="Toggle Paperclip Google Workspace connector experimental setting"
/>
<ExperimentalToggleCard
title="Grok Bot"
description="Connect an existing Grok Bot as an agent. Verify routine delivery and an authenticated reply before assigning work."
+7 -2
View File
@@ -1987,6 +1987,7 @@ describe("AppsConnect — Connect with a link (M4 frame)", () => {
"preserves personal Workspace access for $slug when changing method (enrollment return: $enrollmentReturn)",
async ({ slug, enrollmentReturn }) => {
const definition = CONNECTABLE_APP_DEFINITIONS.find((app) => app.slug === slug)!;
experimentalMock.mockResolvedValue({ enableGoogleWorkspaceConnector: true });
const readMethod = definition.methods.find((method) => method.key === "paperclip-read" || method.key === "paperclip-workspace")!;
mockSearch.value = enrollmentReturn
? `source=${slug}&stage=setup&cloud_connector=enrolled`
@@ -2090,6 +2091,7 @@ describe("AppsConnect — Connect with a link (M4 frame)", () => {
});
it("selects Workspace service groups before creating a single connection", async () => {
experimentalMock.mockResolvedValue({ enableGoogleWorkspaceConnector: true });
mockSearch.value = "source=google-workspace&stage=setup";
const workspace = getAppStoreDefinition("google-workspace")!;
listGalleryMock.mockResolvedValue({ apps: [{
@@ -3074,18 +3076,21 @@ describe("AppsConnect — Connect with a link (M4 frame)", () => {
});
it("keeps Gmail read-only selected when Finish setup resumes a declined Workspace draft", async () => {
experimentalMock.mockResolvedValue({ enableGoogleWorkspaceConnector: true });
const connectionId = "22222222-2222-4222-8222-222222222222";
const workspace = getAppStoreDefinition("google-workspace")!;
mockSearch.value = `source=google-workspace&resume=${connectionId}&oauth=denied`;
listGalleryMock.mockResolvedValue({ apps: [{ ...workspace, ownershipAvailability: { ...workspace.ownershipAvailability, platform_shared: true } }] });
listApplicationsMock.mockResolvedValue({ applications: [{ id: "app-workspace", companyId: "company-1", status: "draft", metadata: { sourceTemplateKey: "google-workspace" } }] });
listConnectionsMock.mockResolvedValue({ connections: [{
const draft = {
...notionConnection(connectionId, "per_user", "draft"), companyId: "company-1", applicationId: "app-workspace",
config: { sourceTemplateKey: "google-workspace", connectionMethodKey: "paperclip-workspace", workspaceSetup: {
version: 1, companyId: "company-1", connectionId, actorType: "user", actorId: "me", grantKind: "user", subjectId: "me",
requestedScopes: ["https://www.googleapis.com/auth/gmail.readonly"],
} },
}] });
};
listConnectionsMock.mockResolvedValue({ connections: [draft] });
getConnectionMock.mockResolvedValue(draft);
listConnectionGrantsMock.mockResolvedValue({ currentUserId: "me", grants: [] });
await render();
await vi.waitFor(() => expect(container.textContent).toContain("Try again"));
+52
View File
@@ -667,6 +667,7 @@ describe("Connectors landing page", () => {
});
it("suppresses aggregator routes for native apps, including hidden native connectors", async () => {
experimentalMock.mockResolvedValue({ enableChatConnectors: true, enableGitHubReviewBots: true, enableGoogleWorkspaceConnector: true });
aggregatorCatalogMock.push(indexedApp("Notion", ["composio", "arcade"]), indexedApp("Google Sheets"), indexedApp("GitHub API"), indexedApp("Context7"), indexedApp("HubSpot", ["composio", "arcade"]));
await renderBrowse();
expect(container.querySelectorAll('[data-app-slug="notion"]')).toHaveLength(1);
@@ -850,6 +851,7 @@ describe("Connectors landing page", () => {
];
it("offers one Google Workspace connector while retaining the individual service definitions", async () => {
experimentalMock.mockResolvedValue({ enableGoogleWorkspaceConnector: true });
const definitions = googleSlugs.map((slug) => getAppStoreDefinition(slug)!);
listGalleryMock.mockResolvedValue({ apps: [...definitions, getAppStoreDefinition("notion")] });
const client = await renderBrowse();
@@ -872,6 +874,56 @@ describe("Connectors landing page", () => {
expect(navigateMock).toHaveBeenCalledWith("/apps/connect?source=google-workspace");
});
function googleAggregatorApp(name: string, toolkit: string): AggregatorAppCatalogEntry {
return { name, slug: toolkit, aliases: [toolkit], routes: [{ provider: "composio", toolkit,
logoUrl: `https://logos.composio.dev/api/${toolkit}`, docsUrl: `https://docs.composio.dev/toolkits/${toolkit}` }] };
}
it("hides the built-in Google Workspace connector by default and offers aggregator Google apps", async () => {
experimentalMock.mockResolvedValue({});
aggregatorCatalogMock.push(googleAggregatorApp("Gmail", "gmail"), googleAggregatorApp("Google Calendar", "googlecalendar"),
googleAggregatorApp("Google Drive", "googledrive"));
listGalleryMock.mockResolvedValue({ apps: [...googleSlugs.map(getAppStoreDefinition), getAppStoreDefinition("notion")] });
await renderBrowse();
expect(container.querySelector('[data-app-slug="google-workspace"]')).toBeNull();
for (const slug of ["gmail", "googlecalendar", "googledrive"]) {
const row = container.querySelector(`[data-app-slug="${slug}"]`);
expect(row, slug).not.toBeNull();
expect(row?.textContent).toContain("Composio");
}
expect(container.querySelector('[data-app-slug="google-calendar"]')).toBeNull();
expect(container.querySelector('[data-app-slug="notion"]')).not.toBeNull();
});
it("shows an existing aggregator Google account once, on the aggregator row, while the built-in connector is off", async () => {
experimentalMock.mockResolvedValue({});
aggregatorCatalogMock.push(googleAggregatorApp("Google Calendar", "googlecalendar"));
listGalleryMock.mockResolvedValue({ apps: [...googleSlugs.map(getAppStoreDefinition), getAppStoreDefinition("composio")] });
listApplicationsMock.mockResolvedValue({ applications: [application({ id: "gateway-app", name: "Composio", metadata: { sourceTemplateKey: "composio" } })] });
listConnectionsMock.mockResolvedValue({ connections: [connection({ applicationId: "gateway-app", name: "Composio account", transport: "mcp_remote", config: { sourceTemplateKey: "composio" } })] });
const snapshot = { connectionId: "conn-notion", toolkit: "googlecalendar", status: "connected", checkedAt: new Date().toISOString(),
accounts: [{ id: "ca-calendar", alias: "Work calendar", status: "ACTIVE", isDefault: true }] };
listComposioAppsMock.mockResolvedValue({ apps: [snapshot] });
refreshComposioAppsMock.mockResolvedValue({ apps: [snapshot] });
await renderBrowse();
expect(container.querySelector('[data-app-slug="google-calendar"]')).toBeNull();
expect(container.querySelector('[data-app-slug="googlecalendar"]')?.textContent).toContain("Work calendar");
expect(container.textContent?.split("Work calendar").length).toBe(2);
});
it("keeps aggregator Google apps folded into the built-in connector when it is enabled", async () => {
experimentalMock.mockResolvedValue({ enableGoogleWorkspaceConnector: true });
aggregatorCatalogMock.push(googleAggregatorApp("Gmail", "gmail"), googleAggregatorApp("Google Calendar", "googlecalendar"));
listGalleryMock.mockResolvedValue({ apps: googleSlugs.map(getAppStoreDefinition) });
await renderBrowse();
expect(container.querySelector('[data-app-slug="google-workspace"]')).not.toBeNull();
expect(container.querySelector('[data-app-slug="gmail"]')).toBeNull();
expect(container.querySelector('[data-app-slug="googlecalendar"]')).toBeNull();
});
it.each(["active", "draft", "disabled"])(
"shows saved Google %s accounts without changing their access or status",
async (status) => {
+13 -4
View File
@@ -154,6 +154,9 @@ const LEGACY_GOOGLE_CONNECTOR_SLUGS = new Set(
.map((profile) => profile.appSlug)
.filter((slug) => slug !== "google-workspace"),
);
// While the built-in connector is experimental, these native Google rows do not
// claim the Google apps offered by MCP aggregators unless they hold an account.
const BUILT_IN_GOOGLE_CONNECTOR_SLUGS = new Set([...LEGACY_GOOGLE_CONNECTOR_SLUGS, "google-workspace"]);
function chatProviderForSlug(slug: string): ChatProvider | null {
const method = getAppStoreDefinition(slug)?.methods.find(
@@ -373,6 +376,7 @@ export function Browse({ renderAccountDetails = (connection) => connection.conne
const { enabled: chatConnectorsEnabled, githubEnabled } = useChatConnectorsEnabled();
const { enabled: memoryConnectorsEnabled } = useMemoryConnectorsEnabled();
const connectorAvailability = useConnectorAvailability();
const googleWorkspaceConnectorEnabled = connectorAvailability.enabled(getConnectableAppDefinition("google-workspace"));
const { setBreadcrumbs } = useBreadcrumbs();
const { status: connectivityStatus } = useConnectivity();
const [query, setQuery] = useState("");
@@ -773,12 +777,16 @@ export function Browse({ renderAccountDetails = (connection) => connection.conne
// Native definitions take precedence even when temporarily hidden or unavailable.
// Public metadata never creates an application, account, or installed status.
const nativeIdentities = new Set([...CONNECTABLE_APP_DEFINITIONS, ...(galleryQuery.data?.apps ?? [])].flatMap((app) =>
[appDefinitionName(app), appDefinitionSlug(app)].map(aggregatorAppIdentity)));
const yieldsToAggregators = (slug: string) => !googleWorkspaceConnectorEnabled
&& BUILT_IN_GOOGLE_CONNECTOR_SLUGS.has(slug) && !(rowsBySlug.get(slug)?.connections.length);
const nativeIdentities = new Set([...CONNECTABLE_APP_DEFINITIONS, ...(galleryQuery.data?.apps ?? [])]
.filter((app) => !yieldsToAggregators(appDefinitionSlug(app)))
.flatMap((app) => [appDefinitionName(app), appDefinitionSlug(app)].map(aggregatorAppIdentity)));
for (const app of managedCatalog) {
const snapshots = upstreamAppsBySlug.get(app.slug) ?? [];
const aliases = new Set([app.name, app.slug, ...app.aliases].map(aggregatorAppIdentity));
const nativeRow = [...rowsBySlug.values()].find(row => aliases.has(aggregatorAppIdentity(row.slug)) || aliases.has(aggregatorAppIdentity(row.name)));
const nativeRow = [...rowsBySlug.values()].find(row => !yieldsToAggregators(row.slug)
&& (aliases.has(aggregatorAppIdentity(row.slug)) || aliases.has(aggregatorAppIdentity(row.name))));
if (nativeRow) { nativeRow.upstreamApps = [...(nativeRow.upstreamApps ?? []), ...snapshots]; nativeRow.upstreamCatalogApp = app; }
}
const aggregatorRows: ConnectorRowModel[] = managedCatalog.filter((app) =>
@@ -792,7 +800,7 @@ export function Browse({ renderAccountDetails = (connection) => connection.conne
}));
return [...rowsBySlug.values(), ...customRows, ...aggregatorRows]
.filter((row) => !LEGACY_GOOGLE_CONNECTOR_SLUGS.has(row.slug)
.filter((row) => row.aggregatorApp || !LEGACY_GOOGLE_CONNECTOR_SLUGS.has(row.slug)
|| row.connections.length > 0
|| row.upstreamApps?.some(snapshot => snapshot.accounts.length > 0))
.map((row) => ({
@@ -822,6 +830,7 @@ export function Browse({ renderAccountDetails = (connection) => connection.conne
connectionsQuery.data,
gallery,
galleryQuery.data,
googleWorkspaceConnectorEnabled,
upstreamAppsBySlug,
]);