fix(dataset): fix dataset list overlay issue (#35244)

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: yyh <yuanyouhuilyz@gmail.com>
This commit is contained in:
Wu Tianwei 2026-04-15 16:03:02 +08:00 committed by GitHub
parent 79332c0e5e
commit 5542329554
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 198 additions and 335 deletions

View File

@ -7,8 +7,6 @@ import { ChunkingMode, DatasetPermission, DataSourceType } from '@/models/datase
import DatasetCardFooter from '../components/dataset-card-footer'
import Description from '../components/description'
import DatasetCard from '../index'
import OperationItem from '../operation-item'
import Operations from '../operations'
// Mock external hooks only
vi.mock('@/hooks/use-format-time-from-now', () => ({
@ -62,8 +60,8 @@ vi.mock('../components/tag-area', () => ({
<div ref={ref} data-testid="tag-area" onClick={onClick} />
)),
}))
vi.mock('../components/operations-popover', () => ({
default: () => <div data-testid="operations-popover" />,
vi.mock('../components/operations-dropdown', () => ({
default: () => <div data-testid="operations-dropdown" />,
}))
// Factory function for DataSet mock data
@ -233,152 +231,6 @@ describe('DatasetCard Integration', () => {
})
})
})
// Integration tests for OperationItem component
describe('OperationItem', () => {
const MockIcon = ({ className }: { className?: string }) => (
<svg data-testid="mock-icon" className={className} />
)
describe('Rendering', () => {
it('should render icon and name', () => {
render(<OperationItem Icon={MockIcon as never} name="Edit" />)
expect(screen.getByText('Edit')).toBeInTheDocument()
expect(screen.getByTestId('mock-icon')).toBeInTheDocument()
})
})
describe('User Interactions', () => {
it('should call handleClick when clicked', () => {
const handleClick = vi.fn()
render(<OperationItem Icon={MockIcon as never} name="Delete" handleClick={handleClick} />)
const item = screen.getByText('Delete').closest('div')
fireEvent.click(item!)
expect(handleClick).toHaveBeenCalledTimes(1)
})
it('should prevent default and stop propagation on click', () => {
const handleClick = vi.fn()
render(<OperationItem Icon={MockIcon as never} name="Action" handleClick={handleClick} />)
const item = screen.getByText('Action').closest('div')
const event = new MouseEvent('click', { bubbles: true, cancelable: true })
const preventDefaultSpy = vi.spyOn(event, 'preventDefault')
const stopPropagationSpy = vi.spyOn(event, 'stopPropagation')
item!.dispatchEvent(event)
expect(preventDefaultSpy).toHaveBeenCalled()
expect(stopPropagationSpy).toHaveBeenCalled()
})
})
describe('Edge Cases', () => {
it('should not throw when handleClick is undefined', () => {
render(<OperationItem Icon={MockIcon as never} name="No handler" />)
const item = screen.getByText('No handler').closest('div')
expect(() => {
fireEvent.click(item!)
}).not.toThrow()
})
it('should handle empty name', () => {
render(<OperationItem Icon={MockIcon as never} name="" />)
expect(screen.getByTestId('mock-icon')).toBeInTheDocument()
})
})
})
// Integration tests for Operations component
describe('Operations', () => {
const defaultProps = {
showDelete: true,
showExportPipeline: true,
openRenameModal: vi.fn(),
handleExportPipeline: vi.fn(),
detectIsUsedByApp: vi.fn(),
}
describe('Rendering', () => {
it('should always render edit operation', () => {
render(<Operations {...defaultProps} />)
expect(screen.getByText(/operation\.edit/)).toBeInTheDocument()
})
it('should render export pipeline when showExportPipeline is true', () => {
render(<Operations {...defaultProps} showExportPipeline={true} />)
expect(screen.getByText(/exportPipeline/)).toBeInTheDocument()
})
it('should not render export pipeline when showExportPipeline is false', () => {
render(<Operations {...defaultProps} showExportPipeline={false} />)
expect(screen.queryByText(/exportPipeline/)).not.toBeInTheDocument()
})
it('should render delete when showDelete is true', () => {
render(<Operations {...defaultProps} showDelete={true} />)
expect(screen.getByText(/operation\.delete/)).toBeInTheDocument()
})
it('should not render delete when showDelete is false', () => {
render(<Operations {...defaultProps} showDelete={false} />)
expect(screen.queryByText(/operation\.delete/)).not.toBeInTheDocument()
})
})
describe('User Interactions', () => {
it('should call openRenameModal when edit is clicked', () => {
const openRenameModal = vi.fn()
render(<Operations {...defaultProps} openRenameModal={openRenameModal} />)
const editItem = screen.getByText(/operation\.edit/).closest('div')
fireEvent.click(editItem!)
expect(openRenameModal).toHaveBeenCalledTimes(1)
})
it('should call handleExportPipeline when export is clicked', () => {
const handleExportPipeline = vi.fn()
render(<Operations {...defaultProps} handleExportPipeline={handleExportPipeline} />)
const exportItem = screen.getByText(/exportPipeline/).closest('div')
fireEvent.click(exportItem!)
expect(handleExportPipeline).toHaveBeenCalledTimes(1)
})
it('should call detectIsUsedByApp when delete is clicked', () => {
const detectIsUsedByApp = vi.fn()
render(<Operations {...defaultProps} detectIsUsedByApp={detectIsUsedByApp} />)
const deleteItem = screen.getByText(/operation\.delete/).closest('div')
fireEvent.click(deleteItem!)
expect(detectIsUsedByApp).toHaveBeenCalledTimes(1)
})
})
describe('Edge Cases', () => {
it('should render only edit when both showDelete and showExportPipeline are false', () => {
render(<Operations {...defaultProps} showDelete={false} showExportPipeline={false} />)
expect(screen.getByText(/operation\.edit/)).toBeInTheDocument()
expect(screen.queryByText(/exportPipeline/)).not.toBeInTheDocument()
expect(screen.queryByText(/operation\.delete/)).not.toBeInTheDocument()
})
it('should render divider before delete section when showDelete is true', () => {
const { container } = render(<Operations {...defaultProps} showDelete={true} />)
expect(container.querySelector('.bg-divider-subtle')).toBeInTheDocument()
})
it('should not render divider when showDelete is false', () => {
const { container } = render(<Operations {...defaultProps} showDelete={false} />)
expect(container.querySelector('.bg-divider-subtle')).toBeNull()
})
})
})
})
describe('DatasetCard Component', () => {

View File

@ -1,7 +1,16 @@
import { fireEvent, render, screen } from '@testing-library/react'
import { describe, expect, it, vi } from 'vitest'
import { DropdownMenu } from '@/app/components/base/ui/dropdown-menu'
import Operations from '../operations'
function renderInMenu(ui: React.ReactElement) {
return render(
<DropdownMenu open>
{ui}
</DropdownMenu>,
)
}
describe('Operations', () => {
const defaultProps = {
showDelete: true,
@ -17,100 +26,65 @@ describe('Operations', () => {
describe('Rendering', () => {
it('should render without crashing', () => {
render(<Operations {...defaultProps} />)
// Edit operation should always be visible
renderInMenu(<Operations {...defaultProps} />)
expect(screen.getByText(/operation\.edit/)).toBeInTheDocument()
})
it('should render edit operation', () => {
render(<Operations {...defaultProps} />)
renderInMenu(<Operations {...defaultProps} />)
expect(screen.getByText(/operation\.edit/)).toBeInTheDocument()
})
it('should render export pipeline operation when showExportPipeline is true', () => {
render(<Operations {...defaultProps} showExportPipeline={true} />)
renderInMenu(<Operations {...defaultProps} showExportPipeline={true} />)
expect(screen.getByText(/exportPipeline/)).toBeInTheDocument()
})
it('should not render export pipeline operation when showExportPipeline is false', () => {
render(<Operations {...defaultProps} showExportPipeline={false} />)
renderInMenu(<Operations {...defaultProps} showExportPipeline={false} />)
expect(screen.queryByText(/exportPipeline/)).not.toBeInTheDocument()
})
it('should render delete operation when showDelete is true', () => {
render(<Operations {...defaultProps} showDelete={true} />)
renderInMenu(<Operations {...defaultProps} showDelete={true} />)
expect(screen.getByText(/operation\.delete/)).toBeInTheDocument()
})
it('should not render delete operation when showDelete is false', () => {
render(<Operations {...defaultProps} showDelete={false} />)
renderInMenu(<Operations {...defaultProps} showDelete={false} />)
expect(screen.queryByText(/operation\.delete/)).not.toBeInTheDocument()
})
})
describe('Props', () => {
it('should render divider when showDelete is true', () => {
const { container } = render(<Operations {...defaultProps} showDelete={true} />)
const divider = container.querySelector('.bg-divider-subtle')
expect(divider).toBeInTheDocument()
})
it('should not render divider when showDelete is false', () => {
const { container } = render(<Operations {...defaultProps} showDelete={false} />)
// Should not have the divider-subtle one (the separator before delete)
expect(container.querySelector('.bg-divider-subtle')).toBeNull()
})
})
describe('User Interactions', () => {
it('should call openRenameModal when edit is clicked', () => {
const openRenameModal = vi.fn()
render(<Operations {...defaultProps} openRenameModal={openRenameModal} />)
const editItem = screen.getByText(/operation\.edit/).closest('div')
fireEvent.click(editItem!)
renderInMenu(<Operations {...defaultProps} openRenameModal={openRenameModal} />)
fireEvent.click(screen.getByText(/operation\.edit/))
expect(openRenameModal).toHaveBeenCalledTimes(1)
})
it('should call handleExportPipeline when export is clicked', () => {
const handleExportPipeline = vi.fn()
render(<Operations {...defaultProps} handleExportPipeline={handleExportPipeline} />)
const exportItem = screen.getByText(/exportPipeline/).closest('div')
fireEvent.click(exportItem!)
renderInMenu(<Operations {...defaultProps} handleExportPipeline={handleExportPipeline} />)
fireEvent.click(screen.getByText(/exportPipeline/))
expect(handleExportPipeline).toHaveBeenCalledTimes(1)
})
it('should call detectIsUsedByApp when delete is clicked', () => {
const detectIsUsedByApp = vi.fn()
render(<Operations {...defaultProps} detectIsUsedByApp={detectIsUsedByApp} />)
const deleteItem = screen.getByText(/operation\.delete/).closest('div')
fireEvent.click(deleteItem!)
renderInMenu(<Operations {...defaultProps} detectIsUsedByApp={detectIsUsedByApp} />)
fireEvent.click(screen.getByText(/operation\.delete/))
expect(detectIsUsedByApp).toHaveBeenCalledTimes(1)
})
})
describe('Styles', () => {
it('should have correct container styling', () => {
const { container } = render(<Operations {...defaultProps} />)
const operationsContainer = container.firstChild
expect(operationsContainer).toHaveClass(
'relative',
'flex',
'w-full',
'flex-col',
'rounded-xl',
)
})
})
describe('Edge Cases', () => {
it('should render only edit when both showDelete and showExportPipeline are false', () => {
render(<Operations {...defaultProps} showDelete={false} showExportPipeline={false} />)
renderInMenu(<Operations {...defaultProps} showDelete={false} showExportPipeline={false} />)
expect(screen.getByText(/operation\.edit/)).toBeInTheDocument()
expect(screen.queryByText(/exportPipeline/)).not.toBeInTheDocument()
expect(screen.queryByText(/operation\.delete/)).not.toBeInTheDocument()

View File

@ -80,7 +80,7 @@ describe('CornerLabels', () => {
const dataset = createMockDataset({ embedding_available: false })
const { container } = render(<CornerLabels dataset={dataset} />)
const labelContainer = container.firstChild as HTMLElement
expect(labelContainer).toHaveClass('absolute', 'right-0', 'top-0', 'z-10')
expect(labelContainer).toHaveClass('absolute', 'right-0', 'top-0', 'z-5')
})
it('should have correct positioning for pipeline label', () => {
@ -90,7 +90,7 @@ describe('CornerLabels', () => {
})
const { container } = render(<CornerLabels dataset={dataset} />)
const labelContainer = container.firstChild as HTMLElement
expect(labelContainer).toHaveClass('absolute', 'right-0', 'top-0', 'z-10')
expect(labelContainer).toHaveClass('absolute', 'right-0', 'top-0', 'z-5')
})
})

View File

@ -1,11 +1,11 @@
import type { DataSet } from '@/models/datasets'
import { fireEvent, render } from '@testing-library/react'
import { fireEvent, render, screen } from '@testing-library/react'
import { describe, expect, it, vi } from 'vitest'
import { IndexingType } from '@/app/components/datasets/create/step-two'
import { ChunkingMode, DatasetPermission, DataSourceType } from '@/models/datasets'
import OperationsPopover from '../operations-popover'
import OperationsDropdown from '../operations-dropdown'
describe('OperationsPopover', () => {
describe('OperationsDropdown', () => {
const createMockDataset = (overrides: Partial<DataSet> = {}): DataSet => ({
id: 'dataset-1',
name: 'Test Dataset',
@ -42,102 +42,143 @@ describe('OperationsPopover', () => {
describe('Rendering', () => {
it('should render without crashing', () => {
const { container } = render(<OperationsPopover {...defaultProps} />)
const { container } = render(<OperationsDropdown {...defaultProps} />)
expect(container.firstChild).toBeInTheDocument()
})
it('should render the more icon button', () => {
const { container } = render(<OperationsPopover {...defaultProps} />)
const moreIcon = container.querySelector('svg')
const { container } = render(<OperationsDropdown {...defaultProps} />)
const moreIcon = container.querySelector('.i-ri-more-fill')
expect(moreIcon).toBeInTheDocument()
})
it('should render in hidden state initially (group-hover)', () => {
const { container } = render(<OperationsPopover {...defaultProps} />)
const { container } = render(<OperationsDropdown {...defaultProps} />)
const wrapper = container.firstChild as HTMLElement
expect(wrapper).toHaveClass('hidden', 'group-hover:block')
expect(wrapper).toHaveClass(
'invisible',
'pointer-events-none',
'group-hover:visible',
'group-hover:pointer-events-auto',
)
})
})
describe('Props', () => {
it('should show delete option when not workspace dataset operator', () => {
render(<OperationsPopover {...defaultProps} isCurrentWorkspaceDatasetOperator={false} />)
render(<OperationsDropdown {...defaultProps} isCurrentWorkspaceDatasetOperator={false} />)
const triggerButton = document.querySelector('[class*="cursor-pointer"]')
if (triggerButton)
fireEvent.click(triggerButton)
// showDelete should be true (inverse of isCurrentWorkspaceDatasetOperator)
// This means delete operation will be visible
})
it('should hide delete option when is workspace dataset operator', () => {
render(<OperationsPopover {...defaultProps} isCurrentWorkspaceDatasetOperator={true} />)
render(<OperationsDropdown {...defaultProps} isCurrentWorkspaceDatasetOperator={true} />)
const triggerButton = document.querySelector('[class*="cursor-pointer"]')
if (triggerButton)
fireEvent.click(triggerButton)
// showDelete should be false
})
it('should show export pipeline when runtime_mode is rag_pipeline', () => {
const dataset = createMockDataset({ runtime_mode: 'rag_pipeline' })
render(<OperationsPopover {...defaultProps} dataset={dataset} />)
render(<OperationsDropdown {...defaultProps} dataset={dataset} />)
const triggerButton = document.querySelector('[class*="cursor-pointer"]')
if (triggerButton)
fireEvent.click(triggerButton)
// showExportPipeline should be true
})
it('should hide export pipeline when runtime_mode is not rag_pipeline', () => {
const dataset = createMockDataset({ runtime_mode: 'general' })
render(<OperationsPopover {...defaultProps} dataset={dataset} />)
render(<OperationsDropdown {...defaultProps} dataset={dataset} />)
const triggerButton = document.querySelector('[class*="cursor-pointer"]')
if (triggerButton)
fireEvent.click(triggerButton)
// showExportPipeline should be false
})
})
describe('Styles', () => {
it('should have correct positioning styles', () => {
const { container } = render(<OperationsPopover {...defaultProps} />)
const { container } = render(<OperationsDropdown {...defaultProps} />)
const wrapper = container.firstChild as HTMLElement
expect(wrapper).toHaveClass('absolute', 'right-2', 'top-2', 'z-15')
expect(wrapper).toHaveClass('absolute', 'right-2', 'top-2', 'z-5')
})
it('should keep the trigger mounted when closed so menu exit animations retain an anchor', () => {
const { container } = render(<OperationsDropdown {...defaultProps} />)
const wrapper = container.firstChild as HTMLElement
const trigger = container.querySelector('[aria-label="Dataset operations"]')
expect(wrapper).not.toHaveClass('hidden')
expect(trigger).toBeInTheDocument()
})
it('should have icon with correct size classes', () => {
const { container } = render(<OperationsPopover {...defaultProps} />)
const icon = container.querySelector('svg')
const { container } = render(<OperationsDropdown {...defaultProps} />)
const icon = container.querySelector('.i-ri-more-fill')
expect(icon).toHaveClass('h-5', 'w-5', 'text-text-tertiary')
})
it('should have aria-label on trigger for accessibility', () => {
const { container } = render(<OperationsDropdown {...defaultProps} />)
const trigger = container.querySelector('[aria-label="Dataset operations"]')
expect(trigger).toBeInTheDocument()
})
it('should expose visible keyboard focus styles on the trigger', () => {
const { container } = render(<OperationsDropdown {...defaultProps} />)
const trigger = container.querySelector('[aria-label="Dataset operations"]')
expect(trigger).toHaveClass(
'focus-visible:outline-hidden',
'focus-visible:ring-1',
'focus-visible:ring-inset',
'focus-visible:ring-components-input-border-hover',
)
})
it('should use a solid trigger background without backdrop blur on hover states', () => {
const { container } = render(<OperationsDropdown {...defaultProps} />)
const trigger = container.querySelector('[aria-label="Dataset operations"]')
expect(trigger).toHaveClass('bg-components-button-secondary-bg')
expect(trigger).not.toHaveClass('hover:backdrop-blur-[5px]', 'backdrop-blur-[5px]')
})
})
describe('User Interactions', () => {
it('should keep outside interactions available when the menu is open', () => {
const onOutsideClick = vi.fn()
render(
<div>
<button type="button" onClick={onOutsideClick}>Outside action</button>
<OperationsDropdown {...defaultProps} />
</div>,
)
fireEvent.click(screen.getByLabelText('Dataset operations'))
fireEvent.click(screen.getByRole('button', { name: 'Outside action' }))
expect(onOutsideClick).toHaveBeenCalledTimes(1)
})
it('should pass openRenameModal to Operations', () => {
const openRenameModal = vi.fn()
render(<OperationsPopover {...defaultProps} openRenameModal={openRenameModal} />)
// The openRenameModal should be passed to Operations component
expect(openRenameModal).not.toHaveBeenCalled() // Initially not called
render(<OperationsDropdown {...defaultProps} openRenameModal={openRenameModal} />)
expect(openRenameModal).not.toHaveBeenCalled()
})
it('should pass handleExportPipeline to Operations', () => {
const handleExportPipeline = vi.fn()
render(<OperationsPopover {...defaultProps} handleExportPipeline={handleExportPipeline} />)
render(<OperationsDropdown {...defaultProps} handleExportPipeline={handleExportPipeline} />)
expect(handleExportPipeline).not.toHaveBeenCalled()
})
it('should pass detectIsUsedByApp to Operations', () => {
const detectIsUsedByApp = vi.fn()
render(<OperationsPopover {...defaultProps} detectIsUsedByApp={detectIsUsedByApp} />)
render(<OperationsDropdown {...defaultProps} detectIsUsedByApp={detectIsUsedByApp} />)
expect(detectIsUsedByApp).not.toHaveBeenCalled()
})
})
@ -145,13 +186,13 @@ describe('OperationsPopover', () => {
describe('Edge Cases', () => {
it('should handle dataset with external provider', () => {
const dataset = createMockDataset({ provider: 'external' })
const { container } = render(<OperationsPopover {...defaultProps} dataset={dataset} />)
const { container } = render(<OperationsDropdown {...defaultProps} dataset={dataset} />)
expect(container.firstChild).toBeInTheDocument()
})
it('should handle dataset with undefined runtime_mode', () => {
const dataset = createMockDataset({ runtime_mode: undefined })
const { container } = render(<OperationsPopover {...defaultProps} dataset={dataset} />)
const { container } = render(<OperationsDropdown {...defaultProps} dataset={dataset} />)
expect(container.firstChild).toBeInTheDocument()
})
})

View File

@ -14,7 +14,7 @@ const CornerLabels = ({ dataset }: CornerLabelsProps) => {
return (
<CornerLabel
label={t('cornerLabel.unavailable', { ns: 'dataset' })}
className="absolute right-0 top-0 z-10"
className="absolute top-0 right-0 z-5"
labelClassName="rounded-tr-xl"
/>
)
@ -24,7 +24,7 @@ const CornerLabels = ({ dataset }: CornerLabelsProps) => {
return (
<CornerLabel
label={t('cornerLabel.pipeline', { ns: 'dataset' })}
className="absolute right-0 top-0 z-10"
className="absolute top-0 right-0 z-5"
labelClassName="rounded-tr-xl"
/>
)

View File

@ -0,0 +1,68 @@
import type { DataSet } from '@/models/datasets'
import * as React from 'react'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuTrigger,
} from '@/app/components/base/ui/dropdown-menu'
import { cn } from '@/utils/classnames'
import Operations from '../operations'
type OperationsDropdownProps = {
dataset: DataSet
isCurrentWorkspaceDatasetOperator: boolean
openRenameModal: () => void
handleExportPipeline: (include?: boolean) => void
detectIsUsedByApp: () => void
}
const OperationsDropdown = ({
dataset,
isCurrentWorkspaceDatasetOperator,
openRenameModal,
handleExportPipeline,
detectIsUsedByApp,
}: OperationsDropdownProps) => {
const [open, setOpen] = React.useState(false)
return (
<div
className={cn(
'absolute top-2 right-2 z-5',
open
? 'pointer-events-auto visible'
: 'pointer-events-none invisible group-hover:pointer-events-auto group-hover:visible',
)}
onClick={e => e.stopPropagation()}
>
<DropdownMenu modal={false} open={open} onOpenChange={setOpen}>
<DropdownMenuTrigger
className={cn(
'inline-flex size-9 cursor-pointer items-center justify-center radius-lg border-[0.5px]',
'border-components-actionbar-border bg-components-button-secondary-bg p-0 shadow-lg ring-2 shadow-shadow-shadow-5 ring-components-button-secondary-bg ring-inset',
'transition-colors hover:border-components-actionbar-border hover:bg-state-base-hover',
'focus-visible:bg-state-base-hover focus-visible:ring-1 focus-visible:ring-components-input-border-hover focus-visible:outline-hidden focus-visible:ring-inset',
open && 'bg-state-base-hover',
)}
aria-label="Dataset operations"
>
<span className="i-ri-more-fill h-5 w-5 text-text-tertiary" />
</DropdownMenuTrigger>
<DropdownMenuContent
placement="bottom-end"
popupClassName="min-w-[186px]"
>
<Operations
showDelete={!isCurrentWorkspaceDatasetOperator}
showExportPipeline={dataset.runtime_mode === 'rag_pipeline'}
openRenameModal={openRenameModal}
handleExportPipeline={handleExportPipeline}
detectIsUsedByApp={detectIsUsedByApp}
/>
</DropdownMenuContent>
</DropdownMenu>
</div>
)
}
export default React.memo(OperationsDropdown)

View File

@ -1,52 +0,0 @@
import type { DataSet } from '@/models/datasets'
import { RiMoreFill } from '@remixicon/react'
import * as React from 'react'
import CustomPopover from '@/app/components/base/popover'
import { cn } from '@/utils/classnames'
import Operations from '../operations'
type OperationsPopoverProps = {
dataset: DataSet
isCurrentWorkspaceDatasetOperator: boolean
openRenameModal: () => void
handleExportPipeline: (include?: boolean) => void
detectIsUsedByApp: () => void
}
const OperationsPopover = ({
dataset,
isCurrentWorkspaceDatasetOperator,
openRenameModal,
handleExportPipeline,
detectIsUsedByApp,
}: OperationsPopoverProps) => (
<div className="absolute right-2 top-2 z-15 hidden group-hover:block">
<CustomPopover
htmlContent={(
<Operations
showDelete={!isCurrentWorkspaceDatasetOperator}
showExportPipeline={dataset.runtime_mode === 'rag_pipeline'}
openRenameModal={openRenameModal}
handleExportPipeline={handleExportPipeline}
detectIsUsedByApp={detectIsUsedByApp}
/>
)}
className="z-20 min-w-[186px]"
popupClassName="rounded-xl bg-none shadow-none ring-0 min-w-[186px]"
position="br"
trigger="click"
btnElement={(
<div className="flex size-8 items-center justify-center radius-lg hover:bg-state-base-hover">
<RiMoreFill className="h-5 w-5 text-text-tertiary" />
</div>
)}
btnClassName={open =>
cn(
'size-9 cursor-pointer justify-center radius-lg border-[0.5px] border-components-actionbar-border bg-components-actionbar-bg p-0 shadow-lg shadow-shadow-shadow-5 ring-2 ring-inset ring-components-actionbar-bg hover:border-components-actionbar-border',
open ? 'border-components-actionbar-border bg-state-base-hover' : '',
)}
/>
</div>
)
export default React.memo(OperationsPopover)

View File

@ -9,7 +9,7 @@ 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 OperationsPopover from './components/operations-popover'
import OperationsDropdown from './components/operations-dropdown'
import TagArea from './components/tag-area'
import { useDatasetCardState } from './hooks/use-dataset-card-state'
@ -82,7 +82,7 @@ const DatasetCard = ({
onClick={handleTagAreaClick}
/>
<DatasetCardFooter dataset={dataset} />
<OperationsPopover
<OperationsDropdown
dataset={dataset}
isCurrentWorkspaceDatasetOperator={isCurrentWorkspaceDatasetOperator}
openRenameModal={openRenameModal}

View File

@ -1,8 +1,9 @@
import { RiDeleteBinLine, RiEditLine, RiFileDownloadLine } from '@remixicon/react'
import * as React from 'react'
import { useTranslation } from 'react-i18next'
import Divider from '@/app/components/base/divider'
import OperationItem from './operation-item'
import {
DropdownMenuItem,
DropdownMenuSeparator,
} from '@/app/components/base/ui/dropdown-menu'
type OperationsProps = {
showDelete: boolean
@ -22,34 +23,27 @@ const Operations = ({
const { t } = useTranslation()
return (
<div className="relative flex w-full flex-col rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadow-lg shadow-shadow-shadow-5">
<div className="flex flex-col p-1">
<OperationItem
Icon={RiEditLine}
name={t('operation.edit', { ns: 'common' })}
handleClick={openRenameModal}
/>
{showExportPipeline && (
<OperationItem
Icon={RiFileDownloadLine}
name={t('operations.exportPipeline', { ns: 'datasetPipeline' })}
handleClick={handleExportPipeline}
/>
)}
</div>
<>
<DropdownMenuItem onClick={openRenameModal}>
<span aria-hidden className="i-ri-edit-line size-4 text-text-tertiary" />
{t('operation.edit', { ns: 'common' })}
</DropdownMenuItem>
{showExportPipeline && (
<DropdownMenuItem onClick={handleExportPipeline}>
<span aria-hidden className="i-ri-file-download-line size-4 text-text-tertiary" />
{t('operations.exportPipeline', { ns: 'datasetPipeline' })}
</DropdownMenuItem>
)}
{showDelete && (
<>
<Divider type="horizontal" className="my-0 bg-divider-subtle" />
<div className="flex flex-col p-1">
<OperationItem
Icon={RiDeleteBinLine}
name={t('operation.delete', { ns: 'common' })}
handleClick={detectIsUsedByApp}
/>
</div>
<DropdownMenuSeparator />
<DropdownMenuItem destructive onClick={detectIsUsedByApp}>
<span aria-hidden className="i-ri-delete-bin-line size-4" />
{t('operation.delete', { ns: 'common' })}
</DropdownMenuItem>
</>
)}
</div>
</>
)
}

View File

@ -5,7 +5,6 @@ import { useBoolean, useDebounceFn } from 'ahooks'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { ApiConnectionMod } from '@/app/components/base/icons/src/vender/solid/development'
import Input from '@/app/components/base/input'
import TagManagementModal from '@/app/components/base/tag-management'
import TagFilter from '@/app/components/base/tag-management/filter'
@ -85,11 +84,11 @@ const List = () => {
}
<div className="h-4 w-px bg-divider-regular" />
<Button
className="shadows-shadow-xs gap-0.5"
className="gap-0.5 shadow-xs"
onClick={() => setShowExternalApiPanel(true)}
>
<ApiConnectionMod className="h-4 w-4 text-components-button-secondary-text" />
<div className="flex items-center justify-center gap-1 px-0.5 system-sm-medium text-components-button-secondary-text">{t('externalAPIPanelTitle', { ns: 'dataset' })}</div>
<span className="i-custom-vender-solid-development-api-connection-mod h-4 w-4 text-components-button-secondary-text" />
<span className="flex items-center justify-center gap-1 px-0.5 system-sm-medium text-components-button-secondary-text">{t('externalAPIPanelTitle', { ns: 'dataset' })}</span>
</Button>
</div>
</div>

View File

@ -4503,11 +4503,6 @@
"count": 3
}
},
"app/components/datasets/list/dataset-card/components/corner-labels.tsx": {
"tailwindcss/enforce-consistent-class-order": {
"count": 2
}
},
"app/components/datasets/list/dataset-card/components/dataset-card-footer.tsx": {
"no-restricted-imports": {
"count": 1
@ -4526,14 +4521,6 @@
"count": 1
}
},
"app/components/datasets/list/dataset-card/components/operations-popover.tsx": {
"no-restricted-imports": {
"count": 1
},
"tailwindcss/enforce-consistent-class-order": {
"count": 2
}
},
"app/components/datasets/list/dataset-card/components/tag-area.tsx": {
"tailwindcss/enforce-consistent-class-order": {
"count": 1