diff --git a/apps/sim/app/workspace/[workspaceId]/integrations/components/connect-service-account-modal/connect-service-account-modal.tsx b/apps/sim/app/workspace/[workspaceId]/integrations/components/connect-service-account-modal/connect-service-account-modal.tsx index 016455bf012..bb83405f7f6 100644 --- a/apps/sim/app/workspace/[workspaceId]/integrations/components/connect-service-account-modal/connect-service-account-modal.tsx +++ b/apps/sim/app/workspace/[workspaceId]/integrations/components/connect-service-account-modal/connect-service-account-modal.tsx @@ -178,6 +178,7 @@ export function ConnectServiceAccountModal({ credentialId={credentialId} initialDisplayName={credentialDisplayName} initialDescription={credentialDescription} + onCreated={onCreated} /> ) } diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/credential-selector/credential-selector.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/credential-selector/credential-selector.tsx index c30f39fc074..8611bf7ac72 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/credential-selector/credential-selector.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/credential-selector/credential-selector.tsx @@ -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' @@ -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' @@ -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) @@ -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( @@ -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) @@ -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, @@ -248,17 +251,14 @@ 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: , }) @@ -266,9 +266,54 @@ export function CredentialSelector({ return options }, [ isAllCredentials, + isMergedKinds, allWorkspaceCredentials, credentials, credentialKind, + subBlock.credentialLabels, + provider, + getProviderIcon, + getProviderName, + ]) + + const comboboxGroups = useMemo(() => { + 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: , + }, + ], + }, + { + 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: , + }, + ], + }, + ] + }, [ + isMergedKinds, + subBlock.credentialLabels, + credentials, provider, getProviderIcon, getProviderName, @@ -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 @@ -335,13 +388,21 @@ export function CredentialSelector({ setIsEditing(true) setEditingValue(value) }, - [isAllCredentials, allWorkspaceCredentials, credentials, handleAddCredential, handleSelect] + [ + isAllCredentials, + isMergedKinds, + allWorkspaceCredentials, + credentials, + handleAddCredential, + handleSelect, + ] ) return (
c.type !== 'service_account').length, workspaceId, requestedAt: Date.now(), }) @@ -417,22 +480,10 @@ export function CredentialSelector({ /> )} - {showSlackBotModal && ( - { - setStoreValue(newCredentialId) - refetchCredentials() - }} - /> - )} - - {showServiceAccountModal && serviceAccountService?.serviceAccountProviderId && ( + {showSetupModal && serviceAccountService?.serviceAccountProviderId && ( { + // 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 ?? '') return { ...context, oauthCredential } }, } diff --git a/apps/sim/blocks/blocks/slack.ts b/apps/sim/blocks/blocks/slack.ts index b0a69183295..1792db96c52 100644 --- a/apps/sim/blocks/blocks/slack.ts +++ b/apps/sim/blocks/blocks/slack.ts @@ -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 = { ...SlackBlock, @@ -2683,13 +2686,18 @@ export const SlackV2Block: BlockConfig = { ...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'], diff --git a/apps/sim/blocks/types.ts b/apps/sim/blocks/types.ts index 3ed75ecefde..1abe2b8e379 100644 --- a/apps/sim/blocks/types.ts +++ b/apps/sim/blocks/types.ts @@ -367,14 +367,24 @@ export interface SubBlockConfig { serviceId?: string requiredScopes?: string[] /** - * Narrows an `oauth-input` selector to a specific credential kind. `'custom-bot'` - * lists only reusable custom Slack bot credentials (service-account type) and its - * connect row opens the custom-bot setup modal instead of the OAuth flow. - * `'service-account'` is the generic equivalent for a no-OAuth provider: it lists - * only service-account credentials and its connect row opens the descriptor-driven - * token-paste modal (`ConnectServiceAccountModal`). + * Narrows an `oauth-input` selector to a specific credential kind. + * `'service-account'` lists only service-account credentials; its connect row + * opens the provider's setup modal (resolved from the service-account setup + * registry — a bespoke wizard when registered, the generic token-paste modal + * otherwise). `'any'` lists OAuth accounts and service accounts together in a + * grouped dropdown with a connect action for each kind. */ - credentialKind?: 'custom-bot' | 'service-account' + credentialKind?: 'service-account' | 'any' + /** + * Overrides the credential picker's section and connect-row copy. Unset keys + * fall back to generic provider-derived labels. + */ + credentialLabels?: { + oauthGroup?: string + oauthConnect?: string + serviceAccountGroup?: string + serviceAccountConnect?: string + } /** * Opts a trigger-mode `oauth-input` selector into listing service-account * credentials, which are otherwise excluded in trigger mode. Set only when the diff --git a/apps/sim/lib/webhooks/deploy.test.ts b/apps/sim/lib/webhooks/deploy.test.ts index c5a96499b86..8409705850d 100644 --- a/apps/sim/lib/webhooks/deploy.test.ts +++ b/apps/sim/lib/webhooks/deploy.test.ts @@ -62,6 +62,23 @@ const tableTrigger = trigger([ { id: 'manualTableId', mode: 'trigger-advanced', canonicalParamId: 'tableId', required: true }, ]) +const slackTrigger = trigger([ + { id: 'eventType', mode: 'trigger', required: true }, + { + id: 'customBotCredential', + mode: 'trigger', + canonicalParamId: 'botCredential', + serviceId: 'slack', + required: true, + }, + { + id: 'manualBotCredential', + mode: 'trigger-advanced', + canonicalParamId: 'botCredential', + required: true, + }, +]) + function makeBlock( type: string, subBlockValues: Record, @@ -155,6 +172,28 @@ describe('buildProviderConfig canonical collapse', () => { const { providerConfig } = buildProviderConfig(block, 'table_new_row', tableTrigger) expect(providerConfig.tableId).toBe('ACTIVE') }) + + it('collapses the slack bot credential pair under botCredential for the routing branch', () => { + const block = makeBlock('slack_v2', { + eventType: 'message', + customBotCredential: 'cred_bot_1', + }) + const result = buildProviderConfig(block, 'slack_oauth', slackTrigger) + + expect(result.providerConfig.botCredential).toBe('cred_bot_1') + expect(result.providerConfig.eventType).toBe('message') + // The slack trigger has no generic triggerCredentials field — the routing + // branch resolves botCredential itself. + expect(result.credentialReference).toBeUndefined() + expect(result.credentialServiceId).toBeUndefined() + }) + + it('reports a missing required slack bot credential as a missing field', () => { + const block = makeBlock('slack_v2', { eventType: 'message' }) + const result = buildProviderConfig(block, 'slack_oauth', slackTrigger) + + expect(result.missingFields.length).toBeGreaterThan(0) + }) }) describe('resolveTriggerCredentialId', () => { diff --git a/apps/sim/lib/webhooks/deploy.ts b/apps/sim/lib/webhooks/deploy.ts index d667f0391d5..1678159e2f7 100644 --- a/apps/sim/lib/webhooks/deploy.ts +++ b/apps/sim/lib/webhooks/deploy.ts @@ -1,5 +1,5 @@ import { db } from '@sim/db' -import { account, credential, webhook, workflowDeploymentVersion } from '@sim/db/schema' +import { credential, webhook, workflowDeploymentVersion } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { getErrorMessage } from '@sim/utils/errors' import { generateShortId } from '@sim/utils/id' @@ -15,7 +15,6 @@ import { projectDesiredWebhookProviderConfig, } from '@/lib/webhooks/provider-subscriptions' import { getProviderHandler } from '@/lib/webhooks/providers' -import { fetchSlackTeamId } from '@/lib/webhooks/providers/slack' import { prepareStableWebhookRegistrations, type StableDesiredWebhookRegistration, @@ -27,17 +26,12 @@ import { isCanonicalPair, resolveActiveCanonicalValue, } from '@/lib/workflows/subblocks/visibility' -import { - getSlackBotCredential, - refreshAccessTokenIfNeeded, - resolveOAuthAccountId, -} from '@/app/api/auth/oauth/utils' +import { getSlackBotCredential } from '@/app/api/auth/oauth/utils' import { getBlock } from '@/blocks' import type { SubBlockConfig } from '@/blocks/types' import type { BlockState } from '@/stores/workflows/workflow/types' import { getTrigger, isTriggerValid } from '@/triggers' import { SYSTEM_SUBBLOCK_IDS } from '@/triggers/constants' -import { SIM_SUBSCRIBED_EVENTS } from '@/triggers/slack/shared' const logger = createLogger('DeployWebhookSync') @@ -431,106 +425,43 @@ async function resolveWebhookConfigForBlock(input: { let effectivePath: string | null = triggerPath let routingKey: string | null = null if (triggerId === 'slack_oauth') { - const appType = typeof providerConfig.appType === 'string' ? providerConfig.appType : 'custom' - if (appType === 'sim') { - const eventType = - typeof providerConfig.eventType === 'string' ? providerConfig.eventType : null - if (eventType && !SIM_SUBSCRIBED_EVENTS.includes(eventType)) { - return { - success: false, - error: { - message: - 'This event is not available on the Sim Slack app. Use a custom app or choose a supported event.', - status: 400, - }, - } - } - if (!credentialId) { - return { - success: false, - error: { message: 'Select a Slack account for the trigger.', status: 400 }, - } - } - - let tokenOwnerUserId = input.userId - const resolvedAccount = await resolveOAuthAccountId(credentialId) - if (resolvedAccount?.accountId) { - const [owner] = await db - .select({ userId: account.userId }) - .from(account) - .where(eq(account.id, resolvedAccount.accountId)) - .limit(1) - if (owner?.userId) tokenOwnerUserId = owner.userId - } - const botToken = await refreshAccessTokenIfNeeded( - credentialId, - tokenOwnerUserId, - input.requestId - ) - if (!botToken) { - return { - success: false, - error: { - message: 'Could not access the connected Slack account. Reconnect it and try again.', - status: 400, - }, - } - } - try { - const { teamId, userId: botUserId } = await fetchSlackTeamId(botToken) - routingKey = teamId - if (botUserId) providerConfig.bot_user_id = botUserId - } catch (error: unknown) { - logger.error( - `[${input.requestId}] Slack team_id resolution failed for ${input.block.id}`, - error - ) - return { - success: false, - error: { - message: 'Could not verify the connected Slack workspace. Reconnect it and try again.', - status: 400, - }, - } - } - effectiveProvider = 'slack_app' - effectivePath = null - } else { - const botCredentialId = - typeof providerConfig.botCredential === 'string' ? providerConfig.botCredential : undefined - if (!botCredentialId) { - return { - success: false, - error: { message: 'Select a Slack bot credential for the trigger.', status: 400 }, - } + // Custom-bot only: events route by the bot credential to one shared ingest + // URL verified with the bot's own signing secret. The native Sim-app path + // (team_id routing via the official app) is intentionally not supported yet. + const botCredentialId = + typeof providerConfig.botCredential === 'string' ? providerConfig.botCredential : undefined + if (!botCredentialId) { + return { + success: false, + error: { message: 'Select a Slack bot credential for the trigger.', status: 400 }, } - const botCredential = await getSlackBotCredential(botCredentialId) - if (!botCredential) { - return { - success: false, - error: { - message: 'The selected Slack bot credential is missing or invalid. Reconnect it.', - status: 400, - }, - } + } + const botCredential = await getSlackBotCredential(botCredentialId) + if (!botCredential) { + return { + success: false, + error: { + message: 'The selected Slack bot credential is missing or invalid. Reconnect it.', + status: 400, + }, } - const workflowWorkspace = - typeof input.workflow.workspaceId === 'string' ? input.workflow.workspaceId : undefined - if (!workflowWorkspace || botCredential.workspaceId !== workflowWorkspace) { - return { - success: false, - error: { - message: 'The selected Slack bot credential is not available in this workspace.', - status: 400, - }, - } + } + const workflowWorkspace = + typeof input.workflow.workspaceId === 'string' ? input.workflow.workspaceId : undefined + if (!workflowWorkspace || botCredential.workspaceId !== workflowWorkspace) { + return { + success: false, + error: { + message: 'The selected Slack bot credential is not available in this workspace.', + status: 400, + }, } - effectiveProvider = 'slack' - effectivePath = null - routingKey = botCredentialId - providerConfig.credentialId = botCredentialId - if (botCredential.botUserId) providerConfig.bot_user_id = botCredential.botUserId } + effectiveProvider = 'slack' + effectivePath = null + routingKey = botCredentialId + providerConfig.credentialId = botCredentialId + if (botCredential.botUserId) providerConfig.bot_user_id = botCredential.botUserId } return { diff --git a/apps/sim/lib/workflows/persistence/utils.ts b/apps/sim/lib/workflows/persistence/utils.ts index 1de604a8d80..f762108e528 100644 --- a/apps/sim/lib/workflows/persistence/utils.ts +++ b/apps/sim/lib/workflows/persistence/utils.ts @@ -356,6 +356,8 @@ export const CREDENTIAL_SUBBLOCK_IDS = new Set([ 'credential', 'manualCredential', 'triggerCredentials', + 'customBotCredential', + 'manualBotCredential', ]) async function migrateCredentialIds( diff --git a/apps/sim/triggers/slack/oauth.ts b/apps/sim/triggers/slack/oauth.ts index 1591889831b..368b4128dc5 100644 --- a/apps/sim/triggers/slack/oauth.ts +++ b/apps/sim/triggers/slack/oauth.ts @@ -1,9 +1,7 @@ import { SlackIcon } from '@/components/icons' import { getScopesForService } from '@/lib/oauth/utils' -import { useSubBlockStore } from '@/stores/workflows/subblock/store' import { SLACK_ALL_EVENT_OPTIONS, - SLACK_SIM_EVENT_OPTIONS, SLACK_SOURCE_OPTIONS, SLACK_THREAD_OPTIONS, SLACK_TRIGGER_OUTPUTS, @@ -25,14 +23,11 @@ const BOT_FILTER_EVENTS = ['message', 'app_mention'] const OWN_MESSAGE_EVENTS = ['message', 'app_mention', 'reaction_added', 'reaction_removed'] /** - * Unified Slack trigger. App Type selects how events arrive: - * - `sim` (default): the official Sim Slack app the user OAuth-connects — events - * route to this workflow by Slack `team_id` (stored as the webhook `routingKey`, - * derived at deploy time). No signing secret, bot token, or app setup. - * - `custom`: a bring-your-own Slack app, selected as a reusable bot credential - * (set up once). Events route by that credential (`webhook.routingKey = - * credentialId`) to one shared ingest URL, so many triggers on the same bot - * share a single Request URL, verified with the bot's own signing secret. + * Slack trigger backed by a reusable custom-bot credential (set up once, reused + * across triggers and actions). Events route by that credential + * (`webhook.routingKey = credentialId`) to one shared ingest URL, so many + * triggers on the same bot share a single Request URL, verified with the bot's + * own signing secret. */ export const slackOAuthTrigger: TriggerConfig = { id: 'slack_oauth', @@ -47,50 +42,12 @@ export const slackOAuthTrigger: TriggerConfig = { id: 'eventType', title: 'Event', type: 'dropdown', - options: [...SLACK_SIM_EVENT_OPTIONS], + options: [...SLACK_ALL_EVENT_OPTIONS], placeholder: 'Select an event', description: 'The single Slack event this trigger fires on. Add another trigger block for another event.', required: true, mode: 'trigger', - dependsOn: ['appType'], - fetchOptions: async (blockId: string) => { - const appType = useSubBlockStore.getState().getValue(blockId, 'appType') - return appType === 'custom' ? [...SLACK_ALL_EVENT_OPTIONS] : [...SLACK_SIM_EVENT_OPTIONS] - }, - }, - { - id: 'appType', - title: 'App Type', - type: 'dropdown', - // Ship 1 exposes custom bots only; the native "Sim" app mode returns in a - // later ship (un-hide + default back to 'sim'). Hidden — not removed — so - // the seeded 'custom' value keeps every `appType==='custom'` condition, the - // event-catalog fetch, and the deploy routing branch resolving correctly. - hidden: true, - options: [ - { label: 'Sim', id: 'sim' }, - { label: 'Custom', id: 'custom' }, - ], - value: () => 'custom', - // `value()` only seeds editor-created blocks; `defaultValue` is what - // buildProviderConfig persists when the stored value is absent - // (imported / programmatically-created workflows). - defaultValue: 'custom', - description: 'Use the official Sim Slack app, or your own custom Slack app.', - mode: 'trigger', - }, - { - id: 'triggerCredentials', - title: 'Slack Account', - type: 'oauth-input', - canonicalParamId: 'oauthCredential', - serviceId: 'slack', - requiredScopes: getScopesForService('slack'), - placeholder: 'Select Slack account', - required: { field: 'appType', value: 'sim' }, - mode: 'trigger', - condition: { field: 'appType', value: 'sim' }, }, { id: 'customBotCredential', @@ -98,14 +55,14 @@ export const slackOAuthTrigger: TriggerConfig = { type: 'oauth-input', canonicalParamId: 'botCredential', serviceId: 'slack', - credentialKind: 'custom-bot', + credentialKind: 'service-account', + credentialLabels: { serviceAccountConnect: 'Set up a custom bot' }, requiredScopes: getScopesForService('slack'), placeholder: 'Select a connected bot', description: 'Choose a custom Slack bot you set up once and reuse across triggers and actions.', - required: { field: 'appType', value: 'custom' }, + required: true, mode: 'trigger', - condition: { field: 'appType', value: 'custom' }, }, { id: 'manualBotCredential', @@ -114,9 +71,8 @@ export const slackOAuthTrigger: TriggerConfig = { canonicalParamId: 'botCredential', placeholder: 'Enter bot credential ID', description: 'Set the custom bot credential ID directly.', - required: { field: 'appType', value: 'custom' }, + required: true, mode: 'trigger-advanced', - condition: { field: 'appType', value: 'custom' }, }, { id: 'source', @@ -142,7 +98,7 @@ export const slackOAuthTrigger: TriggerConfig = { placeholder: 'Any channel the bot is in', description: 'Restrict to specific channels. Leave empty to trigger on any channel the bot has been added to.', - dependsOn: { any: ['triggerCredentials', 'customBotCredential'] }, + dependsOn: ['customBotCredential'], required: false, mode: 'trigger', condition: { field: 'eventType', value: CHANNEL_FILTER_EVENTS }, diff --git a/apps/sim/triggers/slack/shared.ts b/apps/sim/triggers/slack/shared.ts index 0efca9c72ac..4372f7ba68b 100644 --- a/apps/sim/triggers/slack/shared.ts +++ b/apps/sim/triggers/slack/shared.ts @@ -271,17 +271,7 @@ export const slackEventById = new Map( SLACK_EVENT_CATALOG.map((entry) => [entry.id, entry]) ) -/** Event ids the official shared Sim app subscribes to (Sim-mode gating SOT). */ -export const SIM_SUBSCRIBED_EVENTS: readonly string[] = SLACK_EVENT_CATALOG.filter( - (entry) => entry.simSubscribed -).map((entry) => entry.id) - -/** Dropdown options for Sim mode — only officially-subscribed events. */ -export const SLACK_SIM_EVENT_OPTIONS = SLACK_EVENT_CATALOG.filter( - (entry) => entry.simSubscribed -).map((entry) => ({ label: entry.label, id: entry.id })) - -/** Dropdown options for Custom mode — every event (manifest generated on demand). */ +/** Dropdown options for the event picker — every event (manifest generated on demand). */ export const SLACK_ALL_EVENT_OPTIONS = SLACK_EVENT_CATALOG.map((entry) => ({ label: entry.label, id: entry.id,