refactor(files): migrate supported format queries to generated contracts (#42727)

This commit is contained in:
yyh
2026-09-22 08:14:04 +00:00
committed by GitHub
parent 8253405c0b
commit 0cc53efe06
11 changed files with 292 additions and 62 deletions
+1 -1
View File
@@ -4712,7 +4712,7 @@
},
"web/service/use-common.ts": {
"@tanstack/query/prefer-query-options": {
"count": 10
"count": 9
},
"dify/require-disable-directive-description": {
"count": 1
@@ -0,0 +1,136 @@
import type { DocExtractorNodeType } from '@/app/components/workflow/nodes/document-extractor/types'
import type { PanelProps } from '@/types/workflow'
import { screen, waitFor } from '@testing-library/react'
import FileUploader from '@/app/components/datasets/create/file-uploader'
import Panel from '@/app/components/workflow/nodes/document-extractor/panel'
import useConfig from '@/app/components/workflow/nodes/document-extractor/use-config'
import { BlockEnum } from '@/app/components/workflow/types'
import { LanguagesSupported } from '@/i18n/language'
import { consoleQuery } from '@/service/console'
import { createConsoleQueryClient, renderWithConsoleQuery } from '@/test/console/query-data'
let mockLocale = 'en-US'
const request = vi.hoisted(() => vi.fn<(url: string) => Promise<Response>>())
vi.mock('@/service/base', async (importOriginal) => ({
...(await importOriginal<typeof import('@/service/base')>()),
request,
}))
vi.mock('@/service/use-common', () => ({
useFileUploadConfig: () => ({
data: { file_size_limit: 15, batch_count_limit: 5, file_upload_limit: 10 },
}),
}))
vi.mock('#i18n', async (importOriginal) => ({
...(await importOriginal<typeof import('#i18n')>()),
useLocale: () => mockLocale,
}))
vi.mock('@/app/components/workflow/nodes/_base/components/variable/var-reference-picker', () => ({
default: () => null,
}))
vi.mock('@/app/components/workflow/nodes/_base/hooks/use-node-help-link', () => ({
useNodeHelpLink: () => 'https://docs.example.com/document-extractor',
}))
vi.mock('@/app/components/workflow/nodes/document-extractor/use-config', () => ({
default: vi.fn(),
}))
const createData = (): DocExtractorNodeType => ({
title: 'Document Extractor',
desc: '',
type: BlockEnum.DocExtractor,
variable_selector: ['node-1', 'files'],
is_array_file: false,
})
const panelProps: PanelProps = {
getInputVars: vi.fn(() => []),
toVarInputs: vi.fn(() => []),
runInputData: {},
runInputDataRef: { current: {} },
setRunInputData: vi.fn(),
runResult: null,
}
beforeEach(() => {
vi.clearAllMocks()
mockLocale = 'en-US'
vi.mocked(useConfig).mockReturnValue({
readOnly: false,
inputs: createData(),
handleVarChanges: vi.fn(),
filterVar: () => true,
})
})
describe('file supported formats across upload and document extraction', () => {
it('shares the raw supported-formats response with the uploader across locale changes', async () => {
const queryClient = createConsoleQueryClient()
const response = { allowed_extensions: ['PDF', 'pdf', 'md', 'md', 'DOCX', 'docx'] }
request.mockImplementation(async () => Response.json(response))
const consumers = () => (
<>
<FileUploader
fileList={[]}
prepareFileList={vi.fn()}
onFileUpdate={vi.fn()}
onPreview={vi.fn()}
/>
<Panel id="doc-node" data={createData()} panelProps={panelProps} />
</>
)
const { container, rerender } = renderWithConsoleQuery(consumers(), {
queryClient,
systemFeatures: { deployment_edition: 'CLOUD' },
})
expect(container.querySelector('input[type="file"]')).toHaveAttribute('accept', '')
expect(
screen.queryByRole('button', { name: 'datasetCreation.stepOne.uploader.browse' }),
).not.toBeInTheDocument()
await screen.findByText(
'workflow.nodes.docExtractor.supportFileTypes:{"types":"pdf, markdown, docx"}',
)
expect(container.querySelector('input[type="file"]')).toHaveAttribute(
'accept',
'.PDF,.pdf,.md,.md,.DOCX,.docx',
)
expect(
screen.getByRole('button', { name: 'datasetCreation.stepOne.uploader.browse' }),
).toBeInTheDocument()
expect(screen.getByText(/"supportTypes":"PDF, MARKDOWN, DOCX"/)).toBeInTheDocument()
expect(request).toHaveBeenCalledTimes(1)
expect(new URL(request.mock.calls[0]![0]).pathname).toBe('/console/api/files/support-type')
expect(request).toHaveBeenCalledWith(
expect.any(String),
expect.any(Object),
expect.objectContaining({
fetchCompat: true,
request: expect.objectContaining({ method: 'GET' }),
silent: undefined,
}),
)
mockLocale = LanguagesSupported[1]!
rerender(consumers())
await waitFor(() =>
expect(
screen.getByText(
'workflow.nodes.docExtractor.supportFileTypes:{"types":"pdf、 markdown、 docx"}',
),
).toBeInTheDocument(),
)
expect(screen.getByText(/"supportTypes":"PDF、 MARKDOWN、 DOCX"/)).toBeInTheDocument()
expect(request).toHaveBeenCalledTimes(1)
expect(
queryClient.getQueryData(consoleQuery.files.supportType.get.queryOptions().queryKey),
).toEqual(response)
})
})
@@ -1,12 +1,21 @@
import type { CustomFile as File, FileItem } from '@/models/datasets'
import { fireEvent, screen } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vite-plus/test'
import { renderWithConsoleQuery } from '@/test/console/query-data'
import { consoleQuery } from '@/service/console'
import { createConsoleQueryClient, renderWithConsoleQuery } from '@/test/console/query-data'
import { PROGRESS_NOT_STARTED } from '../constants'
import FileUploader from '../index'
const render = (ui: React.ReactElement) =>
renderWithConsoleQuery(ui, { systemFeatures: { deployment_edition: 'CLOUD' } })
const render = (ui: React.ReactElement) => {
const queryClient = createConsoleQueryClient()
queryClient.setQueryData(consoleQuery.files.supportType.get.queryOptions().queryKey, {
allowed_extensions: ['pdf', 'docx', 'txt'],
})
return renderWithConsoleQuery(ui, {
queryClient,
systemFeatures: { deployment_edition: 'CLOUD' },
})
}
const mockNotify = vi.fn()
vi.mock('use-context-selector', async () => {
@@ -27,9 +36,6 @@ vi.mock('@/service/use-common', () => ({
useFileUploadConfig: () => ({
data: { file_size_limit: 15, batch_count_limit: 5, file_upload_limit: 10 },
}),
useFileSupportTypes: () => ({
data: { allowed_extensions: ['pdf', 'docx', 'txt'] },
}),
}))
vi.mock('@/i18n/language', () => ({
LanguagesSupported: ['en-US', 'zh-Hans'],
@@ -1,8 +1,13 @@
import type { ReactElement } from 'react'
import type { CustomFile, FileItem } from '@/models/datasets'
import { act, renderHook, waitFor } from '@testing-library/react'
import { act, fireEvent, renderHook, screen, waitFor } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vite-plus/test'
import { createConsoleQueryWrapper, renderWithConsoleQuery } from '@/test/console/query-data'
import { consoleQuery } from '@/service/console'
import {
createConsoleQueryClient,
createConsoleQueryWrapper,
renderWithConsoleQuery,
} from '@/test/console/query-data'
import { PROGRESS_COMPLETE, PROGRESS_ERROR, PROGRESS_NOT_STARTED } from '../../constants'
// Import after mocks
import { useFileUpload } from '../use-file-upload'
@@ -18,7 +23,9 @@ vi.mock('@/app/notifications', () => ({
// Mock upload service
const mockUpload = vi.fn()
const request = vi.hoisted(() => vi.fn<(url: string) => Promise<Response>>())
vi.mock('@/service/base', () => ({
request,
upload: (...args: unknown[]) => mockUpload(...args),
}))
@@ -30,13 +37,12 @@ const mockFileUploadConfig = {
file_upload_limit: 10,
}
const mockSupportTypes = {
const supportedFormats = {
allowed_extensions: ['pdf', 'docx', 'txt', 'md'],
}
vi.mock('@/service/use-common', () => ({
useFileUploadConfig: () => ({ data: mockFileUploadConfig }),
useFileSupportTypes: () => ({ data: mockSupportTypes }),
}))
vi.mock('@/i18n/language', () => ({
LanguagesSupported: ['en-US', 'zh-Hans'],
@@ -47,10 +53,24 @@ vi.mock('@/app/components/base/file-uploader/utils', () => ({
getFileUploadErrorMessage: (_e: unknown, defaultMsg: string) => defaultMsg,
}))
const createQueryClient = () => {
const queryClient = createConsoleQueryClient()
queryClient.setQueryData(
consoleQuery.files.supportType.get.queryOptions().queryKey,
supportedFormats,
)
return queryClient
}
const createWrapper = () =>
createConsoleQueryWrapper({ systemFeatures: { deployment_edition: 'CLOUD' } }).wrapper
createConsoleQueryWrapper({
queryClient: createQueryClient(),
systemFeatures: { deployment_edition: 'CLOUD' },
}).wrapper
const render = (ui: ReactElement) =>
renderWithConsoleQuery(ui, { systemFeatures: { deployment_edition: 'CLOUD' } })
renderWithConsoleQuery(ui, {
queryClient: createQueryClient(),
systemFeatures: { deployment_edition: 'CLOUD' },
})
describe('useFileUpload', () => {
const defaultOptions = {
@@ -133,6 +153,66 @@ describe('useFileUpload', () => {
})
})
it.each([
{ allowedExtensions: ['csv'], expectedAccept: '.csv', uploadCount: 1 },
{ allowedExtensions: [], expectedAccept: '', uploadCount: 0 },
])(
'keeps the explicit $allowedExtensions override while subscribing to supported formats',
async ({ allowedExtensions, expectedAccept, uploadCount }) => {
const queryClient = createConsoleQueryClient()
request.mockImplementation(async () => Response.json(supportedFormats))
const Uploader = () => {
const { fileUploaderRef, fileChangeHandle, acceptTypes } = useFileUpload({
...defaultOptions,
allowedExtensions,
})
return (
<input
aria-label="Upload file"
type="file"
ref={fileUploaderRef}
accept={acceptTypes.join(',')}
onChange={fileChangeHandle}
/>
)
}
renderWithConsoleQuery(<Uploader />, {
queryClient,
systemFeatures: { deployment_edition: 'CLOUD' },
})
const input = screen.getByLabelText('Upload file')
expect(input).toHaveAttribute('accept', expectedAccept)
await waitFor(() =>
expect(
queryClient.getQueryData(consoleQuery.files.supportType.get.queryOptions().queryKey),
).toEqual(supportedFormats),
)
expect(request).toHaveBeenCalledTimes(1)
expect(input).toHaveAttribute('accept', expectedAccept)
fireEvent.change(input, {
target: { files: [new File(['row'], 'data.csv', { type: 'text/csv' })] },
})
await waitFor(() => expect(mockUpload).toHaveBeenCalledTimes(uploadCount))
if (uploadCount === 0) {
expect(mockNotify).toHaveBeenCalledWith({
type: 'error',
message: 'datasetCreation.stepOne.uploader.validation.typeError',
})
expect(defaultOptions.prepareFileList).not.toHaveBeenCalled()
} else {
expect(defaultOptions.prepareFileList).toHaveBeenCalledOnce()
expect(defaultOptions.onFileUpdate).toHaveBeenCalledWith(
expect.objectContaining({ file: { id: 'default-id' } }),
PROGRESS_COMPLETE,
expect.any(Array),
)
expect(mockNotify).not.toHaveBeenCalled()
}
},
)
describe('selectHandle', () => {
it('should trigger click on file input', () => {
const { result } = renderHook(() => useFileUpload(defaultOptions), {
@@ -1,7 +1,7 @@
'use client'
import type { RefObject } from 'react'
import type { CustomFile as File, FileItem } from '@/models/datasets'
import { useSuspenseQuery } from '@tanstack/react-query'
import { useQuery, useSuspenseQuery } from '@tanstack/react-query'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useLocale } from '#i18n'
@@ -10,7 +10,8 @@ import { toast } from '@/app/notifications'
import { systemFeaturesQueryOptions } from '@/features/system-features/client'
import { LanguagesSupported } from '@/i18n/language'
import { upload } from '@/service/base'
import { useFileSupportTypes, useFileUploadConfig } from '@/service/use-common'
import { consoleQuery } from '@/service/console'
import { useFileUploadConfig } from '@/service/use-common'
import { getFileExtension } from '@/utils/format'
import { PROGRESS_COMPLETE, PROGRESS_ERROR, PROGRESS_NOT_STARTED } from '../constants'
@@ -28,8 +29,7 @@ type UseFileUploadOptions = {
onPreview: (file: File) => void
supportBatchUpload?: boolean
/**
* Optional list of allowed file extensions. If not provided, fetches from API.
* Pass this when you need custom extension filtering instead of using the global config.
* Overrides the supported extensions from the API, including an empty list.
*/
allowedExtensions?: string[]
}
@@ -86,8 +86,9 @@ export const useFileUpload = ({
const hideUpload = !supportBatchUpload && fileList.length > 0
const { data: fileUploadConfigResponse } = useFileUploadConfig()
const { data: supportFileTypesResponse } = useFileSupportTypes()
// Use provided allowedExtensions or fetch from API
const { data: supportFileTypesResponse } = useQuery(
consoleQuery.files.supportType.get.queryOptions(),
)
const supportTypes = useMemo(
() => allowedExtensions ?? supportFileTypesResponse?.allowed_extensions ?? [],
[allowedExtensions, supportFileTypesResponse?.allowed_extensions],
@@ -2,7 +2,12 @@ import type { ReactElement } from 'react'
import type { CustomFile, FileItem } from '@/models/datasets'
import { act, renderHook, waitFor } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vite-plus/test'
import { createConsoleQueryWrapper, renderWithConsoleQuery } from '@/test/console/query-data'
import { consoleQuery } from '@/service/console'
import {
createConsoleQueryClient,
createConsoleQueryWrapper,
renderWithConsoleQuery,
} from '@/test/console/query-data'
import { PROGRESS_ERROR } from '../../constants'
const { mockNotify, mockToast } = vi.hoisted(() => {
@@ -66,12 +71,6 @@ vi.mock('@/service/use-common', () => ({
file_upload_limit: 10,
},
})),
// Required by the shared useFileUpload hook
useFileSupportTypes: vi.fn(() => ({
data: {
allowed_extensions: ['pdf', 'docx', 'txt'],
},
})),
}))
// Mock upload service
@@ -83,10 +82,23 @@ vi.mock('@/service/base', () => ({
// Import after all mocks are set up
const { useLocalFileUpload } = await import('../use-local-file-upload')
const createQueryClient = () => {
const queryClient = createConsoleQueryClient()
queryClient.setQueryData(consoleQuery.files.supportType.get.queryOptions().queryKey, {
allowed_extensions: ['pdf', 'docx', 'txt'],
})
return queryClient
}
const createWrapper = () =>
createConsoleQueryWrapper({ systemFeatures: { deployment_edition: 'CLOUD' } }).wrapper
createConsoleQueryWrapper({
queryClient: createQueryClient(),
systemFeatures: { deployment_edition: 'CLOUD' },
}).wrapper
const render = (ui: ReactElement) =>
renderWithConsoleQuery(ui, { systemFeatures: { deployment_edition: 'CLOUD' } })
renderWithConsoleQuery(ui, {
queryClient: createQueryClient(),
systemFeatures: { deployment_edition: 'CLOUD' },
})
describe('useLocalFileUpload', () => {
beforeEach(() => {
@@ -1,14 +1,24 @@
import type { ReactNode } from 'react'
import type { ReactElement, ReactNode } from 'react'
import type { DocExtractorNodeType } from '../types'
import type { PanelProps } from '@/types/workflow'
import { render, screen } from '@testing-library/react'
import { screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { LanguagesSupported } from '@/i18n/language'
import { consoleQuery } from '@/service/console'
import { createConsoleQueryClient, renderWithConsoleQuery } from '@/test/console/query-data'
import { BlockEnum } from '../../../types'
import Node from '../node'
import Panel from '../panel'
import useConfig from '../use-config'
const render = (ui: ReactElement) => {
const queryClient = createConsoleQueryClient()
queryClient.setQueryData(consoleQuery.files.supportType.get.queryOptions().queryKey, {
allowed_extensions: ['PDF', 'md', 'md', 'DOCX'],
})
return renderWithConsoleQuery(ui, { queryClient })
}
let mockLocale = 'en-US'
vi.mock('reactflow', async () => {
@@ -73,14 +83,6 @@ vi.mock('@/app/components/workflow/nodes/_base/hooks/use-node-help-link', () =>
useNodeHelpLink: () => 'https://docs.example.com/document-extractor',
}))
vi.mock('@/service/use-common', () => ({
useFileSupportTypes: () => ({
data: {
allowed_extensions: ['PDF', 'md', 'md', 'DOCX'],
},
}),
}))
vi.mock('#i18n', async (importOriginal) => ({
...(await importOriginal<typeof import('#i18n')>()),
useLocale: () => mockLocale,
@@ -1,13 +1,23 @@
import type { ReactNode } from 'react'
import type { ReactElement, ReactNode } from 'react'
import type { DocExtractorNodeType } from '../types'
import type { PanelProps } from '@/types/workflow'
import { render, screen } from '@testing-library/react'
import { screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { LanguagesSupported } from '@/i18n/language'
import { consoleQuery } from '@/service/console'
import { createConsoleQueryClient, renderWithConsoleQuery } from '@/test/console/query-data'
import { BlockEnum } from '../../../types'
import Panel from '../panel'
import useConfig from '../use-config'
const render = (ui: ReactElement) => {
const queryClient = createConsoleQueryClient()
queryClient.setQueryData(consoleQuery.files.supportType.get.queryOptions().queryKey, {
allowed_extensions: ['PDF', 'md', 'md', 'DOCX'],
})
return renderWithConsoleQuery(ui, { queryClient })
}
let mockLocale = 'en-US'
vi.mock('@/app/components/workflow/nodes/_base/components/field', () => ({
@@ -44,14 +54,6 @@ vi.mock('@/app/components/workflow/nodes/_base/hooks/use-node-help-link', () =>
useNodeHelpLink: () => 'https://docs.example.com/document-extractor',
}))
vi.mock('@/service/use-common', () => ({
useFileSupportTypes: () => ({
data: {
allowed_extensions: ['PDF', 'md', 'md', 'DOCX'],
},
}),
}))
vi.mock('#i18n', async (importOriginal) => ({
...(await importOriginal<typeof import('#i18n')>()),
useLocale: () => mockLocale,
@@ -1,13 +1,14 @@
import type { FC } from 'react'
import type { DocExtractorNodeType } from './types'
import type { NodePanelProps } from '@/app/components/workflow/types'
import { useQuery } from '@tanstack/react-query'
import * as React from 'react'
import { useTranslation } from 'react-i18next'
import { useLocale } from '#i18n'
import Field from '@/app/components/workflow/nodes/_base/components/field'
import { BlockEnum } from '@/app/components/workflow/types'
import { LanguagesSupported } from '@/i18n/language'
import { useFileSupportTypes } from '@/service/use-common'
import { consoleQuery } from '@/service/console'
import OutputVars, { VarItem } from '../_base/components/output-vars'
import Split from '../_base/components/split'
import VarReferencePicker from '../_base/components/variable/var-reference-picker'
@@ -20,7 +21,9 @@ const Panel: FC<NodePanelProps<DocExtractorNodeType>> = ({ id, data }) => {
const { t } = useTranslation()
const locale = useLocale()
const link = useNodeHelpLink(BlockEnum.DocExtractor)
const { data: supportFileTypesResponse } = useFileSupportTypes()
const { data: supportFileTypesResponse } = useQuery(
consoleQuery.files.supportType.get.queryOptions(),
)
const supportTypes = supportFileTypesResponse?.allowed_extensions || []
const supportTypesShowNames = (() => {
const extensionMap: { [key: string]: string } = {
-4
View File
@@ -325,10 +325,6 @@ export const checkWatercrawlTaskStatus = (jobId: string): Promise<CommonResponse
)
}
export type FileTypesRes = {
allowed_extensions: string[]
}
export const retryErrorDocs = ({
datasetId,
document_ids,
-8
View File
@@ -1,4 +1,3 @@
import type { FileTypesRes } from './datasets'
import type {
ModelParameterRule,
ModelProvider,
@@ -103,13 +102,6 @@ export const useMailRegister = () => {
})
}
export const useFileSupportTypes = () => {
return useQuery<FileTypesRes>({
queryKey: [NAME_SPACE, 'file-types'],
queryFn: () => get<FileTypesRes>('/files/support-type'),
})
}
type MemberResponse = {
accounts: Member[] | null
}