fix(web): align document drop state (WTA-2106)

Linear: https://linear.app/dify/issue/WTA-2106
This commit is contained in:
Stephen Zhou 2026-08-19 17:13:19 +08:00
parent 91006dfd2e
commit ac5ba51ec0
No known key found for this signature in database
4 changed files with 89 additions and 27 deletions

View File

@ -1773,7 +1773,7 @@ describe('DocumentsPage', () => {
).not.toBeInTheDocument()
})
it('opens the upload form when files are dropped on a populated document page', () => {
it('shows the drop target while dragging and previews dropped files in the upload form', () => {
documentsQuery.data = { pages: [{ items: [document()] }] }
const droppedFile = new File(['# handbook'], 'handbook.md', { type: 'text/markdown' })
@ -1783,6 +1783,21 @@ describe('DocumentsPage', () => {
.getByRole('heading', { name: 'dataset.newKnowledge.documents' })
.closest('section')
expect(documentSurface).not.toBeNull()
fireEvent.dragEnter(documentSurface!, {
dataTransfer: { files: [droppedFile], types: ['Files'] },
})
expect(screen.getByText('dataset.newKnowledge.dropFilesHere')).toBeInTheDocument()
fireEvent.dragLeave(documentSurface!, {
dataTransfer: { files: [droppedFile], types: ['Files'] },
})
expect(screen.queryByText('dataset.newKnowledge.dropFilesHere')).not.toBeInTheDocument()
fireEvent.dragEnter(documentSurface!, {
dataTransfer: { files: [droppedFile], types: ['Files'] },
})
fireEvent.drop(documentSurface!, {
dataTransfer: {
dropEffect: 'copy',
@ -1794,9 +1809,38 @@ describe('DocumentsPage', () => {
expect(
screen.getByRole('heading', { name: 'dataset.newKnowledge.addDocument' }),
).toBeInTheDocument()
expect(screen.queryByText('dataset.newKnowledge.dropFilesHere')).not.toBeInTheDocument()
expect(screen.getByText('handbook.md')).toBeInTheDocument()
})
it('keeps the designed drop target active over the open upload form', async () => {
const droppedFile = new File(['# handbook'], 'handbook.md', { type: 'text/markdown' })
render(<DocumentsPage knowledgeSpaceId="space-1" />, { searchParams: '?upload=1' })
const documentSurface = screen
.getByRole('heading', { name: 'dataset.newKnowledge.addDocument' })
.closest('section')
expect(documentSurface).not.toBeNull()
fireEvent.dragEnter(documentSurface!, {
dataTransfer: { files: [droppedFile], types: ['Files'] },
})
expect(screen.getByText('dataset.newKnowledge.dropFilesHere')).toBeInTheDocument()
fireEvent.drop(documentSurface!, {
dataTransfer: {
dropEffect: 'copy',
files: [droppedFile],
types: ['Files'],
},
})
expect(screen.queryByText('dataset.newKnowledge.dropFilesHere')).not.toBeInTheDocument()
expect(await screen.findByText('handbook.md')).toBeInTheDocument()
})
it('keeps direct-upload actions unavailable until the deployment is verified', () => {
systemFeaturesStateMock.uploadEnabled = false

View File

@ -921,7 +921,7 @@ export function DocumentDropOverlay() {
className="pointer-events-none absolute inset-0 z-30 flex flex-col items-center justify-center rounded-lg border-2 border-dashed border-divider-regular bg-[rgba(255,255,255,0.5)] text-center backdrop-blur-[5px]"
role="status"
>
<div className="flex w-57 items-center justify-center gap-5 rounded-xl border border-dashed border-divider-regular bg-components-panel-bg px-8 py-7 shadow-xs">
<div className="flex h-22 w-57 items-center justify-center gap-2 rounded-xl border border-dashed border-divider-regular bg-components-panel-bg shadow-xs">
<span aria-hidden className="i-ri-file-word-2-fill size-6 text-text-accent" />
<span aria-hidden className="i-ri-file-pdf-2-fill size-6 text-text-destructive" />
<span aria-hidden className="i-ri-file-excel-fill size-6 text-text-success" />
@ -930,7 +930,7 @@ export function DocumentDropOverlay() {
<p className="mt-4 system-md-semibold text-text-primary">
{t(($) => $['newKnowledge.dropFilesHere'])}
</p>
<p className="mt-2 system-xs-regular text-text-tertiary">
<p className="mt-1 system-xs-regular text-text-tertiary">
{t(($) => $['newKnowledge.documentUploadFormats'])}
</p>
</div>

View File

@ -1,8 +1,9 @@
'use client'
import type { Ref } from 'react'
import type { KnowledgeFsUploadPhase } from './knowledge-fs-upload'
import { Button } from '@langgenius/dify-ui/button'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { DocumentUploadFileList } from './document-upload-file-list'
import {
@ -12,6 +13,21 @@ import {
uniqueDocumentUploadFiles,
} from './document-upload-policy'
export type DocumentUploadFormHandle = {
addFiles: (files: File[]) => void
}
type DocumentUploadFormProps = {
initialFiles?: File[]
onCancel: () => void
onFilesAdded: (files: File[]) => Promise<void>
onFileRemoved: (file: File) => void
onSubmit: (files: File[]) => Promise<boolean>
uploadProgress?: ReadonlyMap<File, KnowledgeFsUploadPhase>
uploading: boolean
ref?: Ref<DocumentUploadFormHandle>
}
export function DocumentUploadForm({
initialFiles = [],
onCancel,
@ -20,15 +36,8 @@ export function DocumentUploadForm({
onSubmit,
uploadProgress = new Map(),
uploading,
}: {
initialFiles?: File[]
onCancel: () => void
onFilesAdded: (files: File[]) => Promise<void>
onFileRemoved: (file: File) => void
onSubmit: (files: File[]) => Promise<boolean>
uploadProgress?: ReadonlyMap<File, KnowledgeFsUploadPhase>
uploading: boolean
}) {
ref,
}: DocumentUploadFormProps) {
const { t } = useTranslation('dataset')
const { t: tCommon } = useTranslation('common')
const inputRef = useRef<HTMLInputElement>(null)
@ -62,6 +71,19 @@ export function DocumentUploadForm({
[onFilesAdded],
)
const addFiles = useCallback(
(nextFiles: File[]) => {
const uniqueFiles = uniqueDocumentUploadFiles(filesRef.current, nextFiles)
const validUniqueFiles = uniqueFiles.filter((file) => !documentUploadIssue(file))
filesRef.current = [...filesRef.current, ...uniqueFiles]
setFiles(filesRef.current)
stageAddedFiles(validUniqueFiles)
},
[stageAddedFiles],
)
useImperativeHandle(ref, () => ({ addFiles }), [addFiles])
useEffect(() => {
if (initialFilesAnnouncedRef.current) return
initialFilesAnnouncedRef.current = true
@ -78,14 +100,6 @@ export function DocumentUploadForm({
})
}, [initialFiles, onFilesAdded])
const addFiles = (nextFiles: File[]) => {
const uniqueFiles = uniqueDocumentUploadFiles(filesRef.current, nextFiles)
const validUniqueFiles = uniqueFiles.filter((file) => !documentUploadIssue(file))
filesRef.current = [...filesRef.current, ...uniqueFiles]
setFiles(filesRef.current)
stageAddedFiles(validUniqueFiles)
}
return (
<form
aria-labelledby="new-knowledge-documents-title"

View File

@ -2,6 +2,7 @@
import type { DocumentAction } from './document-actions-dropdown'
import type { DocumentProcessingTask } from './document-models'
import type { DocumentUploadFormHandle } from './document-upload-form'
import type { DocumentUploadIssue } from './document-upload-policy'
import type { KnowledgeFsUploadPhase, KnowledgeFsUploadProgress } from './knowledge-fs-upload'
import type {
@ -299,6 +300,7 @@ export function DocumentsPage({ knowledgeSpaceId }: { knowledgeSpaceId: string }
)
const [selectedDocumentIds, setSelectedDocumentIds] = useState<Set<string>>(() => new Set())
const [uploadFormInitialFiles, setUploadFormInitialFiles] = useState<File[]>([])
const uploadFormRef = useRef<DocumentUploadFormHandle>(null)
const [isFileDragActive, setIsFileDragActive] = useState(false)
const fileDragDepthRef = useRef(0)
const [tasksOpen, setTasksOpen] = useState(false)
@ -2594,7 +2596,7 @@ export function DocumentsPage({ knowledgeSpaceId }: { knowledgeSpaceId: string }
const types = Array.from(event.dataTransfer.types ?? [])
if (types.length && !types.includes('Files')) return
event.preventDefault()
if (!canUpload || uploadFormOpen || uploading) return
if (!canUpload || uploading) return
fileDragDepthRef.current += 1
setIsFileDragActive(true)
}}
@ -2607,8 +2609,7 @@ export function DocumentsPage({ knowledgeSpaceId }: { knowledgeSpaceId: string }
const types = Array.from(event.dataTransfer.types ?? [])
if (types.length && !types.includes('Files')) return
event.preventDefault()
if (!uploadFormOpen)
event.dataTransfer.dropEffect = canUpload && !uploading ? 'copy' : 'none'
event.dataTransfer.dropEffect = canUpload && !uploading ? 'copy' : 'none'
}}
onDrop={(event) => {
const types = Array.from(event.dataTransfer.types ?? [])
@ -2616,9 +2617,11 @@ export function DocumentsPage({ knowledgeSpaceId }: { knowledgeSpaceId: string }
event.preventDefault()
fileDragDepthRef.current = 0
setIsFileDragActive(false)
if (!canUpload || uploadFormOpen || uploading) return
if (!canUpload || uploading) return
const files = [...event.dataTransfer.files]
if (files.length) openUploadForm(files)
if (!files.length) return
if (uploadFormOpen) uploadFormRef.current?.addFiles(files)
else openUploadForm(files)
}}
onBlurCapture={(event) => {
if (!event.currentTarget.contains(event.relatedTarget as Node | null)) {
@ -2860,6 +2863,7 @@ export function DocumentsPage({ knowledgeSpaceId }: { knowledgeSpaceId: string }
</div>
) : uploadFormOpen ? (
<DocumentUploadForm
ref={uploadFormRef}
initialFiles={uploadFormInitialFiles}
uploadProgress={stagedUploadProgress}
uploading={uploading}
@ -2952,7 +2956,7 @@ export function DocumentsPage({ knowledgeSpaceId }: { knowledgeSpaceId: string }
uploading={uploading}
/>
)}
{isFileDragActive && canUpload && !uploadFormOpen && <DocumentDropOverlay />}
{isFileDragActive && canUpload && <DocumentDropOverlay />}
</section>
{bulkActionsVisible && (
<DocumentBulkActions