refactor(knowledge-fs): move document row action ownership

This commit is contained in:
Stephen Zhou 2026-08-31 20:12:18 +08:00
parent d18325cab7
commit da6fb87ebe
No known key found for this signature in database
5 changed files with 408 additions and 329 deletions

View File

@ -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,

View File

@ -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<boolean>
onRemove: (documentId: string) => Promise<boolean>
onRename: (documentId: string, title: string) => Promise<boolean>
knowledgeSpaceId: string
onDocumentRemoved: (documentId: string) => void
onSelectedChange: (documentId: string) => void
onReindex: (documentId: string) => void
onRetry: (documentId: string) => Promise<boolean>
onToggleAvailability: (documentId: string) => Promise<boolean>
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 (
<tr className="h-12 border-t border-divider-subtle">
@ -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<string, string>
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<boolean>
onOpenMetadata: () => void
onOpenTasks: () => void
onRemoveDocument: (documentId: string) => Promise<boolean>
onRenameDocument: (documentId: string, title: string) => Promise<boolean>
onReindexDocument: (documentId: string) => void
onRetryDocument: (documentId: string) => Promise<boolean>
onSearchChange: (search: string) => void
onSelectAll: () => void
onSelectDocument: (documentId: string) => void
onToggleDocumentAvailability: (documentId: string) => Promise<boolean>
pendingDocumentAction?: { action: DocumentAction; documentId: string }
onTaskUpdated: (task: DocumentProcessingTask) => void
onWriteDenied: () => void
readOnlyReasonId?: string
resultsIncomplete: boolean
retryableDocumentIds: Set<string>
search: string
selectionDisabled: boolean
selectedDocumentIds: Set<string>
@ -470,6 +477,7 @@ export function DocumentsList({
sourceNames: Map<string, string>
statusPending: boolean
statuses: Map<string, DocumentDisplayStatus>
tasksByDocument: Map<string, DocumentProcessingTask>
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}
/>
))}

View File

@ -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}

View File

@ -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<DocumentAction>()
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,
}
}

View File

@ -81,3 +81,7 @@ export function useKnowledgeModelSetupGuard(knowledgeSpaceId: string) {
setModelSetupDialogOpen,
}
}
export type EnsureKnowledgeModelReady = ReturnType<
typeof useKnowledgeModelSetupGuard
>['ensureModelReady']