Skip to content

Commit 4d98153

Browse files
fix(chat-deploy): distinguish subworkflow output navigation
1 parent 06521ce commit 4d98153

3 files changed

Lines changed: 78 additions & 41 deletions

File tree

‎apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/chat/components/output-select/output-select.dom.test.tsx‎

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,26 @@ afterEach(() => {
134134
})
135135

136136
describe('OutputSelect DOM interaction', () => {
137+
it('selects a workflow block field without entering its child workflow', () => {
138+
renderPicker()
139+
140+
const trigger = document.querySelector<HTMLElement>('[role="combobox"]')
141+
if (!trigger) throw new Error('Output picker trigger did not render')
142+
act(() => trigger.click())
143+
144+
const resultOption = [...document.querySelectorAll<HTMLElement>('[role="option"]')].find(
145+
(candidate) => candidate.textContent === 'result'
146+
)
147+
if (!resultOption) throw new Error('Workflow block result output did not render')
148+
149+
act(() => resultOption.dispatchEvent(new MouseEvent('mousedown', { bubbles: true })))
150+
151+
expect(trigger.textContent).toContain('1 output')
152+
expect(document.body.textContent).toContain('Selected')
153+
expect(document.body.textContent).toContain('invokeChild.result')
154+
expect(document.body.textContent).not.toContain('researchAgent')
155+
})
156+
137157
it('drills into and selects a child workflow output through the real medium combobox', () => {
138158
renderPicker()
139159

@@ -143,16 +163,20 @@ describe('OutputSelect DOM interaction', () => {
143163

144164
expect(document.body.textContent).toContain('invokeChild')
145165
const rootOptions = [...document.querySelectorAll<HTMLElement>('[role="option"]')]
146-
const folderOption = rootOptions.find((candidate) => candidate.textContent === 'Outputs')
147-
if (!folderOption) throw new Error('Child workflow folder did not render')
148-
expect(rootOptions.indexOf(folderOption)).toBeLessThan(
166+
expect(document.body.textContent).toContain('Subworkflows')
167+
const subworkflowOption = rootOptions.find(
168+
(candidate) => candidate.textContent === 'invokeChild'
169+
)
170+
if (!subworkflowOption) throw new Error('Child workflow navigation did not render')
171+
expect(rootOptions.indexOf(subworkflowOption)).toBeLessThan(
149172
rootOptions.findIndex((candidate) => candidate.textContent === 'result')
150173
)
151-
const floatingSurface = folderOption.closest<HTMLElement>('[data-native-surface-overlay]')
174+
175+
const floatingSurface = subworkflowOption.closest<HTMLElement>('[data-native-surface-overlay]')
152176
if (!floatingSurface) throw new Error('Output picker floating surface did not render')
153177

154178
expect(floatingSurface.className).toContain('pointer-events-auto')
155-
act(() => folderOption.dispatchEvent(new MouseEvent('mousedown', { bubbles: true })))
179+
act(() => subworkflowOption.dispatchEvent(new MouseEvent('mousedown', { bubbles: true })))
156180

157181
expect(document.body.textContent).toContain('researchAgent')
158182
const outputOption = [...document.querySelectorAll<HTMLElement>('[role="option"]')].find(
@@ -164,6 +188,6 @@ describe('OutputSelect DOM interaction', () => {
164188

165189
expect(trigger.textContent).toContain('1 output')
166190
expect(document.body.textContent).toContain('Selected')
167-
expect(document.body.textContent).toContain('invokeChild / researchAgent / content')
191+
expect(document.body.textContent).toContain('invokeChild / researchAgent.content')
168192
})
169193
})

‎apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/chat/components/output-select/output-select.test.tsx‎

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -256,7 +256,8 @@ describe('OutputSelect nested workflow menu', () => {
256256
expect(document.body.textContent).toContain('Research')
257257
expect(document.body.textContent).not.toContain('Writer')
258258

259-
clickOption('Outputs')
259+
expect([...document.querySelectorAll('[data-section]')][0]?.textContent).toBe('Subworkflows')
260+
clickOption('Research')
260261
expect(document.body.textContent).toContain('Back')
261262
expect(document.body.textContent).toContain('Writer')
262263
expect(document.body.textContent).toContain('answer')
@@ -291,7 +292,7 @@ describe('OutputSelect nested workflow menu', () => {
291292

292293
it('keeps workflow-scoped values when toggling nested outputs', () => {
293294
const onOutputSelect = renderOutputSelect([])
294-
clickOption('Outputs')
295+
clickOption('Research')
295296
clickOption('answer')
296297

297298
expect(onOutputSelect).toHaveBeenCalledWith(['child-workflow.agent_answer'])
@@ -302,16 +303,16 @@ describe('OutputSelect nested workflow menu', () => {
302303

303304
const sections = [...document.querySelectorAll('[data-section]')]
304305
expect(sections[0]?.textContent).toBe('Selected')
305-
expect(document.body.textContent).toContain('Research / Writer / answer')
306+
expect(document.body.textContent).toContain('Research / Writer.answer')
306307

307-
clickOption('Research / Writer / answer')
308+
clickOption('Research / Writer.answer')
308309
expect(onOutputSelect).toHaveBeenCalledWith([])
309310
})
310311

311312
it('preserves existing selections when choosing a nested output', () => {
312313
const onOutputSelect = renderOutputSelect(['summary_content'])
313314

314-
clickOption('Outputs')
315+
clickOption('Research')
315316
clickOption('answer')
316317

317318
expect(onOutputSelect).toHaveBeenCalledWith(['summary_content', 'child-workflow.agent_answer'])
@@ -332,14 +333,14 @@ describe('OutputSelect nested workflow menu', () => {
332333
clickOption('content')
333334
expect(onOutputSelect).toHaveBeenCalledWith(['summarizer.content'])
334335

335-
clickOption('Outputs')
336+
clickOption('Research')
336337
clickOption('answer')
337338
expect(onOutputSelect).toHaveBeenCalledWith(['child-workflow.writer.answer'])
338339
})
339340

340341
it('returns to the root menu when the owning workflow changes', () => {
341342
const onOutputSelect = renderOutputSelect([])
342-
clickOption('Outputs')
343+
clickOption('Research')
343344

344345
rerenderOutputSelect('replacement', [], onOutputSelect)
345346

@@ -349,7 +350,7 @@ describe('OutputSelect nested workflow menu', () => {
349350

350351
it('returns to the root menu when a workflow edit invalidates the active path', () => {
351352
const onOutputSelect = renderOutputSelect([])
352-
clickOption('Outputs')
353+
clickOption('Research')
353354

354355
outputMenuState.includeNestedWorkflow = false
355356
rerenderOutputSelect('root', [], onOutputSelect)

‎apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/chat/components/output-select/output-select.tsx‎

Lines changed: 39 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -274,9 +274,9 @@ function OutputSelectMenu({
274274
}
275275
}
276276

277-
const folderOption = (node: WorkflowOutputMenuNode): ComboboxOption => ({
278-
label: 'Outputs',
279-
value: `folder:${node.blockId}`,
277+
const subworkflowOption = (node: WorkflowOutputMenuNode): ComboboxOption => ({
278+
label: node.blockName,
279+
value: `subworkflow:${node.blockId}`,
280280
suffixElement: <ChevronRight className='size-[12px] text-[var(--text-tertiary)]' />,
281281
onSelect: () => setMenuPath((currentPath) => [...currentPath, node.blockId]),
282282
keepOpen: true,
@@ -293,38 +293,50 @@ function OutputSelectMenu({
293293
<span className='text-small'>{node.blockName}</span>
294294
</div>
295295
),
296-
items: [
297-
...(node.children.length > 0 ? [folderOption(node)] : []),
298-
...node.outputs
299-
.filter((output) => !selectedValueSet.has(getOutputValue(output, valueMode)))
300-
.map((output) => outputOption(output)),
301-
],
296+
items: node.outputs
297+
.filter((output) => !selectedValueSet.has(getOutputValue(output, valueMode)))
298+
.map((output) => outputOption(output)),
302299
})
303300

304-
const menuGroups: ComboboxOptionGroup[] = activeMenuNode
305-
? [
306-
{
307-
section: activeMenuNode.blockName,
308-
items: [
309-
{
310-
label: 'Back',
311-
value: `back:${activeMenuNode.blockId}`,
312-
iconElement: <ArrowLeft className='size-[14px] text-[var(--text-tertiary)]' />,
313-
onSelect: () => setMenuPath((currentPath) => currentPath.slice(0, -1)),
314-
keepOpen: true,
315-
},
316-
],
317-
},
318-
...activeMenuNode.children.map(outputGroup),
319-
]
320-
: outputMenu.map(outputGroup)
301+
const menuNodes = activeMenuNode ? activeMenuNode.children : outputMenu
302+
const subworkflowNodes = menuNodes.filter((node) => node.children.length > 0)
303+
const availableOutputNodes = menuNodes.filter((node) =>
304+
node.outputs.some((output) => !selectedValueSet.has(getOutputValue(output, valueMode)))
305+
)
306+
const menuGroups: ComboboxOptionGroup[] = [
307+
...(activeMenuNode
308+
? [
309+
{
310+
section: activeMenuNode.blockName,
311+
items: [
312+
{
313+
label: 'Back',
314+
value: `back:${activeMenuNode.blockId}`,
315+
iconElement: <ArrowLeft className='size-[14px] text-[var(--text-tertiary)]' />,
316+
onSelect: () => setMenuPath((currentPath) => currentPath.slice(0, -1)),
317+
keepOpen: true,
318+
},
319+
],
320+
},
321+
]
322+
: []),
323+
...(subworkflowNodes.length > 0
324+
? [
325+
{
326+
section: 'Subworkflows',
327+
items: subworkflowNodes.map(subworkflowOption),
328+
},
329+
]
330+
: []),
331+
...availableOutputNodes.map(outputGroup),
332+
]
321333
const selectedGroup: ComboboxOptionGroup[] =
322334
selectedOutputOptions.length > 0
323335
? [
324336
{
325337
section: 'Selected',
326338
items: selectedOutputOptions.map((output) =>
327-
outputOption(output, `${output.groupLabel} / ${output.path}`)
339+
outputOption(output, `${output.groupLabel}.${output.path}`)
328340
),
329341
},
330342
]

0 commit comments

Comments
 (0)