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 (
>(() => new Set()) const [uploadFormInitialFiles, setUploadFormInitialFiles] = useState([]) + const uploadFormRef = useRef(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 }
) : uploadFormOpen ? ( )} - {isFileDragActive && canUpload && !uploadFormOpen && } + {isFileDragActive && canUpload && } {bulkActionsVisible && (