Skip to content

Commit a63b6cb

Browse files
committed
fix(integrations): read every service mark from one registry
A service looked like itself on the canvas and like nothing in particular everywhere it was connected. `OAUTH_PROVIDERS` registers 93 icons and no colour at all, so the surfaces built on it — the connect dialog above all — drew a flat grey mark for a block whose config already carries its brand icon and `bgColor`. Bridges the two: `resolveIntegrationBlockTypeForOAuth` maps any OAuth id (a service id, a provider id, an extra authorization server) to the catalog block behind it, so a credential surface holding only an OAuth identity can still reach the registry. The connect dialog now wears the block's tile, and `ChipModalHeader` takes a rendered mark so a tile can carry its own chrome instead of being tinted with the header's grey. Folds in the copies that had grown around the gap: `IntegrationTile` resolved its fill from the registry but took its icon from whatever the caller passed — one tile, two sources — and now defaults to the registry, with an override kept for the family service-account marks that genuinely are not the block's. The letter fallback it grew alongside was reading the catalog's `bgColor` while the tile beside it read the registry's; both are the tile now. Two `getProviderIcon` implementations for the same job (one tinted, one not) become one `ProviderIcon`, the connector tile duplicated verbatim across two knowledge-base surfaces becomes one `ConnectorTile`, and the permission rows that hardcoded `text-white` — which renders white-on-white on a pale brand tile — go through `BlockTile`. Public pages keep their generated catalog: importing the registry there would ship 282 block configs to a marketing page, and `integrations.json` is generated from the same `bgColor`, so the two cannot drift.
1 parent 1424809 commit a63b6cb

19 files changed

Lines changed: 330 additions & 141 deletions

File tree

apps/sim/app/workspace/[workspaceId]/components/connect-oauth-modal/connect-oauth-modal.tsx

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,15 @@ import { useSession } from '@/lib/auth/auth-client'
1919
import type { OAuthReturnContext } from '@/lib/credentials/client-state'
2020
import { ADD_CONNECTOR_SEARCH_PARAM, writeOAuthReturnContext } from '@/lib/credentials/client-state'
2121
import { defaultCredentialDisplayName } from '@/lib/credentials/display-name'
22+
import { resolveIntegrationBlockTypeForOAuth } from '@/lib/integrations'
2223
import {
2324
getProviderIdFromServiceId,
2425
OAUTH_PROVIDERS,
2526
type OAuthProvider,
2627
parseProvider,
2728
} from '@/lib/oauth'
2829
import { getScopeDescription, getServiceConfigByProviderId } from '@/lib/oauth/utils'
30+
import { BlockTile } from '@/blocks/block-tile'
2931
import { useCreateCredentialDraft, useWorkspaceCredentials } from '@/hooks/queries/credentials'
3032
import { useConnectOAuthService } from '@/hooks/queries/oauth/oauth-connections'
3133

@@ -173,6 +175,20 @@ export function ConnectOAuthModal(props: ConnectOAuthModalProps) {
173175
return resolveService(provider, props.serviceId ?? providerId)
174176
}, [props.serviceName, props.serviceIcon, props.provider, props.serviceId, providerId])
175177

178+
/**
179+
* The block behind this OAuth identity, so the dialog wears the same brand
180+
* tile the canvas and the integrations catalog do. Falls back to the bare
181+
* `OAUTH_PROVIDERS` mark for an id no catalog integration claims.
182+
*/
183+
const headerIcon = useMemo(() => {
184+
const blockType = resolveIntegrationBlockTypeForOAuth(
185+
props.serviceId,
186+
props.provider,
187+
providerId
188+
)
189+
return blockType ? <BlockTile blockType={blockType} size='md' /> : ProviderIcon
190+
}, [props.serviceId, props.provider, providerId, ProviderIcon])
191+
176192
const workspaceId = isConnect ? props.workspaceId : ''
177193
const { data: credentials = [], isPending: credentialsLoading } = useWorkspaceCredentials({
178194
workspaceId,
@@ -343,7 +359,7 @@ export function ConnectOAuthModal(props: ConnectOAuthModalProps) {
343359

344360
return (
345361
<ChipModal open={open} onOpenChange={onOpenChange} srTitle={title}>
346-
<ChipModalHeader icon={ProviderIcon} onClose={handleClose}>
362+
<ChipModalHeader icon={headerIcon} onClose={handleClose}>
347363
{title}
348364
</ChipModalHeader>
349365
<ChipModalBody>
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { ProviderIcon } from './provider-icon'
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
'use client'
2+
3+
import { cn } from '@sim/emcn'
4+
import { SquareArrowUpRight } from '@sim/emcn/icons'
5+
import { OAUTH_PROVIDERS, type OAuthProvider, parseProvider } from '@/lib/oauth'
6+
import { getBareIconStyle, type StyleableIcon } from '@/blocks/brand-icon-style'
7+
8+
interface ProviderIconProps {
9+
provider: OAuthProvider
10+
className?: string
11+
}
12+
13+
/**
14+
* The mark for an OAuth provider, tinted with the brand colour its block
15+
* config registers. Credential rows show a bare icon rather than the filled
16+
* tile the canvas uses, so the colour has to come through `iconColor` — but it
17+
* still comes from the same registry, which is what keeps a provider looking
18+
* like itself everywhere it is listed.
19+
*
20+
* `OAUTH_PROVIDERS` carries the icon and no colour at all, so rendering
21+
* straight from it is what left credential surfaces grey while the same
22+
* service was branded a panel away. Falls back to a generic mark for a
23+
* provider that map does not know.
24+
*/
25+
export function ProviderIcon({ provider, className }: ProviderIconProps) {
26+
const { baseProvider } = parseProvider(provider)
27+
const config = OAUTH_PROVIDERS[baseProvider]
28+
29+
if (!config) return <SquareArrowUpRight className={className} />
30+
31+
const Icon = config.icon as StyleableIcon
32+
return (
33+
<Icon className={cn('text-[var(--text-icon)]', className)} style={getBareIconStyle(Icon)} />
34+
)
35+
}

apps/sim/app/workspace/[workspaceId]/home/components/suggested-actions/suggested-actions.tsx

Lines changed: 25 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import { randomFloat } from '@sim/utils/random'
77
import { stripVersionSuffix } from '@sim/utils/string'
88
import { useParams } from 'next/navigation'
99
import { usePostHog } from 'posthog-js/react'
10-
import { GmailIcon, SlackIcon } from '@/components/icons'
1110
import {
1211
INTEGRATIONS,
1312
type OAuthServiceMatch,
@@ -16,6 +15,7 @@ import {
1615
} from '@/lib/integrations'
1716
import { captureEvent } from '@/lib/posthog/client'
1817
import { ConnectOAuthModal } from '@/app/workspace/[workspaceId]/components/connect-oauth-modal'
18+
import { getBlockTileIcon } from '@/blocks/accent'
1919
import { getBareIconStyle } from '@/blocks/brand-icon-style'
2020
import { getAllBlockMeta } from '@/blocks/registry'
2121
import type { ModuleTag } from '@/blocks/types'
@@ -224,27 +224,37 @@ function computeActions(services: readonly ServiceInfo[], signals: Signals): Act
224224
return [...integrations, ...prompts]
225225
}
226226

227+
/**
228+
* Integrations pinned to the first paint. Named by block type so the mark comes
229+
* from the same registry every other surface reads, rather than a second copy
230+
* imported here that could drift from the block's own icon.
231+
*/
232+
const INITIAL_INTEGRATIONS = [
233+
{ blockType: 'slack', slug: 'slack', name: 'Slack' },
234+
{ blockType: 'gmail', slug: 'gmail', name: 'Gmail' },
235+
] as const
236+
227237
/**
228238
* Initial actions rendered on first paint, before OAuth/credentials queries
229239
* resolve. For users with no connections this is also the final result, so the
230240
* section never flashes. Users with existing connections briefly see this
231241
* before the personalized recompute replaces it.
232242
*/
233243
const INITIAL_ACTIONS: Action[] = [
234-
{
235-
kind: 'integration',
236-
id: 'integrate-slack',
237-
label: 'Integrate with Slack',
238-
icon: SlackIcon,
239-
slug: 'slack',
240-
},
241-
{
242-
kind: 'integration',
243-
id: 'integrate-gmail',
244-
label: 'Integrate with Gmail',
245-
icon: GmailIcon,
246-
slug: 'gmail',
247-
},
244+
...INITIAL_INTEGRATIONS.flatMap<Action>(({ blockType, slug, name }) => {
245+
const icon = getBlockTileIcon(blockType)
246+
return icon
247+
? [
248+
{
249+
kind: 'integration',
250+
id: `integrate-${slug}`,
251+
label: `Integrate with ${name}`,
252+
icon,
253+
slug,
254+
},
255+
]
256+
: []
257+
}),
248258
toPromptAction(TABLE_STARTERS[0]),
249259
...CANDIDATES.filter((c) => c.blockType === 'github' && c.featured)
250260
.slice(0, 1)

apps/sim/app/workspace/[workspaceId]/integrations/[block]/integration-block-detail.tsx

Lines changed: 8 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,12 @@ import { useQueryState } from 'nuqs'
88
import { HEADER_ACTION_CLUSTER, PAGE_HEADER_BAR } from '@/components/page-header-bar'
99
import { isChatEnabled } from '@/lib/core/config/env-flags'
1010
import {
11-
blockTypeToIconMap,
1211
type Integration,
1312
resolveCredentialDisplay,
1413
resolveOAuthServiceForIntegration,
1514
} from '@/lib/integrations'
1615
import { credentialProviderMatchesService } from '@/lib/oauth'
1716
import { ConnectOAuthModal } from '@/app/workspace/[workspaceId]/components/connect-oauth-modal'
18-
import { RESOURCE_TILE_BASE } from '@/app/workspace/[workspaceId]/components/resource-tile'
1917
import { IntegrationSkillsSection } from '@/app/workspace/[workspaceId]/integrations/[block]/integration-skills-section'
2018
import { connectParam } from '@/app/workspace/[workspaceId]/integrations/[block]/search-params'
2119
import {
@@ -34,7 +32,7 @@ import {
3432
SettingsResourceRow,
3533
} from '@/app/workspace/[workspaceId]/settings/components/settings-resource-row'
3634
import { SettingsSection } from '@/app/workspace/[workspaceId]/settings/components/settings-section/settings-section'
37-
import { getTileIconColorClass } from '@/blocks/icon-color'
35+
import { getBlockTileIcon } from '@/blocks/accent'
3836
import { storeCuratedPrompt } from '@/blocks/integration-matcher'
3937
import {
4038
getSuggestedSkillsForBlock,
@@ -64,7 +62,6 @@ export function IntegrationBlockDetail({ integration, workspaceId }: Integration
6462
useOAuthReturnRouter()
6563
const router = useRouter()
6664
const [connectMode, setConnectMode] = useQueryState(connectParam.key, connectParam.parser)
67-
const Icon = blockTypeToIconMap[integration.type]
6865
const matchingTemplates = getTemplatesForBlock(integration.type)
6966
const suggestedSkills = getSuggestedSkillsForBlock(integration.type)
7067
const oauthService = resolveOAuthServiceForIntegration(integration)
@@ -233,16 +230,10 @@ export function IntegrationBlockDetail({ integration, workspaceId }: Integration
233230
>
234231
<div className='mx-auto flex max-w-[48rem] flex-col gap-7 pb-3'>
235232
<div className='flex flex-col gap-3'>
236-
{Icon ? (
237-
<IntegrationTile blockType={integration.type} icon={Icon} />
238-
) : (
239-
<div
240-
className={cn(RESOURCE_TILE_BASE, getTileIconColorClass(integration.bgColor))}
241-
style={{ background: integration.bgColor }}
242-
>
243-
{integration.name.charAt(0)}
244-
</div>
245-
)}
233+
<IntegrationTile
234+
blockType={integration.type}
235+
fallbackLabel={integration.name.charAt(0)}
236+
/>
246237
<div className='flex flex-col gap-1'>
247238
<h1 className='text-[var(--text-body)] text-lg'>{integration.name}</h1>
248239
<p className='text-[var(--text-muted)] text-md'>{integration.description}</p>
@@ -255,7 +246,7 @@ export function IntegrationBlockDetail({ integration, workspaceId }: Integration
255246
<SettingsResourceRow
256247
key={credential.id}
257248
iconVariant='custom'
258-
icon={Icon && <IntegrationTile blockType={integration.type} icon={Icon} />}
249+
icon={<IntegrationTile blockType={integration.type} />}
259250
title={credential.displayName}
260251
description={
261252
credential.description || resolveCredentialDisplay(credential).subtitle
@@ -374,8 +365,7 @@ function TemplateIcons({ blockTypes }: TemplateIconsProps) {
374365
return (
375366
<span aria-hidden className='flex items-center'>
376367
{blockTypes.map((bt, idx) => {
377-
const ToolIcon = blockTypeToIconMap[bt]
378-
if (!ToolIcon) return null
368+
if (!getBlockTileIcon(bt)) return null
379369
const z = TEMPLATE_TILE_Z[idx]
380370
if (!z) return null
381371
const isTrailing = idx > 0
@@ -389,7 +379,7 @@ function TemplateIcons({ blockTypes }: TemplateIconsProps) {
389379
'outline outline-2 outline-[var(--bg)] transition-[outline-color] duration-150 group-hover:outline-[var(--surface-active)]'
390380
)}
391381
>
392-
<IntegrationTile blockType={bt} icon={ToolIcon} />
382+
<IntegrationTile blockType={bt} />
393383
</span>
394384
)
395385
})}

apps/sim/app/workspace/[workspaceId]/integrations/components/integrations-showcase/integrations-showcase.tsx

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
RESOURCE_TILE_PLAIN,
66
} from '@/app/workspace/[workspaceId]/components/resource-tile'
77
import { getBlock } from '@/blocks'
8+
import { getBlockTileIcon } from '@/blocks/accent'
89
import { getTileIconColorClass } from '@/blocks/icon-color'
910

1011
/**
@@ -59,7 +60,15 @@ function resolveBrandTileBg(blockType: string): string | null {
5960

6061
interface IntegrationTileProps {
6162
blockType: string
62-
icon: ComponentType<{ className?: string }>
63+
/**
64+
* Overrides the block's registered mark. Only for a tile whose identity is
65+
* not the block itself — a credential issued by a family service account
66+
* wears the family's corporate mark. Everything else takes the registry's,
67+
* so the tile cannot end up with its fill and its icon from two sources.
68+
*/
69+
icon?: ComponentType<{ className?: string }>
70+
/** Drawn when neither the override nor the registry supplies a mark. */
71+
fallbackLabel?: string
6372
framed?: boolean
6473
}
6574

@@ -68,27 +77,37 @@ interface IntegrationTileProps {
6877
* is a 36px tile used in list rows and headers; the framed variant adds an
6978
* outer 44px halo used inside the showcase grid.
7079
*/
71-
export function IntegrationTile({ blockType, icon: Icon, framed = false }: IntegrationTileProps) {
80+
export function IntegrationTile({
81+
blockType,
82+
icon,
83+
fallbackLabel,
84+
framed = false,
85+
}: IntegrationTileProps) {
7286
const brandBg = resolveBrandTileBg(blockType)
87+
const Icon = icon ?? getBlockTileIcon(blockType)
88+
const contentClass = getTileIconColorClass(brandBg)
7389

7490
if (!framed) {
7591
return (
7692
<div
77-
className={cn(RESOURCE_TILE_BASE, RESOURCE_TILE_PLAIN)}
93+
className={cn(RESOURCE_TILE_BASE, RESOURCE_TILE_PLAIN, !Icon && contentClass)}
7894
style={brandBg ? { background: brandBg } : undefined}
7995
>
80-
<Icon className={getTileIconColorClass(brandBg)} />
96+
{Icon ? <Icon className={contentClass} /> : fallbackLabel}
8197
</div>
8298
)
8399
}
84100

85101
return (
86102
<div className='size-11 flex-shrink-0 rounded-xl border border-[var(--border-muted)] bg-[var(--surface-4)] p-[3px] shadow-sm dark:bg-[var(--surface-5)]'>
87103
<div
88-
className='flex size-full items-center justify-center rounded-[9px] border border-[var(--border-1)] bg-[var(--bg)]'
104+
className={cn(
105+
'flex size-full items-center justify-center rounded-[9px] border border-[var(--border-1)] bg-[var(--bg)]',
106+
!Icon && contentClass
107+
)}
89108
style={brandBg ? { background: brandBg } : undefined}
90109
>
91-
<Icon className={cn('size-6', getTileIconColorClass(brandBg))} />
110+
{Icon ? <Icon className={cn('size-6', contentClass)} /> : fallbackLabel}
92111
</div>
93112
</div>
94113
)

apps/sim/app/workspace/[workspaceId]/integrations/connected/[credentialId]/connected-credential-detail.tsx

Lines changed: 5 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import {
88
ChipInput,
99
ChipLink,
1010
ChipTextarea,
11-
cn,
1211
Send,
1312
toast,
1413
} from '@sim/emcn'
@@ -28,10 +27,6 @@ import {
2827
UnsavedChangesModal,
2928
useCredentialDetailForm,
3029
} from '@/app/workspace/[workspaceId]/components/credential-detail'
31-
import {
32-
RESOURCE_TILE_BASE,
33-
RESOURCE_TILE_PLAIN,
34-
} from '@/app/workspace/[workspaceId]/components/resource-tile'
3530
import {
3631
ConnectServiceAccountModal,
3732
type ServiceAccountProviderId,
@@ -244,15 +239,11 @@ export function ConnectedCredentialDetail({
244239
<CredentialDetailLayout back={back} actions={actions}>
245240
<CredentialDetailHeading
246241
leading={
247-
display?.icon ? (
248-
<IntegrationTile blockType={integrationBlockType} icon={display.icon} />
249-
) : (
250-
<div className={cn(RESOURCE_TILE_BASE, RESOURCE_TILE_PLAIN)}>
251-
<span className='text-[var(--text-tertiary)] text-small'>
252-
{resolveProviderLabel(credential.providerId).slice(0, 1) || '?'}
253-
</span>
254-
</div>
255-
)
242+
<IntegrationTile
243+
blockType={integrationBlockType}
244+
icon={display?.icon ?? undefined}
245+
fallbackLabel={resolveProviderLabel(credential.providerId).slice(0, 1) || '?'}
246+
/>
256247
}
257248
title={headingTitle}
258249
subtitle={display?.detailSubtitle ?? 'Connected service'}

apps/sim/app/workspace/[workspaceId]/integrations/integrations.tsx

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ import {
1414
import { useParams } from 'next/navigation'
1515
import { useQueryStates } from 'nuqs'
1616
import {
17-
blockTypeToIconMap,
1817
formatIntegrationType,
1918
INTEGRATIONS,
2019
type Integration,
@@ -34,6 +33,7 @@ import {
3433
} from '@/app/workspace/[workspaceId]/integrations/search-params'
3534
import { SettingsEmptyState } from '@/app/workspace/[workspaceId]/settings/components/settings-empty-state'
3635
import { SettingsResourceRow } from '@/app/workspace/[workspaceId]/settings/components/settings-resource-row'
36+
import { getBlockTileIcon } from '@/blocks/accent'
3737
import { useWorkspaceCredentials, type WorkspaceCredential } from '@/hooks/queries/credentials'
3838
import { useDebouncedSearchSetter } from '@/hooks/use-debounced-search-setter'
3939
import { usePermissionConfig } from '@/hooks/use-permission-config'
@@ -68,7 +68,6 @@ interface IntegrationItemProps {
6868
workspaceId: string
6969
name: string
7070
description?: string | null
71-
icon: ComponentType<{ className?: string }>
7271
unavailable?: boolean
7372
}
7473

@@ -78,13 +77,12 @@ function IntegrationItem({
7877
workspaceId,
7978
name,
8079
description,
81-
icon: Icon,
8280
unavailable = false,
8381
}: IntegrationItemProps) {
8482
return (
8583
<SettingsResourceRow
8684
iconVariant='custom'
87-
icon={<IntegrationTile blockType={blockType} icon={Icon} />}
85+
icon={<IntegrationTile blockType={blockType} />}
8886
title={name}
8987
description={
9088
unavailable
@@ -348,8 +346,7 @@ export function Integrations() {
348346
{filteredCategorySections.map((section) => (
349347
<IntegrationSection key={section.label} label={formatIntegrationType(section.label)}>
350348
{section.integrations.map((integration) => {
351-
const Icon = blockTypeToIconMap[integration.type]
352-
if (!Icon) return null
349+
if (!getBlockTileIcon(integration.type)) return null
353350
const availability = integrationAvailability.get(integration.type.toLowerCase())
354351
const deploymentUnavailable =
355352
availability?.state === 'unavailable' || availability?.state === 'misconfigured'
@@ -361,7 +358,6 @@ export function Integrations() {
361358
workspaceId={workspaceId}
362359
name={integration.name}
363360
description={integration.description}
364-
icon={Icon}
365361
unavailable={integration.authType === 'oauth' && deploymentUnavailable}
366362
/>
367363
)

0 commit comments

Comments
 (0)