diff --git a/web/features/new-rag/__tests__/document-detail-page.spec.tsx b/web/features/new-rag/__tests__/document-detail-page.spec.tsx index ab0e66fc967..3f4d0c7b22d 100644 --- a/web/features/new-rag/__tests__/document-detail-page.spec.tsx +++ b/web/features/new-rag/__tests__/document-detail-page.spec.tsx @@ -439,7 +439,8 @@ describe('DocumentDetailPage', () => { queryClient.invalidateQueries.mockResolvedValue(undefined) }) - it('loads the document, revisions, chunks, and task status through generated contracts', () => { + it('loads the document, revisions, chunks, and task status through generated contracts', async () => { + const user = userEvent.setup() render() expect(documentOptions).toHaveBeenCalledWith( @@ -466,6 +467,19 @@ describe('DocumentDetailPage', () => { params: { control_space_id: 'space-1' }, query: { limit: 100 }, }) + await user.click( + screen.getByRole('button', { + name: /dataset\.newKnowledge\.documentActions/, + }), + ) + expect(screen.getAllByRole('menuitem')).toHaveLength(5) + expect(screen.getByRole('menuitem', { name: 'common.operation.rename' })).toBeInTheDocument() + expect( + screen.queryByRole('menuitem', { name: 'dataset.newKnowledge.reindexDocument' }), + ).not.toBeInTheDocument() + expect( + screen.getByRole('menuitem', { name: 'dataset.newKnowledge.removeSource' }), + ).toBeInTheDocument() }) it('does not construct a chunks request while the document is loading', () => { diff --git a/web/features/new-rag/__tests__/documents-page.spec.tsx b/web/features/new-rag/__tests__/documents-page.spec.tsx index a4d1d98708e..e43d3db4258 100644 --- a/web/features/new-rag/__tests__/documents-page.spec.tsx +++ b/web/features/new-rag/__tests__/documents-page.spec.tsx @@ -692,8 +692,16 @@ describe('DocumentsPage', () => { }) expect(rowActions).toBeEnabled() await user.click(rowActions) + const rowMenuItems = await screen.findAllByRole('menuitem') + expect(rowMenuItems).toHaveLength(6) + expect(rowMenuItems[0]).toHaveAccessibleName('common.operation.rename') + expect(rowMenuItems[1]).toHaveAccessibleName('dataset.newKnowledge.reindexDocument') + expect(rowMenuItems[2]).toHaveAccessibleName('dataset.newKnowledge.disableSource') + expect(rowMenuItems[3]).toHaveAccessibleName('dataset.batchAction.archive') + expect(rowMenuItems[4]).toHaveAccessibleName('dataset.newKnowledge.downloadDocuments') + expect(rowMenuItems[5]).toHaveAccessibleName('dataset.newKnowledge.removeSource') await user.click( - await screen.findByRole('menuitem', { name: 'dataset.newKnowledge.downloadDocuments' }), + screen.getByRole('menuitem', { name: 'dataset.newKnowledge.downloadDocuments' }), ) expect(toastMock.info).toHaveBeenCalledWith('dataset.newKnowledge.documentActionsUnavailable') @@ -816,6 +824,30 @@ describe('DocumentsPage', () => { ).not.toBeInTheDocument() }) + it('opens the upload form when files are dropped on a populated document page', () => { + documentsQuery.data = { pages: [{ items: [document()] }] } + const droppedFile = new File(['# handbook'], 'handbook.md', { type: 'text/markdown' }) + + render() + + const documentSurface = screen + .getByRole('heading', { name: 'dataset.newKnowledge.documents' }) + .closest('section') + expect(documentSurface).not.toBeNull() + fireEvent.drop(documentSurface!, { + dataTransfer: { + dropEffect: 'copy', + files: [droppedFile], + types: ['Files'], + }, + }) + + expect( + screen.getByRole('heading', { name: 'dataset.newKnowledge.addDocument' }), + ).toBeInTheDocument() + expect(screen.getByText('handbook.md')).toBeInTheDocument() + }) + it('keeps direct-upload actions unavailable until the deployment is verified', () => { systemFeaturesStateMock.uploadEnabled = false @@ -1884,7 +1916,7 @@ describe('DocumentsPage', () => { await waitFor(() => expect(screen.getByRole('table').parentElement).toHaveFocus()) }) - it('re-indexes selected documents and disables backend-dependent actions', async () => { + it('re-indexes selected documents and keeps the designed bulk action order', async () => { const user = userEvent.setup() documentsQuery.data = { pages: [{ items: [document({ id: 'one', title: 'One.pdf' }), document({ id: 'two' })] }], @@ -1908,25 +1940,24 @@ describe('DocumentsPage', () => { expect(reindex).toBeEnabled() const orderedActions = within(actions).getAllByRole('button') expect(orderedActions[0]).toHaveAccessibleName('dataset.newKnowledge.reindexDocuments') - expect(orderedActions[1]).toHaveAccessibleName('dataset.newKnowledge.clearDocumentSelection') - expect(actions.firstElementChild).toBe(orderedActions[0]) - expect(orderedActions[1]!.nextElementSibling).toHaveTextContent( + expect(orderedActions[1]).toHaveAccessibleName('dataset.newKnowledge.downloadDocuments') + expect(orderedActions[2]).toHaveAccessibleName('dataset.newKnowledge.deleteDocuments') + expect(orderedActions[3]).toHaveAccessibleName('dataset.newKnowledge.clearDocumentSelection') + expect(actions.firstElementChild).toHaveTextContent( 'dataset.newKnowledge.documentsSelected:{"count":1}', ) - expect( - within(actions).getByText( - 'dataset.newKnowledge.downloadDocuments / dataset.newKnowledge.deleteDocuments · dataset.cornerLabel.unavailable', - ), - ).toBeVisible() const download = within(actions).getByRole('button', { name: 'dataset.newKnowledge.downloadDocuments', }) const remove = within(actions).getByRole('button', { name: 'dataset.newKnowledge.deleteDocuments', }) - expect(download).toBeDisabled() - expect(remove).toBeDisabled() - expect(toastMock.info).not.toHaveBeenCalled() + expect(download).toBeEnabled() + expect(remove).toBeEnabled() + await user.click(download) + await user.click(remove) + expect(toastMock.info).toHaveBeenCalledTimes(2) + expect(toastMock.info).toHaveBeenCalledWith('dataset.newKnowledge.documentActionsUnavailable') await user.dblClick(reindex) expect(reindexMutation.mutateAsync).toHaveBeenCalledOnce() expect(reindexMutation.mutateAsync).toHaveBeenCalledWith({ diff --git a/web/features/new-rag/__tests__/knowledge-space-shell.spec.tsx b/web/features/new-rag/__tests__/knowledge-space-shell.spec.tsx index 07eb64b636b..32f039fc2e8 100644 --- a/web/features/new-rag/__tests__/knowledge-space-shell.spec.tsx +++ b/web/features/new-rag/__tests__/knowledge-space-shell.spec.tsx @@ -96,6 +96,25 @@ describe('KnowledgeSpaceShell', () => { expect(screen.getByText('source content')).toBeInTheDocument() }) + it('lets users collapse and restore the knowledge detail sidebar', async () => { + const user = userEvent.setup() + queryMock.data = { + control_space_id: 'space-1', + state: 'active', + technical_summary: { name: 'Support knowledge' }, + } + + render(source content) + + await user.click(screen.getByRole('button', { name: 'layout.sidebar.collapseSidebar' })) + expect(screen.getByRole('button', { name: 'layout.sidebar.expandSidebar' })).toBeInTheDocument() + + await user.click(screen.getByRole('button', { name: 'layout.sidebar.expandSidebar' })) + expect( + screen.getByRole('button', { name: 'layout.sidebar.collapseSidebar' }), + ).toBeInTheDocument() + }) + it('polls provisioning spaces without mounting data-plane children', () => { queryMock.data = { control_space_id: 'space-1', diff --git a/web/features/new-rag/document-actions-dropdown.tsx b/web/features/new-rag/document-actions-dropdown.tsx new file mode 100644 index 00000000000..c95f1f993c9 --- /dev/null +++ b/web/features/new-rag/document-actions-dropdown.tsx @@ -0,0 +1,69 @@ +'use client' + +import { cn } from '@langgenius/dify-ui/cn' +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from '@langgenius/dify-ui/dropdown-menu' +import { toast } from '@langgenius/dify-ui/toast' +import { useTranslation } from 'react-i18next' + +export function DocumentActionsDropdown({ + className, + documentTitle, + showReindex = true, +}: { + className?: string + documentTitle: string + showReindex?: boolean +}) { + const { t } = useTranslation('dataset') + const { t: tCommon } = useTranslation('common') + const unavailable = () => toast.info(t(($) => $['newKnowledge.documentActionsUnavailable'])) + + return ( + + $['newKnowledge.documentActions'], { name: documentTitle })} + className={cn( + 'ml-auto flex size-7 items-center justify-end rounded-md text-text-tertiary outline-hidden hover:bg-state-base-hover focus-visible:ring-2 focus-visible:ring-state-accent-solid', + className, + )} + > + + + + + + {tCommon(($) => $['operation.rename'])} + + {showReindex && ( + + + {t(($) => $['newKnowledge.reindexDocument'])} + + )} + + + {t(($) => $['newKnowledge.disableSource'])} + + + + {t(($) => $['batchAction.archive'])} + + + + + {t(($) => $['newKnowledge.downloadDocuments'])} + + + + {t(($) => $['newKnowledge.removeSource'])} + + + + ) +} diff --git a/web/features/new-rag/document-detail-header.tsx b/web/features/new-rag/document-detail-header.tsx index 88c046b462a..7fb479060ba 100644 --- a/web/features/new-rag/document-detail-header.tsx +++ b/web/features/new-rag/document-detail-header.tsx @@ -13,6 +13,7 @@ import { import { useEffect, useRef } from 'react' import { useTranslation } from 'react-i18next' import Link from '@/next/link' +import { DocumentActionsDropdown } from './document-actions-dropdown' export function DocumentDetailHeader({ backPath, @@ -126,6 +127,7 @@ export function DocumentDetailHeader({ + {isFetchNextRevisionPageError && ( diff --git a/web/features/new-rag/document-list.tsx b/web/features/new-rag/document-list.tsx index 10d53e565b8..6171c896cd6 100644 --- a/web/features/new-rag/document-list.tsx +++ b/web/features/new-rag/document-list.tsx @@ -6,13 +6,6 @@ import type { LogicalDocument } from './document-models' import { Button } from '@langgenius/dify-ui/button' import { Checkbox } from '@langgenius/dify-ui/checkbox' import { cn } from '@langgenius/dify-ui/cn' -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuSeparator, - DropdownMenuTrigger, -} from '@langgenius/dify-ui/dropdown-menu' import { Select, SelectContent, @@ -29,6 +22,7 @@ import Loading from '@/app/components/base/loading' import { SearchInput } from '@/app/components/base/search-input' import { useFormatTimeFromNow } from '@/hooks/use-format-time-from-now' import Link from '@/next/link' +import { DocumentActionsDropdown } from './document-actions-dropdown' import { sourceName } from './document-model' export type DocumentFilter = DocumentDisplayStatus | 'all' @@ -44,6 +38,14 @@ const statusIconClass: Record = { disabled: 'i-ri-indeterminate-circle-line text-text-tertiary', } +const statusTextClass: Record = { + ready: 'font-normal text-text-secondary', + queued: 'font-normal text-text-tertiary', + processing: 'font-medium text-text-accent', + failed: 'font-medium text-text-destructive', + disabled: 'font-medium text-text-tertiary', +} + function TaskTrigger({ activeTaskCount, attentionTaskBadge, @@ -62,7 +64,12 @@ function TaskTrigger({ const { t } = useTranslation('dataset') return ( <> - {canEdit && ( -

+

{t(($) => $['newKnowledge.documentsDropHint'])}

)} {tasksVisible && ( -
+
-
+
disabled={statusPending} value={filter} @@ -471,11 +442,15 @@ export function DocumentsList({ tasksLiveStatus={tasksLiveStatus} /> )} -