dify/web/app/components/datasets/list/dataset-card/index.tsx
Jingyi cb64446fa3
feat(web): add step-by-step tour shell (#38785)
Co-authored-by: hjlarry <hjlarry@163.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: yyh <yuanyouhuilyz@gmail.com>
Co-authored-by: yyh <92089059+lyzno1@users.noreply.github.com>
2026-07-20 09:23:25 +00:00

174 lines
6.1 KiB
TypeScript

'use client'
import type { KeyboardEvent, MouseEvent } from 'react'
import type { DataSet } from '@/models/datasets'
import { cn } from '@langgenius/dify-ui/cn'
import { toast } from '@langgenius/dify-ui/toast'
import { useAtomValue } from 'jotai'
import { useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import { userProfileIdAtom } from '@/context/account-state'
import { workspacePermissionKeysAtom } from '@/context/permission-state'
import { DatasetCardTags } from '@/features/tag-management/components/dataset-card-tags'
import { useRouter } from '@/next/navigation'
import {
getDatasetACLCapabilities,
hasOnlyDatasetPreviewPermission,
hasPermission,
} from '@/utils/permission'
import CornerLabels from './components/corner-labels'
import DatasetCardFooter from './components/dataset-card-footer'
import DatasetCardHeader from './components/dataset-card-header'
import DatasetCardModals from './components/dataset-card-modals'
import Description from './components/description'
import OperationsDropdown from './components/operations-dropdown'
import { useDatasetCardState as useDatasetCardController } from './hooks/use-dataset-card-state'
const EXTERNAL_PROVIDER = 'external'
type DatasetCardProps = {
dataset: DataSet
onSuccess?: () => void
onOpenTagManagement?: () => void
stepByStepTourActionMenuHighlightPart?: string
stepByStepTourActionMenuOpen?: boolean
stepByStepTourCardTarget?: string
}
const DatasetCard = ({
dataset,
onSuccess,
onOpenTagManagement = () => {},
stepByStepTourActionMenuHighlightPart,
stepByStepTourActionMenuOpen,
stepByStepTourCardTarget,
}: DatasetCardProps) => {
const { t } = useTranslation()
const { push } = useRouter()
const currentUserId = useAtomValue(userProfileIdAtom)
const workspacePermissionKeys = useAtomValue(workspacePermissionKeysAtom)
const datasetCard = useDatasetCardController({ dataset, onSuccess })
const {
modalState,
openRenameModal,
closeRenameModal,
closeConfirmDelete,
openAccessConfig,
closeAccessConfig,
handleExportPipeline,
detectIsUsedByApp,
onConfirmDelete,
} = datasetCard
const isExternalProvider = dataset.provider === EXTERNAL_PROVIDER
const isPipelineUnpublished = useMemo(() => {
return dataset.runtime_mode === 'rag_pipeline' && !dataset.is_published
}, [dataset.runtime_mode, dataset.is_published])
const isPreviewOnly = hasOnlyDatasetPreviewPermission(dataset.permission_keys)
const datasetACLCapabilities = useMemo(
() =>
getDatasetACLCapabilities(dataset.permission_keys, {
currentUserId,
resourceMaintainer: dataset.maintainer,
workspacePermissionKeys,
}),
[dataset.maintainer, dataset.permission_keys, currentUserId, workspacePermissionKeys],
)
const canManageAppTags = hasPermission(workspacePermissionKeys, 'dataset.tag.manage')
const canBindOrUnbindTags = !isPreviewOnly && (canManageAppTags || datasetACLCapabilities.canEdit)
const showPreviewOnlyAccessWarning = () => {
toast.warning(t(($) => $.noAccessResourcePermission, { ns: 'app' }))
}
const handleCardClick = (e: MouseEvent) => {
e.preventDefault()
if (isPreviewOnly) {
showPreviewOnlyAccessWarning()
return
}
if (isExternalProvider) {
push(
datasetACLCapabilities.canRetrievalRecall
? `/datasets/${dataset.id}/hitTesting`
: `/datasets/${dataset.id}/settings`,
)
} else if (isPipelineUnpublished) {
push(`/datasets/${dataset.id}/pipeline`)
} else {
push(`/datasets/${dataset.id}/documents`)
}
}
const handlePreviewOnlyCardKeyDown = (e: KeyboardEvent<HTMLElement>) => {
if (!isPreviewOnly || (e.key !== 'Enter' && e.key !== ' ')) return
e.preventDefault()
showPreviewOnlyAccessWarning()
}
const handleTagAreaClick = (e: MouseEvent) => {
e.stopPropagation()
e.preventDefault()
}
const cardClassName = cn(
'group relative col-span-1 flex h-41.5 flex-col overflow-hidden rounded-xl border-[0.5px] border-solid border-components-card-border bg-components-card-bg shadow-xs shadow-shadow-shadow-3 transition-[background-color,box-shadow] duration-200 ease-in-out',
isPreviewOnly
? 'cursor-not-allowed opacity-60 focus-visible:ring-2 focus-visible:ring-state-accent-solid focus-visible:outline-hidden'
: 'cursor-pointer hover:bg-components-card-bg-alt hover:shadow-md hover:shadow-shadow-shadow-5',
)
return (
<>
<div
role={isPreviewOnly ? 'button' : undefined}
tabIndex={isPreviewOnly ? 0 : undefined}
aria-disabled={isPreviewOnly ? 'true' : undefined}
aria-label={isPreviewOnly ? dataset.name : undefined}
className={cardClassName}
data-disable-nprogress={true}
data-step-by-step-tour-target={stepByStepTourCardTarget}
onClick={handleCardClick}
onKeyDown={handlePreviewOnlyCardKeyDown}
>
<CornerLabels dataset={dataset} />
<DatasetCardHeader dataset={dataset} />
<Description dataset={dataset} />
<DatasetCardTags
datasetId={dataset.id}
embeddingAvailable={dataset.embedding_available}
tags={dataset.tags}
onClick={handleTagAreaClick}
onOpenTagManagement={onOpenTagManagement}
onTagsChange={onSuccess}
canBindOrUnbindTags={canBindOrUnbindTags}
/>
<DatasetCardFooter dataset={dataset} />
{!isPreviewOnly && (
<OperationsDropdown
dataset={dataset}
openRenameModal={openRenameModal}
handleExportPipeline={handleExportPipeline}
detectIsUsedByApp={detectIsUsedByApp}
openAccessConfig={openAccessConfig}
stepByStepTourHighlightPart={stepByStepTourActionMenuHighlightPart}
stepByStepTourOpen={stepByStepTourActionMenuOpen}
/>
)}
</div>
<DatasetCardModals
dataset={dataset}
modalState={modalState}
onCloseRename={closeRenameModal}
onCloseConfirm={closeConfirmDelete}
onCloseAccessConfig={closeAccessConfig}
onConfirmDelete={onConfirmDelete}
onSuccess={onSuccess}
/>
</>
)
}
export default DatasetCard