feat: document support rename in in dataset (#4732)

This commit is contained in:
Joel 2024-06-04 15:10:34 +08:00 committed by GitHub
parent 9cf9720efa
commit 96460d5ea3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 195 additions and 17 deletions

View File

@ -150,6 +150,7 @@ const DocumentDetail: FC<Props> = ({ datasetId, documentId }) => {
scene='detail' scene='detail'
embeddingAvailable={embeddingAvailable} embeddingAvailable={embeddingAvailable}
detail={{ detail={{
name: documentDetail?.name || '',
enabled: documentDetail?.enabled || false, enabled: documentDetail?.enabled || false,
archived: documentDetail?.archived || false, archived: documentDetail?.archived || false,
id: documentId, id: documentId,

View File

@ -1,8 +1,8 @@
/* eslint-disable no-mixed-operators */ /* eslint-disable no-mixed-operators */
'use client' 'use client'
import type { FC, SVGProps } from 'react' import type { FC, SVGProps } from 'react'
import React, { useEffect, useState } from 'react' import React, { useCallback, useEffect, useState } from 'react'
import { useDebounceFn } from 'ahooks' import { useBoolean, useDebounceFn } from 'ahooks'
import { ArrowDownIcon, TrashIcon } from '@heroicons/react/24/outline' import { ArrowDownIcon, TrashIcon } from '@heroicons/react/24/outline'
import { ExclamationCircleIcon } from '@heroicons/react/24/solid' import { ExclamationCircleIcon } from '@heroicons/react/24/solid'
import { pick } from 'lodash-es' import { pick } from 'lodash-es'
@ -11,7 +11,10 @@ import { useRouter } from 'next/navigation'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import cn from 'classnames' import cn from 'classnames'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { Edit03 } from '../../base/icons/src/vender/solid/general'
import TooltipPlus from '../../base/tooltip-plus'
import s from './style.module.css' import s from './style.module.css'
import RenameModal from './rename-modal'
import Switch from '@/app/components/base/switch' import Switch from '@/app/components/base/switch'
import Divider from '@/app/components/base/divider' import Divider from '@/app/components/base/divider'
import Popover from '@/app/components/base/popover' import Popover from '@/app/components/base/popover'
@ -107,6 +110,7 @@ type OperationName = 'delete' | 'archive' | 'enable' | 'disable' | 'sync' | 'un_
export const OperationAction: FC<{ export const OperationAction: FC<{
embeddingAvailable: boolean embeddingAvailable: boolean
detail: { detail: {
name: string
enabled: boolean enabled: boolean
archived: boolean archived: boolean
id: string id: string
@ -164,6 +168,25 @@ export const OperationAction: FC<{
onOperate(operationName) onOperate(operationName)
}, { wait: 500 }) }, { wait: 500 })
const [currDocument, setCurrDocument] = useState<{
id: string
name: string
} | null>(null)
const [isShowRenameModal, {
setTrue: setShowRenameModalTrue,
setFalse: setShowRenameModalFalse,
}] = useBoolean(false)
const handleShowRenameModal = useCallback((doc: {
id: string
name: string
}) => {
setCurrDocument(doc)
setShowRenameModalTrue()
}, [setShowRenameModalTrue])
const handleRenamed = useCallback(() => {
onUpdate()
}, [onUpdate])
return <div className='flex items-center' onClick={e => e.stopPropagation()}> return <div className='flex items-center' onClick={e => e.stopPropagation()}>
{isListScene && !embeddingAvailable && ( {isListScene && !embeddingAvailable && (
<Switch defaultValue={false} onChange={() => { }} disabled={true} size='md' /> <Switch defaultValue={false} onChange={() => { }} disabled={true} size='md' />
@ -213,6 +236,15 @@ export const OperationAction: FC<{
</>} </>}
{!archived && ( {!archived && (
<> <>
<div className={s.actionItem} onClick={() => {
handleShowRenameModal({
id: detail.id,
name: detail.name,
})
}}>
<Edit03 className='w-4 h-4 text-gray-500' />
<span className={s.actionName}>{t('datasetDocuments.list.table.rename')}</span>
</div>
<div className={s.actionItem} onClick={() => router.push(`/datasets/${datasetId}/documents/${detail.id}/settings`)}> <div className={s.actionItem} onClick={() => router.push(`/datasets/${datasetId}/documents/${detail.id}/settings`)}>
<SettingsIcon /> <SettingsIcon />
<span className={s.actionName}>{t('datasetDocuments.list.action.settings')}</span> <span className={s.actionName}>{t('datasetDocuments.list.action.settings')}</span>
@ -272,6 +304,16 @@ export const OperationAction: FC<{
</div> </div>
</div> </div>
</Modal>} </Modal>}
{isShowRenameModal && currDocument && (
<RenameModal
datasetId={datasetId}
documentId={currDocument.id}
name={currDocument.name}
onClose={setShowRenameModalFalse}
onSaved={handleRenamed}
/>
)}
</div> </div>
} }
@ -326,13 +368,30 @@ const DocumentList: FC<IDocumentListProps> = ({ embeddingAvailable, documents =
} }
} }
const [currDocument, setCurrDocument] = useState<LocalDoc | null>(null)
const [isShowRenameModal, {
setTrue: setShowRenameModalTrue,
setFalse: setShowRenameModalFalse,
}] = useBoolean(false)
const handleShowRenameModal = useCallback((doc: LocalDoc) => {
setCurrDocument(doc)
setShowRenameModalTrue()
}, [setShowRenameModalTrue])
const handleRenamed = useCallback(() => {
onUpdate()
}, [onUpdate])
return ( return (
<div className='w-full h-full overflow-x-auto'> <div className='w-full h-full overflow-x-auto'>
<table className={`min-w-[700px] max-w-full w-full border-collapse border-0 text-sm mt-3 ${s.documentTable}`}> <table className={`min-w-[700px] max-w-full w-full border-collapse border-0 text-sm mt-3 ${s.documentTable}`}>
<thead className="h-8 leading-8 border-b border-gray-200 text-gray-500 font-medium text-xs uppercase"> <thead className="h-8 leading-8 border-b border-gray-200 text-gray-500 font-medium text-xs uppercase">
<tr> <tr>
<td className='w-12'>#</td> <td className='w-12'>#</td>
<td>{t('datasetDocuments.list.table.header.fileName')}</td> <td>
<div className='flex'>
{t('datasetDocuments.list.table.header.fileName')}
</div>
</td>
<td className='w-24'>{t('datasetDocuments.list.table.header.words')}</td> <td className='w-24'>{t('datasetDocuments.list.table.header.words')}</td>
<td className='w-44'>{t('datasetDocuments.list.table.header.hitCount')}</td> <td className='w-44'>{t('datasetDocuments.list.table.header.hitCount')}</td>
<td className='w-44'> <td className='w-44'>
@ -347,7 +406,8 @@ const DocumentList: FC<IDocumentListProps> = ({ embeddingAvailable, documents =
</thead> </thead>
<tbody className="text-gray-700"> <tbody className="text-gray-700">
{localDocs.map((doc) => { {localDocs.map((doc) => {
const suffix = doc.name.split('.').pop() || 'txt' const isFile = doc.data_source_type === DataSourceType.FILE
const fileType = isFile ? doc.data_source_detail_dict?.upload_file.extension : ''
return <tr return <tr
key={doc.id} key={doc.id}
className={'border-b border-gray-200 h-8 hover:bg-gray-50 cursor-pointer'} className={'border-b border-gray-200 h-8 hover:bg-gray-50 cursor-pointer'}
@ -355,17 +415,33 @@ const DocumentList: FC<IDocumentListProps> = ({ embeddingAvailable, documents =
router.push(`/datasets/${datasetId}/documents/${doc.id}`) router.push(`/datasets/${datasetId}/documents/${doc.id}`)
}}> }}>
<td className='text-left align-middle text-gray-500 text-xs'>{doc.position}</td> <td className='text-left align-middle text-gray-500 text-xs'>{doc.position}</td>
<td className={s.tdValue}> <td>
<div className='group flex items-center justify-between'>
<span className={s.tdValue}>
{ {
doc?.data_source_type === DataSourceType.NOTION doc?.data_source_type === DataSourceType.NOTION
? <NotionIcon className='inline-flex -mt-[3px] mr-1.5 align-middle' type='page' src={doc.data_source_info.notion_page_icon} /> ? <NotionIcon className='inline-flex -mt-[3px] mr-1.5 align-middle' type='page' src={doc.data_source_info.notion_page_icon} />
: <div className={cn(s[`${doc?.data_source_info?.upload_file?.extension ?? suffix}Icon`], s.commonIcon, 'mr-1.5')}></div> : <div className={cn(s[`${doc?.data_source_info?.upload_file?.extension ?? fileType}Icon`], s.commonIcon, 'mr-1.5')}></div>
} }
{ {
doc.data_source_type === DataSourceType.NOTION doc.name
? <span>{doc.name}</span>
: <span>{doc?.name?.replace(/\.[^/.]+$/, '')}<span className='text-gray-500'>.{suffix}</span></span>
} }
</span>
<div className='group-hover:flex hidden'>
<TooltipPlus popupContent={t('datasetDocuments.list.table.rename')}>
<div
className='p-1 rounded-md cursor-pointer hover:bg-black/5'
onClick={(e) => {
e.stopPropagation()
handleShowRenameModal(doc)
}}
>
<Edit03 className='w-4 h-4 text-gray-500' />
</div>
</TooltipPlus>
</div>
</div>
</td> </td>
<td>{renderCount(doc.word_count)}</td> <td>{renderCount(doc.word_count)}</td>
<td>{renderCount(doc.hit_count)}</td> <td>{renderCount(doc.hit_count)}</td>
@ -383,7 +459,7 @@ const DocumentList: FC<IDocumentListProps> = ({ embeddingAvailable, documents =
<OperationAction <OperationAction
embeddingAvailable={embeddingAvailable} embeddingAvailable={embeddingAvailable}
datasetId={datasetId} datasetId={datasetId}
detail={pick(doc, ['enabled', 'archived', 'id', 'data_source_type', 'doc_form'])} detail={pick(doc, ['name', 'enabled', 'archived', 'id', 'data_source_type', 'doc_form'])}
onUpdate={onUpdate} onUpdate={onUpdate}
/> />
</td> </td>
@ -391,6 +467,16 @@ const DocumentList: FC<IDocumentListProps> = ({ embeddingAvailable, documents =
})} })}
</tbody> </tbody>
</table> </table>
{isShowRenameModal && currDocument && (
<RenameModal
datasetId={datasetId}
documentId={currDocument.id}
name={currDocument.name}
onClose={setShowRenameModalFalse}
onSaved={handleRenamed}
/>
)}
</div> </div>
) )
} }

View File

@ -0,0 +1,75 @@
'use client'
import type { FC } from 'react'
import React, { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useBoolean } from 'ahooks'
import Toast from '../../base/toast'
import Modal from '@/app/components/base/modal'
import Button from '@/app/components/base/button'
import { renameDocumentName } from '@/service/datasets'
type Props = {
datasetId: string
documentId: string
name: string
onClose: () => void
onSaved: () => void
}
const RenameModal: FC<Props> = ({
documentId,
datasetId,
name,
onClose,
onSaved,
}) => {
const { t } = useTranslation()
const [newName, setNewName] = useState(name)
const [saveLoading, {
setTrue: setSaveLoadingTrue,
setFalse: setSaveLoadingFalse,
}] = useBoolean(false)
const handleSave = async () => {
setSaveLoadingTrue()
try {
await renameDocumentName({
datasetId,
documentId,
name: newName,
})
Toast.notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
onSaved()
onClose()
}
catch (error) {
if (error)
Toast.notify({ type: 'error', message: error.toString() })
}
finally {
setSaveLoadingFalse()
}
}
return (
<Modal
title={t('datasetDocuments.list.table.rename')}
isShow
onClose={onClose}
wrapperClassName='!z-50'
>
<div className={'mt-6 font-medium text-sm leading-[21px] text-gray-900'}>{t('datasetDocuments.list.table.name')}</div>
<input className={'mt-2 w-full rounded-lg h-10 box-border px-3 text-sm leading-10 bg-gray-100'}
value={newName}
onChange={e => setNewName(e.target.value)}
/>
<div className='mt-10 flex justify-end'>
<Button className='mr-2 flex-shrink-0' onClick={onClose}>{t('common.operation.cancel')}</Button>
<Button type='primary' className='flex-shrink-0' onClick={handleSave} loading={saveLoading}>{t('common.operation.save')}</Button>
</div>
</Modal>
)
}
export default React.memo(RenameModal)

View File

@ -13,6 +13,8 @@ const translation = {
status: 'STATUS', status: 'STATUS',
action: 'ACTION', action: 'ACTION',
}, },
rename: 'Rename',
name: 'Name',
}, },
action: { action: {
uploadFile: 'Upload new file', uploadFile: 'Upload new file',

View File

@ -13,6 +13,8 @@ const translation = {
status: '状态', status: '状态',
action: '操作', action: '操作',
}, },
rename: '重命名',
name: '名称',
}, },
action: { action: {
uploadFile: '上传新文件', uploadFile: '上传新文件',

View File

@ -178,6 +178,12 @@ export type SimpleDocumentDetail = InitialDocumentDetail & {
updated_at: number updated_at: number
hit_count: number hit_count: number
dataset_process_rule_id?: string dataset_process_rule_id?: string
data_source_detail_dict?: {
upload_file: {
name: string
extension: string
}
}
} }
export type DocumentListResponse = { export type DocumentListResponse = {

View File

@ -114,6 +114,12 @@ export const fetchDocumentDetail: Fetcher<DocumentDetailResponse, CommonDocReq &
return get<DocumentDetailResponse>(`/datasets/${datasetId}/documents/${documentId}`, { params }) return get<DocumentDetailResponse>(`/datasets/${datasetId}/documents/${documentId}`, { params })
} }
export const renameDocumentName: Fetcher<CommonResponse, CommonDocReq & { name: string }> = ({ datasetId, documentId, name }) => {
return post<CommonResponse>(`/datasets/${datasetId}/documents/${documentId}/rename`, {
body: { name },
})
}
export const pauseDocIndexing: Fetcher<CommonResponse, CommonDocReq> = ({ datasetId, documentId }) => { export const pauseDocIndexing: Fetcher<CommonResponse, CommonDocReq> = ({ datasetId, documentId }) => {
return patch<CommonResponse>(`/datasets/${datasetId}/documents/${documentId}/processing/pause`) return patch<CommonResponse>(`/datasets/${datasetId}/documents/${documentId}/processing/pause`)
} }