Skip to content

Commit 5be35b5

Browse files
improvement(slack): merge slack_v2 auth into one credential picker for accounts and custom bots (#5800)
* improvement(slack): merge slack_v2 auth into one credential picker for accounts and custom bots * improvement(slack): label the Sim app group in the merged credential picker * improvement(slack): move provider-specific picker copy and modal dispatch out of the credential selector
1 parent 7db9108 commit 5be35b5

10 files changed

Lines changed: 255 additions & 265 deletions

File tree

apps/sim/app/workspace/[workspaceId]/integrations/components/connect-service-account-modal/connect-service-account-modal.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,7 @@ export function ConnectServiceAccountModal({
178178
credentialId={credentialId}
179179
initialDisplayName={credentialDisplayName}
180180
initialDescription={credentialDescription}
181+
onCreated={onCreated}
181182
/>
182183
)
183184
}

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/credential-selector/credential-selector.tsx

Lines changed: 98 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
'use client'
22

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

106104
const credentialKind = subBlock.credentialKind
105+
const isMergedKinds = credentialKind === 'any'
107106

108107
const credentials = useMemo(() => {
109-
// A custom-bot or service-account picker lists only the reusable
110-
// service-account credentials, including in trigger mode.
111-
if (credentialKind === 'custom-bot' || credentialKind === 'service-account') {
108+
// A service-account picker lists only the reusable service-account
109+
// credentials, including in trigger mode. A merged ('any') picker lists
110+
// OAuth accounts and service accounts together.
111+
if (credentialKind === 'service-account') {
112112
return rawCredentials.filter((cred) => cred.type === 'service_account')
113113
}
114+
if (isMergedKinds) {
115+
return rawCredentials
116+
}
114117
return isTriggerMode && !subBlock.allowServiceAccounts
115118
? rawCredentials.filter((cred) => cred.type !== 'service_account')
116119
: rawCredentials
117-
}, [rawCredentials, isTriggerMode, credentialKind, subBlock.allowServiceAccounts])
120+
}, [rawCredentials, isTriggerMode, credentialKind, isMergedKinds, subBlock.allowServiceAccounts])
118121

119-
// Resolved service-account provider metadata for the token-paste connect
120-
// modal. Gated on `credentialKind` and using the non-throwing lookup so it
121-
// never runs (or throws) for the OAuth / custom-bot pickers.
122+
// Resolved service-account provider metadata for the setup modal. Gated on
123+
// `credentialKind` and using the non-throwing lookup so it never runs (or
124+
// throws) for plain OAuth pickers.
122125
const serviceAccountService = useMemo(
123-
() => (credentialKind === 'service-account' ? getServiceConfigByServiceId(serviceId) : null),
124-
[credentialKind, serviceId]
126+
() =>
127+
credentialKind === 'service-account' || isMergedKinds
128+
? getServiceConfigByServiceId(serviceId)
129+
: null,
130+
[credentialKind, isMergedKinds, serviceId]
125131
)
126132

127133
const selectedCredential = useMemo(
@@ -198,12 +204,8 @@ export function CredentialSelector({
198204
)
199205

200206
const handleAddCredential = useCallback(() => {
201-
if (credentialKind === 'custom-bot') {
202-
setShowSlackBotModal(true)
203-
return
204-
}
205207
if (credentialKind === 'service-account') {
206-
setShowServiceAccountModal(true)
208+
setShowSetupModal(true)
207209
return
208210
}
209211
setShowConnectModal(true)
@@ -239,6 +241,7 @@ export function CredentialSelector({
239241
const oauthCredentials = allWorkspaceCredentials.filter((c) => c.type === 'oauth')
240242
return oauthCredentials.map((cred) => ({ label: cred.displayName, value: cred.id }))
241243
}
244+
if (isMergedKinds) return []
242245

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

249252
options.push({
250253
label:
251-
credentialKind === 'custom-bot'
252-
? credentials.length > 0
253-
? 'Connect another custom bot'
254-
: 'Set up a custom bot'
255-
: credentialKind === 'service-account'
256-
? credentials.length > 0
254+
credentialKind === 'service-account'
255+
? (subBlock.credentialLabels?.serviceAccountConnect ??
256+
(credentials.length > 0
257257
? `Add another ${getProviderName(provider)} key`
258-
: `Add ${getProviderName(provider)} key`
259-
: credentials.length > 0
260-
? `Connect another ${getProviderName(provider)} account`
261-
: `Connect ${getProviderName(provider)} account`,
258+
: `Add ${getProviderName(provider)} key`))
259+
: credentials.length > 0
260+
? `Connect another ${getProviderName(provider)} account`
261+
: `Connect ${getProviderName(provider)} account`,
262262
value: '__connect_account__',
263263
iconElement: <ExternalLink className='size-3' />,
264264
})
265265

266266
return options
267267
}, [
268268
isAllCredentials,
269+
isMergedKinds,
269270
allWorkspaceCredentials,
270271
credentials,
271272
credentialKind,
273+
subBlock.credentialLabels,
274+
provider,
275+
getProviderIcon,
276+
getProviderName,
277+
])
278+
279+
const comboboxGroups = useMemo<ComboboxOptionGroup[] | undefined>(() => {
280+
if (!isMergedKinds) return undefined
281+
282+
const labels = subBlock.credentialLabels
283+
const toOption = (cred: (typeof credentials)[number]) => ({
284+
label: cred.name,
285+
value: cred.id,
286+
iconElement: getProviderIcon((cred.provider ?? provider) as OAuthProvider),
287+
})
288+
289+
return [
290+
{
291+
section: labels?.oauthGroup ?? `${getProviderName(provider)} accounts`,
292+
items: [
293+
...credentials.filter((c) => c.type !== 'service_account').map(toOption),
294+
{
295+
label: labels?.oauthConnect ?? `Connect ${getProviderName(provider)} account`,
296+
value: '__connect_account__',
297+
iconElement: <ExternalLink className='size-3' />,
298+
},
299+
],
300+
},
301+
{
302+
section: labels?.serviceAccountGroup ?? 'Service accounts',
303+
items: [
304+
...credentials.filter((c) => c.type === 'service_account').map(toOption),
305+
{
306+
label: labels?.serviceAccountConnect ?? `Add ${getProviderName(provider)} key`,
307+
value: '__connect_service_account__',
308+
iconElement: <ExternalLink className='size-3' />,
309+
},
310+
],
311+
},
312+
]
313+
}, [
314+
isMergedKinds,
315+
subBlock.credentialLabels,
316+
credentials,
272317
provider,
273318
getProviderIcon,
274319
getProviderName,
@@ -320,9 +365,17 @@ export function CredentialSelector({
320365
const handleComboboxChange = useCallback(
321366
(value: string) => {
322367
if (value === '__connect_account__') {
368+
if (isMergedKinds) {
369+
setShowConnectModal(true)
370+
return
371+
}
323372
handleAddCredential()
324373
return
325374
}
375+
if (value === '__connect_service_account__') {
376+
setShowSetupModal(true)
377+
return
378+
}
326379

327380
const matchedCred = (
328381
isAllCredentials ? allWorkspaceCredentials.filter((c) => c.type === 'oauth') : credentials
@@ -335,13 +388,21 @@ export function CredentialSelector({
335388
setIsEditing(true)
336389
setEditingValue(value)
337390
},
338-
[isAllCredentials, allWorkspaceCredentials, credentials, handleAddCredential, handleSelect]
391+
[
392+
isAllCredentials,
393+
isMergedKinds,
394+
allWorkspaceCredentials,
395+
credentials,
396+
handleAddCredential,
397+
handleSelect,
398+
]
339399
)
340400

341401
return (
342402
<div>
343403
<Combobox
344404
options={comboboxOptions}
405+
groups={comboboxGroups}
345406
value={displayValue}
346407
selectedValue={selectedId}
347408
onChange={handleComboboxChange}
@@ -350,8 +411,10 @@ export function CredentialSelector({
350411
hasDependencies && !depsSatisfied ? 'Fill in required fields above first' : label
351412
}
352413
disabled={effectiveDisabled}
353-
editable={true}
354-
filterOptions={true}
414+
editable={!isMergedKinds}
415+
filterOptions={!isMergedKinds}
416+
searchable={isMergedKinds}
417+
searchPlaceholder='Search credentials...'
355418
isLoading={credentialsLoading}
356419
overlayContent={overlayContent}
357420
className={overlayContent ? 'pl-7' : ''}
@@ -371,7 +434,7 @@ export function CredentialSelector({
371434
workflowId: activeWorkflowId || '',
372435
displayName: selectedCredential?.name ?? getProviderName(provider),
373436
providerId: effectiveProviderId,
374-
preCount: credentials.length,
437+
preCount: credentials.filter((c) => c.type !== 'service_account').length,
375438
workspaceId,
376439
requestedAt: Date.now(),
377440
})
@@ -417,22 +480,10 @@ export function CredentialSelector({
417480
/>
418481
)}
419482

420-
{showSlackBotModal && (
421-
<ConnectSlackBotModal
422-
open={showSlackBotModal}
423-
onOpenChange={setShowSlackBotModal}
424-
workspaceId={workspaceId}
425-
onCreated={(newCredentialId) => {
426-
setStoreValue(newCredentialId)
427-
refetchCredentials()
428-
}}
429-
/>
430-
)}
431-
432-
{showServiceAccountModal && serviceAccountService?.serviceAccountProviderId && (
483+
{showSetupModal && serviceAccountService?.serviceAccountProviderId && (
433484
<ConnectServiceAccountModal
434-
open={showServiceAccountModal}
435-
onOpenChange={setShowServiceAccountModal}
485+
open={showSetupModal}
486+
onOpenChange={setShowSetupModal}
436487
workspaceId={workspaceId}
437488
serviceAccountProviderId={
438489
serviceAccountService.serviceAccountProviderId as ServiceAccountProviderId

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/sub-block.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,11 +56,13 @@ import { useWebhookManagement } from '@/hooks/use-webhook-management'
5656

5757
const SLACK_OVERRIDES: SelectorOverrides = {
5858
transformContext: (context, deps) => {
59+
// v1 gates on authMethod (raw bot token vs OAuth); v2 has one merged
60+
// credential field for actions and customBotCredential for triggers.
5961
const authMethod = deps.authMethod as string
6062
const oauthCredential =
6163
authMethod === 'bot_token'
62-
? String(deps.customBotCredential ?? deps.botToken ?? '')
63-
: String(deps.credential ?? deps.customBotCredential ?? deps.triggerCredentials ?? '')
64+
? String(deps.botToken ?? '')
65+
: String(deps.credential ?? deps.customBotCredential ?? '')
6466
return { ...context, oauthCredential }
6567
},
6668
}

apps/sim/blocks/blocks/slack.ts

Lines changed: 47 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -2627,48 +2627,51 @@ export const SlackBlockMeta = {
26272627
],
26282628
} as const satisfies BlockMeta
26292629

2630-
/**
2631-
* Custom Bot picker used by slack_v2 in place of v1's raw bot-token field — a
2632-
* canonical basic/advanced pair (dropdown + manual credential-ID paste),
2633-
* mirroring the OAuth `credential`/`manualCredential` pair.
2634-
*/
2635-
const SLACK_CUSTOM_BOT_SUBBLOCKS: SubBlockConfig[] = [
2636-
{
2637-
id: 'customBotCredential',
2638-
title: 'Slack Bot',
2639-
type: 'oauth-input',
2640-
canonicalParamId: 'botCredential',
2641-
mode: 'basic',
2642-
serviceId: 'slack',
2643-
credentialKind: 'custom-bot',
2644-
requiredScopes: getScopesForService('slack'),
2645-
placeholder: 'Select a connected bot',
2646-
dependsOn: ['authMethod'],
2647-
condition: { field: 'authMethod', value: 'bot_token' },
2648-
required: true,
2649-
},
2650-
{
2651-
id: 'manualCustomBotCredential',
2652-
title: 'Bot Credential ID',
2653-
type: 'short-input',
2654-
canonicalParamId: 'botCredential',
2655-
mode: 'advanced',
2656-
placeholder: 'Enter bot credential ID',
2657-
dependsOn: ['authMethod'],
2658-
condition: { field: 'authMethod', value: 'bot_token' },
2659-
required: true,
2660-
},
2661-
]
2662-
26632630
const SLACK_WEBHOOK_TRIGGER_SUBBLOCK_IDS = new Set(
26642631
getTrigger('slack_webhook').subBlocks.map((sb) => sb.id)
26652632
)
26662633

2634+
/**
2635+
* Adapts a v1 subblock for slack_v2's merged credential picker: fields gated on
2636+
* the removed `authMethod` dropdown now depend on the single `credential` field.
2637+
*/
2638+
function adaptSubBlockForV2(sb: SubBlockConfig): SubBlockConfig {
2639+
const { dependsOn, condition, ...rest } = sb
2640+
if (sb.id === 'credential') {
2641+
return {
2642+
...rest,
2643+
credentialKind: 'any',
2644+
placeholder: 'Select Slack account or bot',
2645+
credentialLabels: {
2646+
oauthGroup: 'Sim app',
2647+
oauthConnect: 'Connect the Sim app',
2648+
serviceAccountGroup: 'Custom bots',
2649+
serviceAccountConnect: 'Set up a custom bot',
2650+
},
2651+
}
2652+
}
2653+
if (sb.id === 'manualCredential') {
2654+
return { ...rest, placeholder: 'Enter credential ID' }
2655+
}
2656+
if (dependsOn && !Array.isArray(dependsOn) && dependsOn.all?.includes('authMethod')) {
2657+
return { ...sb, dependsOn: ['credential'] }
2658+
}
2659+
return sb
2660+
}
2661+
2662+
const {
2663+
authMethod: _authMethod,
2664+
botToken: _botToken,
2665+
botCredential: _botCredential,
2666+
...slackV2Inputs
2667+
} = SlackBlock.inputs
2668+
26672669
/**
26682670
* slack_v2 — the go-forward Slack action block. Identical operations, tools, and
2669-
* outputs to v1 (shared by reference), but the "Custom Bot" auth method selects
2670-
* a reusable bot credential set up once, instead of pasting a raw token. Also
2671-
* hosts the redesigned slack_oauth trigger (v1 keeps the legacy slack_webhook).
2671+
* outputs to v1 (shared by reference), but auth is a single credential picker
2672+
* listing Sim OAuth accounts and reusable custom bots together — the credential's
2673+
* kind is resolved server-side, so no auth-method choice is needed. Also hosts
2674+
* the redesigned slack_oauth trigger (v1 keeps the legacy slack_webhook).
26722675
*/
26732676
export const SlackV2Block: BlockConfig<SlackResponse> = {
26742677
...SlackBlock,
@@ -2683,13 +2686,18 @@ export const SlackV2Block: BlockConfig<SlackResponse> = {
26832686
...SlackBlock.subBlocks.flatMap((sb) => {
26842687
// Drop the legacy paste-secret trigger config (v1 hosts slack_webhook)
26852688
// and v1's raw bot-token auth field — the trigger set includes an
2686-
// id-colliding 'botToken', so the set check covers both.
2689+
// id-colliding 'botToken', so the set check covers both. The authMethod
2690+
// dropdown is gone: the merged credential picker covers both auth kinds.
26872691
if (SLACK_WEBHOOK_TRIGGER_SUBBLOCK_IDS.has(sb.id)) return []
2688-
if (sb.id === 'authMethod') return [sb, ...SLACK_CUSTOM_BOT_SUBBLOCKS]
2689-
return [sb]
2692+
if (sb.id === 'authMethod') return []
2693+
return [adaptSubBlockForV2(sb)]
26902694
}),
26912695
...getTrigger('slack_oauth').subBlocks,
26922696
],
2697+
inputs: {
2698+
...slackV2Inputs,
2699+
oauthCredential: { type: 'string', description: 'Slack credential (OAuth account or bot)' },
2700+
},
26932701
triggers: {
26942702
enabled: true,
26952703
available: ['slack_oauth'],

0 commit comments

Comments
 (0)