mirror of
https://github.com/langgenius/dify.git
synced 2026-06-01 06:28:14 +08:00
fix(web): name chat image file actions
This commit is contained in:
@ -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'),
|
||||
|
||||
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user