From ac5ba51ec07b31017bf927010a520e00dc7b26e8 Mon Sep 17 00:00:00 2001
From: Stephen Zhou <38493346+hyoban@users.noreply.github.com>
Date: Wed, 19 Aug 2026 17:13:19 +0800
Subject: [PATCH] fix(web): align document drop state (WTA-2106)
Linear: https://linear.app/dify/issue/WTA-2106
---
.../new-rag/__tests__/documents-page.spec.tsx | 46 ++++++++++++++++-
web/features/new-rag/document-list.tsx | 4 +-
web/features/new-rag/document-upload-form.tsx | 50 ++++++++++++-------
web/features/new-rag/documents-page.tsx | 16 +++---
4 files changed, 89 insertions(+), 27 deletions(-)
diff --git a/web/features/new-rag/__tests__/documents-page.spec.tsx b/web/features/new-rag/__tests__/documents-page.spec.tsx
index a4a96ee0525..dc2952aac39 100644
--- a/web/features/new-rag/__tests__/documents-page.spec.tsx
+++ b/web/features/new-rag/__tests__/documents-page.spec.tsx
@@ -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(, { 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
diff --git a/web/features/new-rag/document-list.tsx b/web/features/new-rag/document-list.tsx
index dca0c6794f5..fada4ae6224 100644
--- a/web/features/new-rag/document-list.tsx
+++ b/web/features/new-rag/document-list.tsx
@@ -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"
>
-
+
@@ -930,7 +930,7 @@ export function DocumentDropOverlay() {
{t(($) => $['newKnowledge.dropFilesHere'])}
-
+
{t(($) => $['newKnowledge.documentUploadFormats'])}
diff --git a/web/features/new-rag/document-upload-form.tsx b/web/features/new-rag/document-upload-form.tsx
index 83da5224dee..3c0aabd6d9a 100644
--- a/web/features/new-rag/document-upload-form.tsx
+++ b/web/features/new-rag/document-upload-form.tsx
@@ -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
+ onFileRemoved: (file: File) => void
+ onSubmit: (files: File[]) => Promise
+ uploadProgress?: ReadonlyMap
+ uploading: boolean
+ ref?: Ref
+}
+
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
- onFileRemoved: (file: File) => void
- onSubmit: (files: File[]) => Promise
- uploadProgress?: ReadonlyMap
- uploading: boolean
-}) {
+ ref,
+}: DocumentUploadFormProps) {
const { t } = useTranslation('dataset')
const { t: tCommon } = useTranslation('common')
const inputRef = useRef(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 (
) : uploadFormOpen ? (
)}
- {isFileDragActive && canUpload && !uploadFormOpen && }
+ {isFileDragActive && canUpload && }
{bulkActionsVisible && (