mirror of
https://github.com/langgenius/dify.git
synced 2026-02-09 23:20:12 -05:00
refactor(web): consolidate download helpers (#31664)
This commit is contained in:
@@ -31,6 +31,7 @@ import { fetchWorkflowDraft } from '@/service/workflow'
|
|||||||
import { AppModeEnum } from '@/types/app'
|
import { AppModeEnum } from '@/types/app'
|
||||||
import { getRedirection } from '@/utils/app-redirection'
|
import { getRedirection } from '@/utils/app-redirection'
|
||||||
import { cn } from '@/utils/classnames'
|
import { cn } from '@/utils/classnames'
|
||||||
|
import { downloadBlob } from '@/utils/download'
|
||||||
import AppIcon from '../base/app-icon'
|
import AppIcon from '../base/app-icon'
|
||||||
import AppOperations from './app-operations'
|
import AppOperations from './app-operations'
|
||||||
|
|
||||||
@@ -145,13 +146,8 @@ const AppInfo = ({ expand, onlyShowDetail = false, openState = false, onDetailEx
|
|||||||
appID: appDetail.id,
|
appID: appDetail.id,
|
||||||
include,
|
include,
|
||||||
})
|
})
|
||||||
const a = document.createElement('a')
|
|
||||||
const file = new Blob([data], { type: 'application/yaml' })
|
const file = new Blob([data], { type: 'application/yaml' })
|
||||||
const url = URL.createObjectURL(file)
|
downloadBlob({ data: file, fileName: `${appDetail.name}.yml` })
|
||||||
a.href = url
|
|
||||||
a.download = `${appDetail.name}.yml`
|
|
||||||
a.click()
|
|
||||||
URL.revokeObjectURL(url)
|
|
||||||
}
|
}
|
||||||
catch {
|
catch {
|
||||||
notify({ type: 'error', message: t('exportFailed', { ns: 'app' }) })
|
notify({ type: 'error', message: t('exportFailed', { ns: 'app' }) })
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { datasetDetailQueryKeyPrefix, useInvalidDatasetList } from '@/service/kn
|
|||||||
import { useInvalid } from '@/service/use-base'
|
import { useInvalid } from '@/service/use-base'
|
||||||
import { useExportPipelineDSL } from '@/service/use-pipeline'
|
import { useExportPipelineDSL } from '@/service/use-pipeline'
|
||||||
import { cn } from '@/utils/classnames'
|
import { cn } from '@/utils/classnames'
|
||||||
|
import { downloadBlob } from '@/utils/download'
|
||||||
import ActionButton from '../../base/action-button'
|
import ActionButton from '../../base/action-button'
|
||||||
import Confirm from '../../base/confirm'
|
import Confirm from '../../base/confirm'
|
||||||
import { PortalToFollowElem, PortalToFollowElemContent, PortalToFollowElemTrigger } from '../../base/portal-to-follow-elem'
|
import { PortalToFollowElem, PortalToFollowElemContent, PortalToFollowElemTrigger } from '../../base/portal-to-follow-elem'
|
||||||
@@ -64,13 +65,8 @@ const DropDown = ({
|
|||||||
pipelineId: pipeline_id,
|
pipelineId: pipeline_id,
|
||||||
include,
|
include,
|
||||||
})
|
})
|
||||||
const a = document.createElement('a')
|
|
||||||
const file = new Blob([data], { type: 'application/yaml' })
|
const file = new Blob([data], { type: 'application/yaml' })
|
||||||
const url = URL.createObjectURL(file)
|
downloadBlob({ data: file, fileName: `${name}.pipeline` })
|
||||||
a.href = url
|
|
||||||
a.download = `${name}.pipeline`
|
|
||||||
a.click()
|
|
||||||
URL.revokeObjectURL(url)
|
|
||||||
}
|
}
|
||||||
catch {
|
catch {
|
||||||
Toast.notify({ type: 'error', message: t('exportFailed', { ns: 'app' }) })
|
Toast.notify({ type: 'error', message: t('exportFailed', { ns: 'app' }) })
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import { LanguagesSupported } from '@/i18n-config/language'
|
|||||||
import { clearAllAnnotations, fetchExportAnnotationList } from '@/service/annotation'
|
import { clearAllAnnotations, fetchExportAnnotationList } from '@/service/annotation'
|
||||||
|
|
||||||
import { cn } from '@/utils/classnames'
|
import { cn } from '@/utils/classnames'
|
||||||
|
import { downloadBlob } from '@/utils/download'
|
||||||
import Button from '../../../base/button'
|
import Button from '../../../base/button'
|
||||||
import AddAnnotationModal from '../add-annotation-modal'
|
import AddAnnotationModal from '../add-annotation-modal'
|
||||||
import BatchAddModal from '../batch-add-annotation-modal'
|
import BatchAddModal from '../batch-add-annotation-modal'
|
||||||
@@ -56,28 +57,23 @@ const HeaderOptions: FC<Props> = ({
|
|||||||
)
|
)
|
||||||
|
|
||||||
const JSONLOutput = () => {
|
const JSONLOutput = () => {
|
||||||
const a = document.createElement('a')
|
|
||||||
const content = listTransformer(list).join('\n')
|
const content = listTransformer(list).join('\n')
|
||||||
const file = new Blob([content], { type: 'application/jsonl' })
|
const file = new Blob([content], { type: 'application/jsonl' })
|
||||||
const url = URL.createObjectURL(file)
|
downloadBlob({ data: file, fileName: `annotations-${locale}.jsonl` })
|
||||||
a.href = url
|
|
||||||
a.download = `annotations-${locale}.jsonl`
|
|
||||||
a.click()
|
|
||||||
URL.revokeObjectURL(url)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const fetchList = async () => {
|
const fetchList = React.useCallback(async () => {
|
||||||
const { data }: any = await fetchExportAnnotationList(appId)
|
const { data }: any = await fetchExportAnnotationList(appId)
|
||||||
setList(data as AnnotationItemBasic[])
|
setList(data as AnnotationItemBasic[])
|
||||||
}
|
}, [appId])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchList()
|
fetchList()
|
||||||
}, [])
|
}, [fetchList])
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (controlUpdateList)
|
if (controlUpdateList)
|
||||||
fetchList()
|
fetchList()
|
||||||
}, [controlUpdateList])
|
}, [controlUpdateList, fetchList])
|
||||||
|
|
||||||
const [showBulkImportModal, setShowBulkImportModal] = useState(false)
|
const [showBulkImportModal, setShowBulkImportModal] = useState(false)
|
||||||
const [showClearConfirm, setShowClearConfirm] = useState(false)
|
const [showClearConfirm, setShowClearConfirm] = useState(false)
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import type { ReactNode } from 'react'
|
|||||||
import type { IConfigVarProps } from './index'
|
import type { IConfigVarProps } from './index'
|
||||||
import type { ExternalDataTool } from '@/models/common'
|
import type { ExternalDataTool } from '@/models/common'
|
||||||
import type { PromptVariable } from '@/models/debug'
|
import type { PromptVariable } from '@/models/debug'
|
||||||
import { act, fireEvent, render, screen } from '@testing-library/react'
|
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import { vi } from 'vitest'
|
import { vi } from 'vitest'
|
||||||
import Toast from '@/app/components/base/toast'
|
import Toast from '@/app/components/base/toast'
|
||||||
@@ -240,7 +240,9 @@ describe('ConfigVar', () => {
|
|||||||
const saveButton = await screen.findByRole('button', { name: 'common.operation.save' })
|
const saveButton = await screen.findByRole('button', { name: 'common.operation.save' })
|
||||||
fireEvent.click(saveButton)
|
fireEvent.click(saveButton)
|
||||||
|
|
||||||
expect(onPromptVariablesChange).toHaveBeenCalledTimes(1)
|
await waitFor(() => {
|
||||||
|
expect(onPromptVariablesChange).toHaveBeenCalledTimes(1)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should show error when variable key is duplicated', async () => {
|
it('should show error when variable key is duplicated', async () => {
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ import { fetchWorkflowDraft } from '@/service/workflow'
|
|||||||
import { AppModeEnum } from '@/types/app'
|
import { AppModeEnum } from '@/types/app'
|
||||||
import { getRedirection } from '@/utils/app-redirection'
|
import { getRedirection } from '@/utils/app-redirection'
|
||||||
import { cn } from '@/utils/classnames'
|
import { cn } from '@/utils/classnames'
|
||||||
|
import { downloadBlob } from '@/utils/download'
|
||||||
import { formatTime } from '@/utils/time'
|
import { formatTime } from '@/utils/time'
|
||||||
import { basePath } from '@/utils/var'
|
import { basePath } from '@/utils/var'
|
||||||
|
|
||||||
@@ -161,13 +162,8 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => {
|
|||||||
appID: app.id,
|
appID: app.id,
|
||||||
include,
|
include,
|
||||||
})
|
})
|
||||||
const a = document.createElement('a')
|
|
||||||
const file = new Blob([data], { type: 'application/yaml' })
|
const file = new Blob([data], { type: 'application/yaml' })
|
||||||
const url = URL.createObjectURL(file)
|
downloadBlob({ data: file, fileName: `${app.name}.yml` })
|
||||||
a.href = url
|
|
||||||
a.download = `${app.name}.yml`
|
|
||||||
a.click()
|
|
||||||
URL.revokeObjectURL(url)
|
|
||||||
}
|
}
|
||||||
catch {
|
catch {
|
||||||
notify({ type: 'error', message: t('exportFailed', { ns: 'app' }) })
|
notify({ type: 'error', message: t('exportFailed', { ns: 'app' }) })
|
||||||
@@ -346,7 +342,7 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => {
|
|||||||
dateFormat: `${t('segment.dateTimeFormat', { ns: 'datasetDocuments' })}`,
|
dateFormat: `${t('segment.dateTimeFormat', { ns: 'datasetDocuments' })}`,
|
||||||
})
|
})
|
||||||
return `${t('segment.editedAt', { ns: 'datasetDocuments' })} ${timeText}`
|
return `${t('segment.editedAt', { ns: 'datasetDocuments' })} ${timeText}`
|
||||||
}, [app.updated_at, app.created_at])
|
}, [app.updated_at, app.created_at, t])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -15,11 +15,11 @@ import ImagePreview from '@/app/components/base/image-uploader/image-preview'
|
|||||||
import ProgressCircle from '@/app/components/base/progress-bar/progress-circle'
|
import ProgressCircle from '@/app/components/base/progress-bar/progress-circle'
|
||||||
import { SupportUploadFileTypes } from '@/app/components/workflow/types'
|
import { SupportUploadFileTypes } from '@/app/components/workflow/types'
|
||||||
import { cn } from '@/utils/classnames'
|
import { cn } from '@/utils/classnames'
|
||||||
|
import { downloadUrl } from '@/utils/download'
|
||||||
import { formatFileSize } from '@/utils/format'
|
import { formatFileSize } from '@/utils/format'
|
||||||
import FileImageRender from '../file-image-render'
|
import FileImageRender from '../file-image-render'
|
||||||
import FileTypeIcon from '../file-type-icon'
|
import FileTypeIcon from '../file-type-icon'
|
||||||
import {
|
import {
|
||||||
downloadFile,
|
|
||||||
fileIsUploaded,
|
fileIsUploaded,
|
||||||
getFileAppearanceType,
|
getFileAppearanceType,
|
||||||
getFileExtension,
|
getFileExtension,
|
||||||
@@ -140,7 +140,7 @@ const FileInAttachmentItem = ({
|
|||||||
showDownloadAction && (
|
showDownloadAction && (
|
||||||
<ActionButton onClick={(e) => {
|
<ActionButton onClick={(e) => {
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
downloadFile(url || base64Url || '', name)
|
downloadUrl({ url: url || base64Url || '', fileName: name, target: '_blank' })
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<RiDownloadLine className="h-4 w-4" />
|
<RiDownloadLine className="h-4 w-4" />
|
||||||
|
|||||||
@@ -8,9 +8,9 @@ import Button from '@/app/components/base/button'
|
|||||||
import { ReplayLine } from '@/app/components/base/icons/src/vender/other'
|
import { ReplayLine } from '@/app/components/base/icons/src/vender/other'
|
||||||
import ImagePreview from '@/app/components/base/image-uploader/image-preview'
|
import ImagePreview from '@/app/components/base/image-uploader/image-preview'
|
||||||
import ProgressCircle from '@/app/components/base/progress-bar/progress-circle'
|
import ProgressCircle from '@/app/components/base/progress-bar/progress-circle'
|
||||||
|
import { downloadUrl } from '@/utils/download'
|
||||||
import FileImageRender from '../file-image-render'
|
import FileImageRender from '../file-image-render'
|
||||||
import {
|
import {
|
||||||
downloadFile,
|
|
||||||
fileIsUploaded,
|
fileIsUploaded,
|
||||||
} from '../utils'
|
} from '../utils'
|
||||||
|
|
||||||
@@ -85,7 +85,7 @@ const FileImageItem = ({
|
|||||||
className="absolute bottom-0.5 right-0.5 flex h-6 w-6 items-center justify-center rounded-lg bg-components-actionbar-bg shadow-md"
|
className="absolute bottom-0.5 right-0.5 flex h-6 w-6 items-center justify-center rounded-lg bg-components-actionbar-bg shadow-md"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
downloadFile(download_url || '', name)
|
downloadUrl({ url: download_url || '', fileName: name, target: '_blank' })
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<RiDownloadLine className="h-4 w-4 text-text-tertiary" />
|
<RiDownloadLine className="h-4 w-4 text-text-tertiary" />
|
||||||
|
|||||||
@@ -12,10 +12,10 @@ import VideoPreview from '@/app/components/base/file-uploader/video-preview'
|
|||||||
import { ReplayLine } from '@/app/components/base/icons/src/vender/other'
|
import { ReplayLine } from '@/app/components/base/icons/src/vender/other'
|
||||||
import ProgressCircle from '@/app/components/base/progress-bar/progress-circle'
|
import ProgressCircle from '@/app/components/base/progress-bar/progress-circle'
|
||||||
import { cn } from '@/utils/classnames'
|
import { cn } from '@/utils/classnames'
|
||||||
|
import { downloadUrl } from '@/utils/download'
|
||||||
import { formatFileSize } from '@/utils/format'
|
import { formatFileSize } from '@/utils/format'
|
||||||
import FileTypeIcon from '../file-type-icon'
|
import FileTypeIcon from '../file-type-icon'
|
||||||
import {
|
import {
|
||||||
downloadFile,
|
|
||||||
fileIsUploaded,
|
fileIsUploaded,
|
||||||
getFileAppearanceType,
|
getFileAppearanceType,
|
||||||
getFileExtension,
|
getFileExtension,
|
||||||
@@ -100,7 +100,7 @@ const FileItem = ({
|
|||||||
className="absolute -right-1 -top-1 hidden group-hover/file-item:flex"
|
className="absolute -right-1 -top-1 hidden group-hover/file-item:flex"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
downloadFile(download_url || '', name)
|
downloadUrl({ url: download_url || '', fileName: name, target: '_blank' })
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<RiDownloadLine className="h-3.5 w-3.5 text-text-tertiary" />
|
<RiDownloadLine className="h-3.5 w-3.5 text-text-tertiary" />
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import type { MockInstance } from 'vitest'
|
|
||||||
import mime from 'mime'
|
import mime from 'mime'
|
||||||
import { SupportUploadFileTypes } from '@/app/components/workflow/types'
|
import { SupportUploadFileTypes } from '@/app/components/workflow/types'
|
||||||
import { upload } from '@/service/base'
|
import { upload } from '@/service/base'
|
||||||
@@ -6,7 +5,6 @@ import { TransferMethod } from '@/types/app'
|
|||||||
import { FILE_EXTS } from '../prompt-editor/constants'
|
import { FILE_EXTS } from '../prompt-editor/constants'
|
||||||
import { FileAppearanceTypeEnum } from './types'
|
import { FileAppearanceTypeEnum } from './types'
|
||||||
import {
|
import {
|
||||||
downloadFile,
|
|
||||||
fileIsUploaded,
|
fileIsUploaded,
|
||||||
fileUpload,
|
fileUpload,
|
||||||
getFileAppearanceType,
|
getFileAppearanceType,
|
||||||
@@ -782,74 +780,4 @@ describe('file-uploader utils', () => {
|
|||||||
} as any)).toBe(true)
|
} as any)).toBe(true)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('downloadFile', () => {
|
|
||||||
let mockAnchor: HTMLAnchorElement
|
|
||||||
let createElementMock: MockInstance
|
|
||||||
let appendChildMock: MockInstance
|
|
||||||
let removeChildMock: MockInstance
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
// Mock createElement and appendChild
|
|
||||||
mockAnchor = {
|
|
||||||
href: '',
|
|
||||||
download: '',
|
|
||||||
style: { display: '' },
|
|
||||||
target: '',
|
|
||||||
title: '',
|
|
||||||
click: vi.fn(),
|
|
||||||
} as unknown as HTMLAnchorElement
|
|
||||||
|
|
||||||
createElementMock = vi.spyOn(document, 'createElement').mockReturnValue(mockAnchor as any)
|
|
||||||
appendChildMock = vi.spyOn(document.body, 'appendChild').mockImplementation((node: Node) => {
|
|
||||||
return node
|
|
||||||
})
|
|
||||||
removeChildMock = vi.spyOn(document.body, 'removeChild').mockImplementation((node: Node) => {
|
|
||||||
return node
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
vi.resetAllMocks()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should create and trigger download with correct attributes', () => {
|
|
||||||
const url = 'https://example.com/test.pdf'
|
|
||||||
const filename = 'test.pdf'
|
|
||||||
|
|
||||||
downloadFile(url, filename)
|
|
||||||
|
|
||||||
// Verify anchor element was created with correct properties
|
|
||||||
expect(createElementMock).toHaveBeenCalledWith('a')
|
|
||||||
expect(mockAnchor.href).toBe(url)
|
|
||||||
expect(mockAnchor.download).toBe(filename)
|
|
||||||
expect(mockAnchor.style.display).toBe('none')
|
|
||||||
expect(mockAnchor.target).toBe('_blank')
|
|
||||||
expect(mockAnchor.title).toBe(filename)
|
|
||||||
|
|
||||||
// Verify DOM operations
|
|
||||||
expect(appendChildMock).toHaveBeenCalledWith(mockAnchor)
|
|
||||||
expect(mockAnchor.click).toHaveBeenCalled()
|
|
||||||
expect(removeChildMock).toHaveBeenCalledWith(mockAnchor)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should handle empty filename', () => {
|
|
||||||
const url = 'https://example.com/test.pdf'
|
|
||||||
const filename = ''
|
|
||||||
|
|
||||||
downloadFile(url, filename)
|
|
||||||
|
|
||||||
expect(mockAnchor.download).toBe('')
|
|
||||||
expect(mockAnchor.title).toBe('')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should handle empty url', () => {
|
|
||||||
const url = ''
|
|
||||||
const filename = 'test.pdf'
|
|
||||||
|
|
||||||
downloadFile(url, filename)
|
|
||||||
|
|
||||||
expect(mockAnchor.href).toBe('')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -249,15 +249,3 @@ export const fileIsUploaded = (file: FileEntity) => {
|
|||||||
if (file.transferMethod === TransferMethod.remote_url && file.progress === 100)
|
if (file.transferMethod === TransferMethod.remote_url && file.progress === 100)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
export const downloadFile = (url: string, filename: string) => {
|
|
||||||
const anchor = document.createElement('a')
|
|
||||||
anchor.href = url
|
|
||||||
anchor.download = filename
|
|
||||||
anchor.style.display = 'none'
|
|
||||||
anchor.target = '_blank'
|
|
||||||
anchor.title = filename
|
|
||||||
document.body.appendChild(anchor)
|
|
||||||
anchor.click()
|
|
||||||
document.body.removeChild(anchor)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { createPortal } from 'react-dom'
|
|||||||
import { useHotkeys } from 'react-hotkeys-hook'
|
import { useHotkeys } from 'react-hotkeys-hook'
|
||||||
import Toast from '@/app/components/base/toast'
|
import Toast from '@/app/components/base/toast'
|
||||||
import Tooltip from '@/app/components/base/tooltip'
|
import Tooltip from '@/app/components/base/tooltip'
|
||||||
|
import { downloadUrl } from '@/utils/download'
|
||||||
|
|
||||||
type ImagePreviewProps = {
|
type ImagePreviewProps = {
|
||||||
url: string
|
url: string
|
||||||
@@ -60,27 +61,14 @@ const ImagePreview: FC<ImagePreviewProps> = ({
|
|||||||
|
|
||||||
const downloadImage = () => {
|
const downloadImage = () => {
|
||||||
// Open in a new window, considering the case when the page is inside an iframe
|
// Open in a new window, considering the case when the page is inside an iframe
|
||||||
if (url.startsWith('http') || url.startsWith('https')) {
|
if (url.startsWith('http') || url.startsWith('https') || url.startsWith('data:image')) {
|
||||||
const a = document.createElement('a')
|
downloadUrl({ url, fileName: title, target: '_blank' })
|
||||||
a.href = url
|
return
|
||||||
a.target = '_blank'
|
|
||||||
a.download = title
|
|
||||||
a.click()
|
|
||||||
}
|
|
||||||
else if (url.startsWith('data:image')) {
|
|
||||||
// Base64 image
|
|
||||||
const a = document.createElement('a')
|
|
||||||
a.href = url
|
|
||||||
a.target = '_blank'
|
|
||||||
a.download = title
|
|
||||||
a.click()
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
Toast.notify({
|
|
||||||
type: 'error',
|
|
||||||
message: `Unable to open image: ${url}`,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
Toast.notify({
|
||||||
|
type: 'error',
|
||||||
|
message: `Unable to open image: ${url}`,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const zoomIn = () => {
|
const zoomIn = () => {
|
||||||
@@ -135,12 +123,7 @@ const ImagePreview: FC<ImagePreviewProps> = ({
|
|||||||
catch (err) {
|
catch (err) {
|
||||||
console.error('Failed to copy image:', err)
|
console.error('Failed to copy image:', err)
|
||||||
|
|
||||||
const link = document.createElement('a')
|
downloadUrl({ url, fileName: `${title}.png` })
|
||||||
link.href = url
|
|
||||||
link.download = `${title}.png`
|
|
||||||
document.body.appendChild(link)
|
|
||||||
link.click()
|
|
||||||
document.body.removeChild(link)
|
|
||||||
|
|
||||||
Toast.notify({
|
Toast.notify({
|
||||||
type: 'info',
|
type: 'info',
|
||||||
@@ -215,6 +198,7 @@ const ImagePreview: FC<ImagePreviewProps> = ({
|
|||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
>
|
>
|
||||||
{ }
|
{ }
|
||||||
|
{/* eslint-disable-next-line next/no-img-element */}
|
||||||
<img
|
<img
|
||||||
ref={imgRef}
|
ref={imgRef}
|
||||||
alt={title}
|
alt={title}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { useEffect, useRef, useState } from 'react'
|
|||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import ActionButton from '@/app/components/base/action-button'
|
import ActionButton from '@/app/components/base/action-button'
|
||||||
import Tooltip from '@/app/components/base/tooltip'
|
import Tooltip from '@/app/components/base/tooltip'
|
||||||
|
import { downloadUrl } from '@/utils/download'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
content: string
|
content: string
|
||||||
@@ -40,11 +41,10 @@ const ShareQRCode = ({ content }: Props) => {
|
|||||||
}, [isShow])
|
}, [isShow])
|
||||||
|
|
||||||
const downloadQR = () => {
|
const downloadQR = () => {
|
||||||
const canvas = document.getElementsByTagName('canvas')[0]
|
const canvas = qrCodeRef.current?.querySelector('canvas')
|
||||||
const link = document.createElement('a')
|
if (!(canvas instanceof HTMLCanvasElement))
|
||||||
link.download = 'qrcode.png'
|
return
|
||||||
link.href = canvas.toDataURL()
|
downloadUrl({ url: canvas.toDataURL(), fileName: 'qrcode.png' })
|
||||||
link.click()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const handlePanelClick = (event: React.MouseEvent) => {
|
const handlePanelClick = (event: React.MouseEvent) => {
|
||||||
|
|||||||
@@ -179,8 +179,10 @@ describe('RetryButton (IndexFailed)', () => {
|
|||||||
}, false),
|
}, false),
|
||||||
)
|
)
|
||||||
|
|
||||||
// Delay the response to test loading state
|
let resolveRetry: ((value: { result: 'success' }) => void) | undefined
|
||||||
mockRetryErrorDocs.mockImplementation(() => new Promise(resolve => setTimeout(() => resolve({ result: 'success' }), 100)))
|
mockRetryErrorDocs.mockImplementation(() => new Promise((resolve) => {
|
||||||
|
resolveRetry = resolve
|
||||||
|
}))
|
||||||
|
|
||||||
render(<RetryButton datasetId="test-dataset" />)
|
render(<RetryButton datasetId="test-dataset" />)
|
||||||
|
|
||||||
@@ -193,6 +195,11 @@ describe('RetryButton (IndexFailed)', () => {
|
|||||||
expect(button).toHaveClass('cursor-not-allowed')
|
expect(button).toHaveClass('cursor-not-allowed')
|
||||||
expect(button).toHaveClass('text-text-disabled')
|
expect(button).toHaveClass('text-text-disabled')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
resolveRetry?.({ result: 'success' })
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(mockRefetch).toHaveBeenCalled()
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -23,9 +23,10 @@ vi.mock('@/app/components/base/toast', () => ({
|
|||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
|
|
||||||
// Mock downloadFile utility
|
// Mock download utilities
|
||||||
vi.mock('@/utils/format', () => ({
|
vi.mock('@/utils/download', () => ({
|
||||||
downloadFile: vi.fn(),
|
downloadBlob: vi.fn(),
|
||||||
|
downloadUrl: vi.fn(),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
// Capture Confirm callbacks
|
// Capture Confirm callbacks
|
||||||
@@ -502,8 +503,8 @@ describe('TemplateCard', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should call downloadFile on successful export', async () => {
|
it('should call downloadBlob on successful export', async () => {
|
||||||
const { downloadFile } = await import('@/utils/format')
|
const { downloadBlob } = await import('@/utils/download')
|
||||||
mockExportPipelineDSL.mockImplementation((_id, callbacks) => {
|
mockExportPipelineDSL.mockImplementation((_id, callbacks) => {
|
||||||
callbacks.onSuccess({ data: 'yaml_content' })
|
callbacks.onSuccess({ data: 'yaml_content' })
|
||||||
return Promise.resolve()
|
return Promise.resolve()
|
||||||
@@ -514,7 +515,7 @@ describe('TemplateCard', () => {
|
|||||||
fireEvent.click(exportButton)
|
fireEvent.click(exportButton)
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(downloadFile).toHaveBeenCalledWith(expect.objectContaining({
|
expect(downloadBlob).toHaveBeenCalledWith(expect.objectContaining({
|
||||||
fileName: 'Test Pipeline.pipeline',
|
fileName: 'Test Pipeline.pipeline',
|
||||||
}))
|
}))
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import {
|
|||||||
useInvalidCustomizedTemplateList,
|
useInvalidCustomizedTemplateList,
|
||||||
usePipelineTemplateById,
|
usePipelineTemplateById,
|
||||||
} from '@/service/use-pipeline'
|
} from '@/service/use-pipeline'
|
||||||
import { downloadFile } from '@/utils/format'
|
import { downloadBlob } from '@/utils/download'
|
||||||
import Actions from './actions'
|
import Actions from './actions'
|
||||||
import Content from './content'
|
import Content from './content'
|
||||||
import Details from './details'
|
import Details from './details'
|
||||||
@@ -108,10 +108,7 @@ const TemplateCard = ({
|
|||||||
await exportPipelineDSL(pipeline.id, {
|
await exportPipelineDSL(pipeline.id, {
|
||||||
onSuccess: (res) => {
|
onSuccess: (res) => {
|
||||||
const blob = new Blob([res.data], { type: 'application/yaml' })
|
const blob = new Blob([res.data], { type: 'application/yaml' })
|
||||||
downloadFile({
|
downloadBlob({ data: blob, fileName: `${pipeline.name}.pipeline` })
|
||||||
data: blob,
|
|
||||||
fileName: `${pipeline.name}.pipeline`,
|
|
||||||
})
|
|
||||||
Toast.notify({
|
Toast.notify({
|
||||||
type: 'success',
|
type: 'success',
|
||||||
message: t('exportDSL.successTip', { ns: 'datasetPipeline' }),
|
message: t('exportDSL.successTip', { ns: 'datasetPipeline' }),
|
||||||
|
|||||||
@@ -125,11 +125,25 @@ const WaterCrawl: FC<Props> = ({
|
|||||||
await sleep(2500)
|
await sleep(2500)
|
||||||
return await waitForCrawlFinished(jobId)
|
return await waitForCrawlFinished(jobId)
|
||||||
}
|
}
|
||||||
catch (e: any) {
|
catch (error: unknown) {
|
||||||
const errorBody = await e.json()
|
let errorMessage = ''
|
||||||
|
|
||||||
|
const maybeErrorWithJson = error as { json?: () => Promise<unknown>, message?: unknown } | null
|
||||||
|
if (maybeErrorWithJson?.json) {
|
||||||
|
try {
|
||||||
|
const errorBody = await maybeErrorWithJson.json() as { message?: unknown } | null
|
||||||
|
if (typeof errorBody?.message === 'string')
|
||||||
|
errorMessage = errorBody.message
|
||||||
|
}
|
||||||
|
catch {}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!errorMessage && typeof maybeErrorWithJson?.message === 'string')
|
||||||
|
errorMessage = maybeErrorWithJson.message
|
||||||
|
|
||||||
return {
|
return {
|
||||||
isError: true,
|
isError: true,
|
||||||
errorMessage: errorBody.message,
|
errorMessage,
|
||||||
data: {
|
data: {
|
||||||
data: [],
|
data: [],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { useTranslation } from 'react-i18next'
|
|||||||
import Toast from '@/app/components/base/toast'
|
import Toast from '@/app/components/base/toast'
|
||||||
import { useCheckDatasetUsage, useDeleteDataset } from '@/service/use-dataset-card'
|
import { useCheckDatasetUsage, useDeleteDataset } from '@/service/use-dataset-card'
|
||||||
import { useExportPipelineDSL } from '@/service/use-pipeline'
|
import { useExportPipelineDSL } from '@/service/use-pipeline'
|
||||||
|
import { downloadBlob } from '@/utils/download'
|
||||||
|
|
||||||
type ModalState = {
|
type ModalState = {
|
||||||
showRenameModal: boolean
|
showRenameModal: boolean
|
||||||
@@ -65,13 +66,8 @@ export const useDatasetCardState = ({ dataset, onSuccess }: UseDatasetCardStateO
|
|||||||
pipelineId: pipeline_id,
|
pipelineId: pipeline_id,
|
||||||
include,
|
include,
|
||||||
})
|
})
|
||||||
const a = document.createElement('a')
|
|
||||||
const file = new Blob([data], { type: 'application/yaml' })
|
const file = new Blob([data], { type: 'application/yaml' })
|
||||||
const url = URL.createObjectURL(file)
|
downloadBlob({ data: file, fileName: `${name}.pipeline` })
|
||||||
a.href = url
|
|
||||||
a.download = `${name}.pipeline`
|
|
||||||
a.click()
|
|
||||||
URL.revokeObjectURL(url)
|
|
||||||
}
|
}
|
||||||
catch {
|
catch {
|
||||||
Toast.notify({ type: 'error', message: t('exportFailed', { ns: 'app' }) })
|
Toast.notify({ type: 'error', message: t('exportFailed', { ns: 'app' }) })
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { useModalContext } from '@/context/modal-context'
|
|||||||
import { useProviderContext } from '@/context/provider-context'
|
import { useProviderContext } from '@/context/provider-context'
|
||||||
import { getDocDownloadUrl } from '@/service/common'
|
import { getDocDownloadUrl } from '@/service/common'
|
||||||
import { cn } from '@/utils/classnames'
|
import { cn } from '@/utils/classnames'
|
||||||
|
import { downloadUrl } from '@/utils/download'
|
||||||
import Button from '../../base/button'
|
import Button from '../../base/button'
|
||||||
import Gdpr from '../../base/icons/src/public/common/Gdpr'
|
import Gdpr from '../../base/icons/src/public/common/Gdpr'
|
||||||
import Iso from '../../base/icons/src/public/common/Iso'
|
import Iso from '../../base/icons/src/public/common/Iso'
|
||||||
@@ -47,9 +48,7 @@ const UpgradeOrDownload: FC<UpgradeOrDownloadProps> = ({ doc_name }) => {
|
|||||||
mutationFn: async () => {
|
mutationFn: async () => {
|
||||||
try {
|
try {
|
||||||
const ret = await getDocDownloadUrl(doc_name)
|
const ret = await getDocDownloadUrl(doc_name)
|
||||||
const a = document.createElement('a')
|
downloadUrl({ url: ret.url })
|
||||||
a.href = ret.url
|
|
||||||
a.click()
|
|
||||||
Toast.notify({
|
Toast.notify({
|
||||||
type: 'success',
|
type: 'success',
|
||||||
message: t('operation.downloadSuccess', { ns: 'common' }),
|
message: t('operation.downloadSuccess', { ns: 'common' }),
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { useWorkflowStore } from '@/app/components/workflow/store'
|
|||||||
import { useEventEmitterContextContext } from '@/context/event-emitter'
|
import { useEventEmitterContextContext } from '@/context/event-emitter'
|
||||||
import { useExportPipelineDSL } from '@/service/use-pipeline'
|
import { useExportPipelineDSL } from '@/service/use-pipeline'
|
||||||
import { fetchWorkflowDraft } from '@/service/workflow'
|
import { fetchWorkflowDraft } from '@/service/workflow'
|
||||||
|
import { downloadBlob } from '@/utils/download'
|
||||||
import { useNodesSyncDraft } from './use-nodes-sync-draft'
|
import { useNodesSyncDraft } from './use-nodes-sync-draft'
|
||||||
|
|
||||||
export const useDSL = () => {
|
export const useDSL = () => {
|
||||||
@@ -37,13 +38,8 @@ export const useDSL = () => {
|
|||||||
pipelineId,
|
pipelineId,
|
||||||
include,
|
include,
|
||||||
})
|
})
|
||||||
const a = document.createElement('a')
|
|
||||||
const file = new Blob([data], { type: 'application/yaml' })
|
const file = new Blob([data], { type: 'application/yaml' })
|
||||||
const url = URL.createObjectURL(file)
|
downloadBlob({ data: file, fileName: `${knowledgeName}.pipeline` })
|
||||||
a.href = url
|
|
||||||
a.download = `${knowledgeName}.pipeline`
|
|
||||||
a.click()
|
|
||||||
URL.revokeObjectURL(url)
|
|
||||||
}
|
}
|
||||||
catch {
|
catch {
|
||||||
notify({ type: 'error', message: t('exportFailed', { ns: 'app' }) })
|
notify({ type: 'error', message: t('exportFailed', { ns: 'app' }) })
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
import { useEventEmitterContextContext } from '@/context/event-emitter'
|
import { useEventEmitterContextContext } from '@/context/event-emitter'
|
||||||
import { exportAppConfig } from '@/service/apps'
|
import { exportAppConfig } from '@/service/apps'
|
||||||
import { fetchWorkflowDraft } from '@/service/workflow'
|
import { fetchWorkflowDraft } from '@/service/workflow'
|
||||||
|
import { downloadBlob } from '@/utils/download'
|
||||||
import { useNodesSyncDraft } from './use-nodes-sync-draft'
|
import { useNodesSyncDraft } from './use-nodes-sync-draft'
|
||||||
|
|
||||||
export const useDSL = () => {
|
export const useDSL = () => {
|
||||||
@@ -37,13 +38,8 @@ export const useDSL = () => {
|
|||||||
include,
|
include,
|
||||||
workflowID: workflowId,
|
workflowID: workflowId,
|
||||||
})
|
})
|
||||||
const a = document.createElement('a')
|
|
||||||
const file = new Blob([data], { type: 'application/yaml' })
|
const file = new Blob([data], { type: 'application/yaml' })
|
||||||
const url = URL.createObjectURL(file)
|
downloadBlob({ data: file, fileName: `${appDetail.name}.yml` })
|
||||||
a.href = url
|
|
||||||
a.download = `${appDetail.name}.yml`
|
|
||||||
a.click()
|
|
||||||
URL.revokeObjectURL(url)
|
|
||||||
}
|
}
|
||||||
catch {
|
catch {
|
||||||
notify({ type: 'error', message: t('exportFailed', { ns: 'app' }) })
|
notify({ type: 'error', message: t('exportFailed', { ns: 'app' }) })
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import {
|
|||||||
} from '@/app/components/base/portal-to-follow-elem'
|
} from '@/app/components/base/portal-to-follow-elem'
|
||||||
import { useDownloadPlugin } from '@/service/use-plugins'
|
import { useDownloadPlugin } from '@/service/use-plugins'
|
||||||
import { cn } from '@/utils/classnames'
|
import { cn } from '@/utils/classnames'
|
||||||
import { downloadFile } from '@/utils/format'
|
import { downloadBlob } from '@/utils/download'
|
||||||
import { getMarketplaceUrl } from '@/utils/var'
|
import { getMarketplaceUrl } from '@/utils/var'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@@ -67,7 +67,7 @@ const OperationDropdown: FC<Props> = ({
|
|||||||
if (!needDownload || !blob)
|
if (!needDownload || !blob)
|
||||||
return
|
return
|
||||||
const fileName = `${author}-${name}_${version}.zip`
|
const fileName = `${author}-${name}_${version}.zip`
|
||||||
downloadFile({ data: blob, fileName })
|
downloadBlob({ data: blob, fileName })
|
||||||
setNeedDownload(false)
|
setNeedDownload(false)
|
||||||
queryClient.removeQueries({
|
queryClient.removeQueries({
|
||||||
queryKey: ['plugins', 'downloadPlugin', downloadInfo],
|
queryKey: ['plugins', 'downloadPlugin', downloadInfo],
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import {
|
|||||||
} from '@/app/components/base/portal-to-follow-elem'
|
} from '@/app/components/base/portal-to-follow-elem'
|
||||||
import { useStore } from '@/app/components/workflow/store'
|
import { useStore } from '@/app/components/workflow/store'
|
||||||
import { cn } from '@/utils/classnames'
|
import { cn } from '@/utils/classnames'
|
||||||
|
import { downloadUrl } from '@/utils/download'
|
||||||
import { useNodesReadOnly } from '../hooks'
|
import { useNodesReadOnly } from '../hooks'
|
||||||
import TipPopup from './tip-popup'
|
import TipPopup from './tip-popup'
|
||||||
|
|
||||||
@@ -146,26 +147,14 @@ const MoreActions: FC = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const fileName = `${filename}.${type}`
|
||||||
|
|
||||||
if (currentWorkflow) {
|
if (currentWorkflow) {
|
||||||
setPreviewUrl(dataUrl)
|
setPreviewUrl(dataUrl)
|
||||||
setPreviewTitle(`${filename}.${type}`)
|
setPreviewTitle(fileName)
|
||||||
|
}
|
||||||
|
|
||||||
const link = document.createElement('a')
|
downloadUrl({ url: dataUrl, fileName })
|
||||||
link.href = dataUrl
|
|
||||||
link.download = `${filename}.${type}`
|
|
||||||
document.body.appendChild(link)
|
|
||||||
link.click()
|
|
||||||
document.body.removeChild(link)
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// For current view, just download
|
|
||||||
const link = document.createElement('a')
|
|
||||||
link.href = dataUrl
|
|
||||||
link.download = `${filename}.${type}`
|
|
||||||
document.body.appendChild(link)
|
|
||||||
link.click()
|
|
||||||
document.body.removeChild(link)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
catch (error) {
|
catch (error) {
|
||||||
console.error('Export image failed:', error)
|
console.error('Export image failed:', error)
|
||||||
|
|||||||
@@ -994,7 +994,7 @@
|
|||||||
"count": 1
|
"count": 1
|
||||||
},
|
},
|
||||||
"ts/no-explicit-any": {
|
"ts/no-explicit-any": {
|
||||||
"count": 3
|
"count": 2
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"app/components/base/file-uploader/utils.ts": {
|
"app/components/base/file-uploader/utils.ts": {
|
||||||
@@ -1661,7 +1661,7 @@
|
|||||||
"count": 1
|
"count": 1
|
||||||
},
|
},
|
||||||
"ts/no-explicit-any": {
|
"ts/no-explicit-any": {
|
||||||
"count": 5
|
"count": 4
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"app/components/datasets/create/website/watercrawl/options.tsx": {
|
"app/components/datasets/create/website/watercrawl/options.tsx": {
|
||||||
@@ -4376,11 +4376,6 @@
|
|||||||
"count": 1
|
"count": 1
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"utils/format.spec.ts": {
|
|
||||||
"ts/no-explicit-any": {
|
|
||||||
"count": 1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"utils/get-icon.spec.ts": {
|
"utils/get-icon.spec.ts": {
|
||||||
"ts/no-explicit-any": {
|
"ts/no-explicit-any": {
|
||||||
"count": 2
|
"count": 2
|
||||||
|
|||||||
75
web/utils/download.spec.ts
Normal file
75
web/utils/download.spec.ts
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
import { downloadBlob, downloadUrl } from './download'
|
||||||
|
|
||||||
|
describe('downloadUrl', () => {
|
||||||
|
let mockAnchor: HTMLAnchorElement
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
mockAnchor = {
|
||||||
|
href: '',
|
||||||
|
download: '',
|
||||||
|
rel: '',
|
||||||
|
target: '',
|
||||||
|
style: { display: '' },
|
||||||
|
click: vi.fn(),
|
||||||
|
remove: vi.fn(),
|
||||||
|
} as unknown as HTMLAnchorElement
|
||||||
|
|
||||||
|
vi.spyOn(document, 'createElement').mockReturnValue(mockAnchor)
|
||||||
|
vi.spyOn(document.body, 'appendChild').mockImplementation((node: Node) => node)
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.restoreAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should create a link and trigger a download correctly', () => {
|
||||||
|
downloadUrl({ url: 'https://example.com/file.txt', fileName: 'file.txt', target: '_blank' })
|
||||||
|
|
||||||
|
expect(mockAnchor.href).toBe('https://example.com/file.txt')
|
||||||
|
expect(mockAnchor.download).toBe('file.txt')
|
||||||
|
expect(mockAnchor.rel).toBe('noopener noreferrer')
|
||||||
|
expect(mockAnchor.target).toBe('_blank')
|
||||||
|
expect(mockAnchor.style.display).toBe('none')
|
||||||
|
expect(mockAnchor.click).toHaveBeenCalled()
|
||||||
|
expect(mockAnchor.remove).toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should skip when url is empty', () => {
|
||||||
|
downloadUrl({ url: '' })
|
||||||
|
expect(document.createElement).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('downloadBlob', () => {
|
||||||
|
it('should create a blob url, trigger download, and revoke url', () => {
|
||||||
|
const blob = new Blob(['test'], { type: 'text/plain' })
|
||||||
|
const mockUrl = 'blob:mock-url'
|
||||||
|
const createObjectURLMock = vi.spyOn(window.URL, 'createObjectURL').mockReturnValue(mockUrl)
|
||||||
|
const revokeObjectURLMock = vi.spyOn(window.URL, 'revokeObjectURL').mockImplementation(() => {})
|
||||||
|
|
||||||
|
const mockAnchor = {
|
||||||
|
href: '',
|
||||||
|
download: '',
|
||||||
|
rel: '',
|
||||||
|
target: '',
|
||||||
|
style: { display: '' },
|
||||||
|
click: vi.fn(),
|
||||||
|
remove: vi.fn(),
|
||||||
|
} as unknown as HTMLAnchorElement
|
||||||
|
|
||||||
|
vi.spyOn(document, 'createElement').mockReturnValue(mockAnchor)
|
||||||
|
vi.spyOn(document.body, 'appendChild').mockImplementation((node: Node) => node)
|
||||||
|
|
||||||
|
downloadBlob({ data: blob, fileName: 'file.txt' })
|
||||||
|
|
||||||
|
expect(createObjectURLMock).toHaveBeenCalledWith(blob)
|
||||||
|
expect(mockAnchor.href).toBe(mockUrl)
|
||||||
|
expect(mockAnchor.download).toBe('file.txt')
|
||||||
|
expect(mockAnchor.rel).toBe('noopener noreferrer')
|
||||||
|
expect(mockAnchor.click).toHaveBeenCalled()
|
||||||
|
expect(mockAnchor.remove).toHaveBeenCalled()
|
||||||
|
expect(revokeObjectURLMock).toHaveBeenCalledWith(mockUrl)
|
||||||
|
|
||||||
|
vi.restoreAllMocks()
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { downloadFile, formatFileSize, formatNumber, formatNumberAbbreviated, formatTime } from './format'
|
import { formatFileSize, formatNumber, formatNumberAbbreviated, formatTime } from './format'
|
||||||
|
|
||||||
describe('formatNumber', () => {
|
describe('formatNumber', () => {
|
||||||
it('should correctly format integers', () => {
|
it('should correctly format integers', () => {
|
||||||
@@ -82,49 +82,6 @@ describe('formatTime', () => {
|
|||||||
expect(formatTime(7200)).toBe('2.00 h')
|
expect(formatTime(7200)).toBe('2.00 h')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
describe('downloadFile', () => {
|
|
||||||
it('should create a link and trigger a download correctly', () => {
|
|
||||||
// Mock data
|
|
||||||
const blob = new Blob(['test content'], { type: 'text/plain' })
|
|
||||||
const fileName = 'test-file.txt'
|
|
||||||
const mockUrl = 'blob:mockUrl'
|
|
||||||
|
|
||||||
// Mock URL.createObjectURL
|
|
||||||
const createObjectURLMock = vi.fn().mockReturnValue(mockUrl)
|
|
||||||
const revokeObjectURLMock = vi.fn()
|
|
||||||
Object.defineProperty(window.URL, 'createObjectURL', { value: createObjectURLMock })
|
|
||||||
Object.defineProperty(window.URL, 'revokeObjectURL', { value: revokeObjectURLMock })
|
|
||||||
|
|
||||||
// Mock createElement and appendChild
|
|
||||||
const mockLink = {
|
|
||||||
href: '',
|
|
||||||
download: '',
|
|
||||||
click: vi.fn(),
|
|
||||||
remove: vi.fn(),
|
|
||||||
}
|
|
||||||
const createElementMock = vi.spyOn(document, 'createElement').mockReturnValue(mockLink as any)
|
|
||||||
const appendChildMock = vi.spyOn(document.body, 'appendChild').mockImplementation((node: Node) => {
|
|
||||||
return node
|
|
||||||
})
|
|
||||||
|
|
||||||
// Call the function
|
|
||||||
downloadFile({ data: blob, fileName })
|
|
||||||
|
|
||||||
// Assertions
|
|
||||||
expect(createObjectURLMock).toHaveBeenCalledWith(blob)
|
|
||||||
expect(createElementMock).toHaveBeenCalledWith('a')
|
|
||||||
expect(mockLink.href).toBe(mockUrl)
|
|
||||||
expect(mockLink.download).toBe(fileName)
|
|
||||||
expect(appendChildMock).toHaveBeenCalledWith(mockLink)
|
|
||||||
expect(mockLink.click).toHaveBeenCalled()
|
|
||||||
expect(mockLink.remove).toHaveBeenCalled()
|
|
||||||
expect(revokeObjectURLMock).toHaveBeenCalledWith(mockUrl)
|
|
||||||
|
|
||||||
// Clean up mocks
|
|
||||||
vi.restoreAllMocks()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('formatNumberAbbreviated', () => {
|
describe('formatNumberAbbreviated', () => {
|
||||||
it('should return number as string when less than 1000', () => {
|
it('should return number as string when less than 1000', () => {
|
||||||
expect(formatNumberAbbreviated(0)).toBe('0')
|
expect(formatNumberAbbreviated(0)).toBe('0')
|
||||||
|
|||||||
@@ -100,17 +100,6 @@ export const formatTime = (seconds: number) => {
|
|||||||
return `${seconds.toFixed(2)} ${units[index]}`
|
return `${seconds.toFixed(2)} ${units[index]}`
|
||||||
}
|
}
|
||||||
|
|
||||||
export const downloadFile = ({ data, fileName }: { data: Blob, fileName: string }) => {
|
|
||||||
const url = window.URL.createObjectURL(data)
|
|
||||||
const a = document.createElement('a')
|
|
||||||
a.href = url
|
|
||||||
a.download = fileName
|
|
||||||
document.body.appendChild(a)
|
|
||||||
a.click()
|
|
||||||
a.remove()
|
|
||||||
window.URL.revokeObjectURL(url)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Formats a number into a readable string using "k", "M", or "B" suffix.
|
* Formats a number into a readable string using "k", "M", or "B" suffix.
|
||||||
* @example
|
* @example
|
||||||
|
|||||||
Reference in New Issue
Block a user