Skip to content

Commit 0059be1

Browse files
BillLeoutsakosvl346Bill Leoutsakos
andauthored
fix(chat): use native attachment action (#8265)
* fix(chat): use native attachment action * fix(chat): retain disabled attachment tooltip --------- Co-authored-by: Bill Leoutsakos <billleoutsakos@Bills-MacBook-Pro.local>
1 parent 91dedb0 commit 0059be1

2 files changed

Lines changed: 95 additions & 19 deletions

File tree

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

Lines changed: 81 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@
44
import {
55
act,
66
type ButtonHTMLAttributes,
7+
cloneElement,
78
type InputHTMLAttributes,
9+
type ReactElement,
810
type ReactNode,
911
type Ref,
1012
} from 'react'
@@ -21,6 +23,9 @@ const {
2123
mockHandleRunWorkflow,
2224
mockReadSSEEvents,
2325
mockRevokeObjectURL,
26+
mockTooltipPointerEnter,
27+
executionState,
28+
registryState,
2429
WorkflowAttachmentUploadErrorMock,
2530
} = vi.hoisted(() => {
2631
class WorkflowAttachmentUploadErrorMock extends Error {
@@ -61,16 +66,14 @@ const {
6166
mockHandleRunWorkflow: vi.fn(),
6267
mockReadSSEEvents: vi.fn(),
6368
mockRevokeObjectURL: vi.fn(),
69+
mockTooltipPointerEnter: vi.fn(),
70+
executionState: { isExecuting: false },
71+
registryState: { activeWorkflowId: 'workflow-1' },
6472
WorkflowAttachmentUploadErrorMock,
6573
}
6674
})
6775

6876
vi.mock('@sim/emcn', () => ({
69-
Badge: ({
70-
children,
71-
className: _className,
72-
...props
73-
}: ButtonHTMLAttributes<HTMLButtonElement>) => <button {...props}>{children}</button>,
7477
Button: ({
7578
children,
7679
className: _className,
@@ -104,7 +107,10 @@ vi.mock('@sim/emcn', () => ({
104107
PopoverTrigger: ({ children }: { children: ReactNode }) => <div>{children}</div>,
105108
Tooltip: {
106109
Root: ({ children }: { children: ReactNode }) => <div>{children}</div>,
107-
Trigger: ({ children }: { children: ReactNode }) => <div>{children}</div>,
110+
Trigger: ({ children }: { children: ReactElement }) =>
111+
cloneElement(children as ReactElement<{ onPointerEnter?: () => void }>, {
112+
onPointerEnter: mockTooltipPointerEnter,
113+
}),
108114
Content: ({ children }: { children: ReactNode }) => <div>{children}</div>,
109115
},
110116
Trash: () => <span data-icon='Trash' />,
@@ -199,7 +205,7 @@ vi.mock('@/stores/chat/store', () => ({
199205
}))
200206

201207
vi.mock('@/stores/execution', () => ({
202-
useIsCurrentWorkflowExecuting: () => false,
208+
useIsCurrentWorkflowExecuting: () => executionState.isExecuting,
203209
}))
204210

205211
vi.mock('@/stores/operation-queue/store', () => ({
@@ -214,7 +220,7 @@ vi.mock('@/stores/terminal', () => ({
214220

215221
vi.mock('@/stores/workflows/registry/store', () => ({
216222
useWorkflowRegistry: (selector: (state: { activeWorkflowId: string }) => unknown) =>
217-
selector({ activeWorkflowId: 'workflow-1' }),
223+
selector(registryState),
218224
}))
219225

220226
vi.mock('@/stores/workflows/subblock/store', () => ({
@@ -234,6 +240,7 @@ vi.mock('@/stores/chat/utils', () => ({
234240
}))
235241

236242
import { Chat } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/chat/chat'
243+
import { MAX_CHAT_FILES } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/chat/hooks'
237244

238245
let container: HTMLDivElement
239246
let root: Root
@@ -280,6 +287,8 @@ describe('floating chat attachment uploads', () => {
280287
beforeEach(async () => {
281288
vi.useFakeTimers()
282289
vi.clearAllMocks()
290+
executionState.isExecuting = false
291+
registryState.activeWorkflowId = 'workflow-1'
283292
mockCreateObjectURL.mockReturnValue('blob:diagram-preview')
284293
mockReadSSEEvents.mockResolvedValue(undefined)
285294
vi.stubGlobal('FileReader', mockFileReader)
@@ -308,6 +317,70 @@ describe('floating chat attachment uploads', () => {
308317
vi.unstubAllGlobals()
309318
})
310319

320+
it('opens file selection through a named native action', () => {
321+
const action = container.querySelector<HTMLButtonElement>('button[aria-label="Attach file"]')
322+
const fileInput = container.querySelector<HTMLInputElement>('#floating-chat-file-input')
323+
expect(action?.type).toBe('button')
324+
expect(action?.disabled).toBe(false)
325+
expect(action?.querySelector('[data-icon="Paperclip"]')).not.toBeNull()
326+
expect(fileInput).not.toBeNull()
327+
328+
const openPicker = vi.fn()
329+
if (fileInput) fileInput.click = openPicker
330+
act(() => action?.click())
331+
expect(openPicker).toHaveBeenCalledTimes(1)
332+
})
333+
334+
it.each(['executing', 'no workflow'] as const)('prevents file selection with %s', (condition) => {
335+
executionState.isExecuting = condition === 'executing'
336+
registryState.activeWorkflowId = condition === 'no workflow' ? '' : 'workflow-1'
337+
act(() => root.render(<Chat />))
338+
339+
const action = container.querySelector<HTMLButtonElement>('button[aria-label="Attach file"]')
340+
const fileInput = container.querySelector<HTMLInputElement>('#floating-chat-file-input')
341+
expect(action?.disabled).toBe(true)
342+
expect(fileInput?.disabled).toBe(true)
343+
344+
const openPicker = vi.fn()
345+
if (fileInput) fileInput.click = openPicker
346+
act(() => action?.click())
347+
expect(openPicker).not.toHaveBeenCalled()
348+
})
349+
350+
it('keeps the tooltip hover target available when attachment is disabled', () => {
351+
executionState.isExecuting = true
352+
act(() => root.render(<Chat />))
353+
354+
const action = container.querySelector<HTMLButtonElement>('button[aria-label="Attach file"]')
355+
const trigger = action?.parentElement
356+
expect(action?.disabled).toBe(true)
357+
expect(trigger?.tagName).toBe('SPAN')
358+
expect(trigger?.hasAttribute('disabled')).toBe(false)
359+
360+
act(() => trigger?.dispatchEvent(new Event('pointerover', { bubbles: true })))
361+
expect(mockTooltipPointerEnter).toHaveBeenCalledTimes(1)
362+
})
363+
364+
it('disables attachment selection at the file limit', () => {
365+
const fileInput = container.querySelector<HTMLInputElement>('#floating-chat-file-input')
366+
if (!fileInput) throw new Error('Expected file input')
367+
Object.defineProperty(fileInput, 'files', {
368+
configurable: true,
369+
value: Array.from(
370+
{ length: MAX_CHAT_FILES },
371+
(_, index) => new File(['x'], `file-${index}.txt`)
372+
),
373+
})
374+
act(() => fileInput.dispatchEvent(new Event('change', { bubbles: true })))
375+
376+
const action = container.querySelector<HTMLButtonElement>('button[aria-label="Attach file"]')
377+
expect(action?.disabled).toBe(true)
378+
const openPicker = vi.fn()
379+
fileInput.click = openPicker
380+
act(() => action?.click())
381+
expect(openPicker).not.toHaveBeenCalled()
382+
})
383+
311384
it('uses uploaded URLs for message previews without base64 conversion', async () => {
312385
const file = new File(['diagram'], 'diagram.png', { type: 'image/png' })
313386
mockHandleRunWorkflow.mockResolvedValue({

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

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
import { type KeyboardEvent, useCallback, useEffect, useMemo, useRef, useState } from 'react'
44
import {
5-
Badge,
65
Button,
76
ComposerActionButton,
87
cn,
@@ -1090,16 +1089,20 @@ export function Chat() {
10901089
<div className='-translate-y-1/2 absolute top-1/2 right-[2px] flex items-center gap-2.5'>
10911090
<Tooltip.Root>
10921091
<Tooltip.Trigger asChild>
1093-
<Badge
1094-
onClick={() => document.getElementById('floating-chat-file-input')?.click()}
1095-
className={cn(
1096-
'cursor-pointer rounded-md border-0! bg-transparent! p-[0px]',
1097-
(!activeWorkflowId || isExecuting || chatFiles.length >= MAX_CHAT_FILES) &&
1098-
'cursor-not-allowed opacity-50'
1099-
)}
1100-
>
1101-
<Paperclip className='h-3.5! w-3.5!' />
1102-
</Badge>
1092+
<span className='inline-flex'>
1093+
<Button
1094+
type='button'
1095+
variant='ghost'
1096+
aria-label='Attach file'
1097+
onClick={() => document.getElementById('floating-chat-file-input')?.click()}
1098+
disabled={
1099+
!activeWorkflowId || isExecuting || chatFiles.length >= MAX_CHAT_FILES
1100+
}
1101+
className='size-3.5 rounded-md p-0 focus-visible:ring-1 focus-visible:ring-[var(--text-muted)] disabled:opacity-50'
1102+
>
1103+
<Paperclip className='h-3.5! w-3.5!' />
1104+
</Button>
1105+
</span>
11031106
</Tooltip.Trigger>
11041107
<Tooltip.Content>Attach file</Tooltip.Content>
11051108
</Tooltip.Root>

0 commit comments

Comments
 (0)