Skip to content

Commit 51b58bd

Browse files
feat(slack): add List sharing operation
1 parent d9db35e commit 51b58bd

14 files changed

Lines changed: 321 additions & 7 deletions

File tree

‎apps/docs/content/docs/integrations/slack.mdx‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1546,6 +1546,25 @@ Rename an existing Slack List.
15461546
| --------- | ---- | ----------- |
15471547
| `ok` | boolean | Whether Slack completed the operation |
15481548

1549+
### Slack Lists Share List
1550+
1551+
Grant users or channels access to a Slack List. Provide exactly one of userIds or channelIds. Read allows viewing, write allows editing, and owner grants ownership to users only; only a current owner can grant ownership.
1552+
1553+
#### Input
1554+
1555+
| Parameter | Type | Required | Description |
1556+
| --------- | ---- | -------- | ----------- |
1557+
| `listId` | string | Yes | Slack List ID, from the List URL or Create List output |
1558+
| `accessLevel` | string | Yes | Access to grant: read, write, or owner. Ownership can only be granted to users. |
1559+
| `userIds` | json | No | Nonempty JSON array of Slack user IDs, e.g. \["U0123456789"\]. Omit channelIds. |
1560+
| `channelIds` | json | No | Nonempty JSON array of Slack channel IDs, e.g. \["C0123456789"\]. Omit userIds. Supports read or write access. |
1561+
1562+
#### Output
1563+
1564+
| Parameter | Type | Description |
1565+
| --------- | ---- | ----------- |
1566+
| `ok` | boolean | Whether Slack completed the operation |
1567+
15491568
### Slack Lists List Items
15501569

15511570
Read a page of Slack List rows and optionally its column schema. Pass nextCursor as cursor to continue; an empty nextCursor ends pagination.
@@ -2186,6 +2205,7 @@ Set the purpose (description) for a Slack channel (max 250 characters).
21862205

21872206
Lists actions require a custom Slack bot and a paid Slack plan. **Manage Lists** is selected by default during bot setup. For an existing app, add `lists:read` and `lists:write` in Slack, reinstall it, then reconnect in Sim.
21882207

2208+
- **Share List** grants access to users or channels. Choose **Share With**, enter a JSON array of Slack IDs, and select **Can view**, **Can edit**, or **Owner**. Ownership is for users only and requires the current owner. Creating a List does not automatically share it with your personal Slack account. See Slack's [sharing reference](https://docs.slack.dev/reference/methods/slackLists.access.set/).
21892209
- Run **Read List Items** with **Include List Schema** enabled to get the column IDs used in `column_id`. Updates also need the row's `id` as `row_id`.
21902210
- **Initial Fields** and **Cells** accept JSON arrays of typed values. Text uses `rich_text`; checkboxes use booleans. See Slack's [field formats](https://docs.slack.dev/reference/methods/slackLists.items.create/) for examples.
21912211
- To read every row, pass `nextCursor` into **Cursor** until it is empty. **Archived Rows** reads archived rows instead of active rows.

‎apps/sim/blocks/blocks/slack-resources.test.ts‎

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,17 @@ import { buildSlackManifest } from '@/triggers/slack/capabilities'
99
const LIST_OPERATIONS = [
1010
['create_list', 'slack_lists_create'],
1111
['rename_list', 'slack_lists_update'],
12+
['share_list', 'slack_lists_access_set'],
1213
['list_items', 'slack_lists_items_list'],
1314
['get_list_item', 'slack_lists_items_info'],
1415
['create_list_item', 'slack_lists_items_create'],
1516
['update_list_items', 'slack_lists_items_update'],
1617
['delete_list_item', 'slack_lists_items_delete'],
1718
] as const
1819

19-
function visibleFields(operation: string) {
20+
function visibleFields(operation: string, values: Record<string, unknown> = {}) {
2021
return getSlackV2ActionSubBlocks()
21-
.filter((field) => evaluateSubBlockCondition(field.condition, { operation }))
22+
.filter((field) => evaluateSubBlockCondition(field.condition, { operation, ...values }))
2223
.map((field) => field.id)
2324
}
2425

@@ -27,6 +28,42 @@ function mapParams(params: Record<string, unknown>) {
2728
}
2829

2930
describe('Slack List and Canvas operations', () => {
31+
it('shares with only the selected recipient kind and defaults to view access', () => {
32+
expect(
33+
mapParams({
34+
operation: 'share_list',
35+
listCredentialId: 'bot',
36+
listId: 'F1',
37+
listShareUserIds: '["U1"]',
38+
listShareChannelIds: 'stale invalid JSON',
39+
})
40+
).toEqual({ credential: 'bot', listId: 'F1', accessLevel: 'read', userIds: ['U1'] })
41+
expect(
42+
mapParams({
43+
operation: 'share_list',
44+
listCredentialId: 'bot',
45+
listId: 'F1',
46+
listShareTarget: 'channels',
47+
listAccessLevel: 'write',
48+
listShareChannelIds: ['C1'],
49+
listShareUserIds: 'stale invalid JSON',
50+
})
51+
).toEqual({ credential: 'bot', listId: 'F1', accessLevel: 'write', channelIds: ['C1'] })
52+
for (const [target, shown, hidden] of [
53+
['users', 'listShareUserIds', 'listShareChannelIds'],
54+
['channels', 'listShareChannelIds', 'listShareUserIds'],
55+
]) {
56+
const fields = visibleFields('share_list', { listShareTarget: target })
57+
expect(fields).toEqual(expect.arrayContaining(['listId', 'listAccessLevel', shown]))
58+
expect(fields).not.toContain(hidden)
59+
}
60+
expect(() => mapParams({ operation: 'share_list', listShareUserIds: 'bad JSON' })).toThrow(
61+
'User IDs'
62+
)
63+
expect(() => mapParams({ operation: 'share_list', listShareTarget: 'everyone' })).toThrow(
64+
'Share With'
65+
)
66+
})
3067
it.each(LIST_OPERATIONS)('offers %s inside the Slack block using %s', (operationId, toolId) => {
3168
const operation = SlackV2Block.subBlocks.find((field) => field.id === 'operation')!
3269
const options =

‎apps/sim/blocks/blocks/slack.ts‎

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ const SLACK_V2_AGENT_OPERATIONS = [
3030
const SLACK_V2_LIST_OPERATIONS = [
3131
'create_list',
3232
'rename_list',
33+
'share_list',
3334
'list_items',
3435
'get_list_item',
3536
'create_list_item',
@@ -3206,6 +3207,19 @@ function mapSlackListParams(params: Record<string, unknown>): Record<string, unk
32063207
case 'rename_list':
32073208
result.name = params.listName
32083209
break
3210+
case 'share_list':
3211+
result.accessLevel = params.listAccessLevel ?? 'read'
3212+
switch (params.listShareTarget ?? 'users') {
3213+
case 'users':
3214+
result.userIds = parseOptionalJsonInput(params.listShareUserIds, 'User IDs')
3215+
break
3216+
case 'channels':
3217+
result.channelIds = parseOptionalJsonInput(params.listShareChannelIds, 'Channel IDs')
3218+
break
3219+
default:
3220+
throw new Error('Share With must be users or channels')
3221+
}
3222+
break
32093223
case 'list_items':
32103224
result.limit = parseOptionalNumberInput(params.listLimit, 'Page Size', {
32113225
integer: true,
@@ -3272,6 +3286,7 @@ function getSlackV2ListSubBlocks(): SubBlockConfig[] {
32723286
field: 'operation',
32733287
value: [
32743288
'rename_list',
3289+
'share_list',
32753290
'list_items',
32763291
'get_list_item',
32773292
'create_list_item',
@@ -3280,6 +3295,57 @@ function getSlackV2ListSubBlocks(): SubBlockConfig[] {
32803295
],
32813296
},
32823297
},
3298+
{
3299+
id: 'listShareTarget',
3300+
title: 'Share With',
3301+
type: 'dropdown',
3302+
options: [
3303+
{ label: 'Users', id: 'users' },
3304+
{ label: 'Channels', id: 'channels' },
3305+
],
3306+
value: () => 'users',
3307+
required: true,
3308+
condition: { field: 'operation', value: 'share_list' },
3309+
},
3310+
{
3311+
id: 'listShareUserIds',
3312+
title: 'User IDs',
3313+
type: 'code',
3314+
language: 'json',
3315+
placeholder: '["U0123456789"]',
3316+
required: true,
3317+
condition: {
3318+
field: 'operation',
3319+
value: 'share_list',
3320+
and: { field: 'listShareTarget', value: 'users' },
3321+
},
3322+
},
3323+
{
3324+
id: 'listShareChannelIds',
3325+
title: 'Channel IDs',
3326+
type: 'code',
3327+
language: 'json',
3328+
placeholder: '["C0123456789"]',
3329+
required: true,
3330+
condition: {
3331+
field: 'operation',
3332+
value: 'share_list',
3333+
and: { field: 'listShareTarget', value: 'channels' },
3334+
},
3335+
},
3336+
{
3337+
id: 'listAccessLevel',
3338+
title: 'Access Level',
3339+
type: 'dropdown',
3340+
options: [
3341+
{ label: 'Can view', id: 'read' },
3342+
{ label: 'Can edit', id: 'write' },
3343+
{ label: 'Owner (users only)', id: 'owner' },
3344+
],
3345+
value: () => 'read',
3346+
required: true,
3347+
condition: { field: 'operation', value: 'share_list' },
3348+
},
32833349
{
32843350
id: 'listName',
32853351
title: 'Name',
@@ -3429,6 +3495,10 @@ export function getSlackV2OperationSentences() {
34293495
{ text: 'Rename list', field: 'listId', core: true },
34303496
{ text: 'to', field: 'listName' },
34313497
],
3498+
share_list: [
3499+
{ text: 'Share list', field: 'listId', core: true },
3500+
{ text: 'with', field: ['listShareUserIds', 'listShareChannelIds'], core: true },
3501+
],
34323502
list_items: [{ text: 'Read rows from', field: 'listId', core: true }],
34333503
get_list_item: [
34343504
{ text: 'Read row', field: 'listItemId', core: true },
@@ -3549,6 +3619,7 @@ export const SlackV2Block: BlockConfig<SlackResponse> = {
35493619
{ label: 'Delete Canvas', id: 'delete_canvas' },
35503620
{ label: 'Create List', id: 'create_list' },
35513621
{ label: 'Rename List', id: 'rename_list' },
3622+
{ label: 'Share List', id: 'share_list' },
35523623
{ label: 'Read List Items', id: 'list_items' },
35533624
{ label: 'Get List Item', id: 'get_list_item' },
35543625
{ label: 'Create List Item', id: 'create_list_item' },
@@ -3611,6 +3682,7 @@ export const SlackV2Block: BlockConfig<SlackResponse> = {
36113682
'slack_delete_canvas',
36123683
'slack_lists_create',
36133684
'slack_lists_update',
3685+
'slack_lists_access_set',
36143686
'slack_lists_items_list',
36153687
'slack_lists_items_info',
36163688
'slack_lists_items_create',
@@ -3638,6 +3710,8 @@ export const SlackV2Block: BlockConfig<SlackResponse> = {
36383710
return 'slack_lists_create'
36393711
case 'rename_list':
36403712
return 'slack_lists_update'
3713+
case 'share_list':
3714+
return 'slack_lists_access_set'
36413715
case 'list_items':
36423716
return 'slack_lists_items_list'
36433717
case 'get_list_item':
@@ -3696,6 +3770,13 @@ export const SlackV2Block: BlockConfig<SlackResponse> = {
36963770
listId: { type: 'string', description: 'Slack List ID' },
36973771
listItemId: { type: 'string', description: 'Slack row ID' },
36983772
listName: { type: 'string', description: 'List name' },
3773+
listShareTarget: { type: 'string', description: 'Share with users or channels' },
3774+
listShareUserIds: { type: 'json', description: 'Slack user IDs to grant List access' },
3775+
listShareChannelIds: { type: 'json', description: 'Slack channel IDs to grant List access' },
3776+
listAccessLevel: {
3777+
type: 'string',
3778+
description: 'List access: read, write, or owner (users only)',
3779+
},
36993780
listSchema: { type: 'json', description: 'Column definitions' },
37003781
listInitialFields: { type: 'json', description: 'Initial typed cell values' },
37013782
listCells: { type: 'json', description: 'Typed cell updates with row_id and column_id' },

‎apps/sim/tools/generated/tool-ids.ts‎

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

‎apps/sim/tools/generated/tool-metadata.ts‎

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

‎apps/sim/tools/generated/tool-outputs.ts‎

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

‎apps/sim/tools/registry.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4765,6 +4765,7 @@ import {
47654765
slackUpdateViewTool,
47664766
} from '@/tools/slack'
47674767
import {
4768+
slackListsAccessSetTool,
47684769
slackListsCreateTool,
47694770
slackListsItemsCreateTool,
47704771
slackListsItemsDeleteTool,
@@ -7290,6 +7291,7 @@ export const tools: Record<string, ExecutableToolConfig> = {
72907291
slack_set_suggested_prompts_v2: slackSetSuggestedPromptsV2Tool,
72917292
slack_set_agent_session_status_v2: slackSetAgentSessionStatusV2Tool,
72927293
slack_rename_agent_session_v2: slackRenameAgentSessionV2Tool,
7294+
slack_lists_access_set: slackListsAccessSetTool,
72937295
slack_lists_create: slackListsCreateTool,
72947296
slack_lists_update: slackListsUpdateTool,
72957297
slack_lists_items_list: slackListsItemsListTool,

‎apps/sim/tools/slack/oauth-scopes.test.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ describe('Slack personal-token scope policy', () => {
5454
})
5555

5656
it.each([
57+
'slack_lists_access_set',
5758
'slack_lists_create',
5859
'slack_lists_update',
5960
'slack_lists_items_list',
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import { readSlackResponse } from '@/tools/slack/api'
2+
import type { SlackListsAccessSetParams, SlackListsOkResponse } from '@/tools/slack_lists/types'
3+
import { OK_OUTPUT } from '@/tools/slack_lists/types'
4+
import { slackListsHeaders } from '@/tools/slack_lists/utils'
5+
import { parseSlackListsJson, slackListAccessSchema } from '@/tools/slack_lists/validation'
6+
import type { ToolConfig } from '@/tools/types'
7+
8+
export const slackListsAccessSetTool: ToolConfig<SlackListsAccessSetParams, SlackListsOkResponse> =
9+
{
10+
id: 'slack_lists_access_set',
11+
name: 'Slack Lists Share List',
12+
description:
13+
'Grant users or channels access to a Slack List. Provide exactly one of userIds or channelIds. Read allows viewing, write allows editing, and owner grants ownership to users only; only a current owner can grant ownership.',
14+
version: '1.0.0',
15+
oauth: {
16+
required: true,
17+
provider: 'slack',
18+
credentialKind: 'service-account',
19+
requiredScopes: ['lists:write'],
20+
},
21+
params: {
22+
accessToken: {
23+
type: 'string',
24+
required: true,
25+
visibility: 'hidden',
26+
description: 'Resolved custom Slack bot token',
27+
},
28+
listId: {
29+
type: 'string',
30+
required: true,
31+
visibility: 'user-or-llm',
32+
description: 'Slack List ID, from the List URL or Create List output',
33+
},
34+
accessLevel: {
35+
type: 'string',
36+
required: true,
37+
visibility: 'user-or-llm',
38+
description:
39+
'Access to grant: read, write, or owner. Ownership can only be granted to users.',
40+
},
41+
userIds: {
42+
type: 'json',
43+
required: false,
44+
visibility: 'user-or-llm',
45+
description:
46+
'Nonempty JSON array of Slack user IDs, e.g. ["U0123456789"]. Omit channelIds.',
47+
},
48+
channelIds: {
49+
type: 'json',
50+
required: false,
51+
visibility: 'user-or-llm',
52+
description:
53+
'Nonempty JSON array of Slack channel IDs, e.g. ["C0123456789"]. Omit userIds. Supports read or write access.',
54+
},
55+
},
56+
request: {
57+
url: 'https://slack.com/api/slackLists.access.set',
58+
method: 'POST',
59+
headers: slackListsHeaders,
60+
body: (params) =>
61+
slackListAccessSchema.parse({
62+
list_id: params.listId,
63+
access_level: params.accessLevel,
64+
user_ids: parseSlackListsJson(
65+
params.userIds,
66+
slackListAccessSchema.shape.user_ids,
67+
'userIds'
68+
),
69+
channel_ids: parseSlackListsJson(
70+
params.channelIds,
71+
slackListAccessSchema.shape.channel_ids,
72+
'channelIds'
73+
),
74+
}),
75+
},
76+
transformResponse: async (response) => {
77+
await readSlackResponse(response)
78+
return { success: true, output: { ok: true } }
79+
},
80+
outputs: { ok: OK_OUTPUT },
81+
}

‎apps/sim/tools/slack_lists/index.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
export { slackListsAccessSetTool } from '@/tools/slack_lists/access_set'
12
export { slackListsCreateTool } from '@/tools/slack_lists/create'
23
export { slackListsItemsCreateTool } from '@/tools/slack_lists/items_create'
34
export { slackListsItemsDeleteTool } from '@/tools/slack_lists/items_delete'

0 commit comments

Comments
 (0)