From da6fb87ebe28af9be8e771bb218af64173eeb5a3 Mon Sep 17 00:00:00 2001 From: Stephen Zhou <38493346+hyoban@users.noreply.github.com> Date: Mon, 31 Aug 2026 20:12:18 +0800 Subject: [PATCH] refactor(knowledge-fs): move document row action ownership --- .../new-rag/documents/actions-dropdown.tsx | 8 +- web/features/new-rag/documents/list.tsx | 108 +++--- web/features/new-rag/documents/page.tsx | 294 ++-------------- .../row-actions/use-document-row-actions.ts | 323 ++++++++++++++++++ .../use-knowledge-model-setup-guard.ts | 4 + 5 files changed, 408 insertions(+), 329 deletions(-) create mode 100644 web/features/new-rag/documents/row-actions/use-document-row-actions.ts diff --git a/web/features/new-rag/documents/actions-dropdown.tsx b/web/features/new-rag/documents/actions-dropdown.tsx index 5c262fcab56..821f9287576 100644 --- a/web/features/new-rag/documents/actions-dropdown.tsx +++ b/web/features/new-rag/documents/actions-dropdown.tsx @@ -23,7 +23,13 @@ import { Input } from '@langgenius/dify-ui/input' import { useState } from 'react' import { useTranslation } from 'react-i18next' -export type DocumentAction = 'download' | 'remove' | 'rename' | 'retry' | 'toggle-availability' +export type DocumentAction = + | 'download' + | 'reindex' + | 'remove' + | 'rename' + | 'retry' + | 'toggle-availability' export function DocumentActionsDropdown({ canEdit, diff --git a/web/features/new-rag/documents/list.tsx b/web/features/new-rag/documents/list.tsx index 9312ee800b4..c48ce07bf5c 100644 --- a/web/features/new-rag/documents/list.tsx +++ b/web/features/new-rag/documents/list.tsx @@ -1,8 +1,8 @@ 'use client' -import type { DocumentAction } from './actions-dropdown' +import type { EnsureKnowledgeModelReady } from '../use-knowledge-model-setup-guard' import type { DocumentDisplayStatus } from './model' -import type { LogicalDocument } from './models' +import type { DocumentProcessingTask, LogicalDocument } from './models' import { AlertDialog, AlertDialogActions, @@ -43,7 +43,9 @@ import { documentCanToggleAvailability, documentShowsAvailabilityAction, sourceName, + taskCanRetry, } from './model' +import { useDocumentRowActions } from './row-actions/use-document-row-actions' export type DocumentFilter = DocumentDisplayStatus | 'all' @@ -180,45 +182,41 @@ const DocumentRow = memo( failureReason, formatTimeFromNow, canDownload, - onDownload, - onRemove, - onRename, + ensureModelReady, + knowledgeSpaceId, + onDocumentRemoved, onSelectedChange, - onReindex, - onRetry, - onToggleAvailability, - pendingAction, + onTaskUpdated, + onWriteDenied, readOnlyReasonId, - retryable, selected, selectionDisabled, source, sourcePending, status, statusPending, + task, tasksPending, }: { canDownload: boolean document: LogicalDocument documentHref: string + ensureModelReady: EnsureKnowledgeModelReady failureReason?: string formatTimeFromNow: (time: number) => string - onDownload: (documentId: string) => Promise - onRemove: (documentId: string) => Promise - onRename: (documentId: string, title: string) => Promise + knowledgeSpaceId: string + onDocumentRemoved: (documentId: string) => void onSelectedChange: (documentId: string) => void - onReindex: (documentId: string) => void - onRetry: (documentId: string) => Promise - onToggleAvailability: (documentId: string) => Promise - pendingAction?: DocumentAction + onTaskUpdated: (task: DocumentProcessingTask) => void + onWriteDenied: () => void readOnlyReasonId?: string - retryable: boolean selected: boolean selectionDisabled: boolean source?: string sourcePending: boolean status: DocumentDisplayStatus statusPending: boolean + task?: DocumentProcessingTask tasksPending: boolean }) => { const { t } = useTranslation('dataset') @@ -226,6 +224,20 @@ const DocumentRow = memo( const titleId = `new-document-${document.id}` const revision = document.activeRevision ?? document.active?.revision const updatedTime = Date.parse(document.updatedAt) + const { download, pendingAction, reindex, remove, rename, retry, toggleAvailability } = + useDocumentRowActions({ + canDownload, + canWrite: !selectionDisabled, + document, + ensureModelReady, + knowledgeSpaceId, + onDocumentRemoved, + onTaskUpdated, + onWriteDenied, + status, + task, + taskResultsIncomplete: tasksPending, + }) return ( @@ -297,16 +309,16 @@ const DocumentRow = memo( documentEnabled={document.enabled} documentTitle={document.title} downloadDisabled={tasksPending || !documentCanDownload(document, status)} - onDownload={() => onDownload(document.id)} - onRemove={() => onRemove(document.id)} - onRename={(title) => onRename(document.id, title)} - onReindex={() => onReindex(document.id)} - onRetry={() => onRetry(document.id)} - onToggleAvailability={() => onToggleAvailability(document.id)} + onDownload={download} + onRemove={remove} + onRename={rename} + onReindex={() => void reindex()} + onRetry={retry} + onToggleAvailability={toggleAvailability} pendingAction={pendingAction} removeDisabled={document.status === 'deleting'} reindexDisabled={selectionDisabled || !documentCanReindex(status)} - retryDisabled={selectionDisabled || !retryable} + retryDisabled={selectionDisabled || !task || !taskCanRetry(task)} showAvailabilityAction={documentShowsAvailabilityAction(status)} showRetry={status === 'failed'} toggleAvailabilityDisabled={ @@ -384,6 +396,7 @@ export function DocumentsList({ canUpload, completingResults, documents, + ensureModelReady, failureReasons, filter, getDocumentHref, @@ -393,24 +406,20 @@ export function DocumentsList({ isFetchNextPageError, isFetchingNextDocumentPage, isFetchingNextPage, + knowledgeSpaceId, onAddDocument, + onDocumentRemoved, onFilterChange, onLoadMore, - onDownloadDocument, onOpenMetadata, onOpenTasks, - onRemoveDocument, - onRenameDocument, - onReindexDocument, - onRetryDocument, onSearchChange, onSelectAll, onSelectDocument, - onToggleDocumentAvailability, - pendingDocumentAction, + onTaskUpdated, + onWriteDenied, readOnlyReasonId, resultsIncomplete, - retryableDocumentIds, search, selectionDisabled, selectedDocumentIds, @@ -420,6 +429,7 @@ export function DocumentsList({ sourceNames, statusPending, statuses, + tasksByDocument, tasksPending, tasksButtonLabel, tasksLiveStatus, @@ -434,6 +444,7 @@ export function DocumentsList({ canUpload: boolean completingResults: boolean documents: LogicalDocument[] + ensureModelReady: EnsureKnowledgeModelReady failureReasons: Map filter: DocumentFilter getDocumentHref: (documentId: string) => string @@ -443,24 +454,20 @@ export function DocumentsList({ isFetchNextPageError: boolean isFetchingNextDocumentPage: boolean isFetchingNextPage: boolean + knowledgeSpaceId: string onAddDocument: () => void + onDocumentRemoved: (documentId: string) => void onFilterChange: (filter: DocumentFilter) => void onLoadMore: () => void - onDownloadDocument: (documentId: string) => Promise onOpenMetadata: () => void onOpenTasks: () => void - onRemoveDocument: (documentId: string) => Promise - onRenameDocument: (documentId: string, title: string) => Promise - onReindexDocument: (documentId: string) => void - onRetryDocument: (documentId: string) => Promise onSearchChange: (search: string) => void onSelectAll: () => void onSelectDocument: (documentId: string) => void - onToggleDocumentAvailability: (documentId: string) => Promise - pendingDocumentAction?: { action: DocumentAction; documentId: string } + onTaskUpdated: (task: DocumentProcessingTask) => void + onWriteDenied: () => void readOnlyReasonId?: string resultsIncomplete: boolean - retryableDocumentIds: Set search: string selectionDisabled: boolean selectedDocumentIds: Set @@ -470,6 +477,7 @@ export function DocumentsList({ sourceNames: Map statusPending: boolean statuses: Map + tasksByDocument: Map tasksPending: boolean tasksButtonLabel: string tasksLiveStatus: string @@ -624,20 +632,14 @@ export function DocumentsList({ canDownload={canDownload} document={document} documentHref={getDocumentHref(document.id)} + ensureModelReady={ensureModelReady} failureReason={failureReasons.get(document.id)} formatTimeFromNow={formatTimeFromNow} - onDownload={onDownloadDocument} - onRemove={onRemoveDocument} - onRename={onRenameDocument} + knowledgeSpaceId={knowledgeSpaceId} + onDocumentRemoved={onDocumentRemoved} onSelectedChange={onSelectDocument} - onReindex={onReindexDocument} - onRetry={onRetryDocument} - onToggleAvailability={onToggleDocumentAvailability} - pendingAction={ - pendingDocumentAction?.documentId === document.id - ? pendingDocumentAction.action - : undefined - } + onTaskUpdated={onTaskUpdated} + onWriteDenied={onWriteDenied} readOnlyReasonId={ !canEdit ? readOnlyReasonId @@ -645,7 +647,6 @@ export function DocumentsList({ ? PARTIAL_RESULTS_DESCRIPTION_ID : undefined } - retryable={retryableDocumentIds.has(document.id)} selected={selectedDocumentIds.has(document.id)} selectionDisabled={!canEdit || selectionDisabled} source={ @@ -660,6 +661,7 @@ export function DocumentsList({ tasksPending || (statusPending && document.sourceId && !sourceNames.has(document.sourceId)), )} + task={tasksByDocument.get(document.id)} tasksPending={tasksPending} /> ))} diff --git a/web/features/new-rag/documents/page.tsx b/web/features/new-rag/documents/page.tsx index 7c6462cadc1..bd2f516eaea 100644 --- a/web/features/new-rag/documents/page.tsx +++ b/web/features/new-rag/documents/page.tsx @@ -1,7 +1,5 @@ 'use client' -import type { DocumentAction } from './actions-dropdown' -import type { DocumentProcessingTask } from './models' import { Button } from '@langgenius/dify-ui/button' import { toast } from '@langgenius/dify-ui/toast' import { useInfiniteQuery, useMutation, useQueryClient } from '@tanstack/react-query' @@ -33,13 +31,11 @@ import { documentCanToggleAvailability, documentDisplayStatus, documentShowsAvailabilityAction, - documentTitle, newestTaskByDocument, sourceName, - taskCanRetry, taskNeedsAttention, } from './model' -import { backgroundTaskFromApi, logicalDocumentListFromApi } from './models' +import { logicalDocumentListFromApi } from './models' import { DocumentPermissionRecoveryBoundary, DocumentPermissionRecoveryBulkRegion, @@ -77,8 +73,7 @@ export function DocumentsPage({ knowledgeSpaceId }: { knowledgeSpaceId: string } hasDocumentDownloadPermission && !workspacePermissionKeysLoading && !workspacePermissionKeysError - const reindexPendingRef = useRef(false) - const documentActionPendingRef = useRef(false) + const bulkReindexPendingRef = useRef(false) const bulkActionPendingRef = useRef(false) const [filter, setFilter] = useQueryState('status', documentFilterParser) const [search, setSearch] = useQueryState('query', documentSearchParser) @@ -99,9 +94,6 @@ export function DocumentsPage({ knowledgeSpaceId }: { knowledgeSpaceId: string } const [bulkActionPending, setBulkActionPending] = useState< 'availability' | 'download' | 'reindex' | 'remove' | undefined >() - const [pendingDocumentAction, setPendingDocumentAction] = useState< - { action: DocumentAction; documentId: string } | undefined - >() const { mutateAsync: reindexDocuments } = useMutation( consoleQuery.knowledgeFs.spaces.byControlSpaceId.documents.reindex.post.mutationOptions(), ) @@ -255,15 +247,6 @@ export function DocumentsPage({ knowledgeSpaceId }: { knowledgeSpaceId: string } hasRelevantNextSourcePage && (sourcesQuery.data?.pages.length ?? 0) < MAX_AUTO_CURSOR_PAGES, ) const taskByDocument = useMemo(() => newestTaskByDocument(tasks), [tasks]) - const retryableDocumentIds = useMemo( - () => - new Set( - [...taskByDocument].flatMap(([documentId, task]) => - taskCanRetry(task) ? [documentId] : [], - ), - ), - [taskByDocument], - ) const documentStatuses = useMemo( () => new Map( @@ -545,11 +528,11 @@ export function DocumentsPage({ knowledgeSpaceId }: { knowledgeSpaceId: string } selectionDisabled || !validSelectedDocumentIds.size || bulkReindexDisabled || - reindexPendingRef.current || + bulkReindexPendingRef.current || bulkActionPendingRef.current ) return - reindexPendingRef.current = true + bulkReindexPendingRef.current = true bulkActionPendingRef.current = true setBulkActionPending('reindex') try { @@ -595,7 +578,7 @@ export function DocumentsPage({ knowledgeSpaceId }: { knowledgeSpaceId: string } if (responseStatus(error) === 403) handleWritePermissionDenied() else toast.error(t(($) => $['newKnowledge.documentsReindexFailed'])) } finally { - reindexPendingRef.current = false + bulkReindexPendingRef.current = false bulkActionPendingRef.current = false setBulkActionPending(undefined) } @@ -612,167 +595,6 @@ export function DocumentsPage({ knowledgeSpaceId }: { knowledgeSpaceId: string } validSelectedDocumentIds, ]) - const handleReindexDocument = useCallback( - async (documentId: string) => { - const status = documentStatuses.get(documentId) - if (!canWrite || !status || !documentCanReindex(status) || reindexPendingRef.current) return - reindexPendingRef.current = true - try { - if ((await ensureModelReady({ capability: 'index', intent: 'reindex' })).status !== 'ready') - return - const result = await reindexDocuments({ - body: { documentIds: [documentId] }, - params: { control_space_id: knowledgeSpaceId }, - }) - const item = result.items[0] - if (!item || item.status === 'not_found') - toast.error( - t(($) => $['newKnowledge.documentsReindexPartial'], { - missing: 1, - queued: 0, - }), - ) - else if (item.status === 'disabled') - toast.error(t(($) => $['newKnowledge.documentsReindexFailed'])) - else toast.success(t(($) => $['newKnowledge.documentsReindexStarted'])) - refreshDocumentsAndTasks() - } catch (error) { - if (responseStatus(error) === 403) handleWritePermissionDenied() - else toast.error(t(($) => $['newKnowledge.documentsReindexFailed'])) - } finally { - reindexPendingRef.current = false - } - }, - [ - canWrite, - documentStatuses, - ensureModelReady, - handleWritePermissionDenied, - knowledgeSpaceId, - refreshDocumentsAndTasks, - reindexDocuments, - t, - ], - ) - - const handleRenameDocument = useCallback( - async (documentId: string, title: string) => { - if (!canWrite || documentActionPendingRef.current) return false - const currentDocument = documents.find((document) => document.id === documentId) - const normalizedTitle = title.trim() - if ( - !currentDocument || - !normalizedTitle || - normalizedTitle === documentTitle(currentDocument) - ) - return false - documentActionPendingRef.current = true - setPendingDocumentAction({ action: 'rename', documentId }) - try { - await consoleClient.knowledgeFs.spaces.byControlSpaceId.documents.byDocumentId.patch({ - body: { - expectedRowVersion: currentDocument.rowVersion, - patch: { displayName: normalizedTitle }, - }, - params: { control_space_id: knowledgeSpaceId, document_id: documentId }, - }) - refreshDocuments() - return true - } catch (error) { - if (responseStatus(error) === 403) handleWritePermissionDenied() - else toast.error(t(($) => $['newKnowledge.settings.saveFailed'])) - return false - } finally { - documentActionPendingRef.current = false - setPendingDocumentAction(undefined) - } - }, - [canWrite, documents, handleWritePermissionDenied, knowledgeSpaceId, refreshDocuments, t], - ) - - const handleDownloadDocument = useCallback( - async (documentId: string) => { - if (!canDownload || documentActionPendingRef.current) return false - const currentDocument = documents.find((document) => document.id === documentId) - const status = documentStatuses.get(documentId) - if ( - taskResultsIncomplete || - !currentDocument || - !status || - !documentCanDownload(currentDocument, status) - ) - return false - documentActionPendingRef.current = true - setPendingDocumentAction({ action: 'download', documentId }) - try { - const file = - await consoleClient.knowledgeFs.spaces.byControlSpaceId.logicalDocuments.byDocumentId.download.get( - { - params: { control_space_id: knowledgeSpaceId, document_id: documentId }, - }, - ) - downloadBlob({ - data: file, - fileName: - typeof File !== 'undefined' && file instanceof File && file.name - ? file.name - : currentDocument.title, - }) - return true - } catch { - toast.error(tCommon(($) => $['actionMsg.downloadUnsuccessfully'])) - return false - } finally { - documentActionPendingRef.current = false - setPendingDocumentAction(undefined) - } - }, - [canDownload, documentStatuses, documents, knowledgeSpaceId, taskResultsIncomplete, tCommon], - ) - - const handleToggleDocumentAvailability = useCallback( - async (documentId: string) => { - if (!canWrite || documentActionPendingRef.current) return false - const currentDocument = documents.find((document) => document.id === documentId) - const status = documentStatuses.get(documentId) - if (!currentDocument || !status || !documentCanToggleAvailability(status)) return false - documentActionPendingRef.current = true - setPendingDocumentAction({ action: 'toggle-availability', documentId }) - try { - await consoleClient.knowledgeFs.spaces.byControlSpaceId.logicalDocuments.byDocumentId.patch( - { - body: { - enabled: !currentDocument.enabled, - expectedRowVersion: currentDocument.rowVersion, - }, - params: { control_space_id: knowledgeSpaceId, document_id: documentId }, - }, - ) - refreshDocuments() - return true - } catch (error) { - if (responseStatus(error) === 403) handleWritePermissionDenied() - else if (responseStatus(error) === 409) { - refreshDocuments() - toast.warning(t(($) => $['newKnowledge.taskActionFailed'])) - } else toast.error(t(($) => $['newKnowledge.documentsErrorDescription'])) - return false - } finally { - documentActionPendingRef.current = false - setPendingDocumentAction(undefined) - } - }, - [ - canWrite, - documentStatuses, - documents, - handleWritePermissionDenied, - knowledgeSpaceId, - refreshDocuments, - t, - ], - ) - const handleUpdateDocumentsAvailability = useCallback(async () => { if ( !canWrite || @@ -897,90 +719,6 @@ export function DocumentsPage({ knowledgeSpaceId }: { knowledgeSpaceId: string } validSelectedDocumentIds, ]) - const handleRemoveDocument = useCallback( - async (documentId: string) => { - if (!canWrite || documentActionPendingRef.current) return false - const currentDocument = documents.find((document) => document.id === documentId) - if (!currentDocument) return false - documentActionPendingRef.current = true - setPendingDocumentAction({ action: 'remove', documentId }) - try { - await consoleClient.knowledgeFs.spaces.byControlSpaceId.logicalDocuments.byDocumentId.delete( - { - body: { expectedRevision: currentDocument.rowVersion }, - headers: { 'Idempotency-Key': createRequestId() }, - params: { control_space_id: knowledgeSpaceId, document_id: documentId }, - }, - ) - setSelectedDocumentIds((current) => { - const next = new Set(current) - next.delete(documentId) - return next - }) - refreshDocumentsAndTasks() - return true - } catch (error) { - if (responseStatus(error) === 403) handleWritePermissionDenied() - else toast.error(t(($) => $['newKnowledge.documentsErrorDescription'])) - return false - } finally { - documentActionPendingRef.current = false - setPendingDocumentAction(undefined) - } - }, - [ - canWrite, - documents, - handleWritePermissionDenied, - knowledgeSpaceId, - refreshDocumentsAndTasks, - t, - ], - ) - - const handleRetryDocument = useCallback( - async (documentId: string) => { - if (!canWrite || documentActionPendingRef.current) return false - const task = taskByDocument.get(documentId) - if (!task || !taskCanRetry(task)) return false - documentActionPendingRef.current = true - setPendingDocumentAction({ action: 'retry', documentId }) - try { - const updated = backgroundTaskFromApi( - await consoleClient.knowledgeFs.spaces.byControlSpaceId.backgroundTasks.byTaskKind.byTaskId.retry.post( - { - params: { - control_space_id: knowledgeSpaceId, - task_id: task.id, - task_kind: task.taskKind, - }, - }, - ), - ) - if (updated.documentId && updated.documentRevision) - handleTaskUpdated(updated as DocumentProcessingTask) - refreshDocumentsAndTasks() - return true - } catch (error) { - if (responseStatus(error) === 403) handleWritePermissionDenied() - else toast.error(t(($) => $['newKnowledge.taskActionFailed'])) - return false - } finally { - documentActionPendingRef.current = false - setPendingDocumentAction(undefined) - } - }, - [ - canWrite, - handleTaskUpdated, - handleWritePermissionDenied, - knowledgeSpaceId, - refreshDocumentsAndTasks, - t, - taskByDocument, - ], - ) - const toggleDocument = useCallback( (documentId: string) => { if (!canWrite || selectionDisabled) return @@ -994,6 +732,14 @@ export function DocumentsPage({ knowledgeSpaceId }: { knowledgeSpaceId: string } [canWrite, selectionDisabled], ) + const handleDocumentRemoved = useCallback((documentId: string) => { + setSelectedDocumentIds((current) => { + const next = new Set(current) + next.delete(documentId) + return next + }) + }, []) + const toggleAllFiltered = () => { if (!canWrite || selectionDisabled) return setSelectedDocumentIds((current) => { @@ -1218,6 +964,7 @@ export function DocumentsPage({ knowledgeSpaceId }: { knowledgeSpaceId: string } canUpload={upload.canUpload} completingResults={completingFilteredResults} documents={filteredDocuments} + ensureModelReady={ensureModelReady} failureReasons={documentFailureReasons} filter={filter} getDocumentHref={(documentId) => @@ -1229,24 +976,20 @@ export function DocumentsPage({ knowledgeSpaceId }: { knowledgeSpaceId: string } isFetchNextPageError={documentsQuery.isFetchNextPageError} isFetchingNextDocumentPage={isFetchingNextDocumentPage} isFetchingNextPage={isFetchingNextResultsPage} + knowledgeSpaceId={knowledgeSpaceId} onAddDocument={() => upload.openUpload()} + onDocumentRemoved={handleDocumentRemoved} onFilterChange={setFilter} onLoadMore={loadMoreResults} - onDownloadDocument={handleDownloadDocument} onOpenMetadata={() => setMetadataOpen(true)} onOpenTasks={() => setTasksOpen(true)} - onRemoveDocument={handleRemoveDocument} - onRenameDocument={handleRenameDocument} - onReindexDocument={(documentId) => void handleReindexDocument(documentId)} - onRetryDocument={handleRetryDocument} onSearchChange={setSearch} onSelectAll={toggleAllFiltered} onSelectDocument={toggleDocument} - onToggleDocumentAvailability={handleToggleDocumentAvailability} - pendingDocumentAction={pendingDocumentAction} + onTaskUpdated={handleTaskUpdated} + onWriteDenied={handleWritePermissionDenied} readOnlyReasonId={upload.readOnlyReasonId} resultsIncomplete={filteredResultsIncomplete} - retryableDocumentIds={retryableDocumentIds} search={search} selectionDisabled={selectionDisabled} selectedDocumentIds={validSelectedDocumentIds} @@ -1261,6 +1004,7 @@ export function DocumentsPage({ knowledgeSpaceId }: { knowledgeSpaceId: string } sourceNames={sourceNames} statusPending={dependencyResultsIncomplete} statuses={documentStatuses} + tasksByDocument={taskByDocument} tasksPending={taskResultsIncomplete} tasksButtonLabel={tasksButtonLabel} tasksLiveStatus={tasksLiveStatus} diff --git a/web/features/new-rag/documents/row-actions/use-document-row-actions.ts b/web/features/new-rag/documents/row-actions/use-document-row-actions.ts new file mode 100644 index 00000000000..30aec4e1854 --- /dev/null +++ b/web/features/new-rag/documents/row-actions/use-document-row-actions.ts @@ -0,0 +1,323 @@ +'use client' + +import type { EnsureKnowledgeModelReady } from '../../use-knowledge-model-setup-guard' +import type { DocumentAction } from '../actions-dropdown' +import type { DocumentDisplayStatus } from '../model' +import type { DocumentProcessingTask, LogicalDocument } from '../models' +import { toast } from '@langgenius/dify-ui/toast' +import { useMutation, useQueryClient } from '@tanstack/react-query' +import { useCallback, useRef, useState } from 'react' +import { useTranslation } from 'react-i18next' +import { consoleClient, consoleQuery } from '@/service/client' +import { downloadBlob } from '@/utils/download' +import { createRequestId } from '../../request-id' +import { + documentCanDownload, + documentCanReindex, + documentCanToggleAvailability, + documentTitle, + taskCanRetry, +} from '../model' +import { backgroundTaskFromApi } from '../models' +import { responseStatus } from '../request-error' +import { queryKeyMatchesKnowledgeSpace } from '../tasks/recovery' + +type UseDocumentRowActionsOptions = { + canDownload: boolean + canWrite: boolean + document: LogicalDocument + ensureModelReady: EnsureKnowledgeModelReady + knowledgeSpaceId: string + onDocumentRemoved: (documentId: string) => void + onTaskUpdated: (task: DocumentProcessingTask) => void + onWriteDenied: () => void + status: DocumentDisplayStatus + task?: DocumentProcessingTask + taskResultsIncomplete: boolean +} + +export function useDocumentRowActions({ + canDownload, + canWrite, + document, + ensureModelReady, + knowledgeSpaceId, + onDocumentRemoved, + onTaskUpdated, + onWriteDenied, + status, + task, + taskResultsIncomplete, +}: UseDocumentRowActionsOptions) { + const { t } = useTranslation('dataset') + const { t: tCommon } = useTranslation('common') + const queryClient = useQueryClient() + const pendingRef = useRef(false) + const [pendingAction, setPendingAction] = useState() + const { mutateAsync: reindexDocument } = useMutation( + consoleQuery.knowledgeFs.spaces.byControlSpaceId.documents.reindex.post.mutationOptions(), + ) + + const invalidateDocuments = useCallback(() => { + void queryClient.invalidateQueries({ + predicate: (query) => queryKeyMatchesKnowledgeSpace(query.queryKey, knowledgeSpaceId), + queryKey: consoleQuery.knowledgeFs.spaces.byControlSpaceId.logicalDocuments.get.key(), + }) + }, [knowledgeSpaceId, queryClient]) + + const invalidateDocumentsAndTasks = useCallback(() => { + void Promise.allSettled([ + queryClient.invalidateQueries({ + predicate: (query) => queryKeyMatchesKnowledgeSpace(query.queryKey, knowledgeSpaceId), + queryKey: consoleQuery.knowledgeFs.spaces.byControlSpaceId.logicalDocuments.get.key(), + }), + queryClient.invalidateQueries({ + predicate: (query) => queryKeyMatchesKnowledgeSpace(query.queryKey, knowledgeSpaceId), + queryKey: consoleQuery.knowledgeFs.spaces.byControlSpaceId.backgroundTasks.get.key(), + }), + ]) + }, [knowledgeSpaceId, queryClient]) + + const beginAction = useCallback((action: DocumentAction) => { + if (pendingRef.current) return false + pendingRef.current = true + setPendingAction(action) + return true + }, []) + + const finishAction = useCallback(() => { + pendingRef.current = false + setPendingAction(undefined) + }, []) + + const rename = useCallback( + async (title: string) => { + const normalizedTitle = title.trim() + if (!canWrite || !normalizedTitle || normalizedTitle === documentTitle(document)) return false + if (!beginAction('rename')) return false + try { + await consoleClient.knowledgeFs.spaces.byControlSpaceId.documents.byDocumentId.patch({ + body: { + expectedRowVersion: document.rowVersion, + patch: { displayName: normalizedTitle }, + }, + params: { control_space_id: knowledgeSpaceId, document_id: document.id }, + }) + invalidateDocuments() + return true + } catch (error) { + if (responseStatus(error) === 403) onWriteDenied() + else toast.error(t(($) => $['newKnowledge.settings.saveFailed'])) + return false + } finally { + finishAction() + } + }, + [ + beginAction, + canWrite, + document, + finishAction, + invalidateDocuments, + knowledgeSpaceId, + onWriteDenied, + t, + ], + ) + + const download = useCallback(async () => { + if ( + !canDownload || + taskResultsIncomplete || + !documentCanDownload(document, status) || + !beginAction('download') + ) + return false + try { + const file = + await consoleClient.knowledgeFs.spaces.byControlSpaceId.logicalDocuments.byDocumentId.download.get( + { + params: { control_space_id: knowledgeSpaceId, document_id: document.id }, + }, + ) + downloadBlob({ + data: file, + fileName: + typeof File !== 'undefined' && file instanceof File && file.name + ? file.name + : document.title, + }) + return true + } catch { + toast.error(tCommon(($) => $['actionMsg.downloadUnsuccessfully'])) + return false + } finally { + finishAction() + } + }, [ + beginAction, + canDownload, + document, + finishAction, + knowledgeSpaceId, + status, + taskResultsIncomplete, + tCommon, + ]) + + const toggleAvailability = useCallback(async () => { + if (!canWrite || !documentCanToggleAvailability(status) || !beginAction('toggle-availability')) + return false + try { + await consoleClient.knowledgeFs.spaces.byControlSpaceId.logicalDocuments.byDocumentId.patch({ + body: { + enabled: !document.enabled, + expectedRowVersion: document.rowVersion, + }, + params: { control_space_id: knowledgeSpaceId, document_id: document.id }, + }) + invalidateDocuments() + return true + } catch (error) { + if (responseStatus(error) === 403) onWriteDenied() + else if (responseStatus(error) === 409) { + invalidateDocuments() + toast.warning(t(($) => $['newKnowledge.taskActionFailed'])) + } else toast.error(t(($) => $['newKnowledge.documentsErrorDescription'])) + return false + } finally { + finishAction() + } + }, [ + beginAction, + canWrite, + document, + finishAction, + invalidateDocuments, + knowledgeSpaceId, + onWriteDenied, + status, + t, + ]) + + const remove = useCallback(async () => { + if (!canWrite || !beginAction('remove')) return false + try { + await consoleClient.knowledgeFs.spaces.byControlSpaceId.logicalDocuments.byDocumentId.delete({ + body: { expectedRevision: document.rowVersion }, + headers: { 'Idempotency-Key': createRequestId() }, + params: { control_space_id: knowledgeSpaceId, document_id: document.id }, + }) + onDocumentRemoved(document.id) + invalidateDocumentsAndTasks() + return true + } catch (error) { + if (responseStatus(error) === 403) onWriteDenied() + else toast.error(t(($) => $['newKnowledge.documentsErrorDescription'])) + return false + } finally { + finishAction() + } + }, [ + beginAction, + canWrite, + document.id, + document.rowVersion, + finishAction, + invalidateDocumentsAndTasks, + knowledgeSpaceId, + onDocumentRemoved, + onWriteDenied, + t, + ]) + + const retry = useCallback(async () => { + if (!canWrite || !task || !taskCanRetry(task) || !beginAction('retry')) return false + try { + const updated = backgroundTaskFromApi( + await consoleClient.knowledgeFs.spaces.byControlSpaceId.backgroundTasks.byTaskKind.byTaskId.retry.post( + { + params: { + control_space_id: knowledgeSpaceId, + task_id: task.id, + task_kind: task.taskKind, + }, + }, + ), + ) + if (updated.documentId && updated.documentRevision) + onTaskUpdated(updated as DocumentProcessingTask) + invalidateDocumentsAndTasks() + return true + } catch (error) { + if (responseStatus(error) === 403) onWriteDenied() + else toast.error(t(($) => $['newKnowledge.taskActionFailed'])) + return false + } finally { + finishAction() + } + }, [ + beginAction, + canWrite, + finishAction, + invalidateDocumentsAndTasks, + knowledgeSpaceId, + onTaskUpdated, + onWriteDenied, + t, + task, + ]) + + const reindex = useCallback(async () => { + if (!canWrite || !documentCanReindex(status) || !beginAction('reindex')) return false + try { + if ((await ensureModelReady({ capability: 'index', intent: 'reindex' })).status !== 'ready') + return false + const result = await reindexDocument({ + body: { documentIds: [document.id] }, + params: { control_space_id: knowledgeSpaceId }, + }) + const item = result.items[0] + if (!item || item.status === 'not_found') + toast.error( + t(($) => $['newKnowledge.documentsReindexPartial'], { + missing: 1, + queued: 0, + }), + ) + else if (item.status === 'disabled') + toast.error(t(($) => $['newKnowledge.documentsReindexFailed'])) + else toast.success(t(($) => $['newKnowledge.documentsReindexStarted'])) + invalidateDocumentsAndTasks() + return true + } catch (error) { + if (responseStatus(error) === 403) onWriteDenied() + else toast.error(t(($) => $['newKnowledge.documentsReindexFailed'])) + return false + } finally { + finishAction() + } + }, [ + beginAction, + canWrite, + document.id, + ensureModelReady, + finishAction, + invalidateDocumentsAndTasks, + knowledgeSpaceId, + onWriteDenied, + reindexDocument, + status, + t, + ]) + + return { + download, + pendingAction, + reindex, + remove, + rename, + retry, + toggleAvailability, + } +} diff --git a/web/features/new-rag/use-knowledge-model-setup-guard.ts b/web/features/new-rag/use-knowledge-model-setup-guard.ts index b27ae061041..54c71cdba70 100644 --- a/web/features/new-rag/use-knowledge-model-setup-guard.ts +++ b/web/features/new-rag/use-knowledge-model-setup-guard.ts @@ -81,3 +81,7 @@ export function useKnowledgeModelSetupGuard(knowledgeSpaceId: string) { setModelSetupDialogOpen, } } + +export type EnsureKnowledgeModelReady = ReturnType< + typeof useKnowledgeModelSetupGuard +>['ensureModelReady']