diff --git a/apps/docs/content/docs/en/tools/slack.mdx b/apps/docs/content/docs/en/tools/slack.mdx index 6f6feb28856..7ecfe09fe0e 100644 --- a/apps/docs/content/docs/en/tools/slack.mdx +++ b/apps/docs/content/docs/en/tools/slack.mdx @@ -1,6 +1,6 @@ --- title: Slack -description: Send, update, delete messages, add or remove reactions, manage canvases, get channel info and user presence in Slack +description: Send, update, delete messages, manage views and modals, add or remove reactions, manage canvases, get channel info and user presence in Slack --- import { BlockInfoCard } from "@/components/ui/block-info-card" @@ -39,7 +39,7 @@ If you encounter issues with the Slack integration, contact us at [help@sim.ai]( ## Usage Instructions -Integrate Slack into the workflow. Can send, update, and delete messages, send ephemeral messages visible only to a specific user, create canvases, read messages, and add or remove reactions. Requires Bot Token instead of OAuth in advanced mode. Can be used in trigger mode to trigger a workflow when a message is sent to a channel. +Integrate Slack into the workflow. Can send, update, and delete messages, send ephemeral messages visible only to a specific user, open/update/push modal views, publish Home tab views, create canvases, read messages, and add or remove reactions. Requires Bot Token instead of OAuth in advanced mode. Can be used in trigger mode to trigger a workflow when a message is sent to a channel. @@ -923,4 +923,189 @@ Create a canvas pinned to a Slack channel as its resource hub | --------- | ---- | ----------- | | `canvas_id` | string | ID of the created channel canvas | +### `slack_open_view` + +Open a modal view in Slack using a trigger_id from an interaction payload. Used to display forms, confirmations, and other interactive modals. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `authMethod` | string | No | Authentication method: oauth or bot_token | +| `botToken` | string | No | Bot token for Custom Bot | +| `triggerId` | string | Yes | Exchange a trigger to post to the user. Obtained from an interaction payload \(e.g., slash command, button click\) | +| `interactivityPointer` | string | No | Alternative to trigger_id for posting to user | +| `view` | json | Yes | A view payload object defining the modal. Must include type \("modal"\), title, and blocks array | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `view` | object | The opened modal view object | +| ↳ `id` | string | Unique view identifier | +| ↳ `team_id` | string | Workspace/team ID | +| ↳ `type` | string | View type \(e.g., "modal"\) | +| ↳ `title` | json | Plain text title object with type and text fields | +| ↳ `type` | string | Text object type \(plain_text\) | +| ↳ `text` | string | Title text content | +| ↳ `submit` | json | Plain text submit button object | +| ↳ `type` | string | Text object type \(plain_text\) | +| ↳ `text` | string | Submit button text | +| ↳ `close` | json | Plain text close button object | +| ↳ `type` | string | Text object type \(plain_text\) | +| ↳ `text` | string | Close button text | +| ↳ `blocks` | array | Block Kit blocks in the view | +| ↳ `type` | string | Block type \(section, divider, image, actions, etc.\) | +| ↳ `block_id` | string | Unique block identifier | +| ↳ `private_metadata` | string | Private metadata string passed with the view | +| ↳ `callback_id` | string | Custom identifier for the view | +| ↳ `external_id` | string | Custom external identifier \(max 255 chars, unique per workspace\) | +| ↳ `state` | json | Current state of the view with input values | +| ↳ `hash` | string | View version hash for updates | +| ↳ `clear_on_close` | boolean | Whether to clear all views in the stack when this view is closed | +| ↳ `notify_on_close` | boolean | Whether to send a view_closed event when this view is closed | +| ↳ `root_view_id` | string | ID of the root view in the view stack | +| ↳ `previous_view_id` | string | ID of the previous view in the view stack | +| ↳ `app_id` | string | Application identifier | +| ↳ `bot_id` | string | Bot identifier | + +### `slack_update_view` + +Update an existing modal view in Slack. Identify the view by view_id or external_id, and provide the updated view payload. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `authMethod` | string | No | Authentication method: oauth or bot_token | +| `botToken` | string | No | Bot token for Custom Bot | +| `viewId` | string | No | Unique identifier of the view to update. Either viewId or externalId is required | +| `externalId` | string | No | Developer-set unique identifier of the view to update \(max 255 chars\). Either viewId or externalId is required | +| `hash` | string | No | View state hash to protect against race conditions. Obtained from a previous views response | +| `view` | json | Yes | A view payload object defining the updated modal. Must include type \("modal"\), title, and blocks array. Use identical block_id and action_id values to preserve input data | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `view` | object | The updated modal view object | +| ↳ `id` | string | Unique view identifier | +| ↳ `team_id` | string | Workspace/team ID | +| ↳ `type` | string | View type \(e.g., "modal"\) | +| ↳ `title` | json | Plain text title object with type and text fields | +| ↳ `type` | string | Text object type \(plain_text\) | +| ↳ `text` | string | Title text content | +| ↳ `submit` | json | Plain text submit button object | +| ↳ `type` | string | Text object type \(plain_text\) | +| ↳ `text` | string | Submit button text | +| ↳ `close` | json | Plain text close button object | +| ↳ `type` | string | Text object type \(plain_text\) | +| ↳ `text` | string | Close button text | +| ↳ `blocks` | array | Block Kit blocks in the view | +| ↳ `type` | string | Block type \(section, divider, image, actions, etc.\) | +| ↳ `block_id` | string | Unique block identifier | +| ↳ `private_metadata` | string | Private metadata string passed with the view | +| ↳ `callback_id` | string | Custom identifier for the view | +| ↳ `external_id` | string | Custom external identifier \(max 255 chars, unique per workspace\) | +| ↳ `state` | json | Current state of the view with input values | +| ↳ `hash` | string | View version hash for updates | +| ↳ `clear_on_close` | boolean | Whether to clear all views in the stack when this view is closed | +| ↳ `notify_on_close` | boolean | Whether to send a view_closed event when this view is closed | +| ↳ `root_view_id` | string | ID of the root view in the view stack | +| ↳ `previous_view_id` | string | ID of the previous view in the view stack | +| ↳ `app_id` | string | Application identifier | +| ↳ `bot_id` | string | Bot identifier | + +### `slack_push_view` + +Push a new view onto an existing modal stack in Slack. Limited to 2 additional views after the initial modal is opened. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `authMethod` | string | No | Authentication method: oauth or bot_token | +| `botToken` | string | No | Bot token for Custom Bot | +| `triggerId` | string | Yes | Exchange a trigger to post to the user. Obtained from an interaction payload \(e.g., button click within an existing modal\) | +| `interactivityPointer` | string | No | Alternative to trigger_id for posting to user | +| `view` | json | Yes | A view payload object defining the modal to push. Must include type \("modal"\), title, and blocks array | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `view` | object | The pushed modal view object | +| ↳ `id` | string | Unique view identifier | +| ↳ `team_id` | string | Workspace/team ID | +| ↳ `type` | string | View type \(e.g., "modal"\) | +| ↳ `title` | json | Plain text title object with type and text fields | +| ↳ `type` | string | Text object type \(plain_text\) | +| ↳ `text` | string | Title text content | +| ↳ `submit` | json | Plain text submit button object | +| ↳ `type` | string | Text object type \(plain_text\) | +| ↳ `text` | string | Submit button text | +| ↳ `close` | json | Plain text close button object | +| ↳ `type` | string | Text object type \(plain_text\) | +| ↳ `text` | string | Close button text | +| ↳ `blocks` | array | Block Kit blocks in the view | +| ↳ `type` | string | Block type \(section, divider, image, actions, etc.\) | +| ↳ `block_id` | string | Unique block identifier | +| ↳ `private_metadata` | string | Private metadata string passed with the view | +| ↳ `callback_id` | string | Custom identifier for the view | +| ↳ `external_id` | string | Custom external identifier \(max 255 chars, unique per workspace\) | +| ↳ `state` | json | Current state of the view with input values | +| ↳ `hash` | string | View version hash for updates | +| ↳ `clear_on_close` | boolean | Whether to clear all views in the stack when this view is closed | +| ↳ `notify_on_close` | boolean | Whether to send a view_closed event when this view is closed | +| ↳ `root_view_id` | string | ID of the root view in the view stack | +| ↳ `previous_view_id` | string | ID of the previous view in the view stack | +| ↳ `app_id` | string | Application identifier | +| ↳ `bot_id` | string | Bot identifier | + +### `slack_publish_view` + +Publish a static view to a user + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `authMethod` | string | No | Authentication method: oauth or bot_token | +| `botToken` | string | No | Bot token for Custom Bot | +| `userId` | string | Yes | The user ID to publish the Home tab view to \(e.g., U0BPQUNTA\) | +| `hash` | string | No | View state hash to protect against race conditions. Obtained from a previous views response | +| `view` | json | Yes | A view payload object defining the Home tab. Must include type \("home"\) and blocks array | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `view` | object | The published Home tab view object | +| ↳ `id` | string | Unique view identifier | +| ↳ `team_id` | string | Workspace/team ID | +| ↳ `type` | string | View type \(e.g., "modal"\) | +| ↳ `title` | json | Plain text title object with type and text fields | +| ↳ `type` | string | Text object type \(plain_text\) | +| ↳ `text` | string | Title text content | +| ↳ `submit` | json | Plain text submit button object | +| ↳ `type` | string | Text object type \(plain_text\) | +| ↳ `text` | string | Submit button text | +| ↳ `close` | json | Plain text close button object | +| ↳ `type` | string | Text object type \(plain_text\) | +| ↳ `text` | string | Close button text | +| ↳ `blocks` | array | Block Kit blocks in the view | +| ↳ `type` | string | Block type \(section, divider, image, actions, etc.\) | +| ↳ `block_id` | string | Unique block identifier | +| ↳ `private_metadata` | string | Private metadata string passed with the view | +| ↳ `callback_id` | string | Custom identifier for the view | +| ↳ `external_id` | string | Custom external identifier \(max 255 chars, unique per workspace\) | +| ↳ `state` | json | Current state of the view with input values | +| ↳ `hash` | string | View version hash for updates | +| ↳ `clear_on_close` | boolean | Whether to clear all views in the stack when this view is closed | +| ↳ `notify_on_close` | boolean | Whether to send a view_closed event when this view is closed | +| ↳ `root_view_id` | string | ID of the root view in the view stack | +| ↳ `previous_view_id` | string | ID of the previous view in the view stack | +| ↳ `app_id` | string | Application identifier | +| ↳ `bot_id` | string | Bot identifier | + diff --git a/apps/sim/blocks/blocks/slack.ts b/apps/sim/blocks/blocks/slack.ts index 3b30c75cde7..23ba4ccfdd0 100644 --- a/apps/sim/blocks/blocks/slack.ts +++ b/apps/sim/blocks/blocks/slack.ts @@ -9,10 +9,10 @@ export const SlackBlock: BlockConfig = { type: 'slack', name: 'Slack', description: - 'Send, update, delete messages, add or remove reactions, manage canvases, get channel info and user presence in Slack', + 'Send, update, delete messages, manage views and modals, add or remove reactions, manage canvases, get channel info and user presence in Slack', authMode: AuthMode.OAuth, longDescription: - 'Integrate Slack into the workflow. Can send, update, and delete messages, send ephemeral messages visible only to a specific user, create canvases, read messages, and add or remove reactions. Requires Bot Token instead of OAuth in advanced mode. Can be used in trigger mode to trigger a workflow when a message is sent to a channel.', + 'Integrate Slack into the workflow. Can send, update, and delete messages, send ephemeral messages visible only to a specific user, open/update/push modal views, publish Home tab views, create canvases, read messages, and add or remove reactions. Requires Bot Token instead of OAuth in advanced mode. Can be used in trigger mode to trigger a workflow when a message is sent to a channel.', docsLink: 'https://docs.sim.ai/tools/slack', category: 'tools', bgColor: '#611f69', @@ -43,6 +43,10 @@ export const SlackBlock: BlockConfig = { { label: 'Get User Presence', id: 'get_user_presence' }, { label: 'Edit Canvas', id: 'edit_canvas' }, { label: 'Create Channel Canvas', id: 'create_channel_canvas' }, + { label: 'Open View', id: 'open_view' }, + { label: 'Update View', id: 'update_view' }, + { label: 'Push View', id: 'push_view' }, + { label: 'Publish View', id: 'publish_view' }, ], value: () => 'send', }, @@ -146,7 +150,17 @@ export const SlackBlock: BlockConfig = { } return { field: 'operation', - value: ['list_channels', 'list_users', 'get_user', 'get_user_presence', 'edit_canvas'], + value: [ + 'list_channels', + 'list_users', + 'get_user', + 'get_user_presence', + 'edit_canvas', + 'open_view', + 'update_view', + 'push_view', + 'publish_view', + ], not: true, and: { field: 'destinationType', @@ -171,7 +185,17 @@ export const SlackBlock: BlockConfig = { } return { field: 'operation', - value: ['list_channels', 'list_users', 'get_user', 'get_user_presence', 'edit_canvas'], + value: [ + 'list_channels', + 'list_users', + 'get_user', + 'get_user_presence', + 'edit_canvas', + 'open_view', + 'update_view', + 'push_view', + 'publish_view', + ], not: true, and: { field: 'destinationType', @@ -804,6 +828,157 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`, value: 'create_channel_canvas', }, }, + // Open View / Push View specific fields + { + id: 'viewTriggerId', + title: 'Trigger ID', + type: 'short-input', + placeholder: 'Trigger ID from interaction payload', + condition: { + field: 'operation', + value: ['open_view', 'push_view'], + }, + required: true, + }, + { + id: 'viewInteractivityPointer', + title: 'Interactivity Pointer', + type: 'short-input', + placeholder: 'Alternative to trigger_id (optional)', + condition: { + field: 'operation', + value: ['open_view', 'push_view'], + }, + mode: 'advanced', + }, + // Update View specific fields + { + id: 'viewId', + title: 'View ID', + type: 'short-input', + placeholder: 'Unique view identifier (either View ID or External ID required)', + condition: { + field: 'operation', + value: 'update_view', + }, + }, + { + id: 'viewExternalId', + title: 'External ID', + type: 'short-input', + placeholder: 'Developer-set unique identifier (max 255 chars)', + condition: { + field: 'operation', + value: 'update_view', + }, + }, + // Update View / Publish View hash field + { + id: 'viewHash', + title: 'View Hash', + type: 'short-input', + placeholder: 'View state hash for race condition protection', + condition: { + field: 'operation', + value: ['update_view', 'publish_view'], + }, + mode: 'advanced', + }, + // Publish View specific fields + { + id: 'publishUserId', + title: 'User', + type: 'user-selector', + canonicalParamId: 'publishUserId', + serviceId: 'slack', + selectorKey: 'slack.users', + placeholder: 'Select user to publish Home tab to', + mode: 'basic', + dependsOn: { all: ['authMethod'], any: ['credential', 'botToken'] }, + condition: { + field: 'operation', + value: 'publish_view', + }, + required: true, + }, + { + id: 'manualPublishUserId', + title: 'User ID', + type: 'short-input', + canonicalParamId: 'publishUserId', + placeholder: 'Enter Slack user ID (e.g., U0BPQUNTA)', + mode: 'advanced', + condition: { + field: 'operation', + value: 'publish_view', + }, + required: true, + }, + // View payload (shared across all view operations) + { + id: 'viewPayload', + title: 'View Payload', + type: 'code', + language: 'json', + placeholder: 'JSON view payload with type, title, and blocks', + condition: { + field: 'operation', + value: ['open_view', 'update_view', 'push_view', 'publish_view'], + }, + required: true, + wandConfig: { + enabled: true, + maintainHistory: true, + prompt: `You are an expert at Slack Block Kit views. +Generate ONLY a valid JSON view payload object based on the user's request. +The output MUST be a JSON object starting with { and ending with }. + +Current view: {context} + +The view object must include: +- "type": "modal" (for open/update/push) or "home" (for publish) +- "title": { "type": "plain_text", "text": "Title text", "emoji": true } (max 24 chars) +- "blocks": Array of Block Kit blocks + +Optional fields: +- "submit": { "type": "plain_text", "text": "Submit" } - Submit button text +- "close": { "type": "plain_text", "text": "Cancel" } - Close button text +- "private_metadata": String up to 3000 chars +- "callback_id": String identifier for interaction handling +- "clear_on_close": true/false +- "notify_on_close": true/false +- "external_id": Unique string per workspace (max 255 chars) + +Available block types: +- "section": Text with optional accessory. Text uses { "type": "mrkdwn", "text": "..." } or { "type": "plain_text", "text": "..." } +- "input": Form input with a label and element (plain_text_input, static_select, multi_static_select, datepicker, timepicker, checkboxes, radio_buttons) +- "header": Large text header (plain_text only) +- "divider": Horizontal rule separator +- "image": Requires "image_url" and "alt_text" +- "context": Contextual info with "elements" array +- "actions": Interactive elements like buttons + +Example modal: +{ + "type": "modal", + "title": { "type": "plain_text", "text": "My Form" }, + "submit": { "type": "plain_text", "text": "Submit" }, + "close": { "type": "plain_text", "text": "Cancel" }, + "blocks": [ + { + "type": "input", + "block_id": "input_1", + "label": { "type": "plain_text", "text": "Name" }, + "element": { "type": "plain_text_input", "action_id": "name_input" } + } + ] +} + +You can reference workflow variables using angle brackets, e.g., . +Do not include any explanations, markdown formatting, or other text outside the JSON object.`, + placeholder: 'Describe the view/modal you want to create...', + }, + }, ...getTrigger('slack_webhook').subBlocks, ], tools: { @@ -827,6 +1002,10 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`, 'slack_get_user_presence', 'slack_edit_canvas', 'slack_create_channel_canvas', + 'slack_open_view', + 'slack_update_view', + 'slack_push_view', + 'slack_publish_view', ], config: { tool: (params) => { @@ -869,6 +1048,14 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`, return 'slack_edit_canvas' case 'create_channel_canvas': return 'slack_create_channel_canvas' + case 'open_view': + return 'slack_open_view' + case 'update_view': + return 'slack_update_view' + case 'push_view': + return 'slack_push_view' + case 'publish_view': + return 'slack_publish_view' default: throw new Error(`Invalid Slack operation: ${params.operation}`) } @@ -915,6 +1102,13 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`, canvasTitle, channelCanvasTitle, channelCanvasContent, + viewTriggerId, + viewInteractivityPointer, + viewId, + viewExternalId, + viewHash, + publishUserId, + viewPayload, ...rest } = params @@ -1081,6 +1275,43 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`, baseParams.content = channelCanvasContent } break + + case 'open_view': + baseParams.triggerId = viewTriggerId + if (viewInteractivityPointer) { + baseParams.interactivityPointer = viewInteractivityPointer + } + baseParams.view = viewPayload + break + + case 'update_view': + if (viewId) { + baseParams.viewId = viewId + } + if (viewExternalId) { + baseParams.externalId = viewExternalId + } + if (viewHash) { + baseParams.hash = viewHash + } + baseParams.view = viewPayload + break + + case 'push_view': + baseParams.triggerId = viewTriggerId + if (viewInteractivityPointer) { + baseParams.interactivityPointer = viewInteractivityPointer + } + baseParams.view = viewPayload + break + + case 'publish_view': + baseParams.userId = publishUserId + if (viewHash) { + baseParams.hash = viewHash + } + baseParams.view = viewPayload + break } return baseParams @@ -1148,6 +1379,23 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`, // Create Channel Canvas inputs channelCanvasTitle: { type: 'string', description: 'Title for channel canvas' }, channelCanvasContent: { type: 'string', description: 'Content for channel canvas' }, + // View operation inputs + viewTriggerId: { type: 'string', description: 'Trigger ID from interaction payload' }, + viewInteractivityPointer: { + type: 'string', + description: 'Alternative to trigger_id for posting to user', + }, + viewId: { type: 'string', description: 'Unique view identifier for update' }, + viewExternalId: { + type: 'string', + description: 'Developer-set unique identifier for update (max 255 chars)', + }, + viewHash: { type: 'string', description: 'View state hash for race condition protection' }, + publishUserId: { + type: 'string', + description: 'User ID to publish Home tab view to', + }, + viewPayload: { type: 'json', description: 'View payload object with type, title, and blocks' }, }, outputs: { // slack_message outputs (send operation) @@ -1281,6 +1529,13 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`, 'Unix timestamp of last detected activity (only available when checking own presence)', }, + // View operation outputs (open_view, update_view, push_view, publish_view) + view: { + type: 'json', + description: + 'View object with properties: id, team_id, type, title, submit, close, blocks, private_metadata, callback_id, external_id, state, hash, clear_on_close, notify_on_close, root_view_id, previous_view_id, app_id, bot_id', + }, + // Trigger outputs (when used as webhook trigger) event_type: { type: 'string', description: 'Type of Slack event that triggered the workflow' }, channel_name: { type: 'string', description: 'Human-readable channel name' }, diff --git a/apps/sim/lib/workflows/migrations/subblock-migrations.test.ts b/apps/sim/lib/workflows/migrations/subblock-migrations.test.ts index 952e9ee3e60..47b0f26084f 100644 --- a/apps/sim/lib/workflows/migrations/subblock-migrations.test.ts +++ b/apps/sim/lib/workflows/migrations/subblock-migrations.test.ts @@ -37,13 +37,13 @@ describe('migrateSubblockIds', () => { const { blocks, migrated } = migrateSubblockIds(input) expect(migrated).toBe(true) - expect(blocks['b1'].subBlocks['knowledgeBaseSelector']).toEqual({ + expect(blocks.b1.subBlocks.knowledgeBaseSelector).toEqual({ id: 'knowledgeBaseSelector', type: 'knowledge-base-selector', value: 'kb-uuid-123', }) - expect(blocks['b1'].subBlocks['knowledgeBaseId']).toBeUndefined() - expect(blocks['b1'].subBlocks['operation'].value).toBe('search') + expect(blocks.b1.subBlocks.knowledgeBaseId).toBeUndefined() + expect(blocks.b1.subBlocks.operation.value).toBe('search') }) it('should prefer new key when both old and new exist', () => { @@ -68,8 +68,8 @@ describe('migrateSubblockIds', () => { const { blocks, migrated } = migrateSubblockIds(input) expect(migrated).toBe(true) - expect(blocks['b1'].subBlocks['knowledgeBaseSelector'].value).toBe('fresh-kb') - expect(blocks['b1'].subBlocks['knowledgeBaseId']).toBeUndefined() + expect(blocks.b1.subBlocks.knowledgeBaseSelector.value).toBe('fresh-kb') + expect(blocks.b1.subBlocks.knowledgeBaseId).toBeUndefined() }) it('should not touch blocks that already use the new key', () => { @@ -89,7 +89,7 @@ describe('migrateSubblockIds', () => { const { blocks, migrated } = migrateSubblockIds(input) expect(migrated).toBe(false) - expect(blocks['b1'].subBlocks['knowledgeBaseSelector'].value).toBe('kb-uuid') + expect(blocks.b1.subBlocks.knowledgeBaseSelector.value).toBe('kb-uuid') }) }) @@ -109,8 +109,8 @@ describe('migrateSubblockIds', () => { const { blocks } = migrateSubblockIds(input) - expect(input['b1'].subBlocks['knowledgeBaseId']).toBeDefined() - expect(blocks['b1'].subBlocks['knowledgeBaseSelector']).toBeDefined() + expect(input.b1.subBlocks.knowledgeBaseId).toBeDefined() + expect(blocks.b1.subBlocks.knowledgeBaseSelector).toBeDefined() expect(blocks).not.toBe(input) }) @@ -127,7 +127,7 @@ describe('migrateSubblockIds', () => { const { blocks, migrated } = migrateSubblockIds(input) expect(migrated).toBe(false) - expect(blocks['b1'].subBlocks['code'].value).toBe('console.log("hi")') + expect(blocks.b1.subBlocks.code.value).toBe('console.log("hi")') }) it('should migrate multiple blocks in one pass', () => { @@ -166,9 +166,9 @@ describe('migrateSubblockIds', () => { const { blocks, migrated } = migrateSubblockIds(input) expect(migrated).toBe(true) - expect(blocks['b1'].subBlocks['knowledgeBaseSelector'].value).toBe('kb-1') - expect(blocks['b2'].subBlocks['knowledgeBaseSelector'].value).toBe('kb-2') - expect(blocks['b3'].subBlocks['code']).toBeDefined() + expect(blocks.b1.subBlocks.knowledgeBaseSelector.value).toBe('kb-1') + expect(blocks.b2.subBlocks.knowledgeBaseSelector.value).toBe('kb-2') + expect(blocks.b3.subBlocks.code).toBeDefined() }) it('should handle blocks with empty subBlocks', () => { diff --git a/apps/sim/tools/registry.ts b/apps/sim/tools/registry.ts index 8015599df29..dfd9260434e 100644 --- a/apps/sim/tools/registry.ts +++ b/apps/sim/tools/registry.ts @@ -1812,8 +1812,12 @@ import { slackListUsersTool, slackMessageReaderTool, slackMessageTool, + slackOpenViewTool, + slackPublishViewTool, + slackPushViewTool, slackRemoveReactionTool, slackUpdateMessageTool, + slackUpdateViewTool, } from '@/tools/slack' import { smsSendTool } from '@/tools/sms' import { smtpSendMailTool } from '@/tools/smtp' @@ -2619,6 +2623,10 @@ export const tools: Record = { slack_remove_reaction: slackRemoveReactionTool, slack_get_channel_info: slackGetChannelInfoTool, slack_get_user_presence: slackGetUserPresenceTool, + slack_open_view: slackOpenViewTool, + slack_update_view: slackUpdateViewTool, + slack_push_view: slackPushViewTool, + slack_publish_view: slackPublishViewTool, slack_edit_canvas: slackEditCanvasTool, slack_create_channel_canvas: slackCreateChannelCanvasTool, github_repo_info: githubRepoInfoTool, diff --git a/apps/sim/tools/slack/index.ts b/apps/sim/tools/slack/index.ts index 32aba584c3f..ad8aa9ef15f 100644 --- a/apps/sim/tools/slack/index.ts +++ b/apps/sim/tools/slack/index.ts @@ -15,8 +15,12 @@ import { slackListMembersTool } from '@/tools/slack/list_members' import { slackListUsersTool } from '@/tools/slack/list_users' import { slackMessageTool } from '@/tools/slack/message' import { slackMessageReaderTool } from '@/tools/slack/message_reader' +import { slackOpenViewTool } from '@/tools/slack/open_view' +import { slackPublishViewTool } from '@/tools/slack/publish_view' +import { slackPushViewTool } from '@/tools/slack/push_view' import { slackRemoveReactionTool } from '@/tools/slack/remove_reaction' import { slackUpdateMessageTool } from '@/tools/slack/update_message' +import { slackUpdateViewTool } from '@/tools/slack/update_view' export { slackMessageTool, @@ -36,6 +40,10 @@ export { slackListUsersTool, slackGetUserTool, slackGetUserPresenceTool, + slackOpenViewTool, + slackUpdateViewTool, + slackPushViewTool, + slackPublishViewTool, slackGetMessageTool, slackGetThreadTool, } diff --git a/apps/sim/tools/slack/open_view.ts b/apps/sim/tools/slack/open_view.ts new file mode 100644 index 00000000000..9b62b51a8b8 --- /dev/null +++ b/apps/sim/tools/slack/open_view.ts @@ -0,0 +1,166 @@ +import type { SlackOpenViewParams, SlackOpenViewResponse } from '@/tools/slack/types' +import { VIEW_OUTPUT_PROPERTIES } from '@/tools/slack/types' +import type { ToolConfig } from '@/tools/types' + +export const slackOpenViewTool: ToolConfig = { + id: 'slack_open_view', + name: 'Slack Open View', + description: + 'Open a modal view in Slack using a trigger_id from an interaction payload. Used to display forms, confirmations, and other interactive modals.', + version: '1.0.0', + + oauth: { + required: true, + provider: 'slack', + }, + + params: { + authMethod: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Authentication method: oauth or bot_token', + }, + botToken: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Bot token for Custom Bot', + }, + accessToken: { + type: 'string', + required: false, + visibility: 'hidden', + description: 'OAuth access token or bot token for Slack API', + }, + triggerId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: + 'Exchange a trigger to post to the user. Obtained from an interaction payload (e.g., slash command, button click)', + }, + interactivityPointer: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Alternative to trigger_id for posting to user', + }, + view: { + type: 'json', + required: true, + visibility: 'user-or-llm', + description: + 'A view payload object defining the modal. Must include type ("modal"), title, and blocks array', + }, + }, + + request: { + url: 'https://slack.com/api/views.open', + method: 'POST', + headers: (params: SlackOpenViewParams) => ({ + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken || params.botToken}`, + }), + body: (params: SlackOpenViewParams) => { + const body: Record = { + view: typeof params.view === 'string' ? JSON.parse(params.view) : params.view, + } + + if (params.triggerId) { + body.trigger_id = params.triggerId.trim() + } + + if (params.interactivityPointer) { + body.interactivity_pointer = params.interactivityPointer.trim() + } + + return body + }, + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!data.ok) { + if (data.error === 'expired_trigger_id') { + throw new Error( + 'The trigger_id has expired. Trigger IDs are only valid for 3 seconds after the interaction.' + ) + } + if (data.error === 'invalid_trigger_id') { + throw new Error( + 'Invalid trigger_id. Ensure you are using a trigger_id from a valid interaction payload.' + ) + } + if (data.error === 'exchanged_trigger_id') { + throw new Error( + 'This trigger_id has already been used. Each trigger_id can only be used once.' + ) + } + if (data.error === 'view_too_large') { + throw new Error('The view payload is too large. Reduce the number of blocks or content.') + } + if (data.error === 'duplicate_external_id') { + throw new Error( + 'A view with this external_id already exists. Use a unique external_id per workspace.' + ) + } + if (data.error === 'invalid_arguments') { + const messages = data.response_metadata?.messages ?? [] + throw new Error( + `Invalid view arguments: ${messages.length > 0 ? messages.join(', ') : data.error}` + ) + } + if (data.error === 'missing_scope') { + throw new Error( + 'Missing required permissions. Please reconnect your Slack account with the necessary scopes.' + ) + } + if ( + data.error === 'invalid_auth' || + data.error === 'not_authed' || + data.error === 'token_expired' + ) { + throw new Error('Invalid authentication. Please check your Slack credentials.') + } + throw new Error(data.error || 'Failed to open view in Slack') + } + + const view = data.view + + return { + success: true, + output: { + view: { + id: view.id, + team_id: view.team_id ?? null, + type: view.type, + title: view.title ?? null, + submit: view.submit ?? null, + close: view.close ?? null, + blocks: view.blocks ?? [], + private_metadata: view.private_metadata ?? null, + callback_id: view.callback_id ?? null, + external_id: view.external_id ?? null, + state: view.state ?? null, + hash: view.hash ?? null, + clear_on_close: view.clear_on_close ?? false, + notify_on_close: view.notify_on_close ?? false, + root_view_id: view.root_view_id ?? null, + previous_view_id: view.previous_view_id ?? null, + app_id: view.app_id ?? null, + bot_id: view.bot_id ?? null, + }, + }, + } + }, + + outputs: { + view: { + type: 'object', + description: 'The opened modal view object', + properties: VIEW_OUTPUT_PROPERTIES, + }, + }, +} diff --git a/apps/sim/tools/slack/publish_view.ts b/apps/sim/tools/slack/publish_view.ts new file mode 100644 index 00000000000..c9f5a85dffc --- /dev/null +++ b/apps/sim/tools/slack/publish_view.ts @@ -0,0 +1,163 @@ +import type { SlackPublishViewParams, SlackPublishViewResponse } from '@/tools/slack/types' +import { VIEW_OUTPUT_PROPERTIES } from '@/tools/slack/types' +import type { ToolConfig } from '@/tools/types' + +export const slackPublishViewTool: ToolConfig = { + id: 'slack_publish_view', + name: 'Slack Publish View', + description: + "Publish a static view to a user's Home tab in Slack. Used to create or update the app's Home tab experience.", + version: '1.0.0', + + oauth: { + required: true, + provider: 'slack', + }, + + params: { + authMethod: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Authentication method: oauth or bot_token', + }, + botToken: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Bot token for Custom Bot', + }, + accessToken: { + type: 'string', + required: false, + visibility: 'hidden', + description: 'OAuth access token or bot token for Slack API', + }, + userId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The user ID to publish the Home tab view to (e.g., U0BPQUNTA)', + }, + hash: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'View state hash to protect against race conditions. Obtained from a previous views response', + }, + view: { + type: 'json', + required: true, + visibility: 'user-or-llm', + description: + 'A view payload object defining the Home tab. Must include type ("home") and blocks array', + }, + }, + + request: { + url: 'https://slack.com/api/views.publish', + method: 'POST', + headers: (params: SlackPublishViewParams) => ({ + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken || params.botToken}`, + }), + body: (params: SlackPublishViewParams) => { + const body: Record = { + user_id: params.userId.trim(), + view: typeof params.view === 'string' ? JSON.parse(params.view) : params.view, + } + + if (params.hash) { + body.hash = params.hash.trim() + } + + return body + }, + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!data.ok) { + if (data.error === 'not_found') { + throw new Error('User not found. Please check the user ID and try again.') + } + if (data.error === 'not_enabled') { + throw new Error( + 'The Home tab is not enabled for this app. Enable it in your app configuration.' + ) + } + if (data.error === 'hash_conflict') { + throw new Error( + 'The view has been modified since the hash was generated. Retrieve the latest view and try again.' + ) + } + if (data.error === 'view_too_large') { + throw new Error( + 'The view payload is too large (max 250kb). Reduce the number of blocks or content.' + ) + } + if (data.error === 'duplicate_external_id') { + throw new Error( + 'A view with this external_id already exists. Use a unique external_id per workspace.' + ) + } + if (data.error === 'invalid_arguments') { + const messages = data.response_metadata?.messages ?? [] + throw new Error( + `Invalid view arguments: ${messages.length > 0 ? messages.join(', ') : data.error}` + ) + } + if (data.error === 'missing_scope') { + throw new Error( + 'Missing required permissions. Please reconnect your Slack account with the necessary scopes.' + ) + } + if ( + data.error === 'invalid_auth' || + data.error === 'not_authed' || + data.error === 'token_expired' + ) { + throw new Error('Invalid authentication. Please check your Slack credentials.') + } + throw new Error(data.error || 'Failed to publish view in Slack') + } + + const view = data.view + + return { + success: true, + output: { + view: { + id: view.id, + team_id: view.team_id ?? null, + type: view.type, + title: view.title ?? null, + submit: view.submit ?? null, + close: view.close ?? null, + blocks: view.blocks ?? [], + private_metadata: view.private_metadata ?? null, + callback_id: view.callback_id ?? null, + external_id: view.external_id ?? null, + state: view.state ?? null, + hash: view.hash ?? null, + clear_on_close: view.clear_on_close ?? false, + notify_on_close: view.notify_on_close ?? false, + root_view_id: view.root_view_id ?? null, + previous_view_id: view.previous_view_id ?? null, + app_id: view.app_id ?? null, + bot_id: view.bot_id ?? null, + }, + }, + } + }, + + outputs: { + view: { + type: 'object', + description: 'The published Home tab view object', + properties: VIEW_OUTPUT_PROPERTIES, + }, + }, +} diff --git a/apps/sim/tools/slack/push_view.ts b/apps/sim/tools/slack/push_view.ts new file mode 100644 index 00000000000..67271faeeeb --- /dev/null +++ b/apps/sim/tools/slack/push_view.ts @@ -0,0 +1,173 @@ +import type { SlackPushViewParams, SlackPushViewResponse } from '@/tools/slack/types' +import { VIEW_OUTPUT_PROPERTIES } from '@/tools/slack/types' +import type { ToolConfig } from '@/tools/types' + +export const slackPushViewTool: ToolConfig = { + id: 'slack_push_view', + name: 'Slack Push View', + description: + 'Push a new view onto an existing modal stack in Slack. Limited to 2 additional views after the initial modal is opened.', + version: '1.0.0', + + oauth: { + required: true, + provider: 'slack', + }, + + params: { + authMethod: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Authentication method: oauth or bot_token', + }, + botToken: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Bot token for Custom Bot', + }, + accessToken: { + type: 'string', + required: false, + visibility: 'hidden', + description: 'OAuth access token or bot token for Slack API', + }, + triggerId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: + 'Exchange a trigger to post to the user. Obtained from an interaction payload (e.g., button click within an existing modal)', + }, + interactivityPointer: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Alternative to trigger_id for posting to user', + }, + view: { + type: 'json', + required: true, + visibility: 'user-or-llm', + description: + 'A view payload object defining the modal to push. Must include type ("modal"), title, and blocks array', + }, + }, + + request: { + url: 'https://slack.com/api/views.push', + method: 'POST', + headers: (params: SlackPushViewParams) => ({ + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken || params.botToken}`, + }), + body: (params: SlackPushViewParams) => { + const body: Record = { + view: typeof params.view === 'string' ? JSON.parse(params.view) : params.view, + } + + if (params.triggerId) { + body.trigger_id = params.triggerId.trim() + } + + if (params.interactivityPointer) { + body.interactivity_pointer = params.interactivityPointer.trim() + } + + return body + }, + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!data.ok) { + if (data.error === 'expired_trigger_id') { + throw new Error( + 'The trigger_id has expired. Trigger IDs are only valid for 3 seconds after the interaction.' + ) + } + if (data.error === 'invalid_trigger_id') { + throw new Error( + 'Invalid trigger_id. Ensure you are using a trigger_id from a valid interaction payload.' + ) + } + if (data.error === 'exchanged_trigger_id') { + throw new Error( + 'This trigger_id has already been used. Each trigger_id can only be used once.' + ) + } + if (data.error === 'push_limit_reached') { + throw new Error( + 'Cannot push more views. After a modal is opened, only 2 additional views can be pushed onto the stack.' + ) + } + if (data.error === 'view_too_large') { + throw new Error( + 'The view payload is too large (max 250kb). Reduce the number of blocks or content.' + ) + } + if (data.error === 'duplicate_external_id') { + throw new Error( + 'A view with this external_id already exists. Use a unique external_id per workspace.' + ) + } + if (data.error === 'invalid_arguments') { + const messages = data.response_metadata?.messages ?? [] + throw new Error( + `Invalid view arguments: ${messages.length > 0 ? messages.join(', ') : data.error}` + ) + } + if (data.error === 'missing_scope') { + throw new Error( + 'Missing required permissions. Please reconnect your Slack account with the necessary scopes.' + ) + } + if ( + data.error === 'invalid_auth' || + data.error === 'not_authed' || + data.error === 'token_expired' + ) { + throw new Error('Invalid authentication. Please check your Slack credentials.') + } + throw new Error(data.error || 'Failed to push view in Slack') + } + + const view = data.view + + return { + success: true, + output: { + view: { + id: view.id, + team_id: view.team_id ?? null, + type: view.type, + title: view.title ?? null, + submit: view.submit ?? null, + close: view.close ?? null, + blocks: view.blocks ?? [], + private_metadata: view.private_metadata ?? null, + callback_id: view.callback_id ?? null, + external_id: view.external_id ?? null, + state: view.state ?? null, + hash: view.hash ?? null, + clear_on_close: view.clear_on_close ?? false, + notify_on_close: view.notify_on_close ?? false, + root_view_id: view.root_view_id ?? null, + previous_view_id: view.previous_view_id ?? null, + app_id: view.app_id ?? null, + bot_id: view.bot_id ?? null, + }, + }, + } + }, + + outputs: { + view: { + type: 'object', + description: 'The pushed modal view object', + properties: VIEW_OUTPUT_PROPERTIES, + }, + }, +} diff --git a/apps/sim/tools/slack/types.ts b/apps/sim/tools/slack/types.ts index e41a99af21f..d16b3a27e67 100644 --- a/apps/sim/tools/slack/types.ts +++ b/apps/sim/tools/slack/types.ts @@ -478,6 +478,90 @@ export const CANVAS_OUTPUT_PROPERTIES = { title: { type: 'string', description: 'Canvas title' }, } as const satisfies Record +/** + * Output definition for modal view objects + * Based on Slack views.open response structure + */ +export const VIEW_OUTPUT_PROPERTIES = { + id: { type: 'string', description: 'Unique view identifier' }, + team_id: { type: 'string', description: 'Workspace/team ID', optional: true }, + type: { type: 'string', description: 'View type (e.g., "modal")' }, + title: { + type: 'json', + description: 'Plain text title object with type and text fields', + optional: true, + properties: { + type: { type: 'string', description: 'Text object type (plain_text)' }, + text: { type: 'string', description: 'Title text content' }, + }, + }, + submit: { + type: 'json', + description: 'Plain text submit button object', + optional: true, + properties: { + type: { type: 'string', description: 'Text object type (plain_text)' }, + text: { type: 'string', description: 'Submit button text' }, + }, + }, + close: { + type: 'json', + description: 'Plain text close button object', + optional: true, + properties: { + type: { type: 'string', description: 'Text object type (plain_text)' }, + text: { type: 'string', description: 'Close button text' }, + }, + }, + blocks: { + type: 'array', + description: 'Block Kit blocks in the view', + items: { + type: 'object', + properties: BLOCK_OUTPUT_PROPERTIES, + }, + }, + private_metadata: { + type: 'string', + description: 'Private metadata string passed with the view', + optional: true, + }, + callback_id: { type: 'string', description: 'Custom identifier for the view', optional: true }, + external_id: { + type: 'string', + description: 'Custom external identifier (max 255 chars, unique per workspace)', + optional: true, + }, + state: { + type: 'json', + description: 'Current state of the view with input values', + optional: true, + }, + hash: { type: 'string', description: 'View version hash for updates', optional: true }, + clear_on_close: { + type: 'boolean', + description: 'Whether to clear all views in the stack when this view is closed', + optional: true, + }, + notify_on_close: { + type: 'boolean', + description: 'Whether to send a view_closed event when this view is closed', + optional: true, + }, + root_view_id: { + type: 'string', + description: 'ID of the root view in the view stack', + optional: true, + }, + previous_view_id: { + type: 'string', + description: 'ID of the previous view in the view stack', + optional: true, + }, + app_id: { type: 'string', description: 'Application identifier', optional: true }, + bot_id: { type: 'string', description: 'Bot identifier', optional: true }, +} as const satisfies Record + /** * File download output properties */ @@ -629,6 +713,31 @@ export interface SlackCreateChannelCanvasParams extends SlackBaseParams { content?: string } +export interface SlackOpenViewParams extends SlackBaseParams { + triggerId: string + interactivityPointer?: string + view: object | string +} + +export interface SlackUpdateViewParams extends SlackBaseParams { + viewId?: string + externalId?: string + hash?: string + view: object | string +} + +export interface SlackPushViewParams extends SlackBaseParams { + triggerId: string + interactivityPointer?: string + view: object | string +} + +export interface SlackPublishViewParams extends SlackBaseParams { + userId: string + hash?: string + view: object | string +} + export interface SlackMessageResponse extends ToolResponse { output: { // Legacy properties for backward compatibility @@ -933,6 +1042,51 @@ export interface SlackCreateChannelCanvasResponse extends ToolResponse { } } +export interface SlackView { + id: string + team_id?: string | null + type: string + title?: { type: string; text: string } | null + submit?: { type: string; text: string } | null + close?: { type: string; text: string } | null + blocks: SlackBlock[] + private_metadata?: string | null + callback_id?: string | null + external_id?: string | null + state?: Record | null + hash?: string | null + clear_on_close?: boolean + notify_on_close?: boolean + root_view_id?: string | null + previous_view_id?: string | null + app_id?: string | null + bot_id?: string | null +} + +export interface SlackOpenViewResponse extends ToolResponse { + output: { + view: SlackView + } +} + +export interface SlackUpdateViewResponse extends ToolResponse { + output: { + view: SlackView + } +} + +export interface SlackPushViewResponse extends ToolResponse { + output: { + view: SlackView + } +} + +export interface SlackPublishViewResponse extends ToolResponse { + output: { + view: SlackView + } +} + export type SlackResponse = | SlackCanvasResponse | SlackMessageReaderResponse @@ -953,3 +1107,7 @@ export type SlackResponse = | SlackGetUserPresenceResponse | SlackEditCanvasResponse | SlackCreateChannelCanvasResponse + | SlackOpenViewResponse + | SlackUpdateViewResponse + | SlackPushViewResponse + | SlackPublishViewResponse diff --git a/apps/sim/tools/slack/update_view.ts b/apps/sim/tools/slack/update_view.ts new file mode 100644 index 00000000000..727344e74d3 --- /dev/null +++ b/apps/sim/tools/slack/update_view.ts @@ -0,0 +1,175 @@ +import type { SlackUpdateViewParams, SlackUpdateViewResponse } from '@/tools/slack/types' +import { VIEW_OUTPUT_PROPERTIES } from '@/tools/slack/types' +import type { ToolConfig } from '@/tools/types' + +export const slackUpdateViewTool: ToolConfig = { + id: 'slack_update_view', + name: 'Slack Update View', + description: + 'Update an existing modal view in Slack. Identify the view by view_id or external_id, and provide the updated view payload.', + version: '1.0.0', + + oauth: { + required: true, + provider: 'slack', + }, + + params: { + authMethod: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Authentication method: oauth or bot_token', + }, + botToken: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Bot token for Custom Bot', + }, + accessToken: { + type: 'string', + required: false, + visibility: 'hidden', + description: 'OAuth access token or bot token for Slack API', + }, + viewId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Unique identifier of the view to update. Either viewId or externalId is required', + }, + externalId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Developer-set unique identifier of the view to update (max 255 chars). Either viewId or externalId is required', + }, + hash: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'View state hash to protect against race conditions. Obtained from a previous views response', + }, + view: { + type: 'json', + required: true, + visibility: 'user-or-llm', + description: + 'A view payload object defining the updated modal. Must include type ("modal"), title, and blocks array. Use identical block_id and action_id values to preserve input data', + }, + }, + + request: { + url: 'https://slack.com/api/views.update', + method: 'POST', + headers: (params: SlackUpdateViewParams) => ({ + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken || params.botToken}`, + }), + body: (params: SlackUpdateViewParams) => { + const body: Record = { + view: typeof params.view === 'string' ? JSON.parse(params.view) : params.view, + } + + if (params.viewId) { + body.view_id = params.viewId.trim() + } + + if (params.externalId) { + body.external_id = params.externalId.trim() + } + + if (params.hash) { + body.hash = params.hash.trim() + } + + return body + }, + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!data.ok) { + if (data.error === 'not_found') { + throw new Error( + 'View not found. The provided view_id or external_id does not match an existing view.' + ) + } + if (data.error === 'hash_conflict') { + throw new Error( + 'The view has been modified since the hash was generated. Retrieve the latest view and try again.' + ) + } + if (data.error === 'view_too_large') { + throw new Error( + 'The view payload is too large (max 250kb). Reduce the number of blocks or content.' + ) + } + if (data.error === 'duplicate_external_id') { + throw new Error( + 'A view with this external_id already exists. Use a unique external_id per workspace.' + ) + } + if (data.error === 'invalid_arguments') { + const messages = data.response_metadata?.messages ?? [] + throw new Error( + `Invalid view arguments: ${messages.length > 0 ? messages.join(', ') : data.error}` + ) + } + if (data.error === 'missing_scope') { + throw new Error( + 'Missing required permissions. Please reconnect your Slack account with the necessary scopes.' + ) + } + if ( + data.error === 'invalid_auth' || + data.error === 'not_authed' || + data.error === 'token_expired' + ) { + throw new Error('Invalid authentication. Please check your Slack credentials.') + } + throw new Error(data.error || 'Failed to update view in Slack') + } + + const view = data.view + + return { + success: true, + output: { + view: { + id: view.id, + team_id: view.team_id ?? null, + type: view.type, + title: view.title ?? null, + submit: view.submit ?? null, + close: view.close ?? null, + blocks: view.blocks ?? [], + private_metadata: view.private_metadata ?? null, + callback_id: view.callback_id ?? null, + external_id: view.external_id ?? null, + state: view.state ?? null, + hash: view.hash ?? null, + clear_on_close: view.clear_on_close ?? false, + notify_on_close: view.notify_on_close ?? false, + root_view_id: view.root_view_id ?? null, + previous_view_id: view.previous_view_id ?? null, + app_id: view.app_id ?? null, + bot_id: view.bot_id ?? null, + }, + }, + } + }, + + outputs: { + view: { + type: 'object', + description: 'The updated modal view object', + properties: VIEW_OUTPUT_PROPERTIES, + }, + }, +}