improvement(slack-trigger): expose view, message, and state on interactivity payloads (#5279)

The native Slack trigger flattened every interaction into scalar event.* fields
and dropped the structured objects, so view_submission and block-rewrite
workflows could not read view.state.values, view.private_metadata, or
message.blocks. Pass the full Slack view and message objects through, plus the
top-level block_actions state (state.values), and declare them in the trigger
outputs so they surface in the editor. Additive and backwards compatible:
existing flattened fields are unchanged and new fields default to null.
This commit is contained in:
Waleed
2026-06-29 22:15:05 -07:00
committed by GitHub
parent 948a5cb18a
commit 98d84f23c9
3 changed files with 90 additions and 1 deletions
+50 -1
View File
@@ -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',
@@ -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 () => {
+25
View File
@@ -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[]
}
@@ -104,6 +123,9 @@ function createSlackEvent(): SlackTriggerEvent {
callback_id: '',
api_app_id: '',
message_ts: '',
view: null,
message: null,
state: null,
hasFiles: false,
files: [],
}
@@ -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
+15
View File
@@ -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',