Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,15 @@ import { useSession } from '@/lib/auth/auth-client'
import type { OAuthReturnContext } from '@/lib/credentials/client-state'
import { ADD_CONNECTOR_SEARCH_PARAM, writeOAuthReturnContext } from '@/lib/credentials/client-state'
import { defaultCredentialDisplayName } from '@/lib/credentials/display-name'
import { resolveIntegrationBlockTypeForOAuth } from '@/lib/integrations'
import {
getProviderIdFromServiceId,
OAUTH_PROVIDERS,
type OAuthProvider,
parseProvider,
} from '@/lib/oauth'
import { getScopeDescription, getServiceConfigByProviderId } from '@/lib/oauth/utils'
import { BlockTile } from '@/blocks/block-tile'
import { useCreateCredentialDraft, useWorkspaceCredentials } from '@/hooks/queries/credentials'
import { useConnectOAuthService } from '@/hooks/queries/oauth/oauth-connections'

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

/**
* The block behind this OAuth identity, so the dialog wears the same brand
* tile the canvas and the integrations catalog do. Falls back to the bare
* `OAUTH_PROVIDERS` mark for an id no catalog integration claims.
*/
const headerIcon = useMemo(() => {
const blockType = resolveIntegrationBlockTypeForOAuth(
props.serviceId,
props.provider,
providerId
)
return blockType ? <BlockTile blockType={blockType} size='md' /> : ProviderIcon
}, [props.serviceId, props.provider, providerId, ProviderIcon])

const workspaceId = isConnect ? props.workspaceId : ''
const { data: credentials = [], isPending: credentialsLoading } = useWorkspaceCredentials({
workspaceId,
Expand Down Expand Up @@ -343,7 +359,7 @@ export function ConnectOAuthModal(props: ConnectOAuthModalProps) {

return (
<ChipModal open={open} onOpenChange={onOpenChange} srTitle={title}>
<ChipModalHeader icon={ProviderIcon} onClose={handleClose}>
<ChipModalHeader icon={headerIcon} onClose={handleClose}>
{title}
</ChipModalHeader>
<ChipModalBody>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { ProviderIcon } from './provider-icon'
Comment thread
waleedlatif1 marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
'use client'

import { cn } from '@sim/emcn'
import { SquareArrowUpRight } from '@sim/emcn/icons'
import { OAUTH_PROVIDERS, type OAuthProvider, parseProvider } from '@/lib/oauth'
import { getBareIconStyle, type StyleableIcon } from '@/blocks/brand-icon-style'

interface ProviderIconProps {
provider: OAuthProvider
className?: string
}

/**
* The mark for an OAuth provider, tinted with the brand colour its block
* config registers. Credential rows show a bare icon rather than the filled
* tile the canvas uses, so the colour has to come through `iconColor` — but it
* still comes from the same registry, which is what keeps a provider looking
* like itself everywhere it is listed.
*
* `OAUTH_PROVIDERS` carries the icon and no colour at all, so rendering
* straight from it is what left credential surfaces grey while the same
* service was branded a panel away. Falls back to a generic mark for a
* provider that map does not know.
*/
export function ProviderIcon({ provider, className }: ProviderIconProps) {
const { baseProvider } = parseProvider(provider)
const config = OAUTH_PROVIDERS[baseProvider]

if (!config) return <SquareArrowUpRight className={className} />

const Icon = config.icon as StyleableIcon
return (
<Icon className={cn('text-[var(--text-icon)]', className)} style={getBareIconStyle(Icon)} />
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -258,7 +258,6 @@ export function useAvailableResources(
id: integration.blockType,
name: integration.name,
iconComponent: integration.icon,
bgColor: integration.bgColor,
})),
},
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import { randomFloat } from '@sim/utils/random'
import { stripVersionSuffix } from '@sim/utils/string'
import { useParams } from 'next/navigation'
import { usePostHog } from 'posthog-js/react'
import { GmailIcon, SlackIcon } from '@/components/icons'
import {
INTEGRATIONS,
type OAuthServiceMatch,
Expand All @@ -16,6 +15,7 @@ import {
} from '@/lib/integrations'
import { captureEvent } from '@/lib/posthog/client'
import { ConnectOAuthModal } from '@/app/workspace/[workspaceId]/components/connect-oauth-modal'
import { getBlockTileIcon } from '@/blocks/accent'
import { getBareIconStyle } from '@/blocks/brand-icon-style'
import { getAllBlockMeta } from '@/blocks/registry'
import type { ModuleTag } from '@/blocks/types'
Expand Down Expand Up @@ -224,27 +224,37 @@ function computeActions(services: readonly ServiceInfo[], signals: Signals): Act
return [...integrations, ...prompts]
}

/**
* Integrations pinned to the first paint. Named by block type so the mark comes
* from the same registry every other surface reads, rather than a second copy
* imported here that could drift from the block's own icon.
*/
const INITIAL_INTEGRATIONS = [
{ blockType: 'slack', slug: 'slack', name: 'Slack' },
{ blockType: 'gmail', slug: 'gmail', name: 'Gmail' },
] as const

/**
* Initial actions rendered on first paint, before OAuth/credentials queries
* resolve. For users with no connections this is also the final result, so the
* section never flashes. Users with existing connections briefly see this
* before the personalized recompute replaces it.
*/
const INITIAL_ACTIONS: Action[] = [
{
kind: 'integration',
id: 'integrate-slack',
label: 'Integrate with Slack',
icon: SlackIcon,
slug: 'slack',
},
{
kind: 'integration',
id: 'integrate-gmail',
label: 'Integrate with Gmail',
icon: GmailIcon,
slug: 'gmail',
},
...INITIAL_INTEGRATIONS.flatMap<Action>(({ blockType, slug, name }) => {
const icon = getBlockTileIcon(blockType)
return icon
? [
{
kind: 'integration',
id: `integrate-${slug}`,
label: `Integrate with ${name}`,
icon,
slug,
},
]
: []
}),
toPromptAction(TABLE_STARTERS[0]),
...CANDIDATES.filter((c) => c.blockType === 'github' && c.featured)
.slice(0, 1)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,12 @@ import { useQueryState } from 'nuqs'
import { HEADER_ACTION_CLUSTER, PAGE_HEADER_BAR } from '@/components/page-header-bar'
import { isChatEnabled } from '@/lib/core/config/env-flags'
import {
blockTypeToIconMap,
type Integration,
resolveCredentialDisplay,
resolveOAuthServiceForIntegration,
} from '@/lib/integrations'
import { credentialProviderMatchesService } from '@/lib/oauth'
import { ConnectOAuthModal } from '@/app/workspace/[workspaceId]/components/connect-oauth-modal'
import { RESOURCE_TILE_BASE } from '@/app/workspace/[workspaceId]/components/resource-tile'
import { IntegrationSkillsSection } from '@/app/workspace/[workspaceId]/integrations/[block]/integration-skills-section'
import { connectParam } from '@/app/workspace/[workspaceId]/integrations/[block]/search-params'
import {
Expand All @@ -34,7 +32,7 @@ import {
SettingsResourceRow,
} from '@/app/workspace/[workspaceId]/settings/components/settings-resource-row'
import { SettingsSection } from '@/app/workspace/[workspaceId]/settings/components/settings-section/settings-section'
import { getTileIconColorClass } from '@/blocks/icon-color'
import { getBlockTileIcon } from '@/blocks/accent'
import { storeCuratedPrompt } from '@/blocks/integration-matcher'
import {
getSuggestedSkillsForBlock,
Expand Down Expand Up @@ -64,7 +62,6 @@ export function IntegrationBlockDetail({ integration, workspaceId }: Integration
useOAuthReturnRouter()
const router = useRouter()
const [connectMode, setConnectMode] = useQueryState(connectParam.key, connectParam.parser)
const Icon = blockTypeToIconMap[integration.type]
const matchingTemplates = getTemplatesForBlock(integration.type)
const suggestedSkills = getSuggestedSkillsForBlock(integration.type)
const oauthService = resolveOAuthServiceForIntegration(integration)
Expand Down Expand Up @@ -233,16 +230,10 @@ export function IntegrationBlockDetail({ integration, workspaceId }: Integration
>
<div className='mx-auto flex max-w-[48rem] flex-col gap-7 pb-3'>
<div className='flex flex-col gap-3'>
{Icon ? (
<IntegrationTile blockType={integration.type} icon={Icon} />
) : (
<div
className={cn(RESOURCE_TILE_BASE, getTileIconColorClass(integration.bgColor))}
style={{ background: integration.bgColor }}
>
{integration.name.charAt(0)}
</div>
)}
<IntegrationTile
blockType={integration.type}
fallbackLabel={integration.name.charAt(0)}
/>
<div className='flex flex-col gap-1'>
<h1 className='text-[var(--text-body)] text-lg'>{integration.name}</h1>
<p className='text-[var(--text-muted)] text-md'>{integration.description}</p>
Expand All @@ -255,7 +246,7 @@ export function IntegrationBlockDetail({ integration, workspaceId }: Integration
<SettingsResourceRow
key={credential.id}
iconVariant='custom'
icon={Icon && <IntegrationTile blockType={integration.type} icon={Icon} />}
icon={<IntegrationTile blockType={integration.type} />}
title={credential.displayName}
description={
credential.description || resolveCredentialDisplay(credential).subtitle
Expand Down Expand Up @@ -374,8 +365,7 @@ function TemplateIcons({ blockTypes }: TemplateIconsProps) {
return (
<span aria-hidden className='flex items-center'>
{blockTypes.map((bt, idx) => {
const ToolIcon = blockTypeToIconMap[bt]
if (!ToolIcon) return null
if (!getBlockTileIcon(bt)) return null
const z = TEMPLATE_TILE_Z[idx]
if (!z) return null
const isTrailing = idx > 0
Expand All @@ -389,7 +379,7 @@ function TemplateIcons({ blockTypes }: TemplateIconsProps) {
'outline outline-2 outline-[var(--bg)] transition-[outline-color] duration-150 group-hover:outline-[var(--surface-active)]'
)}
>
<IntegrationTile blockType={bt} icon={ToolIcon} />
<IntegrationTile blockType={bt} />
</span>
)
})}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
RESOURCE_TILE_PLAIN,
} from '@/app/workspace/[workspaceId]/components/resource-tile'
import { getBlock } from '@/blocks'
import { getBlockTileIcon } from '@/blocks/accent'
import { getTileIconColorClass } from '@/blocks/icon-color'

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

interface IntegrationTileProps {
blockType: string
icon: ComponentType<{ className?: string }>
/**
* Overrides the block's registered mark. Only for a tile whose identity is
* not the block itself — a credential issued by a family service account
* wears the family's corporate mark. Everything else takes the registry's,
* so the tile cannot end up with its fill and its icon from two sources.
*/
icon?: ComponentType<{ className?: string }>
/** Drawn when neither the override nor the registry supplies a mark. */
fallbackLabel?: string
framed?: boolean
}

Expand All @@ -68,27 +77,37 @@ interface IntegrationTileProps {
* is a 36px tile used in list rows and headers; the framed variant adds an
* outer 44px halo used inside the showcase grid.
*/
export function IntegrationTile({ blockType, icon: Icon, framed = false }: IntegrationTileProps) {
export function IntegrationTile({
blockType,
icon,
fallbackLabel,
framed = false,
}: IntegrationTileProps) {
const brandBg = resolveBrandTileBg(blockType)
const Icon = icon ?? getBlockTileIcon(blockType)
const contentClass = getTileIconColorClass(brandBg)

if (!framed) {
return (
<div
className={cn(RESOURCE_TILE_BASE, RESOURCE_TILE_PLAIN)}
className={cn(RESOURCE_TILE_BASE, RESOURCE_TILE_PLAIN, !Icon && contentClass)}
style={brandBg ? { background: brandBg } : undefined}
>
<Icon className={getTileIconColorClass(brandBg)} />
{Icon ? <Icon className={contentClass} /> : fallbackLabel}
</div>
)
}

return (
<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)]'>
<div
className='flex size-full items-center justify-center rounded-[9px] border border-[var(--border-1)] bg-[var(--bg)]'
className={cn(
'flex size-full items-center justify-center rounded-[9px] border border-[var(--border-1)] bg-[var(--bg)]',
!Icon && contentClass
)}
style={brandBg ? { background: brandBg } : undefined}
>
<Icon className={cn('size-6', getTileIconColorClass(brandBg))} />
{Icon ? <Icon className={cn('size-6', contentClass)} /> : fallbackLabel}
</div>
</div>
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import {
ChipInput,
ChipLink,
ChipTextarea,
cn,
Send,
toast,
} from '@sim/emcn'
Expand All @@ -28,10 +27,6 @@ import {
UnsavedChangesModal,
useCredentialDetailForm,
} from '@/app/workspace/[workspaceId]/components/credential-detail'
import {
RESOURCE_TILE_BASE,
RESOURCE_TILE_PLAIN,
} from '@/app/workspace/[workspaceId]/components/resource-tile'
import {
ConnectServiceAccountModal,
type ServiceAccountProviderId,
Expand Down Expand Up @@ -244,15 +239,11 @@ export function ConnectedCredentialDetail({
<CredentialDetailLayout back={back} actions={actions}>
<CredentialDetailHeading
leading={
display?.icon ? (
<IntegrationTile blockType={integrationBlockType} icon={display.icon} />
) : (
<div className={cn(RESOURCE_TILE_BASE, RESOURCE_TILE_PLAIN)}>
<span className='text-[var(--text-tertiary)] text-small'>
{resolveProviderLabel(credential.providerId).slice(0, 1) || '?'}
</span>
</div>
)
<IntegrationTile
blockType={integrationBlockType}
icon={display?.icon ?? undefined}
fallbackLabel={resolveProviderLabel(credential.providerId).slice(0, 1) || '?'}
/>
}
title={headingTitle}
subtitle={display?.detailSubtitle ?? 'Connected service'}
Expand Down
Loading
Loading