mirror of
https://github.com/simstudioai/sim.git
synced 2026-09-24 15:45:35 +08:00
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:
@@ -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 () => {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user