mirror of https://github.com/langgenius/dify.git
feat: Enhance dataset dropdown functionality with export and delete options
This commit is contained in:
parent
8fc15c83d0
commit
59c3305dcc
|
|
@ -4,6 +4,17 @@ import ActionButton from '../../base/action-button'
|
|||
import { RiMoreFill } from '@remixicon/react'
|
||||
import cn from '@/utils/classnames'
|
||||
import Menu from './menu'
|
||||
import { useSelector as useAppContextWithSelector } from '@/context/app-context'
|
||||
import { useDatasetDetailContextWithSelector } from '@/context/dataset-detail'
|
||||
import type { DataSet } from '@/models/datasets'
|
||||
import { datasetDetailQueryKeyPrefix, useResetDatasetList } from '@/service/knowledge/use-dataset'
|
||||
import { useInvalid } from '@/service/use-base'
|
||||
import { useExportPipelineDSL } from '@/service/use-pipeline'
|
||||
import Toast from '../../base/toast'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import RenameDatasetModal from '../../datasets/rename-modal'
|
||||
import { checkIsUsedInApp, deleteDataset } from '@/service/datasets'
|
||||
import Confirm from '../../base/confirm'
|
||||
|
||||
type DropDownProps = {
|
||||
expand: boolean
|
||||
|
|
@ -12,12 +23,81 @@ type DropDownProps = {
|
|||
const DropDown = ({
|
||||
expand,
|
||||
}: DropDownProps) => {
|
||||
const { t } = useTranslation()
|
||||
const [open, setOpen] = useState(false)
|
||||
const [showRenameModal, setShowRenameModal] = useState(false)
|
||||
const [confirmMessage, setConfirmMessage] = useState<string>('')
|
||||
const [showConfirmDelete, setShowConfirmDelete] = useState(false)
|
||||
|
||||
const isCurrentWorkspaceDatasetOperator = useAppContextWithSelector(state => state.isCurrentWorkspaceDatasetOperator)
|
||||
const dataset = useDatasetDetailContextWithSelector(state => state.dataset) as DataSet
|
||||
|
||||
const handleTrigger = useCallback(() => {
|
||||
setOpen(prev => !prev)
|
||||
}, [])
|
||||
|
||||
const resetDatasetList = useResetDatasetList()
|
||||
const invalidDatasetDetail = useInvalid([...datasetDetailQueryKeyPrefix, dataset.id])
|
||||
|
||||
const refreshDataset = useCallback(() => {
|
||||
resetDatasetList()
|
||||
invalidDatasetDetail()
|
||||
}, [invalidDatasetDetail, resetDatasetList])
|
||||
|
||||
const openRenameModal = useCallback(() => {
|
||||
setShowRenameModal(true)
|
||||
handleTrigger()
|
||||
}, [handleTrigger])
|
||||
|
||||
const { mutateAsync: exportPipelineConfig } = useExportPipelineDSL()
|
||||
|
||||
const handleExportPipeline = useCallback(async (include = false) => {
|
||||
const { pipeline_id, name } = dataset
|
||||
if (!pipeline_id)
|
||||
return
|
||||
handleTrigger()
|
||||
try {
|
||||
const { data } = await exportPipelineConfig({
|
||||
pipelineId: pipeline_id,
|
||||
include,
|
||||
})
|
||||
const a = document.createElement('a')
|
||||
const file = new Blob([data], { type: 'application/yaml' })
|
||||
a.href = URL.createObjectURL(file)
|
||||
a.download = `${name}.yml`
|
||||
a.click()
|
||||
}
|
||||
catch {
|
||||
Toast.notify({ type: 'error', message: t('app.exportFailed') })
|
||||
}
|
||||
}, [dataset, exportPipelineConfig, handleTrigger, t])
|
||||
|
||||
const detectIsUsedByApp = useCallback(async () => {
|
||||
try {
|
||||
const { is_using: isUsedByApp } = await checkIsUsedInApp(dataset.id)
|
||||
setConfirmMessage(isUsedByApp ? t('dataset.datasetUsedByApp')! : t('dataset.deleteDatasetConfirmContent')!)
|
||||
setShowConfirmDelete(true)
|
||||
}
|
||||
catch (e: any) {
|
||||
const res = await e.json()
|
||||
Toast.notify({ type: 'error', message: res?.message || 'Unknown error' })
|
||||
}
|
||||
finally {
|
||||
handleTrigger()
|
||||
}
|
||||
}, [dataset.id, handleTrigger, t])
|
||||
|
||||
const onConfirmDelete = useCallback(async () => {
|
||||
try {
|
||||
await deleteDataset(dataset.id)
|
||||
Toast.notify({ type: 'success', message: t('dataset.datasetDeleted') })
|
||||
refreshDataset()
|
||||
}
|
||||
finally {
|
||||
setShowConfirmDelete(false)
|
||||
}
|
||||
}, [dataset.id, refreshDataset, t])
|
||||
|
||||
return (
|
||||
<PortalToFollowElem
|
||||
open={open}
|
||||
|
|
@ -36,8 +116,30 @@ const DropDown = ({
|
|||
</ActionButton>
|
||||
</PortalToFollowElemTrigger>
|
||||
<PortalToFollowElemContent>
|
||||
<Menu />
|
||||
<Menu
|
||||
showDelete={!isCurrentWorkspaceDatasetOperator}
|
||||
openRenameModal={openRenameModal}
|
||||
handleExportPipeline={handleExportPipeline}
|
||||
detectIsUsedByApp={detectIsUsedByApp}
|
||||
/>
|
||||
</PortalToFollowElemContent>
|
||||
{showRenameModal && (
|
||||
<RenameDatasetModal
|
||||
show={showRenameModal}
|
||||
dataset={dataset!}
|
||||
onClose={() => setShowRenameModal(false)}
|
||||
onSuccess={refreshDataset}
|
||||
/>
|
||||
)}
|
||||
{showConfirmDelete && (
|
||||
<Confirm
|
||||
title={t('dataset.deleteDatasetConfirmTitle')}
|
||||
content={confirmMessage}
|
||||
isShow={showConfirmDelete}
|
||||
onConfirm={onConfirmDelete}
|
||||
onCancel={() => setShowConfirmDelete(false)}
|
||||
/>
|
||||
)}
|
||||
</PortalToFollowElem>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import type { RemixiconComponentType } from '@remixicon/react'
|
|||
type MenuItemProps = {
|
||||
name: string
|
||||
Icon: RemixiconComponentType
|
||||
handleClick?: () => void
|
||||
handleClick?: (e: React.MouseEvent<HTMLDivElement>) => void
|
||||
}
|
||||
|
||||
const MenuItem = ({
|
||||
|
|
@ -15,9 +15,7 @@ const MenuItem = ({
|
|||
return (
|
||||
<div
|
||||
className='flex items-center gap-x-1 rounded-lg px-2 py-1.5 hover:bg-state-base-hover'
|
||||
onClick={() => {
|
||||
handleClick?.()
|
||||
}}
|
||||
onClick={handleClick}
|
||||
>
|
||||
<Icon className='size-4 text-text-tertiary' />
|
||||
<span className='system-md-regular px-1 text-text-secondary'>{name}</span>
|
||||
|
|
|
|||
|
|
@ -1,19 +1,68 @@
|
|||
import { useDatasetDetailContextWithSelector } from '@/context/dataset-detail'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import MenuItem from './menu-item'
|
||||
import { RiEditLine } from '@remixicon/react'
|
||||
import { noop } from 'lodash-es'
|
||||
import { RiDeleteBinLine, RiEditLine, RiFileDownloadLine } from '@remixicon/react'
|
||||
import Divider from '../../base/divider'
|
||||
|
||||
const Menu = () => {
|
||||
type MenuProps = {
|
||||
showDelete: boolean
|
||||
openRenameModal: () => void
|
||||
handleExportPipeline: () => void
|
||||
detectIsUsedByApp: () => void
|
||||
}
|
||||
|
||||
const Menu = ({
|
||||
showDelete,
|
||||
openRenameModal,
|
||||
handleExportPipeline,
|
||||
detectIsUsedByApp,
|
||||
}: MenuProps) => {
|
||||
const { t } = useTranslation()
|
||||
const dataset = useDatasetDetailContextWithSelector(state => state.dataset)
|
||||
|
||||
const onClickRename = async (e: React.MouseEvent<HTMLDivElement>) => {
|
||||
e.stopPropagation()
|
||||
e.preventDefault()
|
||||
openRenameModal()
|
||||
}
|
||||
|
||||
const onClickExport = async (e: React.MouseEvent<HTMLDivElement>) => {
|
||||
e.stopPropagation()
|
||||
e.preventDefault()
|
||||
handleExportPipeline()
|
||||
}
|
||||
|
||||
const onClickDelete = async (e: React.MouseEvent<HTMLDivElement>) => {
|
||||
e.stopPropagation()
|
||||
e.preventDefault()
|
||||
detectIsUsedByApp()
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='flex w-[200px] flex-col rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadow-lg shadow-shadow-shadow-5 backdrop-blur-[5px]'>
|
||||
<div className='flex flex-col p-1'>
|
||||
<MenuItem Icon={RiEditLine} name={t('common.operation.edit')} handleClick={noop} />
|
||||
<MenuItem
|
||||
Icon={RiEditLine}
|
||||
name={t('common.operation.edit')}
|
||||
handleClick={onClickRename}
|
||||
/>
|
||||
<MenuItem
|
||||
Icon={RiFileDownloadLine}
|
||||
name={t('datasetPipeline.operations.exportPipeline')}
|
||||
handleClick={onClickExport}
|
||||
/>
|
||||
</div>
|
||||
{showDelete && (
|
||||
<>
|
||||
<Divider type='horizontal' className='my-0 bg-divider-subtle' />
|
||||
<div className='flex flex-col p-1'>
|
||||
<MenuItem
|
||||
Icon={RiDeleteBinLine}
|
||||
name={t('common.operation.delete')}
|
||||
handleClick={onClickDelete}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -83,6 +83,10 @@ const DatasetCard = ({
|
|||
return dayjs(time * 1_000).locale(language === 'zh_Hans' ? 'zh-cn' : language.replace('_', '-')).fromNow()
|
||||
}, [language])
|
||||
|
||||
const openRenameModal = useCallback(() => {
|
||||
setShowRenameModal(true)
|
||||
}, [])
|
||||
|
||||
const { mutateAsync: exportPipelineConfig } = useExportPipelineDSL()
|
||||
|
||||
const handleExportPipeline = useCallback(async (include = false) => {
|
||||
|
|
@ -117,13 +121,12 @@ const DatasetCard = ({
|
|||
try {
|
||||
const { is_using: isUsedByApp } = await checkIsUsedInApp(dataset.id)
|
||||
setConfirmMessage(isUsedByApp ? t('dataset.datasetUsedByApp')! : t('dataset.deleteDatasetConfirmContent')!)
|
||||
setShowConfirmDelete(true)
|
||||
}
|
||||
catch (e: any) {
|
||||
const res = await e.json()
|
||||
Toast.notify({ type: 'error', message: res?.message || 'Unknown error' })
|
||||
}
|
||||
|
||||
setShowConfirmDelete(true)
|
||||
}, [dataset.id, t])
|
||||
|
||||
const onConfirmDelete = useCallback(async () => {
|
||||
|
|
@ -133,9 +136,9 @@ const DatasetCard = ({
|
|||
if (onSuccess)
|
||||
onSuccess()
|
||||
}
|
||||
catch {
|
||||
finally {
|
||||
setShowConfirmDelete(false)
|
||||
}
|
||||
setShowConfirmDelete(false)
|
||||
}, [dataset.id, onSuccess, t])
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -262,11 +265,9 @@ const DatasetCard = ({
|
|||
htmlContent={
|
||||
<Operations
|
||||
showDelete={!isCurrentWorkspaceDatasetOperator}
|
||||
openRenameModal={() => {
|
||||
setShowRenameModal(true)
|
||||
}}
|
||||
detectIsUsedByApp={detectIsUsedByApp}
|
||||
openRenameModal={openRenameModal}
|
||||
handleExportPipeline={handleExportPipeline}
|
||||
detectIsUsedByApp={detectIsUsedByApp}
|
||||
/>
|
||||
}
|
||||
className={'z-20 min-w-[186px]'}
|
||||
|
|
|
|||
|
|
@ -45,14 +45,18 @@ const DatasetNav = () => {
|
|||
id: currentDataset.id,
|
||||
name: currentDataset.name,
|
||||
icon: currentDataset.icon_info.icon,
|
||||
icon_type: currentDataset.icon_info.icon_type,
|
||||
icon_background: currentDataset.icon_info.icon_background,
|
||||
icon_url: currentDataset.icon_info.icon_url,
|
||||
} as Omit<NavItem, 'link'>}
|
||||
navigationItems={datasetItems.map(dataset => ({
|
||||
id: dataset.id,
|
||||
name: dataset.name,
|
||||
link: dataset.provider === 'external' ? `/datasets/${dataset.id}/hitTesting` : `/datasets/${dataset.id}/documents`,
|
||||
icon: dataset.icon_info.icon,
|
||||
icon_type: dataset.icon_info.icon_type,
|
||||
icon_background: dataset.icon_info.icon_background,
|
||||
icon_url: dataset.icon_info.icon_url,
|
||||
})) as NavItem[]}
|
||||
createText={t('common.menus.newDataset')}
|
||||
onCreate={() => router.push(`${basePath}/datasets/create`)}
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ const Nav = ({
|
|||
useEffect(() => {
|
||||
if (pathname === link)
|
||||
setLinkLastSearchParams(searchParams.toString())
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [pathname, searchParams])
|
||||
|
||||
return (
|
||||
|
|
@ -53,11 +54,11 @@ const Nav = ({
|
|||
<Link href={link + (linkLastSearchParams && `?${linkLastSearchParams}`)}>
|
||||
<div
|
||||
onClick={() => setAppDetail()}
|
||||
className={classNames(`
|
||||
flex items-center h-7 px-2.5 cursor-pointer rounded-[10px]
|
||||
${isActivated ? 'text-components-main-nav-nav-button-text-active' : 'text-components-main-nav-nav-button-text'}
|
||||
${curNav && isActivated && 'hover:bg-components-main-nav-nav-button-bg-active-hover'}
|
||||
`)}
|
||||
className={classNames(
|
||||
'flex h-7 cursor-pointer items-center rounded-[10px] px-2.5',
|
||||
isActivated ? 'text-components-main-nav-nav-button-text-active' : 'text-components-main-nav-nav-button-text',
|
||||
curNav && isActivated && 'hover:bg-components-main-nav-nav-button-bg-active-hover',
|
||||
)}
|
||||
onMouseEnter={() => setHovered(true)}
|
||||
onMouseLeave={() => setHovered(false)}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -84,7 +84,13 @@ const NavSelector = ({ curNav, navigationItems, createText, isApp, onCreate, onL
|
|||
router.push(nav.link)
|
||||
}} title={nav.name}>
|
||||
<div className='relative mr-2 h-6 w-6 rounded-md'>
|
||||
<AppIcon size='tiny' iconType={nav.icon_type} icon={nav.icon} background={nav.icon_background} imageUrl={nav.icon_url} />
|
||||
<AppIcon
|
||||
size='tiny'
|
||||
iconType={nav.icon_type}
|
||||
icon={nav.icon}
|
||||
background={nav.icon_background}
|
||||
imageUrl={nav.icon_url}
|
||||
/>
|
||||
{!!nav.mode && (
|
||||
<span className={cn(
|
||||
'absolute -bottom-0.5 -right-0.5 h-3.5 w-3.5 rounded border-[0.5px] border-[rgba(0,0,0,0.02)] bg-white p-0.5 shadow-sm',
|
||||
|
|
|
|||
|
|
@ -40,9 +40,11 @@ export const useResetDatasetList = () => {
|
|||
return useReset([...DatasetListKey])
|
||||
}
|
||||
|
||||
export const datasetDetailQueryKeyPrefix = [NAME_SPACE, 'detail']
|
||||
|
||||
export const useDatasetDetail = (datasetId: string) => {
|
||||
return useQuery({
|
||||
queryKey: [NAME_SPACE, 'detail', datasetId],
|
||||
queryKey: [...datasetDetailQueryKeyPrefix, datasetId],
|
||||
queryFn: () => get<DataSet>(`/datasets/${datasetId}`),
|
||||
enabled: !!datasetId,
|
||||
})
|
||||
|
|
|
|||
Loading…
Reference in New Issue