mirror of
https://github.com/langgenius/dify.git
synced 2026-09-08 11:04:27 +08:00
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 { RiMoreFill } from '@remixicon/react'
|
||||||
import cn from '@/utils/classnames'
|
import cn from '@/utils/classnames'
|
||||||
import Menu from './menu'
|
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 = {
|
type DropDownProps = {
|
||||||
expand: boolean
|
expand: boolean
|
||||||
@ -12,12 +23,81 @@ type DropDownProps = {
|
|||||||
const DropDown = ({
|
const DropDown = ({
|
||||||
expand,
|
expand,
|
||||||
}: DropDownProps) => {
|
}: DropDownProps) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
const [open, setOpen] = useState(false)
|
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(() => {
|
const handleTrigger = useCallback(() => {
|
||||||
setOpen(prev => !prev)
|
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 (
|
return (
|
||||||
<PortalToFollowElem
|
<PortalToFollowElem
|
||||||
open={open}
|
open={open}
|
||||||
@ -36,8 +116,30 @@ const DropDown = ({
|
|||||||
</ActionButton>
|
</ActionButton>
|
||||||
</PortalToFollowElemTrigger>
|
</PortalToFollowElemTrigger>
|
||||||
<PortalToFollowElemContent>
|
<PortalToFollowElemContent>
|
||||||
<Menu />
|
<Menu
|
||||||
|
showDelete={!isCurrentWorkspaceDatasetOperator}
|
||||||
|
openRenameModal={openRenameModal}
|
||||||
|
handleExportPipeline={handleExportPipeline}
|
||||||
|
detectIsUsedByApp={detectIsUsedByApp}
|
||||||
|
/>
|
||||||
</PortalToFollowElemContent>
|
</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>
|
</PortalToFollowElem>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import type { RemixiconComponentType } from '@remixicon/react'
|
|||||||
type MenuItemProps = {
|
type MenuItemProps = {
|
||||||
name: string
|
name: string
|
||||||
Icon: RemixiconComponentType
|
Icon: RemixiconComponentType
|
||||||
handleClick?: () => void
|
handleClick?: (e: React.MouseEvent<HTMLDivElement>) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const MenuItem = ({
|
const MenuItem = ({
|
||||||
@ -15,9 +15,7 @@ const MenuItem = ({
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className='flex items-center gap-x-1 rounded-lg px-2 py-1.5 hover:bg-state-base-hover'
|
className='flex items-center gap-x-1 rounded-lg px-2 py-1.5 hover:bg-state-base-hover'
|
||||||
onClick={() => {
|
onClick={handleClick}
|
||||||
handleClick?.()
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<Icon className='size-4 text-text-tertiary' />
|
<Icon className='size-4 text-text-tertiary' />
|
||||||
<span className='system-md-regular px-1 text-text-secondary'>{name}</span>
|
<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 React from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import MenuItem from './menu-item'
|
import MenuItem from './menu-item'
|
||||||
import { RiEditLine } from '@remixicon/react'
|
import { RiDeleteBinLine, RiEditLine, RiFileDownloadLine } from '@remixicon/react'
|
||||||
import { noop } from 'lodash-es'
|
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 { 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 (
|
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 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'>
|
<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>
|
</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>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -83,6 +83,10 @@ const DatasetCard = ({
|
|||||||
return dayjs(time * 1_000).locale(language === 'zh_Hans' ? 'zh-cn' : language.replace('_', '-')).fromNow()
|
return dayjs(time * 1_000).locale(language === 'zh_Hans' ? 'zh-cn' : language.replace('_', '-')).fromNow()
|
||||||
}, [language])
|
}, [language])
|
||||||
|
|
||||||
|
const openRenameModal = useCallback(() => {
|
||||||
|
setShowRenameModal(true)
|
||||||
|
}, [])
|
||||||
|
|
||||||
const { mutateAsync: exportPipelineConfig } = useExportPipelineDSL()
|
const { mutateAsync: exportPipelineConfig } = useExportPipelineDSL()
|
||||||
|
|
||||||
const handleExportPipeline = useCallback(async (include = false) => {
|
const handleExportPipeline = useCallback(async (include = false) => {
|
||||||
@ -117,13 +121,12 @@ const DatasetCard = ({
|
|||||||
try {
|
try {
|
||||||
const { is_using: isUsedByApp } = await checkIsUsedInApp(dataset.id)
|
const { is_using: isUsedByApp } = await checkIsUsedInApp(dataset.id)
|
||||||
setConfirmMessage(isUsedByApp ? t('dataset.datasetUsedByApp')! : t('dataset.deleteDatasetConfirmContent')!)
|
setConfirmMessage(isUsedByApp ? t('dataset.datasetUsedByApp')! : t('dataset.deleteDatasetConfirmContent')!)
|
||||||
|
setShowConfirmDelete(true)
|
||||||
}
|
}
|
||||||
catch (e: any) {
|
catch (e: any) {
|
||||||
const res = await e.json()
|
const res = await e.json()
|
||||||
Toast.notify({ type: 'error', message: res?.message || 'Unknown error' })
|
Toast.notify({ type: 'error', message: res?.message || 'Unknown error' })
|
||||||
}
|
}
|
||||||
|
|
||||||
setShowConfirmDelete(true)
|
|
||||||
}, [dataset.id, t])
|
}, [dataset.id, t])
|
||||||
|
|
||||||
const onConfirmDelete = useCallback(async () => {
|
const onConfirmDelete = useCallback(async () => {
|
||||||
@ -133,9 +136,9 @@ const DatasetCard = ({
|
|||||||
if (onSuccess)
|
if (onSuccess)
|
||||||
onSuccess()
|
onSuccess()
|
||||||
}
|
}
|
||||||
catch {
|
finally {
|
||||||
|
setShowConfirmDelete(false)
|
||||||
}
|
}
|
||||||
setShowConfirmDelete(false)
|
|
||||||
}, [dataset.id, onSuccess, t])
|
}, [dataset.id, onSuccess, t])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -262,11 +265,9 @@ const DatasetCard = ({
|
|||||||
htmlContent={
|
htmlContent={
|
||||||
<Operations
|
<Operations
|
||||||
showDelete={!isCurrentWorkspaceDatasetOperator}
|
showDelete={!isCurrentWorkspaceDatasetOperator}
|
||||||
openRenameModal={() => {
|
openRenameModal={openRenameModal}
|
||||||
setShowRenameModal(true)
|
|
||||||
}}
|
|
||||||
detectIsUsedByApp={detectIsUsedByApp}
|
|
||||||
handleExportPipeline={handleExportPipeline}
|
handleExportPipeline={handleExportPipeline}
|
||||||
|
detectIsUsedByApp={detectIsUsedByApp}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
className={'z-20 min-w-[186px]'}
|
className={'z-20 min-w-[186px]'}
|
||||||
|
|||||||
@ -45,14 +45,18 @@ const DatasetNav = () => {
|
|||||||
id: currentDataset.id,
|
id: currentDataset.id,
|
||||||
name: currentDataset.name,
|
name: currentDataset.name,
|
||||||
icon: currentDataset.icon_info.icon,
|
icon: currentDataset.icon_info.icon,
|
||||||
|
icon_type: currentDataset.icon_info.icon_type,
|
||||||
icon_background: currentDataset.icon_info.icon_background,
|
icon_background: currentDataset.icon_info.icon_background,
|
||||||
|
icon_url: currentDataset.icon_info.icon_url,
|
||||||
} as Omit<NavItem, 'link'>}
|
} as Omit<NavItem, 'link'>}
|
||||||
navigationItems={datasetItems.map(dataset => ({
|
navigationItems={datasetItems.map(dataset => ({
|
||||||
id: dataset.id,
|
id: dataset.id,
|
||||||
name: dataset.name,
|
name: dataset.name,
|
||||||
link: dataset.provider === 'external' ? `/datasets/${dataset.id}/hitTesting` : `/datasets/${dataset.id}/documents`,
|
link: dataset.provider === 'external' ? `/datasets/${dataset.id}/hitTesting` : `/datasets/${dataset.id}/documents`,
|
||||||
icon: dataset.icon_info.icon,
|
icon: dataset.icon_info.icon,
|
||||||
|
icon_type: dataset.icon_info.icon_type,
|
||||||
icon_background: dataset.icon_info.icon_background,
|
icon_background: dataset.icon_info.icon_background,
|
||||||
|
icon_url: dataset.icon_info.icon_url,
|
||||||
})) as NavItem[]}
|
})) as NavItem[]}
|
||||||
createText={t('common.menus.newDataset')}
|
createText={t('common.menus.newDataset')}
|
||||||
onCreate={() => router.push(`${basePath}/datasets/create`)}
|
onCreate={() => router.push(`${basePath}/datasets/create`)}
|
||||||
|
|||||||
@ -42,6 +42,7 @@ const Nav = ({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (pathname === link)
|
if (pathname === link)
|
||||||
setLinkLastSearchParams(searchParams.toString())
|
setLinkLastSearchParams(searchParams.toString())
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [pathname, searchParams])
|
}, [pathname, searchParams])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -53,11 +54,11 @@ const Nav = ({
|
|||||||
<Link href={link + (linkLastSearchParams && `?${linkLastSearchParams}`)}>
|
<Link href={link + (linkLastSearchParams && `?${linkLastSearchParams}`)}>
|
||||||
<div
|
<div
|
||||||
onClick={() => setAppDetail()}
|
onClick={() => setAppDetail()}
|
||||||
className={classNames(`
|
className={classNames(
|
||||||
flex items-center h-7 px-2.5 cursor-pointer rounded-[10px]
|
'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'}
|
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'}
|
curNav && isActivated && 'hover:bg-components-main-nav-nav-button-bg-active-hover',
|
||||||
`)}
|
)}
|
||||||
onMouseEnter={() => setHovered(true)}
|
onMouseEnter={() => setHovered(true)}
|
||||||
onMouseLeave={() => setHovered(false)}
|
onMouseLeave={() => setHovered(false)}
|
||||||
>
|
>
|
||||||
|
|||||||
@ -84,7 +84,13 @@ const NavSelector = ({ curNav, navigationItems, createText, isApp, onCreate, onL
|
|||||||
router.push(nav.link)
|
router.push(nav.link)
|
||||||
}} title={nav.name}>
|
}} title={nav.name}>
|
||||||
<div className='relative mr-2 h-6 w-6 rounded-md'>
|
<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 && (
|
{!!nav.mode && (
|
||||||
<span className={cn(
|
<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',
|
'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])
|
return useReset([...DatasetListKey])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const datasetDetailQueryKeyPrefix = [NAME_SPACE, 'detail']
|
||||||
|
|
||||||
export const useDatasetDetail = (datasetId: string) => {
|
export const useDatasetDetail = (datasetId: string) => {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: [NAME_SPACE, 'detail', datasetId],
|
queryKey: [...datasetDetailQueryKeyPrefix, datasetId],
|
||||||
queryFn: () => get<DataSet>(`/datasets/${datasetId}`),
|
queryFn: () => get<DataSet>(`/datasets/${datasetId}`),
|
||||||
enabled: !!datasetId,
|
enabled: !!datasetId,
|
||||||
})
|
})
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user