### What problem does this PR solve? Feat: Delete and rename files in the knowledge base #3221 ### Type of change - [x] New Feature (non-breaking change which adds functionality)tags/v0.19.0
| @@ -16,7 +16,7 @@ export function RenameDialog({ | |||
| initialName, | |||
| onOk, | |||
| loading, | |||
| }: IModalProps<any> & { initialName: string }) { | |||
| }: IModalProps<any> & { initialName?: string }) { | |||
| const { t } = useTranslation(); | |||
| return ( | |||
| @@ -22,7 +22,7 @@ export function RenameForm({ | |||
| initialName, | |||
| hideModal, | |||
| onOk, | |||
| }: IModalProps<any> & { initialName: string }) { | |||
| }: IModalProps<any> & { initialName?: string }) { | |||
| const { t } = useTranslation(); | |||
| const FormSchema = z.object({ | |||
| name: z | |||
| @@ -46,7 +46,9 @@ export function RenameForm({ | |||
| } | |||
| useEffect(() => { | |||
| form.setValue('name', initialName); | |||
| if (initialName) { | |||
| form.setValue('name', initialName); | |||
| } | |||
| }, [form, initialName]); | |||
| return ( | |||
| @@ -18,6 +18,8 @@ export const enum DocumentApiAction { | |||
| FetchDocumentList = 'fetchDocumentList', | |||
| UpdateDocumentStatus = 'updateDocumentStatus', | |||
| RunDocumentByIds = 'runDocumentByIds', | |||
| RemoveDocument = 'removeDocument', | |||
| SaveDocumentName = 'saveDocumentName', | |||
| } | |||
| export const useUploadNextDocument = () => { | |||
| @@ -189,3 +191,59 @@ export const useRunDocument = () => { | |||
| return { runDocumentByIds: mutateAsync, loading, data }; | |||
| }; | |||
| export const useRemoveDocument = () => { | |||
| const queryClient = useQueryClient(); | |||
| const { | |||
| data, | |||
| isPending: loading, | |||
| mutateAsync, | |||
| } = useMutation({ | |||
| mutationKey: [DocumentApiAction.RemoveDocument], | |||
| mutationFn: async (documentIds: string | string[]) => { | |||
| const { data } = await kbService.document_rm({ doc_id: documentIds }); | |||
| if (data.code === 0) { | |||
| message.success(i18n.t('message.deleted')); | |||
| queryClient.invalidateQueries({ | |||
| queryKey: [DocumentApiAction.FetchDocumentList], | |||
| }); | |||
| } | |||
| return data.code; | |||
| }, | |||
| }); | |||
| return { data, loading, removeDocument: mutateAsync }; | |||
| }; | |||
| export const useSaveDocumentName = () => { | |||
| const queryClient = useQueryClient(); | |||
| const { | |||
| data, | |||
| isPending: loading, | |||
| mutateAsync, | |||
| } = useMutation({ | |||
| mutationKey: [DocumentApiAction.SaveDocumentName], | |||
| mutationFn: async ({ | |||
| name, | |||
| documentId, | |||
| }: { | |||
| name: string; | |||
| documentId: string; | |||
| }) => { | |||
| const { data } = await kbService.document_rename({ | |||
| doc_id: documentId, | |||
| name: name, | |||
| }); | |||
| if (data.code === 0) { | |||
| message.success(i18n.t('message.renamed')); | |||
| queryClient.invalidateQueries({ | |||
| queryKey: [DocumentApiAction.FetchDocumentList], | |||
| }); | |||
| } | |||
| return data.code; | |||
| }, | |||
| }); | |||
| return { loading, saveName: mutateAsync, data }; | |||
| }; | |||
| @@ -0,0 +1,101 @@ | |||
| import { ConfirmDeleteDialog } from '@/components/confirm-delete-dialog'; | |||
| import { Button } from '@/components/ui/button'; | |||
| import { | |||
| HoverCard, | |||
| HoverCardContent, | |||
| HoverCardTrigger, | |||
| } from '@/components/ui/hover-card'; | |||
| import { useRemoveDocument } from '@/hooks/use-document-request'; | |||
| import { IDocumentInfo } from '@/interfaces/database/document'; | |||
| import { formatFileSize } from '@/utils/common-util'; | |||
| import { formatDate } from '@/utils/date'; | |||
| import { downloadDocument } from '@/utils/file-util'; | |||
| import { ArrowDownToLine, Pencil, ScrollText, Trash2 } from 'lucide-react'; | |||
| import { useCallback } from 'react'; | |||
| import { UseRenameDocumentShowType } from './use-rename-document'; | |||
| import { isParserRunning } from './utils'; | |||
| const Fields = ['name', 'size', 'type', 'create_time', 'update_time']; | |||
| const FunctionMap = { | |||
| size: formatFileSize, | |||
| create_time: formatDate, | |||
| update_time: formatDate, | |||
| }; | |||
| export function DatasetActionCell({ | |||
| record, | |||
| showRenameModal, | |||
| }: { record: IDocumentInfo } & UseRenameDocumentShowType) { | |||
| const { id, run } = record; | |||
| const isRunning = isParserRunning(run); | |||
| const { removeDocument } = useRemoveDocument(); | |||
| const onDownloadDocument = useCallback(() => { | |||
| downloadDocument({ | |||
| id, | |||
| filename: record.name, | |||
| }); | |||
| }, [id, record.name]); | |||
| const handleRemove = useCallback(() => { | |||
| removeDocument(id); | |||
| }, [id, removeDocument]); | |||
| const handleRename = useCallback(() => { | |||
| showRenameModal(record); | |||
| }, [record, showRenameModal]); | |||
| return ( | |||
| <section className="flex gap-4 items-center"> | |||
| <HoverCard> | |||
| <HoverCardTrigger> | |||
| <Button variant="ghost" size={'icon'} disabled={isRunning}> | |||
| <ScrollText /> | |||
| </Button> | |||
| </HoverCardTrigger> | |||
| <HoverCardContent className="w-[40vw] max-h-[40vh] overflow-auto"> | |||
| <ul className="space-y-2"> | |||
| {Object.entries(record) | |||
| .filter(([key]) => Fields.some((x) => x === key)) | |||
| .map(([key, value], idx) => { | |||
| return ( | |||
| <li key={idx} className="flex gap-2"> | |||
| {key}: | |||
| <div> | |||
| {key in FunctionMap | |||
| ? FunctionMap[key as keyof typeof FunctionMap](value) | |||
| : value} | |||
| </div> | |||
| </li> | |||
| ); | |||
| })} | |||
| </ul> | |||
| </HoverCardContent> | |||
| </HoverCard> | |||
| <Button | |||
| variant={'ghost'} | |||
| size={'icon'} | |||
| disabled={isRunning} | |||
| onClick={handleRename} | |||
| > | |||
| <Pencil /> | |||
| </Button> | |||
| <Button | |||
| variant={'ghost'} | |||
| size={'icon'} | |||
| onClick={onDownloadDocument} | |||
| disabled={isRunning} | |||
| > | |||
| <ArrowDownToLine /> | |||
| </Button> | |||
| <ConfirmDeleteDialog onOk={handleRemove}> | |||
| <Button variant={'ghost'} size={'icon'} disabled={isRunning}> | |||
| <Trash2 className="text-text-delete-red" /> | |||
| </Button> | |||
| </ConfirmDeleteDialog> | |||
| </section> | |||
| ); | |||
| } | |||
| @@ -14,6 +14,7 @@ import { | |||
| import * as React from 'react'; | |||
| import { ChunkMethodDialog } from '@/components/chunk-method-dialog'; | |||
| import { RenameDialog } from '@/components/rename-dialog'; | |||
| import { Button } from '@/components/ui/button'; | |||
| import { | |||
| Table, | |||
| @@ -30,6 +31,7 @@ import { getExtension } from '@/utils/document-util'; | |||
| import { useMemo } from 'react'; | |||
| import { useChangeDocumentParser } from './hooks'; | |||
| import { useDatasetTableColumns } from './use-dataset-table-columns'; | |||
| import { useRenameDocument } from './use-rename-document'; | |||
| export function DatasetTable() { | |||
| const { | |||
| @@ -57,9 +59,19 @@ export function DatasetTable() { | |||
| showChangeParserModal, | |||
| } = useChangeDocumentParser(currentRecord.id); | |||
| const { | |||
| renameLoading, | |||
| onRenameOk, | |||
| renameVisible, | |||
| hideRenameModal, | |||
| showRenameModal, | |||
| initialName, | |||
| } = useRenameDocument(); | |||
| const columns = useDatasetTableColumns({ | |||
| showChangeParserModal, | |||
| setCurrentRecord: setRecord, | |||
| showRenameModal, | |||
| }); | |||
| const currentPagination = useMemo(() => { | |||
| @@ -196,6 +208,16 @@ export function DatasetTable() { | |||
| loading={changeParserLoading} | |||
| ></ChunkMethodDialog> | |||
| )} | |||
| {renameVisible && ( | |||
| <RenameDialog | |||
| visible={renameVisible} | |||
| onOk={onRenameOk} | |||
| loading={renameLoading} | |||
| hideModal={hideRenameModal} | |||
| initialName={initialName} | |||
| ></RenameDialog> | |||
| )} | |||
| </div> | |||
| ); | |||
| } | |||
| @@ -2,7 +2,6 @@ import { useSetModalState } from '@/hooks/common-hooks'; | |||
| import { | |||
| useCreateNextDocument, | |||
| useNextWebCrawl, | |||
| useSaveNextDocumentName, | |||
| useSetNextDocumentParser, | |||
| } from '@/hooks/document-hooks'; | |||
| import { useGetKnowledgeSearchParams } from '@/hooks/route-hook'; | |||
| @@ -23,34 +22,6 @@ export const useNavigateToOtherPage = () => { | |||
| return { linkToUploadPage, toChunk }; | |||
| }; | |||
| export const useRenameDocument = (documentId: string) => { | |||
| const { saveName, loading } = useSaveNextDocumentName(); | |||
| const { | |||
| visible: renameVisible, | |||
| hideModal: hideRenameModal, | |||
| showModal: showRenameModal, | |||
| } = useSetModalState(); | |||
| const onRenameOk = useCallback( | |||
| async (name: string) => { | |||
| const ret = await saveName({ documentId, name }); | |||
| if (ret === 0) { | |||
| hideRenameModal(); | |||
| } | |||
| }, | |||
| [hideRenameModal, saveName, documentId], | |||
| ); | |||
| return { | |||
| renameLoading: loading, | |||
| onRenameOk, | |||
| renameVisible, | |||
| hideRenameModal, | |||
| showRenameModal, | |||
| }; | |||
| }; | |||
| export const useCreateEmptyDocument = () => { | |||
| const { createDocument, loading } = useCreateNextDocument(); | |||
| @@ -4,6 +4,7 @@ import { Progress } from '@/components/ui/progress'; | |||
| import { Separator } from '@/components/ui/separator'; | |||
| import { IDocumentInfo } from '@/interfaces/database/document'; | |||
| import { CircleX, Play, RefreshCw } from 'lucide-react'; | |||
| import { useTranslation } from 'react-i18next'; | |||
| import { RunningStatus } from './constant'; | |||
| import { ParsingCard } from './parsing-card'; | |||
| import { useHandleRunDocumentByIds } from './use-run-document'; | |||
| @@ -18,6 +19,7 @@ const IconMap = { | |||
| }; | |||
| export function ParsingStatusCell({ record }: { record: IDocumentInfo }) { | |||
| const { t } = useTranslation(); | |||
| const { run, parser_id, progress, chunk_num, id } = record; | |||
| const operationIcon = IconMap[run]; | |||
| const p = Number((progress * 100).toFixed(2)); | |||
| @@ -40,20 +42,28 @@ export function ParsingStatusCell({ record }: { record: IDocumentInfo }) { | |||
| <Separator orientation="vertical" /> | |||
| </div> | |||
| <ConfirmDeleteDialog | |||
| hidden={isZeroChunk} | |||
| title={t(`knowledgeDetails.redo`, { chunkNum: chunk_num })} | |||
| hidden={isZeroChunk || isRunning} | |||
| onOk={handleOperationIconClick(true)} | |||
| onCancel={handleOperationIconClick(false)} | |||
| > | |||
| <Button | |||
| variant={'ghost'} | |||
| size={'sm'} | |||
| onClick={isZeroChunk ? handleOperationIconClick(false) : () => {}} | |||
| onClick={ | |||
| isZeroChunk || isRunning | |||
| ? handleOperationIconClick(false) | |||
| : () => {} | |||
| } | |||
| > | |||
| {operationIcon} | |||
| </Button> | |||
| </ConfirmDeleteDialog> | |||
| {isParserRunning(run) ? ( | |||
| <Progress value={p} className="h-1" /> | |||
| <div className="flex items-center gap-1"> | |||
| <Progress value={p} className="h-1 flex-1 min-w-10" /> | |||
| {p}% | |||
| </div> | |||
| ) : ( | |||
| <ParsingCard record={record}></ParsingCard> | |||
| )} | |||
| @@ -1,14 +1,6 @@ | |||
| import SvgIcon from '@/components/svg-icon'; | |||
| import { Button } from '@/components/ui/button'; | |||
| import { Checkbox } from '@/components/ui/checkbox'; | |||
| import { | |||
| DropdownMenu, | |||
| DropdownMenuContent, | |||
| DropdownMenuItem, | |||
| DropdownMenuLabel, | |||
| DropdownMenuSeparator, | |||
| DropdownMenuTrigger, | |||
| } from '@/components/ui/dropdown-menu'; | |||
| import { Switch } from '@/components/ui/switch'; | |||
| import { | |||
| Tooltip, | |||
| @@ -22,20 +14,25 @@ import { cn } from '@/lib/utils'; | |||
| import { formatDate } from '@/utils/date'; | |||
| import { getExtension } from '@/utils/document-util'; | |||
| import { ColumnDef } from '@tanstack/table-core'; | |||
| import { ArrowUpDown, MoreHorizontal, Pencil, Wrench } from 'lucide-react'; | |||
| import { ArrowUpDown } from 'lucide-react'; | |||
| import { useCallback } from 'react'; | |||
| import { useTranslation } from 'react-i18next'; | |||
| import { DatasetActionCell } from './dataset-action-cell'; | |||
| import { useChangeDocumentParser } from './hooks'; | |||
| import { ParsingStatusCell } from './parsing-status-cell'; | |||
| import { UseRenameDocumentShowType } from './use-rename-document'; | |||
| type UseDatasetTableColumnsType = Pick< | |||
| ReturnType<typeof useChangeDocumentParser>, | |||
| 'showChangeParserModal' | |||
| > & { setCurrentRecord: (record: IDocumentInfo) => void }; | |||
| > & { | |||
| setCurrentRecord: (record: IDocumentInfo) => void; | |||
| } & UseRenameDocumentShowType; | |||
| export function useDatasetTableColumns({ | |||
| showChangeParserModal, | |||
| setCurrentRecord, | |||
| showRenameModal, | |||
| }: UseDatasetTableColumnsType) { | |||
| const { t } = useTranslation('translation', { | |||
| keyPrefix: 'knowledgeDetails', | |||
| @@ -182,36 +179,10 @@ export function useDatasetTableColumns({ | |||
| const record = row.original; | |||
| return ( | |||
| <section className="flex gap-4 items-center"> | |||
| <Button | |||
| variant="icon" | |||
| size={'icon'} | |||
| onClick={onShowChangeParserModal(record)} | |||
| > | |||
| <Wrench /> | |||
| </Button> | |||
| <Button variant="icon" size={'icon'}> | |||
| <Pencil /> | |||
| </Button> | |||
| <DropdownMenu> | |||
| <DropdownMenuTrigger asChild> | |||
| <Button variant="icon" size={'icon'}> | |||
| <MoreHorizontal /> | |||
| </Button> | |||
| </DropdownMenuTrigger> | |||
| <DropdownMenuContent align="end"> | |||
| <DropdownMenuLabel>Actions</DropdownMenuLabel> | |||
| <DropdownMenuItem | |||
| onClick={() => navigator.clipboard.writeText(record.id)} | |||
| > | |||
| Copy payment ID | |||
| </DropdownMenuItem> | |||
| <DropdownMenuSeparator /> | |||
| <DropdownMenuItem>View customer</DropdownMenuItem> | |||
| <DropdownMenuItem>View payment details</DropdownMenuItem> | |||
| </DropdownMenuContent> | |||
| </DropdownMenu> | |||
| </section> | |||
| <DatasetActionCell | |||
| record={record} | |||
| showRenameModal={showRenameModal} | |||
| ></DatasetActionCell> | |||
| ); | |||
| }, | |||
| }, | |||
| @@ -0,0 +1,49 @@ | |||
| import { useSetModalState } from '@/hooks/common-hooks'; | |||
| import { useSaveDocumentName } from '@/hooks/use-document-request'; | |||
| import { IDocumentInfo } from '@/interfaces/database/document'; | |||
| import { useCallback, useState } from 'react'; | |||
| export const useRenameDocument = () => { | |||
| const { saveName, loading } = useSaveDocumentName(); | |||
| const [record, setRecord] = useState<IDocumentInfo>(); | |||
| const { | |||
| visible: renameVisible, | |||
| hideModal: hideRenameModal, | |||
| showModal: showRenameModal, | |||
| } = useSetModalState(); | |||
| const onRenameOk = useCallback( | |||
| async (name: string) => { | |||
| if (record?.id) { | |||
| const ret = await saveName({ documentId: record.id, name }); | |||
| if (ret === 0) { | |||
| hideRenameModal(); | |||
| } | |||
| } | |||
| }, | |||
| [record?.id, saveName, hideRenameModal], | |||
| ); | |||
| const handleShow = useCallback( | |||
| (row: IDocumentInfo) => { | |||
| setRecord(row); | |||
| showRenameModal(); | |||
| }, | |||
| [showRenameModal], | |||
| ); | |||
| return { | |||
| renameLoading: loading, | |||
| onRenameOk, | |||
| renameVisible, | |||
| hideRenameModal, | |||
| showRenameModal: handleShow, | |||
| initialName: record?.name, | |||
| }; | |||
| }; | |||
| export type UseRenameDocumentShowType = Pick< | |||
| ReturnType<typeof useRenameDocument>, | |||
| 'showRenameModal' | |||
| >; | |||