Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
24 changes: 12 additions & 12 deletions apps/sim/lib/workflows/migrations/subblock-migrations.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand All @@ -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', () => {
Expand All @@ -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')
})
})

Expand All @@ -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)
})

Expand All @@ -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', () => {
Expand Down Expand Up @@ -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', () => {
Expand Down
8 changes: 8 additions & 0 deletions apps/sim/tools/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -2619,6 +2623,10 @@ export const tools: Record<string, ToolConfig> = {
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,
Expand Down
8 changes: 8 additions & 0 deletions apps/sim/tools/slack/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -36,6 +40,10 @@ export {
slackListUsersTool,
slackGetUserTool,
slackGetUserPresenceTool,
slackOpenViewTool,
slackUpdateViewTool,
slackPushViewTool,
slackPublishViewTool,
slackGetMessageTool,
slackGetThreadTool,
}
166 changes: 166 additions & 0 deletions apps/sim/tools/slack/open_view.ts
Original file line number Diff line number Diff line change
@@ -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<SlackOpenViewParams, SlackOpenViewResponse> = {
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<string, unknown> = {
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,
},
},
}
Comment thread
waleedlatif1 marked this conversation as resolved.
},

outputs: {
view: {
type: 'object',
description: 'The opened modal view object',
properties: VIEW_OUTPUT_PROPERTIES,
},
},
}
Loading