fix(web): name chat image file actions

This commit is contained in:
yyh
2026-05-10 22:26:37 +08:00
parent 28d5c09b90
commit c1db129c51
2 changed files with 39 additions and 81 deletions

View File

@ -44,16 +44,14 @@ describe('FileImageItem', () => {
it('should render delete button when showDeleteAction is true', () => {
render(<FileImageItem file={createFile()} showDeleteAction />)
const buttons = screen.getAllByRole('button')
expect(buttons.length).toBeGreaterThanOrEqual(1)
expect(screen.getByRole('button', { name: 'common.operation.remove' })).toBeInTheDocument()
})
it('should call onRemove when delete button is clicked', () => {
const onRemove = vi.fn()
render(<FileImageItem file={createFile()} showDeleteAction onRemove={onRemove} />)
const buttons = screen.getAllByRole('button')
fireEvent.click(buttons[0]!)
fireEvent.click(screen.getByRole('button', { name: 'common.operation.remove' }))
expect(onRemove).toHaveBeenCalledWith('file-1')
})
@ -69,21 +67,18 @@ describe('FileImageItem', () => {
})
it('should render replay icon when upload failed', () => {
const { container } = render(<FileImageItem file={createFile({ progress: -1 })} />)
render(<FileImageItem file={createFile({ progress: -1 })} />)
// ReplayLine renders as an SVG icon with data-icon attribute
const replaySvg = container.querySelector('svg[data-icon="ReplayLine"]')
expect(replaySvg)!.toBeInTheDocument()
expect(screen.getByRole('button', { name: 'common.operation.retry' })).toBeInTheDocument()
})
it('should call onReUpload when replay icon is clicked', () => {
const onReUpload = vi.fn()
const { container } = render(
render(
<FileImageItem file={createFile({ progress: -1 })} onReUpload={onReUpload} />,
)
const replaySvg = container.querySelector('svg[data-icon="ReplayLine"]')
fireEvent.click(replaySvg!)
fireEvent.click(screen.getByRole('button', { name: 'common.operation.retry' }))
expect(onReUpload).toHaveBeenCalledWith('file-1')
})
@ -125,23 +120,16 @@ describe('FileImageItem', () => {
})
it('should render download overlay when showDownloadAction is true', () => {
const { container } = render(<FileImageItem file={createFile()} showDownloadAction />)
render(<FileImageItem file={createFile()} showDownloadAction />)
// The download icon SVG should be present
const svgs = container.querySelectorAll('svg')
expect(svgs.length).toBeGreaterThanOrEqual(1)
expect(screen.getByRole('button', { name: 'common.operation.download' })).toBeInTheDocument()
})
it('should call downloadUrl when download button is clicked', async () => {
const { downloadUrl } = await import('@/utils/download')
const { container } = render(<FileImageItem file={createFile()} showDownloadAction />)
render(<FileImageItem file={createFile()} showDownloadAction />)
// Find the RiDownloadLine SVG (it doesn't have data-icon attribute, unlike ReplayLine)
const svgs = container.querySelectorAll('svg')
const downloadSvg = Array.from(svgs).find(
svg => !svg.hasAttribute('data-icon') && !svg.querySelector('circle'),
)
fireEvent.click(downloadSvg!.parentElement!)
fireEvent.click(screen.getByRole('button', { name: 'common.operation.download' }))
expect(downloadUrl).toHaveBeenCalled()
})
@ -169,15 +157,9 @@ describe('FileImageItem', () => {
it('should use url with attachment param for download_url when url is available', async () => {
const { downloadUrl } = await import('@/utils/download')
const file = createFile({ url: 'https://example.com/photo.png' })
const { container } = render(<FileImageItem file={file} showDownloadAction />)
render(<FileImageItem file={file} showDownloadAction />)
// The download SVG should be rendered
const svgs = container.querySelectorAll('svg')
expect(svgs.length).toBeGreaterThanOrEqual(1)
const downloadSvg = Array.from(svgs).find(
svg => !svg.hasAttribute('data-icon') && !svg.querySelector('circle'),
)
fireEvent.click(downloadSvg!.parentElement!)
fireEvent.click(screen.getByRole('button', { name: 'common.operation.download' }))
expect(downloadUrl).toHaveBeenCalledWith(expect.objectContaining({
url: expect.stringContaining('as_attachment=true'),
}))
@ -186,13 +168,9 @@ describe('FileImageItem', () => {
it('should use base64Url for download_url when url is not available', async () => {
const { downloadUrl } = await import('@/utils/download')
const file = createFile({ url: undefined, base64Url: 'data:image/png;base64,abc' })
const { container } = render(<FileImageItem file={file} showDownloadAction />)
render(<FileImageItem file={file} showDownloadAction />)
const svgs = container.querySelectorAll('svg')
const downloadSvg = Array.from(svgs).find(
svg => !svg.hasAttribute('data-icon') && !svg.querySelector('circle'),
)
fireEvent.click(downloadSvg!.parentElement!)
fireEvent.click(screen.getByRole('button', { name: 'common.operation.download' }))
expect(downloadUrl).toHaveBeenCalledWith(expect.objectContaining({
url: 'data:image/png;base64,abc',
@ -223,37 +201,6 @@ describe('FileImageItem', () => {
const img = screen.getByRole('img')
fireEvent.click(img.parentElement!)
// Preview won't show because imagePreviewUrl is empty string (falsy)
// Preview won't show because imagePreviewUrl is empty string (falsy)
// Preview won't show because imagePreviewUrl is empty string (falsy)
// Preview won't show because imagePreviewUrl is empty string (falsy)
// Preview won't show because imagePreviewUrl is empty string (falsy)
// Preview won't show because imagePreviewUrl is empty string (falsy)
// Preview won't show because imagePreviewUrl is empty string (falsy)
// Preview won't show because imagePreviewUrl is empty string (falsy)
// Preview won't show because imagePreviewUrl is empty string (falsy)
// Preview won't show because imagePreviewUrl is empty string (falsy)
// Preview won't show because imagePreviewUrl is empty string (falsy)
// Preview won't show because imagePreviewUrl is empty string (falsy)
// Preview won't show because imagePreviewUrl is empty string (falsy)
// Preview won't show because imagePreviewUrl is empty string (falsy)
// Preview won't show because imagePreviewUrl is empty string (falsy)
// Preview won't show because imagePreviewUrl is empty string (falsy)
// Preview won't show because imagePreviewUrl is empty string (falsy)
// Preview won't show because imagePreviewUrl is empty string (falsy)
// Preview won't show because imagePreviewUrl is empty string (falsy)
// Preview won't show because imagePreviewUrl is empty string (falsy)
// Preview won't show because imagePreviewUrl is empty string (falsy)
// Preview won't show because imagePreviewUrl is empty string (falsy)
// Preview won't show because imagePreviewUrl is empty string (falsy)
// Preview won't show because imagePreviewUrl is empty string (falsy)
// Preview won't show because imagePreviewUrl is empty string (falsy)
// Preview won't show because imagePreviewUrl is empty string (falsy)
// Preview won't show because imagePreviewUrl is empty string (falsy)
// Preview won't show because imagePreviewUrl is empty string (falsy)
// Preview won't show because imagePreviewUrl is empty string (falsy)
// Preview won't show because imagePreviewUrl is empty string (falsy)
// Preview won't show because imagePreviewUrl is empty string (falsy)
// Preview won't show because imagePreviewUrl is empty string (falsy)
expect(document.querySelector('.image-preview-container')).not.toBeInTheDocument()
})
@ -261,13 +208,9 @@ describe('FileImageItem', () => {
it('should call downloadUrl with correct params when download button is clicked', async () => {
const { downloadUrl } = await import('@/utils/download')
const file = createFile({ url: 'https://example.com/photo.png', name: 'photo.png' })
const { container } = render(<FileImageItem file={file} showDownloadAction />)
render(<FileImageItem file={file} showDownloadAction />)
const svgs = container.querySelectorAll('svg')
const downloadSvg = Array.from(svgs).find(
svg => !svg.hasAttribute('data-icon') && !svg.querySelector('circle'),
)
fireEvent.click(downloadSvg!.parentElement!)
fireEvent.click(screen.getByRole('button', { name: 'common.operation.download' }))
expect(downloadUrl).toHaveBeenCalledWith(expect.objectContaining({
url: expect.stringContaining('as_attachment=true'),

View File

@ -5,6 +5,7 @@ import {
RiDownloadLine,
} from '@remixicon/react'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { ReplayLine } from '@/app/components/base/icons/src/vender/other'
import ImagePreview from '@/app/components/base/image-uploader/image-preview'
import ProgressCircle from '@/app/components/base/progress-bar/progress-circle'
@ -30,6 +31,7 @@ const FileImageItem = ({
onRemove,
onReUpload,
}: FileImageItemProps) => {
const { t } = useTranslation()
const { id, progress, base64Url, url, name } = file
const [imagePreviewUrl, setImagePreviewUrl] = useState('')
const download_url = url ? `${url}&as_attachment=true` : base64Url
@ -43,10 +45,14 @@ const FileImageItem = ({
{
showDeleteAction && (
<Button
aria-label={t('operation.remove', { ns: 'common' })}
className="absolute -top-1.5 -right-1.5 z-11 hidden h-5 w-5 rounded-full p-0 group-hover/file-image:flex"
onClick={() => onRemove?.(id)}
onClick={(e) => {
e.stopPropagation()
onRemove?.(id)
}}
>
<RiCloseLine className="h-4 w-4 text-components-button-secondary-text" />
<RiCloseLine className="h-4 w-4 text-components-button-secondary-text" aria-hidden="true" />
</Button>
)
}
@ -71,25 +77,34 @@ const FileImageItem = ({
{
progress === -1 && (
<div className="absolute inset-0 z-10 flex items-center justify-center border-2 border-state-destructive-border bg-background-overlay-destructive">
<ReplayLine
<button
type="button"
aria-label={t('operation.retry', { ns: 'common' })}
className="h-5 w-5"
onClick={() => onReUpload?.(id)}
/>
onClick={(e) => {
e.stopPropagation()
onReUpload?.(id)
}}
>
<ReplayLine className="h-5 w-5" aria-hidden="true" />
</button>
</div>
)
}
{
showDownloadAction && (
<div className="absolute inset-0.5 z-10 hidden bg-background-overlay-alt group-hover/file-image:block">
<div
<button
type="button"
aria-label={t('operation.download', { ns: 'common' })}
className="absolute right-0.5 bottom-0.5 flex h-6 w-6 items-center justify-center rounded-lg bg-components-actionbar-bg shadow-md"
onClick={(e) => {
e.stopPropagation()
downloadUrl({ url: download_url || '', fileName: name, target: '_blank' })
}}
>
<RiDownloadLine className="h-4 w-4 text-text-tertiary" />
</div>
<RiDownloadLine className="h-4 w-4 text-text-tertiary" aria-hidden="true" />
</button>
</div>
)
}