From 59c3305dcc2f28a97d977b08bd412fb191386d85 Mon Sep 17 00:00:00 2001 From: twwu Date: Wed, 9 Jul 2025 13:42:24 +0800 Subject: [PATCH] feat: Enhance dataset dropdown functionality with export and delete options --- .../app-sidebar/dataset-info/dropdown.tsx | 104 +++++++++++++++++- .../app-sidebar/dataset-info/menu-item.tsx | 6 +- .../app-sidebar/dataset-info/menu.tsx | 61 +++++++++- .../datasets/list/dataset-card/index.tsx | 17 +-- .../components/header/dataset-nav/index.tsx | 4 + web/app/components/header/nav/index.tsx | 11 +- .../header/nav/nav-selector/index.tsx | 8 +- web/service/knowledge/use-dataset.ts | 4 +- 8 files changed, 189 insertions(+), 26 deletions(-) diff --git a/web/app/components/app-sidebar/dataset-info/dropdown.tsx b/web/app/components/app-sidebar/dataset-info/dropdown.tsx index bd73c8c182..699906f8ef 100644 --- a/web/app/components/app-sidebar/dataset-info/dropdown.tsx +++ b/web/app/components/app-sidebar/dataset-info/dropdown.tsx @@ -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('') + 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 ( - + + {showRenameModal && ( + setShowRenameModal(false)} + onSuccess={refreshDataset} + /> + )} + {showConfirmDelete && ( + setShowConfirmDelete(false)} + /> + )} ) } diff --git a/web/app/components/app-sidebar/dataset-info/menu-item.tsx b/web/app/components/app-sidebar/dataset-info/menu-item.tsx index d81d78ee14..e4cac0030c 100644 --- a/web/app/components/app-sidebar/dataset-info/menu-item.tsx +++ b/web/app/components/app-sidebar/dataset-info/menu-item.tsx @@ -4,7 +4,7 @@ import type { RemixiconComponentType } from '@remixicon/react' type MenuItemProps = { name: string Icon: RemixiconComponentType - handleClick?: () => void + handleClick?: (e: React.MouseEvent) => void } const MenuItem = ({ @@ -15,9 +15,7 @@ const MenuItem = ({ return (
{ - handleClick?.() - }} + onClick={handleClick} > {name} diff --git a/web/app/components/app-sidebar/dataset-info/menu.tsx b/web/app/components/app-sidebar/dataset-info/menu.tsx index f075ca5887..b016763227 100644 --- a/web/app/components/app-sidebar/dataset-info/menu.tsx +++ b/web/app/components/app-sidebar/dataset-info/menu.tsx @@ -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) => { + e.stopPropagation() + e.preventDefault() + openRenameModal() + } + + const onClickExport = async (e: React.MouseEvent) => { + e.stopPropagation() + e.preventDefault() + handleExportPipeline() + } + + const onClickDelete = async (e: React.MouseEvent) => { + e.stopPropagation() + e.preventDefault() + detectIsUsedByApp() + } return (
- + +
+ {showDelete && ( + <> + +
+ +
+ + )}
) } diff --git a/web/app/components/datasets/list/dataset-card/index.tsx b/web/app/components/datasets/list/dataset-card/index.tsx index 8af05e7aa4..5ebf008ecd 100644 --- a/web/app/components/datasets/list/dataset-card/index.tsx +++ b/web/app/components/datasets/list/dataset-card/index.tsx @@ -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={ { - setShowRenameModal(true) - }} - detectIsUsedByApp={detectIsUsedByApp} + openRenameModal={openRenameModal} handleExportPipeline={handleExportPipeline} + detectIsUsedByApp={detectIsUsedByApp} /> } className={'z-20 min-w-[186px]'} diff --git a/web/app/components/header/dataset-nav/index.tsx b/web/app/components/header/dataset-nav/index.tsx index 09cc059848..6165128e82 100644 --- a/web/app/components/header/dataset-nav/index.tsx +++ b/web/app/components/header/dataset-nav/index.tsx @@ -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} 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`)} diff --git a/web/app/components/header/nav/index.tsx b/web/app/components/header/nav/index.tsx index 6500445fdf..8c09a724de 100644 --- a/web/app/components/header/nav/index.tsx +++ b/web/app/components/header/nav/index.tsx @@ -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 = ({
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)} > diff --git a/web/app/components/header/nav/nav-selector/index.tsx b/web/app/components/header/nav/nav-selector/index.tsx index 1093bc4ae1..683469407a 100644 --- a/web/app/components/header/nav/nav-selector/index.tsx +++ b/web/app/components/header/nav/nav-selector/index.tsx @@ -84,7 +84,13 @@ const NavSelector = ({ curNav, navigationItems, createText, isApp, onCreate, onL router.push(nav.link) }} title={nav.name}>
- + {!!nav.mode && ( { 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(`/datasets/${datasetId}`), enabled: !!datasetId, })