From 3200588e67b13bdb09d5475b4e44bd9cff13aca7 Mon Sep 17 00:00:00 2001 From: Theodore Li Date: Fri, 17 Apr 2026 19:38:02 -0700 Subject: [PATCH 01/10] feat(slack): add manifest copying --- .../components/sub-block/components/index.ts | 1 + .../slack-manifest-generator/capabilities.ts | 174 ++++++++++ .../slack-manifest-generator/index.ts | 1 + .../slack-manifest-generator.tsx | 317 ++++++++++++++++++ .../editor/components/sub-block/sub-block.tsx | 5 + apps/sim/blocks/types.ts | 1 + apps/sim/triggers/slack/webhook.ts | 21 +- 7 files changed, 502 insertions(+), 18 deletions(-) create mode 100644 apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/slack-manifest-generator/capabilities.ts create mode 100644 apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/slack-manifest-generator/index.ts create mode 100644 apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/slack-manifest-generator/slack-manifest-generator.tsx diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/index.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/index.ts index af6314a94f6..95d9c87f407 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/index.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/index.ts @@ -22,6 +22,7 @@ export { ScheduleInfo } from './schedule-info/schedule-info' export { SelectorInput, type SelectorOverrides } from './selector-input/selector-input' export { ShortInput } from './short-input/short-input' export { SkillInput } from './skill-input/skill-input' +export { SlackManifestGenerator } from './slack-manifest-generator/slack-manifest-generator' export { SliderInput } from './slider-input/slider-input' export { SortBuilder } from './sort-builder/sort-builder' export { InputFormat } from './starter/input-format' diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/slack-manifest-generator/capabilities.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/slack-manifest-generator/capabilities.ts new file mode 100644 index 00000000000..de8d7f7f2fc --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/slack-manifest-generator/capabilities.ts @@ -0,0 +1,174 @@ +/** + * Slack app capabilities that can be toggled on in the manifest generator. + * + * @remarks + * Each capability maps to a set of bot OAuth scopes and bot events that must + * be declared in the Slack app manifest for the capability to work. + * See https://api.slack.com/reference/manifests for the manifest schema. + */ + +export type SlackCapabilityGroup = 'trigger' | 'action' + +export interface SlackCapability { + id: string + label: string + description: string + group: SlackCapabilityGroup + scopes: readonly string[] + events: readonly string[] + defaultEnabled: boolean +} + +export const SLACK_CAPABILITIES: readonly SlackCapability[] = [ + { + id: 'mention', + label: '@mention', + description: 'Trigger the workflow when someone @-mentions your bot.', + group: 'trigger', + scopes: ['app_mentions:read'], + events: ['app_mention'], + defaultEnabled: true, + }, + { + id: 'dm', + label: 'Direct message', + description: 'Trigger the workflow when a user sends your bot a 1:1 direct message.', + group: 'trigger', + scopes: ['im:history', 'im:read'], + events: ['message.im'], + defaultEnabled: true, + }, + { + id: 'group_dm', + label: 'Group direct message', + description: 'Trigger on messages in multi-person DMs your bot is part of.', + group: 'trigger', + scopes: ['mpim:history', 'mpim:read'], + events: ['message.mpim'], + defaultEnabled: true, + }, + { + id: 'public_channel', + label: 'Public channel message', + description: 'Trigger on messages in public channels your bot has been invited to.', + group: 'trigger', + scopes: ['channels:history', 'channels:read'], + events: ['message.channels'], + defaultEnabled: true, + }, + { + id: 'private_channel', + label: 'Private channel message', + description: 'Trigger on messages in private channels your bot has been invited to.', + group: 'trigger', + scopes: ['groups:history', 'groups:read'], + events: ['message.groups'], + defaultEnabled: true, + }, + { + id: 'public_channel_reaction', + label: 'Public channel reaction', + description: 'Trigger when emoji reactions are added or removed in public channels.', + group: 'trigger', + scopes: ['reactions:read'], + events: ['reaction_added', 'reaction_removed'], + defaultEnabled: true, + }, + { + id: 'any_reaction', + label: 'Reaction (any channel)', + description: 'Trigger on any emoji reaction your bot can see — public or private.', + group: 'trigger', + scopes: ['reactions:read'], + events: ['reaction_added', 'reaction_removed'], + defaultEnabled: true, + }, + { + id: 'send', + label: 'Send messages', + description: 'Let the bot post messages into channels it is a member of.', + group: 'action', + scopes: ['chat:write'], + events: [], + defaultEnabled: true, + }, + { + id: 'add_reaction', + label: 'Add reactions', + description: 'Let the bot add emoji reactions to messages.', + group: 'action', + scopes: ['reactions:write'], + events: [], + defaultEnabled: true, + }, + { + id: 'read_files', + label: 'Read file attachments', + description: 'Let the bot download file attachments on incoming messages.', + group: 'action', + scopes: ['files:read'], + events: [], + defaultEnabled: true, + }, + { + id: 'read_users', + label: 'Look up users', + description: 'Resolve user IDs to names, profiles, and email addresses.', + group: 'action', + scopes: ['users:read', 'users:read.email'], + events: [], + defaultEnabled: true, + }, +] as const + +const WEBHOOK_URL_PLACEHOLDER = '' + +export interface BuildManifestOptions { + appName: string + webhookUrl: string | null +} + +/** + * Builds a Slack app manifest object from a set of enabled capability ids. + * + * @remarks + * - Deduplicates scopes and events across overlapping capabilities. + * - Omits `settings.event_subscriptions` entirely when no events are selected — + * Slack's manifest validator rejects an empty `bot_events` array. + * - When `webhookUrl` is null, embeds a human-readable placeholder so the + * shape is visible before the workflow is deployed. + */ +export function buildSlackManifest( + enabled: ReadonlySet, + { appName, webhookUrl }: BuildManifestOptions +): Record { + const active = SLACK_CAPABILITIES.filter((c) => enabled.has(c.id)) + const scopes = [...new Set(active.flatMap((c) => c.scopes))].sort() + const events = [...new Set(active.flatMap((c) => c.events))].sort() + const displayName = appName.trim() || 'Sim Workflow Bot' + + const manifest: Record = { + display_information: { name: displayName }, + features: { + bot_user: { display_name: displayName, always_online: true }, + }, + oauth_config: { + scopes: { bot: scopes }, + }, + settings: { + org_deploy_enabled: false, + socket_mode_enabled: false, + token_rotation_enabled: false, + }, + } + + if (events.length > 0) { + const settings = manifest.settings as Record + settings.event_subscriptions = { + request_url: webhookUrl ?? WEBHOOK_URL_PLACEHOLDER, + bot_events: events, + } + } + + return manifest +} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/slack-manifest-generator/index.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/slack-manifest-generator/index.ts new file mode 100644 index 00000000000..6b87f854c91 --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/slack-manifest-generator/index.ts @@ -0,0 +1 @@ +export { SlackManifestGenerator } from './slack-manifest-generator' diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/slack-manifest-generator/slack-manifest-generator.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/slack-manifest-generator/slack-manifest-generator.tsx new file mode 100644 index 00000000000..0332c0e5d22 --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/slack-manifest-generator/slack-manifest-generator.tsx @@ -0,0 +1,317 @@ +'use client' + +import { type ReactNode, useCallback, useMemo, useState } from 'react' +import { Check, Clipboard, Info } from 'lucide-react' +import { Checkbox, Input, Label, Tooltip } from '@/components/emcn' +import { cn } from '@/lib/core/utils/cn' +import { + buildSlackManifest, + SLACK_CAPABILITIES, + type SlackCapability, + type SlackCapabilityGroup, +} from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/slack-manifest-generator/capabilities' +import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value' +import { useWebhookManagement } from '@/hooks/use-webhook-management' + +const DEFAULT_APP_NAME = 'Sim Workflow Bot' + +const GROUP_LABELS: Record = { + trigger: 'Triggers', + action: 'Actions', +} + +const GROUP_ORDER: readonly SlackCapabilityGroup[] = ['trigger', 'action'] as const + +const DEFAULT_SELECTED_IDS: readonly string[] = SLACK_CAPABILITIES.filter( + (c) => c.defaultEnabled +).map((c) => c.id) + +interface PersistedManifestConfig { + appName: string + selected: string[] +} + +interface InstallStep { + title: string + detail: ReactNode +} + +const INSTALL_STEPS: readonly InstallStep[] = [ + { + title: 'Create the app from your manifest', + detail: ( + <> + Open the{' '} + + Slack Apps page + + , click Create New AppFrom a manifest, pick your + workspace, paste the manifest you copied, and click Create. + + ), + }, + { + title: 'Paste your Signing Secret', + detail: ( + <> + In your new Slack app, open Basic Information, copy the{' '} + Signing Secret, and paste it into the Signing Secret field + above. + + ), + }, + { + title: 'Install the app', + detail: ( + <> + Go to "Install App" in the left sidebar and install the app into your + desired Slack workspace and channel. + + ), + }, + { + title: 'Copy the Bot User OAuth Token', + detail: ( + <> + Copy the "Bot User OAuth Token" (starts with xoxb-) and paste + it into the Bot Token field above to enable file downloads. + + ), + }, + { + title: 'Save changes', + detail: <>Save changes in both Slack and here., + }, +] as const + +interface SlackManifestGeneratorProps { + blockId: string + isPreview?: boolean + disabled?: boolean +} + +/** + * Slack app manifest generator — a full sub-block. + * + * @remarks + * Persists the bot name and selected capabilities into the sub-block store + * under the `manifestGenerator` key so the user's configuration survives + * reloads. The manifest itself is regenerated on-the-fly from that persisted + * state and copied to the clipboard via a single full-width button. Setup + * steps for actually installing the Slack app live behind inline info + * tooltips instead of consuming vertical space. + */ +export function SlackManifestGenerator({ + blockId, + isPreview = false, + disabled = false, +}: SlackManifestGeneratorProps) { + const { webhookUrl, isLoading } = useWebhookManagement({ + blockId, + triggerId: 'slack_webhook', + useWebhookUrl: true, + isPreview, + }) + + const [persisted, setPersisted] = useSubBlockValue( + blockId, + 'manifestGenerator' + ) + + const appName = persisted?.appName ?? DEFAULT_APP_NAME + const selectedIds = persisted?.selected ?? DEFAULT_SELECTED_IDS + const selected = useMemo(() => new Set(selectedIds), [selectedIds]) + + const [copied, setCopied] = useState(false) + + const effectiveWebhookUrl = !isLoading && webhookUrl ? webhookUrl : null + const canCopy = effectiveWebhookUrl !== null + const controlsDisabled = isPreview || disabled + + const manifestJson = useMemo(() => { + const manifest = buildSlackManifest(selected, { + appName, + webhookUrl: effectiveWebhookUrl, + }) + return JSON.stringify(manifest, null, 2) + }, [selected, appName, effectiveWebhookUrl]) + + const handleAppNameChange = useCallback( + (value: string) => { + if (controlsDisabled) return + setPersisted({ appName: value, selected: Array.from(selected) }) + }, + [controlsDisabled, selected, setPersisted] + ) + + const handleToggle = useCallback( + (id: string, checked: boolean) => { + if (controlsDisabled) return + const next = new Set(selected) + if (checked) { + next.add(id) + } else { + next.delete(id) + } + setPersisted({ appName, selected: Array.from(next) }) + }, + [controlsDisabled, selected, appName, setPersisted] + ) + + const handleCopy = useCallback(() => { + if (!canCopy) return + navigator.clipboard.writeText(manifestJson) + setCopied(true) + setTimeout(() => setCopied(false), 2000) + }, [canCopy, manifestJson]) + + return ( +
+
+
+ Setup +
+
+ + handleAppNameChange(e.target.value)} + disabled={controlsDisabled} + placeholder={DEFAULT_APP_NAME} + className='h-9 text-sm' + /> +
+
+ {GROUP_ORDER.map((group) => { + const items = SLACK_CAPABILITIES.filter((c) => c.group === group) + if (items.length === 0) return null + return ( + + ) + })} +
+
+ + + +
+
+ After you copy, in Slack +
+
    + {INSTALL_STEPS.map((step, index) => ( +
  1. + {index + 1}. + {step.title} + + + + + +

    {step.detail}

    +
    +
    +
  2. + ))} +
+
+
+ ) +} + +interface CapabilityGroupProps { + blockId: string + label: string + capabilities: readonly SlackCapability[] + selected: ReadonlySet + disabled: boolean + onToggle: (id: string, checked: boolean) => void +} + +function CapabilityGroup({ + blockId, + label, + capabilities, + selected, + disabled, + onToggle, +}: CapabilityGroupProps) { + return ( +
+
+ {label} +
+
+ {capabilities.map((c) => { + const id = `${blockId}-slack-manifest-${c.id}` + return ( +
+ onToggle(c.id, checked === true)} + disabled={disabled} + /> + + + + + + +

{c.description}

+
+
+
+ ) + })} +
+
+ ) +} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/sub-block.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/sub-block.tsx index d85adff7b7e..33a33aa376a 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/sub-block.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/sub-block.tsx @@ -39,6 +39,7 @@ import { type SelectorOverrides, ShortInput, SkillInput, + SlackManifestGenerator, SliderInput, SortBuilder, Switch, @@ -1129,6 +1130,10 @@ function SubBlockComponent({ } /> ) + case 'slack-manifest-generator': + return ( + + ) case 'messages-input': return ( Slack Apps page', - 'If you don\'t have an app:
  • Create an app from scratch
  • Give it a name and select your workspace
', - 'Go to "Basic Information", find the "Signing Secret", and paste it in the field above.', - 'Go to "OAuth & Permissions" and add bot token scopes:
  • app_mentions:read - For viewing messages that tag your bot with an @
  • chat:write - To send messages to channels your bot is a part of
  • files:read - To access files and images shared in messages
  • reactions:read - For listening to emoji reactions and fetching reacted-to message text
', - 'Go to "Event Subscriptions":
  • Enable events
  • Under "Subscribe to Bot Events", add app_mention to listen to messages that mention your bot
  • To receive all channel messages, add message.channels. For DMs add message.im, for group DMs add message.mpim, for private channels add message.groups
  • For reaction events, also add reaction_added and/or reaction_removed
  • Paste the Webhook URL above into the "Request URL" field
', - 'Go to "Install App" in the left sidebar and install the app into your desired Slack workspace and channel.', - 'Copy the "Bot User OAuth Token" (starts with xoxb-) and paste it in the Bot Token field above to enable file downloads.', - 'Save changes in both Slack and here.', - ] - .map( - (instruction, index) => - `
${index + 1}. ${instruction}
` - ) - .join(''), + id: 'manifestGenerator', + title: 'Slack App Setup', + type: 'slack-manifest-generator', hideFromPreview: true, mode: 'trigger', }, From c1d3db2d2f914b0de77ccf04b3fc562c4231aaed Mon Sep 17 00:00:00 2001 From: Theodore Li Date: Sat, 18 Apr 2026 18:53:01 -0700 Subject: [PATCH 02/10] Try using wizard modal --- .../import-csv-dialog/import-csv-dialog.tsx | 2 +- .../checkbox-list/checkbox-list.tsx | 48 +- .../components/sub-block/components/index.ts | 2 +- .../slack-manifest-generator/index.ts | 1 - .../slack-manifest-generator.tsx | 317 ---------- .../components/slack-setup-wizard/index.ts | 1 + .../slack-setup-wizard/slack-setup-wizard.tsx | 587 ++++++++++++++++++ .../editor/components/sub-block/sub-block.tsx | 17 +- apps/sim/blocks/types.ts | 6 +- .../lib/copilot/generated/tool-schemas-v1.ts | 174 +++--- .../slack}/capabilities.ts | 60 +- apps/sim/triggers/slack/webhook.ts | 15 +- 12 files changed, 769 insertions(+), 461 deletions(-) delete mode 100644 apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/slack-manifest-generator/index.ts delete mode 100644 apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/slack-manifest-generator/slack-manifest-generator.tsx create mode 100644 apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/slack-setup-wizard/index.ts create mode 100644 apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/slack-setup-wizard/slack-setup-wizard.tsx rename apps/sim/{app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/slack-manifest-generator => triggers/slack}/capabilities.ts (78%) diff --git a/apps/sim/app/workspace/[workspaceId]/tables/components/import-csv-dialog/import-csv-dialog.tsx b/apps/sim/app/workspace/[workspaceId]/tables/components/import-csv-dialog/import-csv-dialog.tsx index 6760a73fd7a..70bde694d3d 100644 --- a/apps/sim/app/workspace/[workspaceId]/tables/components/import-csv-dialog/import-csv-dialog.tsx +++ b/apps/sim/app/workspace/[workspaceId]/tables/components/import-csv-dialog/import-csv-dialog.tsx @@ -363,7 +363,7 @@ export function ImportCsvDialog({
- + {parsed.file.name} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/checkbox-list/checkbox-list.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/checkbox-list/checkbox-list.tsx index 5f04b074361..8773117c72b 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/checkbox-list/checkbox-list.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/checkbox-list/checkbox-list.tsx @@ -1,11 +1,19 @@ -import { Checkbox, Label } from '@/components/emcn' +import { Info } from 'lucide-react' +import { Checkbox, Label, Tooltip } from '@/components/emcn' import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value' +interface CheckboxListOption { + label: string + id: string + defaultChecked?: boolean + description?: string +} + interface CheckboxListProps { blockId: string subBlockId: string title: string - options: { label: string; id: string }[] + options: CheckboxListOption[] isPreview?: boolean subBlockValues?: Record disabled?: boolean @@ -13,36 +21,38 @@ interface CheckboxListProps { interface CheckboxItemProps { blockId: string - option: { label: string; id: string } + option: CheckboxListOption isPreview: boolean subBlockValues?: Record disabled: boolean } /** - * Individual checkbox item component that calls useSubBlockValue hook at top level + * Individual checkbox item component that calls useSubBlockValue hook at top level. + * + * @remarks + * A `null` store value means the user has never toggled the checkbox, in which + * case we fall back to `option.defaultChecked` for the displayed state. Any + * explicit boolean (including `false`) takes precedence over the default. */ function CheckboxItem({ blockId, option, isPreview, subBlockValues, disabled }: CheckboxItemProps) { - const [storeValue, setStoreValue] = useSubBlockValue(blockId, option.id) + const [storeValue, setStoreValue] = useSubBlockValue(blockId, option.id) - // Get preview value for this specific option const previewValue = isPreview && subBlockValues ? subBlockValues[option.id]?.value : undefined - - // Use preview value when in preview mode, otherwise use store value - const value = isPreview ? previewValue : storeValue + const rawValue = isPreview ? previewValue : storeValue + const effectiveValue = rawValue ?? option.defaultChecked ?? false const handleChange = (checked: boolean) => { - // Only update store when not in preview mode or disabled if (!isPreview && !disabled) { setStoreValue(checked) } } return ( -
+
@@ -52,21 +62,29 @@ function CheckboxItem({ blockId, option, isPreview, subBlockValues, disabled }: > {option.label} + {option.description && ( + + + + + +

{option.description}

+
+
+ )}
) } export function CheckboxList({ blockId, - subBlockId, - title, options, isPreview = false, subBlockValues, disabled = false, }: CheckboxListProps) { return ( -
+
{options.map((option) => ( = { - trigger: 'Triggers', - action: 'Actions', -} - -const GROUP_ORDER: readonly SlackCapabilityGroup[] = ['trigger', 'action'] as const - -const DEFAULT_SELECTED_IDS: readonly string[] = SLACK_CAPABILITIES.filter( - (c) => c.defaultEnabled -).map((c) => c.id) - -interface PersistedManifestConfig { - appName: string - selected: string[] -} - -interface InstallStep { - title: string - detail: ReactNode -} - -const INSTALL_STEPS: readonly InstallStep[] = [ - { - title: 'Create the app from your manifest', - detail: ( - <> - Open the{' '} - - Slack Apps page - - , click Create New AppFrom a manifest, pick your - workspace, paste the manifest you copied, and click Create. - - ), - }, - { - title: 'Paste your Signing Secret', - detail: ( - <> - In your new Slack app, open Basic Information, copy the{' '} - Signing Secret, and paste it into the Signing Secret field - above. - - ), - }, - { - title: 'Install the app', - detail: ( - <> - Go to "Install App" in the left sidebar and install the app into your - desired Slack workspace and channel. - - ), - }, - { - title: 'Copy the Bot User OAuth Token', - detail: ( - <> - Copy the "Bot User OAuth Token" (starts with xoxb-) and paste - it into the Bot Token field above to enable file downloads. - - ), - }, - { - title: 'Save changes', - detail: <>Save changes in both Slack and here., - }, -] as const - -interface SlackManifestGeneratorProps { - blockId: string - isPreview?: boolean - disabled?: boolean -} - -/** - * Slack app manifest generator — a full sub-block. - * - * @remarks - * Persists the bot name and selected capabilities into the sub-block store - * under the `manifestGenerator` key so the user's configuration survives - * reloads. The manifest itself is regenerated on-the-fly from that persisted - * state and copied to the clipboard via a single full-width button. Setup - * steps for actually installing the Slack app live behind inline info - * tooltips instead of consuming vertical space. - */ -export function SlackManifestGenerator({ - blockId, - isPreview = false, - disabled = false, -}: SlackManifestGeneratorProps) { - const { webhookUrl, isLoading } = useWebhookManagement({ - blockId, - triggerId: 'slack_webhook', - useWebhookUrl: true, - isPreview, - }) - - const [persisted, setPersisted] = useSubBlockValue( - blockId, - 'manifestGenerator' - ) - - const appName = persisted?.appName ?? DEFAULT_APP_NAME - const selectedIds = persisted?.selected ?? DEFAULT_SELECTED_IDS - const selected = useMemo(() => new Set(selectedIds), [selectedIds]) - - const [copied, setCopied] = useState(false) - - const effectiveWebhookUrl = !isLoading && webhookUrl ? webhookUrl : null - const canCopy = effectiveWebhookUrl !== null - const controlsDisabled = isPreview || disabled - - const manifestJson = useMemo(() => { - const manifest = buildSlackManifest(selected, { - appName, - webhookUrl: effectiveWebhookUrl, - }) - return JSON.stringify(manifest, null, 2) - }, [selected, appName, effectiveWebhookUrl]) - - const handleAppNameChange = useCallback( - (value: string) => { - if (controlsDisabled) return - setPersisted({ appName: value, selected: Array.from(selected) }) - }, - [controlsDisabled, selected, setPersisted] - ) - - const handleToggle = useCallback( - (id: string, checked: boolean) => { - if (controlsDisabled) return - const next = new Set(selected) - if (checked) { - next.add(id) - } else { - next.delete(id) - } - setPersisted({ appName, selected: Array.from(next) }) - }, - [controlsDisabled, selected, appName, setPersisted] - ) - - const handleCopy = useCallback(() => { - if (!canCopy) return - navigator.clipboard.writeText(manifestJson) - setCopied(true) - setTimeout(() => setCopied(false), 2000) - }, [canCopy, manifestJson]) - - return ( -
-
-
- Setup -
-
- - handleAppNameChange(e.target.value)} - disabled={controlsDisabled} - placeholder={DEFAULT_APP_NAME} - className='h-9 text-sm' - /> -
-
- {GROUP_ORDER.map((group) => { - const items = SLACK_CAPABILITIES.filter((c) => c.group === group) - if (items.length === 0) return null - return ( - - ) - })} -
-
- - - -
-
- After you copy, in Slack -
-
    - {INSTALL_STEPS.map((step, index) => ( -
  1. - {index + 1}. - {step.title} - - - - - -

    {step.detail}

    -
    -
    -
  2. - ))} -
-
-
- ) -} - -interface CapabilityGroupProps { - blockId: string - label: string - capabilities: readonly SlackCapability[] - selected: ReadonlySet - disabled: boolean - onToggle: (id: string, checked: boolean) => void -} - -function CapabilityGroup({ - blockId, - label, - capabilities, - selected, - disabled, - onToggle, -}: CapabilityGroupProps) { - return ( -
-
- {label} -
-
- {capabilities.map((c) => { - const id = `${blockId}-slack-manifest-${c.id}` - return ( -
- onToggle(c.id, checked === true)} - disabled={disabled} - /> - - - - - - -

{c.description}

-
-
-
- ) - })} -
-
- ) -} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/slack-setup-wizard/index.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/slack-setup-wizard/index.ts new file mode 100644 index 00000000000..2caab15a639 --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/slack-setup-wizard/index.ts @@ -0,0 +1 @@ +export { SlackSetupWizard } from './slack-setup-wizard' diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/slack-setup-wizard/slack-setup-wizard.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/slack-setup-wizard/slack-setup-wizard.tsx new file mode 100644 index 00000000000..250e6c68e74 --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/slack-setup-wizard/slack-setup-wizard.tsx @@ -0,0 +1,587 @@ +'use client' + +import { useCallback, useMemo, useState } from 'react' +import { Check, Clipboard, Info } from 'lucide-react' +import { useShallow } from 'zustand/react/shallow' +import { + Button, + Checkbox, + Input, + Label, + Modal, + ModalBody, + ModalContent, + ModalFooter, + ModalHeader, + Tooltip, +} from '@/components/emcn' +import { cn } from '@/lib/core/utils/cn' +import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value' +import { useWebhookManagement } from '@/hooks/use-webhook-management' +import { useWorkflowRegistry } from '@/stores/workflows/registry/store' +import { useSubBlockStore } from '@/stores/workflows/subblock/store' +import { + buildSlackManifest, + SLACK_CAPABILITIES, + type SlackCapability, + type SlackCapabilityGroup, +} from '@/triggers/slack/capabilities' + +const DEFAULT_APP_NAME = 'Sim Workflow Bot' + +const GROUP_LABELS: Record = { + trigger: 'Triggers', + action: 'Actions', +} + +const GROUP_ORDER: readonly SlackCapabilityGroup[] = ['trigger', 'action'] as const + +const STEP_TITLES = [ + 'Configure your bot', + 'Copy your manifest', + 'Create the app in Slack', + 'Paste your Signing Secret', + 'Install and paste your Bot Token', + 'All set', +] as const + +const STEP_COUNT = STEP_TITLES.length + +interface SlackSetupWizardProps { + blockId: string + isPreview?: boolean + disabled?: boolean +} + +/** + * Slack app setup wizard sub-block. + * + * @remarks + * The panel renders a single launcher button. The wizard lives in a modal + * that walks the user through: configuring the bot, copying the manifest, + * creating the app in Slack, pasting the Signing Secret, and pasting the + * Bot Token. Credentials are written directly into the sibling + * `signingSecret` and `botToken` sub-blocks via the shared sub-block store, + * so those fields in the panel are populated by the time the user clicks + * Done. + */ +export function SlackSetupWizard({ + blockId, + isPreview = false, + disabled = false, +}: SlackSetupWizardProps) { + const [open, setOpen] = useState(false) + + return ( + <> + + + + + ) +} + +interface WizardModalProps { + blockId: string + open: boolean + onOpenChange: (next: boolean) => void + isPreview: boolean + disabled: boolean +} + +function WizardModal({ blockId, open, onOpenChange, isPreview, disabled }: WizardModalProps) { + const [step, setStep] = useState(0) + + const { webhookUrl, isLoading } = useWebhookManagement({ + blockId, + triggerId: 'slack_webhook', + useWebhookUrl: true, + isPreview, + }) + + const [appName, setAppName] = useSubBlockValue(blockId, 'botDisplayName') + const [signingSecret, setSigningSecret] = useSubBlockValue(blockId, 'signingSecret') + const [botToken, setBotToken] = useSubBlockValue(blockId, 'botToken') + const selected = useCapabilitySelection(blockId) + + const displayAppName = appName ?? DEFAULT_APP_NAME + const effectiveWebhookUrl = !isLoading && webhookUrl ? webhookUrl : null + const canCopy = effectiveWebhookUrl !== null + const controlsDisabled = isPreview || disabled + + const manifestJson = useMemo(() => { + const manifest = buildSlackManifest(selected, { + appName: displayAppName.trim() || DEFAULT_APP_NAME, + webhookUrl: effectiveWebhookUrl, + }) + return JSON.stringify(manifest, null, 2) + }, [selected, displayAppName, effectiveWebhookUrl]) + + const handleOpenChange = useCallback( + (next: boolean) => { + if (!next) setStep(0) + onOpenChange(next) + }, + [onOpenChange] + ) + + const handleBack = useCallback(() => { + setStep((s) => Math.max(0, s - 1)) + }, []) + + const handleNext = useCallback(() => { + setStep((s) => Math.min(STEP_COUNT - 1, s + 1)) + }, []) + + const handleDone = useCallback(() => { + handleOpenChange(false) + }, [handleOpenChange]) + + return ( + + + +
+ {STEP_TITLES[step]} + + Step {step + 1} of {STEP_COUNT} + +
+
+ + + + + {step === 0 && ( + { + if (!controlsDisabled) setAppName(v) + }} + selected={selected} + disabled={controlsDisabled} + /> + )} + {step === 1 && } + {step === 2 && } + {step === 3 && ( + { + if (!controlsDisabled) setSigningSecret(v) + }} + disabled={controlsDisabled} + /> + )} + {step === 4 && ( + { + if (!controlsDisabled) setBotToken(v) + }} + disabled={controlsDisabled} + /> + )} + {step === 5 && ( + + )} + + + + + {step < STEP_COUNT - 1 ? ( + + ) : ( + + )} + +
+
+ ) +} + +interface StepProgressProps { + current: number + total: number +} + +function StepProgress({ current, total: _total }: StepProgressProps) { + return ( +
+ {STEP_TITLES.map((title, i) => ( +
+ ))} +
+ ) +} + +interface StepConfigureProps { + blockId: string + appName: string + onAppNameChange: (next: string) => void + selected: ReadonlySet + disabled: boolean +} + +function StepConfigure({ + blockId, + appName, + onAppNameChange, + selected, + disabled, +}: StepConfigureProps) { + return ( +
+

+ Pick a name and choose what events should trigger your workflow and what actions your bot + can take. +

+
+ + onAppNameChange(e.target.value)} + disabled={disabled} + placeholder={DEFAULT_APP_NAME} + className='h-9 text-sm' + /> +
+
+ {GROUP_ORDER.map((group) => { + const items = SLACK_CAPABILITIES.filter((c) => c.group === group) + if (items.length === 0) return null + return ( + + ) + })} +
+
+ ) +} + +interface StepCopyProps { + manifestJson: string + canCopy: boolean +} + +function StepCopy({ manifestJson, canCopy }: StepCopyProps) { + const [copied, setCopied] = useState(false) + + const handleCopy = useCallback(() => { + if (!canCopy) return + navigator.clipboard.writeText(manifestJson) + setCopied(true) + setTimeout(() => setCopied(false), 2000) + }, [canCopy, manifestJson]) + + return ( +
+

+ Copy the generated manifest. You'll paste it into Slack in the next step. +

+ +
+ ) +} + +function StepCreate() { + return ( +
+

+ Open the{' '} + + Slack Apps page + + , click Create New AppFrom a manifest, pick your + workspace, paste the manifest, and click Create. +

+

Leave that Slack tab open — you'll pull a couple of values out of it in the next steps.

+
+ ) +} + +interface StepSecretProps { + blockId: string + value: string + onChange: (next: string) => void + disabled: boolean +} + +function StepSecret({ blockId, value, onChange, disabled }: StepSecretProps) { + return ( +
+

+ In your new Slack app, open Basic Information, find the{' '} + Signing Secret, and paste it here. +

+
+ + onChange(e.target.value)} + disabled={disabled} + placeholder='Paste your signing secret' + className='h-9 text-sm' + /> +
+
+ ) +} + +interface StepTokenProps { + blockId: string + value: string + onChange: (next: string) => void + disabled: boolean +} + +function StepToken({ blockId, value, onChange, disabled }: StepTokenProps) { + return ( +
+

+ In Slack, open Install AppInstall to Workspace and + authorize. Then copy the Bot User OAuth Token (starts with{' '} + xoxb-) and paste it here. +

+
+ + onChange(e.target.value)} + disabled={disabled} + placeholder='xoxb-...' + className='h-9 text-sm' + /> +
+
+ ) +} + +interface StepDoneProps { + hasSigningSecret: boolean + hasBotToken: boolean +} + +function StepDone({ hasSigningSecret, hasBotToken }: StepDoneProps) { + return ( +
+

+ Your Slack app is set up. The Signing Secret and Bot Token have been saved to the trigger — + you can edit them anytime from the panel. +

+
    + + +
+

Save the workflow and Slack will verify the webhook URL automatically.

+
+ ) +} + +interface StatusRowProps { + label: string + ok: boolean +} + +function StatusRow({ label, ok }: StatusRowProps) { + return ( +
  • + + + {label} + {!ok && — missing} + +
  • + ) +} + +interface CapabilityGroupProps { + blockId: string + label: string + capabilities: readonly SlackCapability[] + selected: ReadonlySet + disabled: boolean +} + +function CapabilityGroup({ + blockId, + label, + capabilities, + selected, + disabled, +}: CapabilityGroupProps) { + return ( +
    +
    + {label} +
    +
    + {capabilities.map((c) => ( + + ))} +
    +
    + ) +} + +interface CapabilityRowProps { + blockId: string + capability: SlackCapability + checked: boolean + disabled: boolean +} + +function CapabilityRow({ blockId, capability, checked, disabled }: CapabilityRowProps) { + const [, setValue] = useSubBlockValue(blockId, capability.id) + const id = `${blockId}-wizard-${capability.id}` + + const handleChange = useCallback( + (next: boolean) => { + if (disabled) return + setValue(next) + }, + [disabled, setValue] + ) + + return ( +
    + handleChange(v === true)} + disabled={disabled} + /> + + + + + + +

    {capability.description}

    +
    +
    +
    + ) +} + +/** + * Builds the set of enabled capability ids by reading each capability's + * individual sub-block value in a single shallow-compared store selector. + * A `null`/`undefined` store value falls back to the capability's + * `defaultChecked` so untouched configs still reflect the defaults. + */ +function useCapabilitySelection(blockId: string): ReadonlySet { + const activeWorkflowId = useWorkflowRegistry((s) => s.activeWorkflowId) + const enabledFlags = useSubBlockStore( + useShallow((state) => { + const blockValues = activeWorkflowId + ? state.workflowValues[activeWorkflowId]?.[blockId] + : undefined + return SLACK_CAPABILITIES.map((c) => { + const raw = blockValues?.[c.id] + return typeof raw === 'boolean' ? raw : c.defaultChecked + }) + }) + ) + return useMemo( + () => new Set(SLACK_CAPABILITIES.filter((_, i) => enabledFlags[i]).map((c) => c.id)), + [enabledFlags] + ) +} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/sub-block.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/sub-block.tsx index 33a33aa376a..2ad16cd23c1 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/sub-block.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/sub-block.tsx @@ -39,7 +39,7 @@ import { type SelectorOverrides, ShortInput, SkillInput, - SlackManifestGenerator, + SlackSetupWizard, SliderInput, SortBuilder, Switch, @@ -818,7 +818,14 @@ function SubBlockComponent({ blockId={blockId} subBlockId={config.id} title={config.title ?? ''} - options={config.options as { label: string; id: string }[]} + options={ + config.options as { + label: string + id: string + defaultChecked?: boolean + description?: string + }[] + } isPreview={isPreview} subBlockValues={subBlockValues} disabled={isDisabled} @@ -1130,10 +1137,8 @@ function SubBlockComponent({ } /> ) - case 'slack-manifest-generator': - return ( - - ) + case 'slack-setup-wizard': + return case 'messages-input': return ( group?: string hidden?: boolean + defaultChecked?: boolean + description?: string }[] | (() => { label: string @@ -310,6 +312,8 @@ export interface SubBlockConfig { icon?: React.ComponentType<{ className?: string }> group?: string hidden?: boolean + defaultChecked?: boolean + description?: string }[]) min?: number max?: number diff --git a/apps/sim/lib/copilot/generated/tool-schemas-v1.ts b/apps/sim/lib/copilot/generated/tool-schemas-v1.ts index 95538651008..8a64b57a766 100644 --- a/apps/sim/lib/copilot/generated/tool-schemas-v1.ts +++ b/apps/sim/lib/copilot/generated/tool-schemas-v1.ts @@ -10,7 +10,7 @@ export interface ToolRuntimeSchemaEntry { } export const TOOL_RUNTIME_SCHEMAS: Record = { - ['agent']: { + agent: { parameters: { properties: { request: { @@ -23,7 +23,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['auth']: { + auth: { parameters: { properties: { request: { @@ -36,7 +36,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['check_deployment_status']: { + check_deployment_status: { parameters: { type: 'object', properties: { @@ -48,7 +48,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['complete_job']: { + complete_job: { parameters: { type: 'object', properties: { @@ -61,7 +61,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['context_write']: { + context_write: { parameters: { type: 'object', properties: { @@ -78,7 +78,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['crawl_website']: { + crawl_website: { parameters: { type: 'object', properties: { @@ -113,7 +113,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['create_file']: { + create_file: { parameters: { type: 'object', properties: { @@ -149,7 +149,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { required: ['success', 'message'], }, }, - ['create_folder']: { + create_folder: { parameters: { type: 'object', properties: { @@ -170,7 +170,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['create_job']: { + create_job: { parameters: { type: 'object', properties: { @@ -220,7 +220,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['create_workflow']: { + create_workflow: { parameters: { type: 'object', properties: { @@ -245,7 +245,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['create_workspace_mcp_server']: { + create_workspace_mcp_server: { parameters: { type: 'object', properties: { @@ -266,7 +266,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['debug']: { + debug: { parameters: { properties: { context: { @@ -285,7 +285,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['delete_file']: { + delete_file: { parameters: { type: 'object', properties: { @@ -314,7 +314,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { required: ['success', 'message'], }, }, - ['delete_folder']: { + delete_folder: { parameters: { type: 'object', properties: { @@ -330,7 +330,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['delete_workflow']: { + delete_workflow: { parameters: { type: 'object', properties: { @@ -346,7 +346,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['delete_workspace_mcp_server']: { + delete_workspace_mcp_server: { parameters: { type: 'object', properties: { @@ -359,7 +359,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['deploy']: { + deploy: { parameters: { properties: { request: { @@ -373,7 +373,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['deploy_api']: { + deploy_api: { parameters: { type: 'object', properties: { @@ -447,7 +447,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { ], }, }, - ['deploy_chat']: { + deploy_chat: { parameters: { type: 'object', properties: { @@ -595,7 +595,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { ], }, }, - ['deploy_mcp']: { + deploy_mcp: { parameters: { type: 'object', properties: { @@ -711,7 +711,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { required: ['deploymentType', 'deploymentStatus'], }, }, - ['download_to_workspace_file']: { + download_to_workspace_file: { parameters: { type: 'object', properties: { @@ -730,7 +730,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['edit_content']: { + edit_content: { parameters: { type: 'object', properties: { @@ -762,7 +762,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { required: ['success', 'message'], }, }, - ['edit_workflow']: { + edit_workflow: { parameters: { type: 'object', properties: { @@ -801,13 +801,13 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['file']: { + file: { parameters: { type: 'object', }, resultSchema: undefined, }, - ['function_execute']: { + function_execute: { parameters: { type: 'object', properties: { @@ -868,7 +868,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['generate_api_key']: { + generate_api_key: { parameters: { type: 'object', properties: { @@ -886,7 +886,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['generate_image']: { + generate_image: { parameters: { type: 'object', properties: { @@ -923,7 +923,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['generate_visualization']: { + generate_visualization: { parameters: { type: 'object', properties: { @@ -963,7 +963,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['get_block_outputs']: { + get_block_outputs: { parameters: { type: 'object', properties: { @@ -984,7 +984,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['get_block_upstream_references']: { + get_block_upstream_references: { parameters: { type: 'object', properties: { @@ -1006,7 +1006,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['get_deployed_workflow_state']: { + get_deployed_workflow_state: { parameters: { type: 'object', properties: { @@ -1019,7 +1019,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['get_deployment_version']: { + get_deployment_version: { parameters: { type: 'object', properties: { @@ -1036,7 +1036,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['get_execution_summary']: { + get_execution_summary: { parameters: { type: 'object', properties: { @@ -1063,7 +1063,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['get_job_logs']: { + get_job_logs: { parameters: { type: 'object', properties: { @@ -1088,7 +1088,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['get_page_contents']: { + get_page_contents: { parameters: { type: 'object', properties: { @@ -1116,14 +1116,14 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['get_platform_actions']: { + get_platform_actions: { parameters: { type: 'object', properties: {}, }, resultSchema: undefined, }, - ['get_workflow_data']: { + get_workflow_data: { parameters: { type: 'object', properties: { @@ -1142,7 +1142,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['get_workflow_logs']: { + get_workflow_logs: { parameters: { type: 'object', properties: { @@ -1168,7 +1168,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['glob']: { + glob: { parameters: { type: 'object', properties: { @@ -1187,7 +1187,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['grep']: { + grep: { parameters: { type: 'object', properties: { @@ -1234,7 +1234,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['job']: { + job: { parameters: { properties: { request: { @@ -1247,7 +1247,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['knowledge']: { + knowledge: { parameters: { properties: { request: { @@ -1260,7 +1260,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['knowledge_base']: { + knowledge_base: { parameters: { type: 'object', properties: { @@ -1452,7 +1452,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { required: ['success', 'message'], }, }, - ['list_folders']: { + list_folders: { parameters: { type: 'object', properties: { @@ -1464,14 +1464,14 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['list_user_workspaces']: { + list_user_workspaces: { parameters: { type: 'object', properties: {}, }, resultSchema: undefined, }, - ['list_workspace_mcp_servers']: { + list_workspace_mcp_servers: { parameters: { type: 'object', properties: { @@ -1483,7 +1483,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['manage_credential']: { + manage_credential: { parameters: { type: 'object', properties: { @@ -1512,7 +1512,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['manage_custom_tool']: { + manage_custom_tool: { parameters: { type: 'object', properties: { @@ -1591,7 +1591,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['manage_job']: { + manage_job: { parameters: { type: 'object', properties: { @@ -1661,7 +1661,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['manage_mcp_tool']: { + manage_mcp_tool: { parameters: { type: 'object', properties: { @@ -1712,7 +1712,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['manage_skill']: { + manage_skill: { parameters: { type: 'object', properties: { @@ -1744,7 +1744,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['materialize_file']: { + materialize_file: { parameters: { type: 'object', properties: { @@ -1778,7 +1778,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['move_folder']: { + move_folder: { parameters: { type: 'object', properties: { @@ -1796,7 +1796,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['move_workflow']: { + move_workflow: { parameters: { type: 'object', properties: { @@ -1816,7 +1816,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['oauth_get_auth_link']: { + oauth_get_auth_link: { parameters: { type: 'object', properties: { @@ -1830,7 +1830,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['oauth_request_access']: { + oauth_request_access: { parameters: { type: 'object', properties: { @@ -1844,7 +1844,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['open_resource']: { + open_resource: { parameters: { type: 'object', properties: { @@ -1872,7 +1872,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['read']: { + read: { parameters: { type: 'object', properties: { @@ -1899,7 +1899,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['redeploy']: { + redeploy: { parameters: { type: 'object', properties: { @@ -1967,7 +1967,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { ], }, }, - ['rename_file']: { + rename_file: { parameters: { type: 'object', properties: { @@ -2002,7 +2002,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { required: ['success', 'message'], }, }, - ['rename_workflow']: { + rename_workflow: { parameters: { type: 'object', properties: { @@ -2019,7 +2019,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['research']: { + research: { parameters: { properties: { topic: { @@ -2032,7 +2032,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['respond']: { + respond: { parameters: { additionalProperties: true, properties: { @@ -2055,7 +2055,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['restore_resource']: { + restore_resource: { parameters: { type: 'object', properties: { @@ -2073,7 +2073,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['revert_to_version']: { + revert_to_version: { parameters: { type: 'object', properties: { @@ -2090,7 +2090,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['run']: { + run: { parameters: { properties: { context: { @@ -2107,7 +2107,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['run_block']: { + run_block: { parameters: { type: 'object', properties: { @@ -2139,7 +2139,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['run_from_block']: { + run_from_block: { parameters: { type: 'object', properties: { @@ -2171,7 +2171,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['run_workflow']: { + run_workflow: { parameters: { type: 'object', properties: { @@ -2199,7 +2199,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['run_workflow_until_block']: { + run_workflow_until_block: { parameters: { type: 'object', properties: { @@ -2231,7 +2231,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['scrape_page']: { + scrape_page: { parameters: { type: 'object', properties: { @@ -2252,7 +2252,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['search_documentation']: { + search_documentation: { parameters: { type: 'object', properties: { @@ -2269,7 +2269,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['search_library_docs']: { + search_library_docs: { parameters: { type: 'object', properties: { @@ -2290,7 +2290,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['search_online']: { + search_online: { parameters: { type: 'object', properties: { @@ -2331,7 +2331,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['search_patterns']: { + search_patterns: { parameters: { type: 'object', properties: { @@ -2353,7 +2353,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['set_block_enabled']: { + set_block_enabled: { parameters: { type: 'object', properties: { @@ -2375,7 +2375,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['set_environment_variables']: { + set_environment_variables: { parameters: { type: 'object', properties: { @@ -2409,7 +2409,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['set_global_workflow_variables']: { + set_global_workflow_variables: { parameters: { type: 'object', properties: { @@ -2447,7 +2447,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['superagent']: { + superagent: { parameters: { properties: { task: { @@ -2461,7 +2461,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['table']: { + table: { parameters: { properties: { request: { @@ -2474,7 +2474,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['tool_search_tool_regex']: { + tool_search_tool_regex: { parameters: { properties: { case_insensitive: { @@ -2495,7 +2495,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['update_job_history']: { + update_job_history: { parameters: { type: 'object', properties: { @@ -2513,7 +2513,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['update_workspace_mcp_server']: { + update_workspace_mcp_server: { parameters: { type: 'object', properties: { @@ -2538,7 +2538,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['user_memory']: { + user_memory: { parameters: { type: 'object', properties: { @@ -2586,7 +2586,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['user_table']: { + user_table: { parameters: { type: 'object', properties: { @@ -2777,13 +2777,13 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { required: ['success', 'message'], }, }, - ['workflow']: { + workflow: { parameters: { type: 'object', }, resultSchema: undefined, }, - ['workspace_file']: { + workspace_file: { parameters: { type: 'object', properties: { diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/slack-manifest-generator/capabilities.ts b/apps/sim/triggers/slack/capabilities.ts similarity index 78% rename from apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/slack-manifest-generator/capabilities.ts rename to apps/sim/triggers/slack/capabilities.ts index de8d7f7f2fc..d4e0e021441 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/slack-manifest-generator/capabilities.ts +++ b/apps/sim/triggers/slack/capabilities.ts @@ -3,8 +3,10 @@ * * @remarks * Each capability maps to a set of bot OAuth scopes and bot events that must - * be declared in the Slack app manifest for the capability to work. - * See https://api.slack.com/reference/manifests for the manifest schema. + * be declared in the Slack app manifest for the capability to work. The `id` + * is also used as the sub-block storage key (shape: `trigger_*` / `action_*`) + * so the same object serves as both a checkbox-list option and a manifest + * builder entry. See https://api.slack.com/reference/manifests. */ export type SlackCapabilityGroup = 'trigger' | 'action' @@ -13,114 +15,122 @@ export interface SlackCapability { id: string label: string description: string + defaultChecked: boolean group: SlackCapabilityGroup scopes: readonly string[] events: readonly string[] - defaultEnabled: boolean } export const SLACK_CAPABILITIES: readonly SlackCapability[] = [ { - id: 'mention', + id: 'trigger_mention', label: '@mention', description: 'Trigger the workflow when someone @-mentions your bot.', + defaultChecked: true, group: 'trigger', scopes: ['app_mentions:read'], events: ['app_mention'], - defaultEnabled: true, }, { - id: 'dm', + id: 'trigger_dm', label: 'Direct message', description: 'Trigger the workflow when a user sends your bot a 1:1 direct message.', + defaultChecked: true, group: 'trigger', scopes: ['im:history', 'im:read'], events: ['message.im'], - defaultEnabled: true, }, { - id: 'group_dm', + id: 'trigger_group_dm', label: 'Group direct message', description: 'Trigger on messages in multi-person DMs your bot is part of.', + defaultChecked: true, group: 'trigger', scopes: ['mpim:history', 'mpim:read'], events: ['message.mpim'], - defaultEnabled: true, }, { - id: 'public_channel', + id: 'trigger_public_channel', label: 'Public channel message', description: 'Trigger on messages in public channels your bot has been invited to.', + defaultChecked: true, group: 'trigger', scopes: ['channels:history', 'channels:read'], events: ['message.channels'], - defaultEnabled: true, }, { - id: 'private_channel', + id: 'trigger_private_channel', label: 'Private channel message', description: 'Trigger on messages in private channels your bot has been invited to.', + defaultChecked: true, group: 'trigger', scopes: ['groups:history', 'groups:read'], events: ['message.groups'], - defaultEnabled: true, }, { - id: 'public_channel_reaction', + id: 'trigger_public_channel_reaction', label: 'Public channel reaction', description: 'Trigger when emoji reactions are added or removed in public channels.', + defaultChecked: true, group: 'trigger', scopes: ['reactions:read'], events: ['reaction_added', 'reaction_removed'], - defaultEnabled: true, }, { - id: 'any_reaction', + id: 'trigger_any_reaction', label: 'Reaction (any channel)', description: 'Trigger on any emoji reaction your bot can see — public or private.', + defaultChecked: true, group: 'trigger', scopes: ['reactions:read'], events: ['reaction_added', 'reaction_removed'], - defaultEnabled: true, }, { - id: 'send', + id: 'action_send', label: 'Send messages', description: 'Let the bot post messages into channels it is a member of.', + defaultChecked: true, group: 'action', scopes: ['chat:write'], events: [], - defaultEnabled: true, }, { - id: 'add_reaction', + id: 'action_add_reaction', label: 'Add reactions', description: 'Let the bot add emoji reactions to messages.', + defaultChecked: true, group: 'action', scopes: ['reactions:write'], events: [], - defaultEnabled: true, }, { - id: 'read_files', + id: 'action_read_files', label: 'Read file attachments', description: 'Let the bot download file attachments on incoming messages.', + defaultChecked: true, group: 'action', scopes: ['files:read'], events: [], - defaultEnabled: true, }, { - id: 'read_users', + id: 'action_read_users', label: 'Look up users', description: 'Resolve user IDs to names, profiles, and email addresses.', + defaultChecked: true, group: 'action', scopes: ['users:read', 'users:read.email'], events: [], - defaultEnabled: true, }, ] as const +export const SLACK_TRIGGER_OPTIONS = SLACK_CAPABILITIES.filter((c) => c.group === 'trigger').map( + ({ id, label, description, defaultChecked }) => ({ id, label, description, defaultChecked }) +) + +export const SLACK_ACTION_OPTIONS = SLACK_CAPABILITIES.filter((c) => c.group === 'action').map( + ({ id, label, description, defaultChecked }) => ({ id, label, description, defaultChecked }) +) + const WEBHOOK_URL_PLACEHOLDER = '' export interface BuildManifestOptions { diff --git a/apps/sim/triggers/slack/webhook.ts b/apps/sim/triggers/slack/webhook.ts index 967ab09d9b7..c3eafab7fe8 100644 --- a/apps/sim/triggers/slack/webhook.ts +++ b/apps/sim/triggers/slack/webhook.ts @@ -10,6 +10,14 @@ export const slackWebhookTrigger: TriggerConfig = { icon: SlackIcon, subBlocks: [ + { + id: 'setupWizard', + title: 'Slack app setup', + type: 'slack-setup-wizard', + description: 'Walk through manifest creation, app install, and pasting credentials.', + hideFromPreview: true, + mode: 'trigger', + }, { id: 'webhookUrlDisplay', title: 'Webhook URL', @@ -51,13 +59,6 @@ export const slackWebhookTrigger: TriggerConfig = { required: false, mode: 'trigger', }, - { - id: 'manifestGenerator', - title: 'Slack App Setup', - type: 'slack-manifest-generator', - hideFromPreview: true, - mode: 'trigger', - }, ], outputs: { From 1563cbc39694e95b3fb24bee61c6a0051cfbcb72 Mon Sep 17 00:00:00 2001 From: Theodore Li Date: Sat, 18 Apr 2026 20:00:23 -0700 Subject: [PATCH 03/10] clean up modal --- .../slack-setup-wizard/slack-setup-wizard.tsx | 235 ++++++++++-------- apps/sim/triggers/slack/webhook.ts | 16 +- 2 files changed, 146 insertions(+), 105 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/slack-setup-wizard/slack-setup-wizard.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/slack-setup-wizard/slack-setup-wizard.tsx index 250e6c68e74..df4d8398b0c 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/slack-setup-wizard/slack-setup-wizard.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/slack-setup-wizard/slack-setup-wizard.tsx @@ -1,7 +1,7 @@ 'use client' -import { useCallback, useMemo, useState } from 'react' -import { Check, Clipboard, Info } from 'lucide-react' +import { type ReactNode, useCallback, useMemo, useState } from 'react' +import { Check, ChevronRight, Clipboard, Info } from 'lucide-react' import { useShallow } from 'zustand/react/shallow' import { Button, @@ -38,7 +38,6 @@ const GROUP_ORDER: readonly SlackCapabilityGroup[] = ['trigger', 'action'] as co const STEP_TITLES = [ 'Configure your bot', - 'Copy your manifest', 'Create the app in Slack', 'Paste your Signing Secret', 'Install and paste your Bot Token', @@ -47,6 +46,8 @@ const STEP_TITLES = [ const STEP_COUNT = STEP_TITLES.length +const MODAL_HEIGHT_CLASS = 'h-[580px]' + interface SlackSetupWizardProps { blockId: string isPreview?: boolean @@ -58,12 +59,10 @@ interface SlackSetupWizardProps { * * @remarks * The panel renders a single launcher button. The wizard lives in a modal - * that walks the user through: configuring the bot, copying the manifest, - * creating the app in Slack, pasting the Signing Secret, and pasting the - * Bot Token. Credentials are written directly into the sibling - * `signingSecret` and `botToken` sub-blocks via the shared sub-block store, - * so those fields in the panel are populated by the time the user clicks - * Done. + * with a fixed-height body so navigating between steps doesn't resize the + * dialog. Credentials are written directly into the sibling `signingSecret` + * and `botToken` sub-blocks via the shared sub-block store, so those fields + * in the panel are populated by the time the user clicks Done. */ export function SlackSetupWizard({ blockId, @@ -71,18 +70,24 @@ export function SlackSetupWizard({ disabled = false, }: SlackSetupWizardProps) { const [open, setOpen] = useState(false) + const launcherDisabled = isPreview || disabled return ( <> - + Setup Slack App + + - +
    {STEP_TITLES[step]} @@ -163,9 +168,9 @@ function WizardModal({ blockId, open, onOpenChange, isPreview, disabled }: Wizar
    - + - + {step === 0 && ( )} - {step === 1 && } - {step === 2 && } - {step === 3 && ( + {step === 1 && } + {step === 2 && ( )} - {step === 4 && ( + {step === 3 && ( )} - {step === 5 && ( + {step === 4 && ( )} @@ -225,10 +229,9 @@ function WizardModal({ blockId, open, onOpenChange, isPreview, disabled }: Wizar interface StepProgressProps { current: number - total: number } -function StepProgress({ current, total: _total }: StepProgressProps) { +function StepProgress({ current }: StepProgressProps) { return (
    {STEP_TITLES.map((title, i) => ( @@ -244,6 +247,30 @@ function StepProgress({ current, total: _total }: StepProgressProps) { ) } +interface SubStepListProps { + children: ReactNode +} + +function SubStepList({ children }: SubStepListProps) { + return
      {children}
    +} + +interface SubStepProps { + n: number + children: ReactNode +} + +function SubStep({ n, children }: SubStepProps) { + return ( +
  • + + {n} + +
    {children}
    +
  • + ) +} + interface StepConfigureProps { blockId: string appName: string @@ -281,7 +308,7 @@ function StepConfigure({ className='h-9 text-sm' />
    -
    +
    {GROUP_ORDER.map((group) => { const items = SLACK_CAPABILITIES.filter((c) => c.group === group) if (items.length === 0) return null @@ -301,12 +328,12 @@ function StepConfigure({ ) } -interface StepCopyProps { +interface StepCreateProps { manifestJson: string canCopy: boolean } -function StepCopy({ manifestJson, canCopy }: StepCopyProps) { +function StepCreate({ manifestJson, canCopy }: StepCreateProps) { const [copied, setCopied] = useState(false) const handleCopy = useCallback(() => { @@ -317,52 +344,52 @@ function StepCopy({ manifestJson, canCopy }: StepCopyProps) { }, [canCopy, manifestJson]) return ( -
    -

    - Copy the generated manifest. You'll paste it into Slack in the next step. -

    - -
    - ) -} - -function StepCreate() { - return ( -
    -

    - Open the{' '} - - Slack Apps page - - , click Create New AppFrom a manifest, pick your - workspace, paste the manifest, and click Create. -

    -

    Leave that Slack tab open — you'll pull a couple of values out of it in the next steps.

    +
    + + +
    Copy your manifest:
    + +
    + + Open the{' '} + + Slack Apps page + + . + + + Click Create New AppFrom a manifest and pick your + workspace. + + + Paste your manifest, then click NextCreate. + +
    ) } @@ -376,11 +403,16 @@ interface StepSecretProps { function StepSecret({ blockId, value, onChange, disabled }: StepSecretProps) { return ( -
    -

    - In your new Slack app, open Basic Information, find the{' '} - Signing Secret, and paste it here. -

    +
    + + + In your new Slack app, open Basic Information. + + + Find Signing Secret and click Show, then copy it. + + Paste it into the field below. +