mirror of
https://github.com/langgenius/dify.git
synced 2026-09-05 08:48:10 +08:00
refactor(knowledge-fs): move document row action ownership
This commit is contained in:
parent
d18325cab7
commit
da6fb87ebe
@ -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,
|
||||
|
||||
@ -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}
|
||||
/>
|
||||
))}
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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,
|
||||
}
|
||||
}
|
||||
@ -81,3 +81,7 @@ export function useKnowledgeModelSetupGuard(knowledgeSpaceId: string) {
|
||||
setModelSetupDialogOpen,
|
||||
}
|
||||
}
|
||||
|
||||
export type EnsureKnowledgeModelReady = ReturnType<
|
||||
typeof useKnowledgeModelSetupGuard
|
||||
>['ensureModelReady']
|
||||
|
||||
Loading…
Reference in New Issue
Block a user