Skip to content
Merged
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 @@ -178,6 +178,7 @@ export function ConnectServiceAccountModal({
credentialId={credentialId}
initialDisplayName={credentialDisplayName}
initialDescription={credentialDescription}
onCreated={onCreated}
/>
)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
'use client'

import { useCallback, useMemo, useState } from 'react'
import { Button, Combobox } from '@sim/emcn'
import { Button, Combobox, type ComboboxOptionGroup } from '@sim/emcn'
import { ExternalLink, KeyRound } from 'lucide-react'
import { useParams } from 'next/navigation'
import { consumeOAuthReturnContext, writeOAuthReturnContext } from '@/lib/credentials/client-state'
Expand All @@ -18,7 +18,6 @@ import {
ConnectServiceAccountModal,
type ServiceAccountProviderId,
} from '@/app/workspace/[workspaceId]/integrations/components/connect-service-account-modal'
import { ConnectSlackBotModal } from '@/app/workspace/[workspaceId]/integrations/components/connect-slack-bot-modal/connect-slack-bot-modal'
import { formatDisplayText } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/formatted-text'
import { getWorkflowSearchLabelHighlight } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/workflow-search-highlight'
import { useDependsOnGate } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-depends-on-gate'
Expand Down Expand Up @@ -53,8 +52,7 @@ export function CredentialSelector({
const workspaceId = (params?.workspaceId as string) || ''
const [showConnectModal, setShowConnectModal] = useState(false)
const [showOAuthModal, setShowOAuthModal] = useState(false)
const [showSlackBotModal, setShowSlackBotModal] = useState(false)
const [showServiceAccountModal, setShowServiceAccountModal] = useState(false)
const [showSetupModal, setShowSetupModal] = useState(false)
const [editingValue, setEditingValue] = useState('')
const [isEditing, setIsEditing] = useState(false)
const activeWorkflowId = useWorkflowRegistry((state) => state.activeWorkflowId)
Expand Down Expand Up @@ -104,24 +102,32 @@ export function CredentialSelector({
const credentialsLoading = isAllCredentials ? allCredentialsLoading : oauthCredentialsLoading

const credentialKind = subBlock.credentialKind
const isMergedKinds = credentialKind === 'any'

const credentials = useMemo(() => {
// A custom-bot or service-account picker lists only the reusable
// service-account credentials, including in trigger mode.
if (credentialKind === 'custom-bot' || credentialKind === 'service-account') {
// A service-account picker lists only the reusable service-account
// credentials, including in trigger mode. A merged ('any') picker lists
// OAuth accounts and service accounts together.
if (credentialKind === 'service-account') {
return rawCredentials.filter((cred) => cred.type === 'service_account')
}
if (isMergedKinds) {
return rawCredentials
}
return isTriggerMode && !subBlock.allowServiceAccounts
? rawCredentials.filter((cred) => cred.type !== 'service_account')
: rawCredentials
}, [rawCredentials, isTriggerMode, credentialKind, subBlock.allowServiceAccounts])
}, [rawCredentials, isTriggerMode, credentialKind, isMergedKinds, subBlock.allowServiceAccounts])

// Resolved service-account provider metadata for the token-paste connect
// modal. Gated on `credentialKind` and using the non-throwing lookup so it
// never runs (or throws) for the OAuth / custom-bot pickers.
// Resolved service-account provider metadata for the setup modal. Gated on
// `credentialKind` and using the non-throwing lookup so it never runs (or
// throws) for plain OAuth pickers.
const serviceAccountService = useMemo(
() => (credentialKind === 'service-account' ? getServiceConfigByServiceId(serviceId) : null),
[credentialKind, serviceId]
() =>
credentialKind === 'service-account' || isMergedKinds
? getServiceConfigByServiceId(serviceId)
: null,
[credentialKind, isMergedKinds, serviceId]
)

const selectedCredential = useMemo(
Expand Down Expand Up @@ -198,12 +204,8 @@ export function CredentialSelector({
)

const handleAddCredential = useCallback(() => {
if (credentialKind === 'custom-bot') {
setShowSlackBotModal(true)
return
}
if (credentialKind === 'service-account') {
setShowServiceAccountModal(true)
setShowSetupModal(true)
return
}
setShowConnectModal(true)
Expand Down Expand Up @@ -239,6 +241,7 @@ export function CredentialSelector({
const oauthCredentials = allWorkspaceCredentials.filter((c) => c.type === 'oauth')
return oauthCredentials.map((cred) => ({ label: cred.displayName, value: cred.id }))
}
if (isMergedKinds) return []

const options = credentials.map((cred) => ({
label: cred.name,
Expand All @@ -248,27 +251,69 @@ export function CredentialSelector({

options.push({
label:
credentialKind === 'custom-bot'
? credentials.length > 0
? 'Connect another custom bot'
: 'Set up a custom bot'
: credentialKind === 'service-account'
? credentials.length > 0
credentialKind === 'service-account'
? (subBlock.credentialLabels?.serviceAccountConnect ??
(credentials.length > 0
? `Add another ${getProviderName(provider)} key`
: `Add ${getProviderName(provider)} key`
: credentials.length > 0
? `Connect another ${getProviderName(provider)} account`
: `Connect ${getProviderName(provider)} account`,
: `Add ${getProviderName(provider)} key`))
: credentials.length > 0
? `Connect another ${getProviderName(provider)} account`
: `Connect ${getProviderName(provider)} account`,
value: '__connect_account__',
iconElement: <ExternalLink className='size-3' />,
})

return options
}, [
isAllCredentials,
isMergedKinds,
allWorkspaceCredentials,
credentials,
credentialKind,
subBlock.credentialLabels,
provider,
getProviderIcon,
getProviderName,
])

const comboboxGroups = useMemo<ComboboxOptionGroup[] | undefined>(() => {
if (!isMergedKinds) return undefined

const labels = subBlock.credentialLabels
const toOption = (cred: (typeof credentials)[number]) => ({
label: cred.name,
value: cred.id,
iconElement: getProviderIcon((cred.provider ?? provider) as OAuthProvider),
})

return [
{
section: labels?.oauthGroup ?? `${getProviderName(provider)} accounts`,
items: [
...credentials.filter((c) => c.type !== 'service_account').map(toOption),
{
label: labels?.oauthConnect ?? `Connect ${getProviderName(provider)} account`,
value: '__connect_account__',
iconElement: <ExternalLink className='size-3' />,
},
],
},
{
section: labels?.serviceAccountGroup ?? 'Service accounts',
items: [
...credentials.filter((c) => c.type === 'service_account').map(toOption),
{
label: labels?.serviceAccountConnect ?? `Add ${getProviderName(provider)} key`,
value: '__connect_service_account__',
iconElement: <ExternalLink className='size-3' />,
},
],
},
]
}, [
isMergedKinds,
subBlock.credentialLabels,
credentials,
provider,
getProviderIcon,
getProviderName,
Expand Down Expand Up @@ -320,9 +365,17 @@ export function CredentialSelector({
const handleComboboxChange = useCallback(
(value: string) => {
if (value === '__connect_account__') {
if (isMergedKinds) {
setShowConnectModal(true)
return
}
handleAddCredential()
return
}
if (value === '__connect_service_account__') {
setShowSetupModal(true)
return
}

const matchedCred = (
isAllCredentials ? allWorkspaceCredentials.filter((c) => c.type === 'oauth') : credentials
Expand All @@ -335,13 +388,21 @@ export function CredentialSelector({
setIsEditing(true)
setEditingValue(value)
},
[isAllCredentials, allWorkspaceCredentials, credentials, handleAddCredential, handleSelect]
[
isAllCredentials,
isMergedKinds,
allWorkspaceCredentials,
credentials,
handleAddCredential,
handleSelect,
]
)

return (
<div>
<Combobox
options={comboboxOptions}
groups={comboboxGroups}
value={displayValue}
selectedValue={selectedId}
onChange={handleComboboxChange}
Expand All @@ -350,8 +411,10 @@ export function CredentialSelector({
hasDependencies && !depsSatisfied ? 'Fill in required fields above first' : label
}
disabled={effectiveDisabled}
editable={true}
filterOptions={true}
editable={!isMergedKinds}
filterOptions={!isMergedKinds}
searchable={isMergedKinds}
searchPlaceholder='Search credentials...'
isLoading={credentialsLoading}
overlayContent={overlayContent}
className={overlayContent ? 'pl-7' : ''}
Expand All @@ -371,7 +434,7 @@ export function CredentialSelector({
workflowId: activeWorkflowId || '',
displayName: selectedCredential?.name ?? getProviderName(provider),
providerId: effectiveProviderId,
preCount: credentials.length,
preCount: credentials.filter((c) => c.type !== 'service_account').length,
workspaceId,
requestedAt: Date.now(),
})
Expand Down Expand Up @@ -417,22 +480,10 @@ export function CredentialSelector({
/>
)}

{showSlackBotModal && (
<ConnectSlackBotModal
open={showSlackBotModal}
onOpenChange={setShowSlackBotModal}
workspaceId={workspaceId}
onCreated={(newCredentialId) => {
setStoreValue(newCredentialId)
refetchCredentials()
}}
/>
)}

{showServiceAccountModal && serviceAccountService?.serviceAccountProviderId && (
{showSetupModal && serviceAccountService?.serviceAccountProviderId && (
<ConnectServiceAccountModal
open={showServiceAccountModal}
onOpenChange={setShowServiceAccountModal}
open={showSetupModal}
onOpenChange={setShowSetupModal}
workspaceId={workspaceId}
serviceAccountProviderId={
serviceAccountService.serviceAccountProviderId as ServiceAccountProviderId
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,11 +56,13 @@ import { useWebhookManagement } from '@/hooks/use-webhook-management'

const SLACK_OVERRIDES: SelectorOverrides = {
transformContext: (context, deps) => {
// v1 gates on authMethod (raw bot token vs OAuth); v2 has one merged
// credential field for actions and customBotCredential for triggers.
const authMethod = deps.authMethod as string
const oauthCredential =
authMethod === 'bot_token'
? String(deps.customBotCredential ?? deps.botToken ?? '')
: String(deps.credential ?? deps.customBotCredential ?? deps.triggerCredentials ?? '')
? String(deps.botToken ?? '')
: String(deps.credential ?? deps.customBotCredential ?? '')
Comment thread
TheodoreSpeaks marked this conversation as resolved.
return { ...context, oauthCredential }
},
}
Expand Down
86 changes: 47 additions & 39 deletions apps/sim/blocks/blocks/slack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2627,48 +2627,51 @@ export const SlackBlockMeta = {
],
} as const satisfies BlockMeta

/**
* Custom Bot picker used by slack_v2 in place of v1's raw bot-token field — a
* canonical basic/advanced pair (dropdown + manual credential-ID paste),
* mirroring the OAuth `credential`/`manualCredential` pair.
*/
const SLACK_CUSTOM_BOT_SUBBLOCKS: SubBlockConfig[] = [
{
id: 'customBotCredential',
title: 'Slack Bot',
type: 'oauth-input',
canonicalParamId: 'botCredential',
mode: 'basic',
serviceId: 'slack',
credentialKind: 'custom-bot',
requiredScopes: getScopesForService('slack'),
placeholder: 'Select a connected bot',
dependsOn: ['authMethod'],
condition: { field: 'authMethod', value: 'bot_token' },
required: true,
},
{
id: 'manualCustomBotCredential',
title: 'Bot Credential ID',
type: 'short-input',
canonicalParamId: 'botCredential',
mode: 'advanced',
placeholder: 'Enter bot credential ID',
dependsOn: ['authMethod'],
condition: { field: 'authMethod', value: 'bot_token' },
required: true,
},
]

const SLACK_WEBHOOK_TRIGGER_SUBBLOCK_IDS = new Set(
getTrigger('slack_webhook').subBlocks.map((sb) => sb.id)
)

/**
* Adapts a v1 subblock for slack_v2's merged credential picker: fields gated on
* the removed `authMethod` dropdown now depend on the single `credential` field.
*/
function adaptSubBlockForV2(sb: SubBlockConfig): SubBlockConfig {
const { dependsOn, condition, ...rest } = sb
if (sb.id === 'credential') {
return {
...rest,
credentialKind: 'any',
placeholder: 'Select Slack account or bot',
credentialLabels: {
oauthGroup: 'Sim app',
oauthConnect: 'Connect the Sim app',
serviceAccountGroup: 'Custom bots',
serviceAccountConnect: 'Set up a custom bot',
},
}
}
if (sb.id === 'manualCredential') {
return { ...rest, placeholder: 'Enter credential ID' }
}
if (dependsOn && !Array.isArray(dependsOn) && dependsOn.all?.includes('authMethod')) {
return { ...sb, dependsOn: ['credential'] }
}
return sb
}

const {
authMethod: _authMethod,
botToken: _botToken,
botCredential: _botCredential,
...slackV2Inputs
} = SlackBlock.inputs

/**
* slack_v2 — the go-forward Slack action block. Identical operations, tools, and
* outputs to v1 (shared by reference), but the "Custom Bot" auth method selects
* a reusable bot credential set up once, instead of pasting a raw token. Also
* hosts the redesigned slack_oauth trigger (v1 keeps the legacy slack_webhook).
* outputs to v1 (shared by reference), but auth is a single credential picker
* listing Sim OAuth accounts and reusable custom bots together — the credential's
* kind is resolved server-side, so no auth-method choice is needed. Also hosts
* the redesigned slack_oauth trigger (v1 keeps the legacy slack_webhook).
*/
export const SlackV2Block: BlockConfig<SlackResponse> = {
...SlackBlock,
Expand All @@ -2683,13 +2686,18 @@ export const SlackV2Block: BlockConfig<SlackResponse> = {
...SlackBlock.subBlocks.flatMap((sb) => {
// Drop the legacy paste-secret trigger config (v1 hosts slack_webhook)
// and v1's raw bot-token auth field — the trigger set includes an
// id-colliding 'botToken', so the set check covers both.
// id-colliding 'botToken', so the set check covers both. The authMethod
// dropdown is gone: the merged credential picker covers both auth kinds.
if (SLACK_WEBHOOK_TRIGGER_SUBBLOCK_IDS.has(sb.id)) return []
if (sb.id === 'authMethod') return [sb, ...SLACK_CUSTOM_BOT_SUBBLOCKS]
return [sb]
if (sb.id === 'authMethod') return []
return [adaptSubBlockForV2(sb)]
}),
...getTrigger('slack_oauth').subBlocks,
],
inputs: {
...slackV2Inputs,
oauthCredential: { type: 'string', description: 'Slack credential (OAuth account or bot)' },
},
triggers: {
enabled: true,
available: ['slack_oauth'],
Expand Down
Loading
Loading