mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-01 23:43:16 +01:00
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:
1 parent
b6b5341e0a
commit
cbb3b579b4
2 files changed
+29
-28
No files matched your search
@@ -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();
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in new issue
Block a user