History

Give per-service integrations their own service icons

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.

Rhys Sullivan committed 4 months ago

Commit a0f4e381 parent b14985d5 files · +103 −2Browse files

Unified · Split

5 files
  1. packages/core/sdk/src/client.ts +8 −0
  2. packages/plugins/google/src/react/source-plugin.ts +13 −1
  3. packages/plugins/microsoft/src/react/source-plugin.ts +12 −1
  4. packages/react/src/components/integration-favicon.test.tsx +62 −0
  5. packages/react/src/components/integration-favicon.tsx +8 −0
packages/core/sdk/src/client.ts+8−0
@@ -197,6 +197,14 @@ export interface IntegrationPlugin { · 196 unmodified lines
197197 readonly accountHandoff?: IntegrationAccountHandoff | null;
198198 }>;
199199 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 }[];
200208 /** Trigger early download of the plugin's lazy component chunks (add/edit/etc.).
201209 * Call from the host on intent (hover/focus) so the chunks land before the
202210 * user navigates into the add page. Idempotent. */
packages/plugins/google/src/react/source-plugin.ts+13−1
@@ -1,16 +1,28 @@
11 import { lazy } from "react";
22 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";
49
510 const importAdd = () => import("./AddGoogleSource");
611 const importAccounts = () => import("./GoogleAccountsPanel");
712
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+
819 export const googleIntegrationPlugin: IntegrationPlugin = {
920 key: "google",
1021 label: "Google",
1122 add: lazy(importAdd),
1223 accounts: lazy(importAccounts),
1324 presets: [googleOpenApiBundlePreset, googlePhotosOpenApiBundlePreset],
25+ serviceIcons: googleServiceIcons,
1426 preload: () => {
1527 void importAdd();
1628 void importAccounts();
packages/plugins/microsoft/src/react/source-plugin.ts+12−1
@@ -1,16 +1,27 @@
11 import { lazy } from "react";
22 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";
48
59 const importAdd = () => import("./AddMicrosoftSource");
610 const importAccounts = () => import("./MicrosoftAccountsPanel");
711
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+
818 export const microsoftIntegrationPlugin: IntegrationPlugin = {
919 key: "microsoft",
1020 label: "Microsoft",
1121 add: lazy(importAdd),
1222 accounts: lazy(importAccounts),
1323 presets: [microsoftGraphPreset],
24+ serviceIcons: microsoftServiceIcons,
1425 preload: () => {
1526 void importAdd();
1627 void importAccounts();
packages/react/src/components/integration-favicon.test.tsx+62−0
@@ -70,6 +70,68 @@ describe("IntegrationFavicon", () => { · 69 unmodified lines
7070 expect(integrationFaviconSrc({ icon, sourceId: slug, url, size: 16 })).toBe("/favicon-32.png");
7171 });
7272
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+
73135 it("finds preset icons from a source URL", () => {
74136 expect(
75137 integrationPresetIconUrl(
packages/react/src/components/integration-favicon.tsx+8−0
@@ -156,6 +156,14 @@ export function integrationPresetIconUrl( · 155 unmodified lines
156156 ): string | null {
157157 const pluginKey = KIND_TO_PLUGIN_KEY[source.kind] ?? source.kind;
158158 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+
159167 const presets = plugin?.presets ?? [];
160168 const sourceUrl = normalizeUrl(source.url);
161169 const sourceGoogleService = googleApiServiceFromUrl(source.url);