Provider plugins that fan out into per-service integrations (Google -> Calendar, Gmail, Drive; Microsoft -> Mail, Calendar, Teams) now declare a per-service slug->icon map. The catalog favicon resolver consults it by the integration slug before the URL/token preset cascade, so a Gmail source shows the Gmail glyph rather than a generic Google logo. Kept separate from presets so the connect picker's popular list is untouched.
Unified · Split
| @@ -197,6 +197,14 @@ export interface IntegrationPlugin { · 196 unmodified lines | ||
| 197 | 197 | readonly accountHandoff?: IntegrationAccountHandoff | null; |
| 198 | 198 | }>; |
| 199 | 199 | readonly presets?: readonly IntegrationPreset[]; |
| 200 | + /** Per-service icon overrides for provider plugins that fan out into several | |
| 201 | + * per-service integrations (e.g. Google → google_calendar, google_gmail). | |
| 202 | + * Keyed by the integration slug the fan-out registers; the catalog favicon | |
| 203 | + * resolver consults this before falling back to a URL-derived favicon, so a | |
| 204 | + * Gmail integration shows the Gmail glyph rather than a generic Google logo. | |
| 205 | + * Kept separate from `presets` so the connect picker's popular-integration | |
| 206 | + * list is not affected. */ | |
| 207 | + readonly serviceIcons?: readonly { readonly slug: string; readonly icon: string }[]; | |
| 200 | 208 | /** Trigger early download of the plugin's lazy component chunks (add/edit/etc.). |
| 201 | 209 | * Call from the host on intent (hover/focus) so the chunks land before the |
| 202 | 210 | * user navigates into the add page. Idempotent. */ |
| @@ -1,16 +1,28 @@ | ||
| 1 | 1 | import { lazy } from "react"; |
| 2 | 2 | import type { IntegrationPlugin } from "@executor-js/sdk/client"; |
| 3 | −import { googleOpenApiBundlePreset, googlePhotosOpenApiBundlePreset } from "../sdk/presets"; | |
| 3 | +import { | |
| 4 | + googleOpenApiBundlePreset, | |
| 5 | + googleOpenApiPresets, | |
| 6 | + googlePhotosOpenApiBundlePreset, | |
| 7 | + googleServiceSlug, | |
| 8 | +} from "../sdk/presets"; | |
| 4 | 9 | |
| 5 | 10 | const importAdd = () => import("./AddGoogleSource"); |
| 6 | 11 | const importAccounts = () => import("./GoogleAccountsPanel"); |
| 7 | 12 | |
| 13 | +// Each per-service integration (google_calendar, google_gmail, …) is keyed by | |
| 14 | +// the slug the fan-out registers, mapped to the preset's own product glyph. | |
| 15 | +const googleServiceIcons = googleOpenApiPresets.flatMap((preset) => | |
| 16 | + preset.icon ? [{ slug: googleServiceSlug(preset.id), icon: preset.icon }] : [], | |
| 17 | +); | |
| 18 | + | |
| 8 | 19 | export const googleIntegrationPlugin: IntegrationPlugin = { |
| 9 | 20 | key: "google", |
| 10 | 21 | label: "Google", |
| 11 | 22 | add: lazy(importAdd), |
| 12 | 23 | accounts: lazy(importAccounts), |
| 13 | 24 | presets: [googleOpenApiBundlePreset, googlePhotosOpenApiBundlePreset], |
| 25 | + serviceIcons: googleServiceIcons, | |
| 14 | 26 | preload: () => { |
| 15 | 27 | void importAdd(); |
| 16 | 28 | void importAccounts(); |
| @@ -1,16 +1,27 @@ | ||
| 1 | 1 | import { lazy } from "react"; |
| 2 | 2 | import type { IntegrationPlugin } from "@executor-js/sdk/client"; |
| 3 | −import { microsoftGraphPreset } from "../sdk/presets"; | |
| 3 | +import { | |
| 4 | + microsoftGraphPreset, | |
| 5 | + microsoftGraphScopePresets, | |
| 6 | + microsoftServiceSlug, | |
| 7 | +} from "../sdk/presets"; | |
| 4 | 8 | |
| 5 | 9 | const importAdd = () => import("./AddMicrosoftSource"); |
| 6 | 10 | const importAccounts = () => import("./MicrosoftAccountsPanel"); |
| 7 | 11 | |
| 12 | +// Each per-workload integration (microsoft_mail, microsoft_calendar, …) is | |
| 13 | +// keyed by the slug the fan-out registers, mapped to the workload's own glyph. | |
| 14 | +const microsoftServiceIcons = microsoftGraphScopePresets.flatMap((preset) => | |
| 15 | + preset.icon ? [{ slug: microsoftServiceSlug(preset.id), icon: preset.icon }] : [], | |
| 16 | +); | |
| 17 | + | |
| 8 | 18 | export const microsoftIntegrationPlugin: IntegrationPlugin = { |
| 9 | 19 | key: "microsoft", |
| 10 | 20 | label: "Microsoft", |
| 11 | 21 | add: lazy(importAdd), |
| 12 | 22 | accounts: lazy(importAccounts), |
| 13 | 23 | presets: [microsoftGraphPreset], |
| 24 | + serviceIcons: microsoftServiceIcons, | |
| 14 | 25 | preload: () => { |
| 15 | 26 | void importAdd(); |
| 16 | 27 | void importAccounts(); |
| @@ -70,6 +70,68 @@ describe("IntegrationFavicon", () => { · 69 unmodified lines | ||
| 70 | 70 | expect(integrationFaviconSrc({ icon, sourceId: slug, url, size: 16 })).toBe("/favicon-32.png"); |
| 71 | 71 | }); |
| 72 | 72 | |
| 73 | + it("prefers a plugin's per-service icon (keyed by slug) over the preset cascade", () => { | |
| 74 | + // A provider that fans out into per-service integrations declares a | |
| 75 | + // slug->icon map; a Gmail integration resolves to the Gmail glyph even | |
| 76 | + // though every Google service shares the same displayUrl host. | |
| 77 | + expect( | |
| 78 | + integrationPresetIconUrl( | |
| 79 | + { | |
| 80 | + id: "google_gmail", | |
| 81 | + kind: "google", | |
| 82 | + name: "Gmail", | |
| 83 | + url: "https://www.googleapis.com/discovery/v1/apis/gmail/v1/rest", | |
| 84 | + }, | |
| 85 | + [ | |
| 86 | + { | |
| 87 | + key: "google", | |
| 88 | + label: "Google", | |
| 89 | + add: () => null, | |
| 90 | + edit: () => null, | |
| 91 | + presets: [ | |
| 92 | + { id: "google", name: "Google", summary: "Bundle.", icon: "https://x/g.svg" }, | |
| 93 | + ], | |
| 94 | + serviceIcons: [ | |
| 95 | + { slug: "google_gmail", icon: "https://x/gmail.png" }, | |
| 96 | + { slug: "google_calendar", icon: "https://x/calendar.svg" }, | |
| 97 | + ], | |
| 98 | + }, | |
| 99 | + ], | |
| 100 | + ), | |
| 101 | + ).toBe("https://x/gmail.png"); | |
| 102 | + }); | |
| 103 | + | |
| 104 | + it("falls through to the preset cascade when no per-service icon matches the slug", () => { | |
| 105 | + expect( | |
| 106 | + integrationPresetIconUrl( | |
| 107 | + { | |
| 108 | + id: "google_sheets", | |
| 109 | + kind: "google", | |
| 110 | + name: "Google Sheets", | |
| 111 | + url: "https://www.googleapis.com/discovery/v1/apis/sheets/v4/rest", | |
| 112 | + }, | |
| 113 | + [ | |
| 114 | + { | |
| 115 | + key: "google", | |
| 116 | + label: "Google", | |
| 117 | + add: () => null, | |
| 118 | + edit: () => null, | |
| 119 | + presets: [ | |
| 120 | + { | |
| 121 | + id: "google-sheets", | |
| 122 | + name: "Google Sheets", | |
| 123 | + summary: "Spreadsheets.", | |
| 124 | + url: "https://www.googleapis.com/discovery/v1/apis/sheets/v4/rest", | |
| 125 | + icon: "https://x/sheets.svg", | |
| 126 | + }, | |
| 127 | + ], | |
| 128 | + serviceIcons: [{ slug: "google_gmail", icon: "https://x/gmail.png" }], | |
| 129 | + }, | |
| 130 | + ], | |
| 131 | + ), | |
| 132 | + ).toBe("https://x/sheets.svg"); | |
| 133 | + }); | |
| 134 | + | |
| 73 | 135 | it("finds preset icons from a source URL", () => { |
| 74 | 136 | expect( |
| 75 | 137 | integrationPresetIconUrl( |
| @@ -156,6 +156,14 @@ export function integrationPresetIconUrl( · 155 unmodified lines | ||
| 156 | 156 | ): string | null { |
| 157 | 157 | const pluginKey = KIND_TO_PLUGIN_KEY[source.kind] ?? source.kind; |
| 158 | 158 | const plugin = integrationPlugins.find((p) => p.key === pluginKey); |
| 159 | + | |
| 160 | + // Per-service integrations (google_calendar, microsoft_mail, …) carry their | |
| 161 | + // own product glyph, keyed by the exact slug the fan-out registered. This is | |
| 162 | + // the most specific match, so it wins over the URL/token preset cascade below | |
| 163 | + // (a provider's per-service integrations often share one displayUrl). | |
| 164 | + const serviceIcon = plugin?.serviceIcons?.find((entry) => entry.slug === source.id); | |
| 165 | + if (serviceIcon) return serviceIcon.icon; | |
| 166 | + | |
| 159 | 167 | const presets = plugin?.presets ?? []; |
| 160 | 168 | const sourceUrl = normalizeUrl(source.url); |
| 161 | 169 | const sourceGoogleService = googleApiServiceFromUrl(source.url); |