Skip to content

Commit b7a5f22

Browse files
authored
fix(integrations): read every service mark from one registry (#6682)
* 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. * fix(blocks): drop the dead copies of a block's colour Sweeping the surfaces above turned up colour data nothing reads and colour data two surfaces disagreed on. Dead: `BLOCK_COLORS.DEFAULT/LOOP/PARALLEL` in the tag dropdown (only `VARIABLE` was ever referenced), `BlockIconInfo.color` on table columns — whose consumer documents that it deliberately ignores the colour, so the `#2F55FF` behind it could never render — and the `bgColor` threaded into the add-resource dropdown, whose row renders a bare tinted icon. Disagreeing: the Variables tile is `#2F8BFF` in the tag dropdown and `#8B5CF6` in the preview panel, for the same "V" on the same concept. Both now read `VARIABLE_TILE_COLOR`, and the preview panel's two hand-rolled squares become `BlockTile` like every other tile. Four spellings of the neutral fallback (`#6B7280`, `#6b7280`, `#666666`, and a `cancelled` status that happened to equal it) now point at `DEFAULT_BLOCK_TILE_COLOR`. The terminal and logs resolvers stay. They look like duplicates of `accent.ts` but carry behaviour it does not have — status fills for synthesized error/validation/cancelled rows, near-black contrast correction, MCP tool-id parsing, and a model-provider branch — so folding them in is a behavioural change, not a deletion. * chore(copilot): delete the mention machinery nothing calls The workflow panel's copilot tab renders `MothershipChat`, and that component brings its own input — so `panel/components/copilot` no longer holds a component at all, only the hook library the old input used. Five of those hooks have no caller anywhere: `useMentionData`, `useMentionKeyboard`, `useCaretViewport`, `useMentionInsertHandlers`, `useTextareaAutoResize`. They are not all of it. `home/components/user-input` still imports `useFileAttachments`, `useMentionMenu`, `useMentionTokens`, `useContextManagement`, and `useIntegrationAutoMention` from this directory, so it survives as a shared hook library rather than dead weight — which is why this removes the uncalled five rather than the folder. What they alone reached goes with them: `getFolderData` / `getFolderLoading` / `getFolderEnsureLoaded` and the `FOLDER_CONFIGS` table describing every mention folder, `buildMentionHighlightNodes`, the `MentionFolderNav` type, and the slash-command tables. Of the 266-line constants file only `SCROLL_TOLERANCE` had a live reader left. * fix(integrations): never answer with a sibling's tile Two integrations can share one OAuth id: Google Slides is authenticated by Drive's `google-drive` service and Jira Service Management by Jira's `jira`. Indexing first-write-wins made those ids resolve to whichever sorted first, so the dialog connecting Slides could wear Drive's brand. An id claimed by more than one block type now resolves to neither, and the caller keeps the service-specific mark it already had. A wrong brand is worse than no tile.
1 parent 5a88ce2 commit b7a5f22

37 files changed

Lines changed: 388 additions & 1692 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/mothership-view/components/add-resource-dropdown/add-resource-dropdown.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -258,7 +258,6 @@ export function useAvailableResources(
258258
id: integration.blockType,
259259
name: integration.name,
260260
iconComponent: integration.icon,
261-
bgColor: integration.bgColor,
262261
})),
263262
},
264263
{

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'}

0 commit comments

Comments
 (0)