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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 50 additions & 1 deletion apps/sim/lib/webhooks/providers/slack.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,13 @@ describe('slackHandler formatInput - interactivity (block_actions)', () => {
trigger_id: 'trigger-1',
response_url: 'https://hooks.slack.com/actions/abc',
container: { message_ts: '999.000' },
message: { ts: '999.000', text: 'Approve this?', thread_ts: '999.aaa' },
message: {
ts: '999.000',
text: 'Approve this?',
thread_ts: '999.aaa',
blocks: [{ type: 'section', block_id: 'b1', text: { type: 'mrkdwn', text: 'Approve?' } }],
},
state: { values: { reason_block: { reason_input: { value: 'looks good' } } } },
actions: [
{
action_id: 'approve_btn',
Expand Down Expand Up @@ -85,6 +91,49 @@ describe('slackHandler formatInput - interactivity (block_actions)', () => {
expect(event.api_app_id).toBe('A123')
expect(Array.isArray(event.actions)).toBe(true)
expect((event.actions as unknown[]).length).toBe(1)
const message = event.message as Record<string, unknown>
expect(message).not.toBeNull()
expect(Array.isArray(message.blocks)).toBe(true)
expect((message.blocks as unknown[]).length).toBe(1)
expect(event.view).toBeNull()
const state = event.state as { values: Record<string, Record<string, { value: string }>> }
expect(state).not.toBeNull()
expect(state.values.reason_block.reason_input.value).toBe('looks good')
})

it('carries the full view (state.values + private_metadata) through for a view_submission', async () => {
const { input } = await slackHandler.formatInput!(
ctx({
type: 'view_submission',
user: { id: 'U1', username: 'alice' },
team: { id: 'T1' },
trigger_id: 'trigger-2',
view: {
id: 'V123',
callback_id: 'create_ticket',
private_metadata: '{"thread_ts":"999.aaa"}',
hash: 'abc.def',
state: {
values: {
summary_block: { summary_input: { type: 'plain_text_input', value: 'Printer down' } },
},
},
},
})
)
const event = eventOf(input)
expect(event.event_type).toBe('view_submission')
expect(event.callback_id).toBe('create_ticket')
const view = event.view as Record<string, unknown>
expect(view).not.toBeNull()
expect(view.private_metadata).toBe('{"thread_ts":"999.aaa"}')
const values = (view.state as Record<string, unknown>).values as Record<
string,
Record<string, Record<string, unknown>>
>
expect(values.summary_block.summary_input.value).toBe('Printer down')
expect(event.message).toBeNull()
expect(event.state).toBeNull()
})

it('normalizes a static_select value and falls back to action value for text', async () => {
Expand Down
25 changes: 25 additions & 0 deletions apps/sim/lib/webhooks/providers/slack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,25 @@ interface SlackTriggerEvent {
callback_id: string
api_app_id: string
message_ts: string
/**
* Full Slack view object for modal interactions (view_submission/view_closed):
* `state.values` (submitted input values), `private_metadata`, `id`,
* `callback_id`, `hash`, etc. Null for non-modal interactions and Events API.
*/
view: Record<string, unknown> | null
/**
* Full Slack message object the interaction originated from (block_actions):
* `blocks`, `text`, `ts`, etc. — needed to rewrite the source message's blocks.
* Null when the interaction has no source message and for slash/Events API.
*/
message: Record<string, unknown> | null
/**
* Top-level interactivity `state` for block_actions: the current values of all
* stateful elements in the surface (`state.values`), e.g. inputs read on a
* button click without a modal submit. Distinct from `view.state` (modal
* submissions). Null for non-block_actions payloads.
*/
state: Record<string, unknown> | null
hasFiles: boolean
files: SlackDownloadedFile[]
}
Expand Down Expand Up @@ -104,6 +123,9 @@ function createSlackEvent(): SlackTriggerEvent {
callback_id: '',
api_app_id: '',
message_ts: '',
view: null,
message: null,
state: null,
hasFiles: false,
files: [],
}
Expand Down Expand Up @@ -206,11 +228,14 @@ function formatSlackInteractive(b: Record<string, unknown>): SlackTriggerEvent {
// Prefer the source message text; fall back to the triggering action's value
// so a blocks-only message still surfaces something useful in `text`.
event.text = asString(message?.text) || event.action_value
event.message = message ?? null

event.response_url = asString(b.response_url)
event.trigger_id = asString(b.trigger_id)
const view = b.view as Record<string, unknown> | undefined
event.callback_id = asString(b.callback_id) || asString(view?.callback_id)
event.view = view ?? null
event.state = (b.state as Record<string, unknown>) ?? null
event.api_app_id = asString(b.api_app_id)

return event
Expand Down
15 changes: 15 additions & 0 deletions apps/sim/triggers/slack/webhook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,21 @@ export const slackWebhookTrigger: TriggerConfig = {
description:
'Timestamp of the message the interaction originated from. Present for block_actions',
},
view: {
type: 'json',
description:
'Full Slack view object for modal interactions: state.values (submitted input values), private_metadata, id, callback_id, and hash. Present for view_submission/view_closed; null otherwise',
},
message: {
type: 'json',
description:
'Full source message object the interaction came from, including its blocks and text. Present for block_actions on a message; null otherwise',
},
state: {
type: 'json',
description:
'Current values of all stateful elements in the surface (state.values) at the time of a block action — e.g. inputs read on a button click. Present for block_actions; null otherwise',
},
hasFiles: {
type: 'boolean',
description: 'Whether the message has file attachments',
Expand Down
Loading