Skip to content

Commit f14579f

Browse files
fix(slack): preserve native event choices and post search results
1 parent cccd7ae commit f14579f

12 files changed

Lines changed: 179 additions & 20 deletions

File tree

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3519,6 +3519,9 @@ Search messages and files visible to the authorized Slack user. Requires a manag
35193519
| ↳ `page_count` | number | Page count |
35203520
| ↳ `per_page` | number | Per page |
35213521
| ↳ `total_count` | number | Total count |
3522+
| `posts` | object | Legacy post search results when returned by Slack |
3523+
| ↳ `matches` | array | Legacy post matches with provider-defined fields |
3524+
| ↳ `total` | number | Total matching posts |
35223525

35233526
### Slack Get Do Not Disturb Info
35243527

‎apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/dropdown/dropdown.test.tsx‎

Lines changed: 85 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,22 @@
22
* @vitest-environment node
33
*/
44
import { renderToStaticMarkup } from 'react-dom/server'
5-
import { describe, expect, it, vi } from 'vitest'
5+
import { beforeEach, describe, expect, it, vi } from 'vitest'
6+
import type { SubBlockConfig } from '@/blocks/types'
67

7-
const { fetched } = vi.hoisted(() => ({
8+
const { fetched, editor } = vi.hoisted(() => ({
9+
editor: {
10+
blocks: {} as Record<string, unknown>,
11+
values: {} as Record<string, unknown>,
12+
subBlocks: [] as SubBlockConfig[],
13+
credentials: {} as Record<string, { type: 'oauth' | 'service_account' }>,
14+
},
815
fetched: {
916
options: [
1017
{ id: 'col_a', label: 'Email' },
1118
{ id: 'col_b', label: 'Name' },
1219
] as { id: string; label: string }[],
20+
isDynamic: true,
1321
isLoadingOptions: false,
1422
hasLoadedOptions: true,
1523
fetchError: null as string | null,
@@ -49,7 +57,7 @@ vi.mock(
4957
() => ({
5058
useFetchedOptions: () => ({
5159
fetchedOptions: fetched.options,
52-
isDynamic: true,
60+
isDynamic: fetched.isDynamic,
5361
isLoadingOptions: fetched.isLoadingOptions,
5462
hasLoadedOptions: fetched.hasLoadedOptions,
5563
fetchError: fetched.fetchError,
@@ -88,17 +96,28 @@ vi.mock('@/hooks/use-operation-access', () => ({
8896
}))
8997
vi.mock('@/executor/handlers/response/response-handler', () => ({ ResponseBlockHandler: {} }))
9098
vi.mock('@/stores/workflows/workflow/store', () => ({
91-
useWorkflowStore: (selector: (state: unknown) => unknown) => selector({ blocks: {} }),
99+
useWorkflowStore: (selector: (state: unknown) => unknown) => selector({ blocks: editor.blocks }),
92100
}))
93101
vi.mock('@/stores/workflows/registry/store', () => ({
94102
useWorkflowRegistry: (selector: (state: unknown) => unknown) =>
95103
selector({ activeWorkflowId: 'wf-1', hydration: { workspaceId: 'workspace-1' } }),
96104
}))
97105
vi.mock('@/stores/workflows/subblock/store', () => ({
98-
useSubBlockStore: (selector: (state: unknown) => unknown) => selector({ workflowValues: {} }),
106+
EMPTY_BLOCK_SUBBLOCK_VALUES: {},
107+
useSubBlockStore: (selector: (state: unknown) => unknown) =>
108+
selector({ workflowValues: { 'wf-1': { 'block-1': editor.values } } }),
109+
}))
110+
111+
vi.mock('@/blocks/registry', () => ({
112+
getBlock: () => ({ subBlocks: editor.subBlocks }),
113+
}))
114+
vi.mock('@/hooks/queries/credentials', () => ({
115+
useWorkspaceCredential: (id?: string) => ({ data: id ? editor.credentials[id] : undefined }),
99116
}))
100117

101118
import { Dropdown } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/dropdown'
119+
import { slackOAuthTrigger } from '@/triggers/slack/oauth'
120+
import { SLACK_ALL_EVENT_OPTIONS } from '@/triggers/slack/shared'
102121

103122
function render(): string {
104123
return renderToStaticMarkup(
@@ -180,3 +199,64 @@ describe('Dropdown multi-select stale selections', () => {
180199
}
181200
})
182201
})
202+
203+
describe('Slack event credential options', () => {
204+
beforeEach(() => {
205+
fetched.isDynamic = false
206+
editor.subBlocks = slackOAuthTrigger.subBlocks
207+
editor.credentials = {
208+
native: { type: 'oauth' },
209+
custom: { type: 'service_account' },
210+
}
211+
editor.values = {}
212+
editor.blocks = {
213+
'block-1': { type: 'slack_v2', triggerMode: true, data: { canonicalModes: {} } },
214+
}
215+
})
216+
217+
function renderEvents() {
218+
return renderToStaticMarkup(
219+
<Dropdown
220+
blockId='block-1'
221+
subBlockId='eventType'
222+
options={[...SLACK_ALL_EVENT_OPTIONS]}
223+
value='message'
224+
/>
225+
)
226+
}
227+
228+
it.each([
229+
['native', true],
230+
['custom', false],
231+
['unresolved', false],
232+
['', false],
233+
])('shows native Assistant options only for a resolved OAuth credential (%s)', (id, visible) => {
234+
editor.values = { customBotCredential: id }
235+
const html = renderEvents()
236+
for (const event of ['assistant_thread_started', 'assistant_thread_context_changed']) {
237+
expect(html.includes(`data-value="${event}"`)).toBe(visible)
238+
}
239+
expect(html).toContain('data-value="message"')
240+
expect(html).toContain('data-value="agent_session_stopped"')
241+
})
242+
243+
it.each([
244+
['basic', 'native', 'custom', true],
245+
['basic', 'custom', 'native', false],
246+
['advanced', 'custom', 'native', true],
247+
['advanced', 'native', 'custom', false],
248+
])(
249+
'uses the active %s credential rather than its stale counterpart',
250+
(mode, basic, advanced, visible) => {
251+
editor.blocks = {
252+
'block-1': {
253+
type: 'slack_v2',
254+
triggerMode: true,
255+
data: { canonicalModes: { botCredential: mode } },
256+
},
257+
}
258+
editor.values = { customBotCredential: basic, manualBotCredential: advanced }
259+
expect(renderEvents().includes('data-value="assistant_thread_started"')).toBe(visible)
260+
}
261+
)
262+
})

‎apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/dropdown/dropdown.tsx‎

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,16 @@ import { ResponseBlockHandler } from '@/executor/handlers/response/response-hand
2121
import { useWorkspaceOrganizationAccounts } from '@/hooks/queries/organization-accounts'
2222
import { useDebounce } from '@/hooks/use-debounce'
2323
import { useOperationAccess } from '@/hooks/use-operation-access'
24+
import { useReactiveConditions } from '@/hooks/use-reactive-conditions'
2425
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
2526
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
2627
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
2728

2829
/** Shared empty list, so a selector-backed field with no static options keeps a stable identity. */
2930
const EMPTY_OPTIONS: DropdownOption[] = []
3031

32+
const EMPTY_SUB_BLOCKS: SubBlockConfig[] = []
33+
3134
/** Shared empty list, so a multi-select with no value keeps a stable identity across renders. */
3235
const EMPTY_MULTI_VALUES: string[] = []
3336

@@ -46,6 +49,7 @@ type DropdownOption =
4649
id: string
4750
icon?: React.ComponentType<{ className?: string }>
4851
hidden?: boolean
52+
reactiveCondition?: SubBlockConfig['reactiveCondition']
4953
}
5054

5155
/**
@@ -124,6 +128,8 @@ export const Dropdown = memo(function Dropdown({
124128
const dependsOnFields = useMemo(() => getDependsOnFields(dependsOn), [dependsOn])
125129

126130
const blockType = useWorkflowStore((state) => state.blocks[blockId]?.type)
131+
const canonicalModes = useWorkflowStore((state) => state.blocks[blockId]?.data?.canonicalModes)
132+
const triggerMode = useWorkflowStore((state) => Boolean(state.blocks[blockId]?.triggerMode))
127133
const workspaceId = useWorkflowRegistry((state) => state.hydration.workspaceId)
128134
const organizationAccounts = useWorkspaceOrganizationAccounts(
129135
workspaceId ?? undefined,
@@ -168,6 +174,23 @@ export const Dropdown = memo(function Dropdown({
168174
return options ?? EMPTY_OPTIONS
169175
}, [options, blockValues])
170176

177+
const reactiveOptions = useMemo(
178+
() =>
179+
evaluatedOptions.filter(
180+
(option): option is Exclude<DropdownOption, string> =>
181+
typeof option !== 'string' && Boolean(option.reactiveCondition)
182+
),
183+
[evaluatedOptions]
184+
)
185+
const hiddenCredentialOptions = useReactiveConditions(
186+
blockConfig?.subBlocks ?? EMPTY_SUB_BLOCKS,
187+
blockId,
188+
activeWorkflowId,
189+
canonicalModes,
190+
triggerMode,
191+
reactiveOptions
192+
)
193+
171194
const [selectorSearch, setSelectorSearch] = useState('')
172195
const debouncedSelectorSearch = useDebounce(selectorSearch.trim(), SEARCH_DEBOUNCE_MS)
173196
const activeSelectorSearch = selectorSearch.trim() === '' ? '' : debouncedSelectorSearch
@@ -286,6 +309,7 @@ export const Dropdown = memo(function Dropdown({
286309
icon: 'icon' in opt ? opt.icon : undefined,
287310
hidden:
288311
opt.hidden ||
312+
hiddenCredentialOptions.has(opt.id) ||
289313
deniedOperationIds.has(opt.id) ||
290314
(hideOrganizationOperations &&
291315
[
@@ -296,7 +320,13 @@ export const Dropdown = memo(function Dropdown({
296320
].includes(opt.id)),
297321
}
298322
})
299-
}, [allOptions, deniedOperationIds, preserveLabelCase, hideOrganizationOperations])
323+
}, [
324+
allOptions,
325+
deniedOperationIds,
326+
preserveLabelCase,
327+
hideOrganizationOperations,
328+
hiddenCredentialOptions,
329+
])
300330

301331
const optionMap = useMemo(() => {
302332
return new Map(comboboxOptions.map((opt) => [opt.value, opt.label]))

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3991,6 +3991,10 @@ export const SlackV2Block: BlockConfig<SlackResponse> = {
39913991
type: 'json',
39923992
description: 'Slack file metadata (id, name, title, mimetype, permalink)',
39933993
},
3994+
posts: {
3995+
type: 'json',
3996+
description: 'Legacy post search results (matches, total), when returned',
3997+
},
39943998
fileSearchResults: {
39953999
type: 'json',
39964000
description: 'File search results with matches and paging',

‎apps/sim/blocks/types.ts‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -354,6 +354,8 @@ export interface SubBlockConfig {
354354
icon?: React.ComponentType<{ className?: string }>
355355
group?: string
356356
hidden?: boolean
357+
/** Credential-type gate for this dropdown option. */
358+
reactiveCondition?: SubBlockConfig['reactiveCondition']
357359
defaultChecked?: boolean
358360
description?: string
359361
}[]
@@ -371,6 +373,8 @@ export interface SubBlockConfig {
371373
icon?: React.ComponentType<{ className?: string }>
372374
group?: string
373375
hidden?: boolean
376+
/** Credential-type gate for this dropdown option. */
377+
reactiveCondition?: SubBlockConfig['reactiveCondition']
374378
defaultChecked?: boolean
375379
description?: string
376380
}[])

‎apps/sim/hooks/use-reactive-conditions.ts‎

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,21 +9,22 @@ import { useWorkspaceCredential } from '@/hooks/queries/credentials'
99
import { EMPTY_BLOCK_SUBBLOCK_VALUES, useSubBlockStore } from '@/stores/workflows/subblock/store'
1010

1111
/**
12-
* Evaluates reactive conditions for subblocks. Always calls the same hooks
12+
* Evaluates reactive conditions for subblocks or supplied option targets. Always calls the same hooks
1313
* regardless of whether a reactive condition exists (Rules of Hooks).
1414
*
15-
* Returns a Set of subblock IDs that should be hidden.
15+
* Returns the target IDs that should be hidden.
1616
*/
1717
export function useReactiveConditions(
1818
subBlocks: SubBlockConfig[],
1919
blockId: string,
2020
activeWorkflowId: string | null,
2121
canonicalModeOverrides?: CanonicalModeOverrides,
22-
triggerSurface = false
22+
triggerSurface = false,
23+
conditionTargets: ReadonlyArray<Pick<SubBlockConfig, 'id' | 'reactiveCondition'>> = subBlocks
2324
): Set<string> {
2425
const reactiveSubBlocks = useMemo(
25-
() => subBlocks.filter((subBlock) => subBlock.reactiveCondition),
26-
[subBlocks]
26+
() => conditionTargets.filter((target) => target.reactiveCondition),
27+
[conditionTargets]
2728
)
2829
const reactiveCond = reactiveSubBlocks[0]?.reactiveCondition
2930

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

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

‎apps/sim/tools/slack/search_all.ts‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,12 +70,32 @@ export const slackSearchAllTool = createSlackWebApiTool({
7070
query: z.string(),
7171
messages: messageSearchSchema,
7272
files: fileSearchSchema,
73+
posts: z
74+
.object({
75+
matches: z.array(z.record(z.string(), z.unknown())),
76+
total: z.number(),
77+
})
78+
.passthrough()
79+
.optional(),
7380
})
7481
.transform(({ files, ...rest }) => ({ ...rest, fileSearchResults: files })),
7582
outputs: {
7683
ok: { type: 'boolean', description: 'Ok' },
7784
query: { type: 'string', description: 'Query' },
7885
messages: { ...messageSearchOutput },
7986
fileSearchResults: { ...fileSearchOutput },
87+
posts: {
88+
type: 'object',
89+
description: 'Legacy post search results when returned by Slack',
90+
optional: true,
91+
properties: {
92+
matches: {
93+
type: 'array',
94+
description: 'Legacy post matches with provider-defined fields',
95+
items: { type: 'object', description: 'Provider-defined post match' },
96+
},
97+
total: { type: 'number', description: 'Total matching posts' },
98+
},
99+
},
80100
},
81101
})

‎apps/sim/tools/slack/workflow-operations.test.ts‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,17 @@ describe('Slack workflow API response contracts', () => {
162162
}
163163
})
164164

165+
it('preserves optional legacy posts and their provider-defined fields in combined search', async () => {
166+
const posts = {
167+
matches: [{ id: 'F_POST', title: 'Example post', custom_field: ['value'] }],
168+
total: 1,
169+
}
170+
const result = await slackSearchAllTool.transformResponse!(
171+
Response.json({ ...responses.slack_search_all[0], posts })
172+
)
173+
expect(result.output.posts).toEqual(posts)
174+
})
175+
165176
it('exercises documented DND and emoji fields beyond the minimal success envelopes', async () => {
166177
const dnd = responses.slack_get_dnd_info.find((sample) => 'dnd_enabled' in sample)!
167178
expect(dnd).toBeDefined()

‎apps/sim/triggers/slack/oauth.test.ts‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,12 +44,14 @@ describe('Slack trigger extended-scope capability', () => {
4444
expect(SIM_SUBSCRIBED_EVENTS).not.toEqual(expect.arrayContaining(agentEvents))
4545
})
4646

47-
it('keeps legacy Assistant events readable but hides them from new trigger selections', () => {
47+
it('gates legacy Assistant choices to native OAuth while keeping Agent View events available', () => {
4848
const field = slackOAuthTrigger.subBlocks.find(({ id }) => id === 'eventType')!
4949
const options = field.options
5050
if (!Array.isArray(options)) throw new Error('Expected event options')
5151
for (const id of ['assistant_thread_started', 'assistant_thread_context_changed']) {
52-
expect(options.find((option) => option.id === id)).toMatchObject({ hidden: true })
52+
expect(options.find((option) => option.id === id)).toMatchObject({
53+
reactiveCondition: { watchFields: ['customBotCredential'], requiredType: 'oauth' },
54+
})
5355
expect(SIM_SUBSCRIBED_EVENTS).toContain(id)
5456
}
5557
for (const id of ['app_home_opened', 'app_context_changed'])

0 commit comments

Comments
 (0)