feat: refactor dataset permissions handling and remove legacy workspace role checks

This commit is contained in:
twwu
2026-05-25 13:01:03 +08:00
parent 18cc5b90d4
commit dad977db55
8 changed files with 75 additions and 55 deletions

View File

@ -4,6 +4,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
import DatasetInfo from '@/app/components/app-sidebar/dataset-info'
import { ChunkingMode, DatasetPermission, DataSourceType } from '@/models/datasets'
import { RETRIEVE_METHOD } from '@/types/app'
import { DatasetACLPermission } from '@/utils/permission'
const mockReplace = vi.fn()
const mockInvalidDatasetList = vi.fn()
@ -33,11 +34,6 @@ vi.mock('@/context/dataset-detail', () => ({
}),
}))
vi.mock('@/context/app-context', () => ({
useSelector: (selector: (state: { isCurrentWorkspaceDatasetOperator: boolean }) => unknown) =>
selector({ isCurrentWorkspaceDatasetOperator: false }),
}))
vi.mock('@/hooks/use-knowledge', () => ({
useKnowledge: () => ({
formatIndexingTechniqueAndMethod: () => 'indexing-technique',
@ -153,6 +149,11 @@ const createDataset = (overrides: Partial<DataSet> = {}): DataSet => ({
enable_api: false,
is_multimodal: false,
is_published: true,
permission_keys: [
DatasetACLPermission.Edit,
DatasetACLPermission.Delete,
DatasetACLPermission.ImportExportDSL,
],
...overrides,
})

View File

@ -8,10 +8,10 @@ import {
DataSourceType,
} from '@/models/datasets'
import { RETRIEVE_METHOD } from '@/types/app'
import { DatasetACLPermission } from '@/utils/permission'
import Dropdown from '../dropdown'
let mockDataset: DataSet
let mockIsDatasetOperator = false
const mockReplace = vi.fn()
const mockInvalidDatasetList = vi.fn()
const mockInvalidDatasetDetail = vi.fn()
@ -78,6 +78,11 @@ const createDataset = (overrides: Partial<DataSet> = {}): DataSet => ({
runtime_mode: 'rag_pipeline',
enable_api: false,
is_multimodal: false,
permission_keys: [
DatasetACLPermission.Edit,
DatasetACLPermission.Delete,
DatasetACLPermission.ImportExportDSL,
],
...overrides,
})
@ -89,11 +94,6 @@ vi.mock('@/context/dataset-detail', () => ({
useDatasetDetailContextWithSelector: (selector: (state: { dataset?: DataSet }) => unknown) => selector({ dataset: mockDataset }),
}))
vi.mock('@/context/app-context', () => ({
useSelector: (selector: (state: { isCurrentWorkspaceDatasetOperator: boolean }) => unknown) =>
selector({ isCurrentWorkspaceDatasetOperator: mockIsDatasetOperator }),
}))
vi.mock('@/service/knowledge/use-dataset', () => ({
datasetDetailQueryKeyPrefix: ['dataset', 'detail'],
useInvalidDatasetList: () => mockInvalidDatasetList,
@ -143,7 +143,6 @@ describe('Dropdown callback coverage', () => {
beforeEach(() => {
vi.clearAllMocks()
mockDataset = createDataset({ pipeline_id: 'pipeline-1', runtime_mode: 'rag_pipeline' })
mockIsDatasetOperator = false
mockExportPipeline.mockResolvedValue({ data: 'pipeline-content' })
mockCheckIsUsedInApp.mockResolvedValue({ is_using: false })
mockDeleteDataset.mockResolvedValue({})

View File

@ -8,13 +8,13 @@ import {
DataSourceType,
} from '@/models/datasets'
import { RETRIEVE_METHOD } from '@/types/app'
import { DatasetACLPermission } from '@/utils/permission'
import DatasetInfo from '..'
import Dropdown from '../dropdown'
import Menu from '../menu'
import MenuItem from '../menu-item'
let mockDataset: DataSet
let mockIsDatasetOperator = false
const mockReplace = vi.fn()
const mockInvalidDatasetList = vi.fn()
const mockInvalidDatasetDetail = vi.fn()
@ -87,6 +87,11 @@ const createDataset = (overrides: Partial<DataSet> = {}): DataSet => ({
runtime_mode: 'rag_pipeline',
enable_api: false,
is_multimodal: false,
permission_keys: [
DatasetACLPermission.Edit,
DatasetACLPermission.Delete,
DatasetACLPermission.ImportExportDSL,
],
...overrides,
})
@ -100,11 +105,6 @@ vi.mock('@/context/dataset-detail', () => ({
useDatasetDetailContextWithSelector: (selector: (state: { dataset?: DataSet }) => unknown) => selector({ dataset: mockDataset }),
}))
vi.mock('@/context/app-context', () => ({
useSelector: (selector: (state: { isCurrentWorkspaceDatasetOperator: boolean }) => unknown) =>
selector({ isCurrentWorkspaceDatasetOperator: mockIsDatasetOperator }),
}))
vi.mock('@/service/knowledge/use-dataset', () => ({
datasetDetailQueryKeyPrefix: ['dataset', 'detail'],
useInvalidDatasetList: () => mockInvalidDatasetList,
@ -161,7 +161,6 @@ describe('DatasetInfo', () => {
beforeEach(() => {
vi.clearAllMocks()
mockDataset = createDataset()
mockIsDatasetOperator = false
})
// Rendering of dataset summary details based on expand and dataset state.
@ -337,7 +336,6 @@ describe('Dropdown', () => {
beforeEach(() => {
vi.clearAllMocks()
mockDataset = createDataset({ pipeline_id: 'pipeline-1', runtime_mode: 'rag_pipeline' })
mockIsDatasetOperator = false
mockExportPipeline.mockResolvedValue({ data: 'pipeline-content' })
mockCheckIsUsedInApp.mockResolvedValue({ is_using: false })
mockDeleteDataset.mockResolvedValue({})
@ -355,12 +353,19 @@ describe('Dropdown', () => {
}
})
// Rendering behavior based on workspace role.
// Rendering behavior based on dataset ACL permission keys.
describe('Rendering', () => {
it('should hide delete option when user is dataset operator', async () => {
it('should hide delete option when dataset lacks delete ACL permission', async () => {
const user = userEvent.setup()
// Arrange
mockIsDatasetOperator = true
mockDataset = createDataset({
pipeline_id: 'pipeline-1',
runtime_mode: 'rag_pipeline',
permission_keys: [
DatasetACLPermission.Edit,
DatasetACLPermission.ImportExportDSL,
],
})
render(<Dropdown expand />)
// Act

View File

@ -18,7 +18,6 @@ import { toast } from '@langgenius/dify-ui/toast'
import * as React from 'react'
import { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useSelector as useAppContextWithSelector } from '@/context/app-context'
import { useDatasetDetailContextWithSelector } from '@/context/dataset-detail'
import { useRouter } from '@/next/navigation'
import { checkIsUsedInApp, deleteDataset } from '@/service/datasets'
@ -64,7 +63,6 @@ const DropDown = ({
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 datasetACLCapabilities = React.useMemo(() => getDatasetACLCapabilities(dataset?.permission_keys), [dataset?.permission_keys])
const canShowOperations = datasetACLCapabilities.canEdit
@ -155,7 +153,7 @@ const DropDown = ({
>
<Menu
showEdit={datasetACLCapabilities.canEdit}
showDelete={!isCurrentWorkspaceDatasetOperator && datasetACLCapabilities.canDelete}
showDelete={datasetACLCapabilities.canDelete}
showExportPipeline={datasetACLCapabilities.canImportExportDSL}
openRenameModal={openRenameModal}
handleExportPipeline={handleExportPipeline}

View File

@ -25,10 +25,6 @@ vi.mock('@/next/navigation', () => ({
useRouter: () => ({ push: mockPush }),
}))
vi.mock('@/context/app-context', () => ({
useSelector: (selector: (state: { isCurrentWorkspaceDatasetOperator: boolean }) => boolean) => selector({ isCurrentWorkspaceDatasetOperator: false }),
}))
vi.mock('../hooks/use-dataset-card-state', () => ({
useDatasetCardState: () => ({
modalState: {
@ -55,6 +51,7 @@ vi.mock('../components/dataset-card-modals', () => ({
default: () => <div data-testid="card-modals" />,
}))
const renderDatasetCardTags = vi.hoisted(() => vi.fn())
const renderOperationsDropdown = vi.hoisted(() => vi.fn())
vi.mock('@/features/tag-management/components/dataset-card-tags', () => ({
DatasetCardTags: (props: { onClick: (e: React.MouseEvent) => void, canBindOrUnbindTags?: boolean }) => {
@ -65,7 +62,10 @@ vi.mock('@/features/tag-management/components/dataset-card-tags', () => ({
},
}))
vi.mock('../components/operations-dropdown', () => ({
default: () => <div data-testid="operations-dropdown" />,
default: (props: Record<string, unknown>) => {
renderOperationsDropdown(props)
return <div data-testid="operations-dropdown" />
},
}))
// Factory function for DataSet mock data
@ -285,4 +285,18 @@ describe('DatasetCard Component', () => {
canBindOrUnbindTags: true,
}))
})
it('should pass dataset operations without legacy workspace-role props', () => {
const dataset = createMockDataset({
permission_keys: [DatasetACLPermission.Delete],
})
render(<DatasetCard dataset={dataset} />)
expect(renderOperationsDropdown).toHaveBeenCalledWith(expect.objectContaining({
dataset,
}))
expect(renderOperationsDropdown).toHaveBeenCalledWith(expect.not.objectContaining({
isCurrentWorkspaceDatasetOperator: expect.any(Boolean),
}))
})
})

View File

@ -3,6 +3,7 @@ import { fireEvent, render, screen } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { IndexingType } from '@/app/components/datasets/create/step-two'
import { ChunkingMode, DatasetPermission, DataSourceType } from '@/models/datasets'
import { DatasetACLPermission } from '@/utils/permission'
import OperationsDropdown from '../operations-dropdown'
describe('OperationsDropdown', () => {
@ -25,12 +26,17 @@ describe('OperationsDropdown', () => {
created_by: 'user-1',
doc_form: ChunkingMode.text,
runtime_mode: 'general',
permission_keys: [
DatasetACLPermission.Edit,
DatasetACLPermission.Delete,
DatasetACLPermission.ImportExportDSL,
DatasetACLPermission.AccessConfig,
],
...overrides,
} as DataSet)
const defaultProps = {
dataset: createMockDataset(),
isCurrentWorkspaceDatasetOperator: false,
openRenameModal: vi.fn(),
handleExportPipeline: vi.fn(),
detectIsUsedByApp: vi.fn(),
@ -66,38 +72,41 @@ describe('OperationsDropdown', () => {
})
describe('Props', () => {
it('should show delete option when not workspace dataset operator', () => {
render(<OperationsDropdown {...defaultProps} isCurrentWorkspaceDatasetOperator={false} />)
it('should show delete option when dataset has delete ACL permission', () => {
render(<OperationsDropdown {...defaultProps} />)
const triggerButton = document.querySelector('[class*="cursor-pointer"]')
if (triggerButton)
fireEvent.click(triggerButton)
fireEvent.click(screen.getByLabelText('Dataset operations'))
expect(screen.getByText('common.operation.delete')).toBeInTheDocument()
})
it('should hide delete option when is workspace dataset operator', () => {
render(<OperationsDropdown {...defaultProps} isCurrentWorkspaceDatasetOperator={true} />)
it('should hide delete option when dataset lacks delete ACL permission', () => {
const dataset = createMockDataset({
permission_keys: [DatasetACLPermission.Edit],
})
render(<OperationsDropdown {...defaultProps} dataset={dataset} />)
const triggerButton = document.querySelector('[class*="cursor-pointer"]')
if (triggerButton)
fireEvent.click(triggerButton)
fireEvent.click(screen.getByLabelText('Dataset operations'))
expect(screen.queryByText('common.operation.delete')).not.toBeInTheDocument()
})
it('should show export pipeline when runtime_mode is rag_pipeline', () => {
const dataset = createMockDataset({ runtime_mode: 'rag_pipeline' })
render(<OperationsDropdown {...defaultProps} dataset={dataset} />)
const triggerButton = document.querySelector('[class*="cursor-pointer"]')
if (triggerButton)
fireEvent.click(triggerButton)
fireEvent.click(screen.getByLabelText('Dataset operations'))
expect(screen.getByText('datasetPipeline.operations.exportPipeline')).toBeInTheDocument()
})
it('should hide export pipeline when runtime_mode is not rag_pipeline', () => {
const dataset = createMockDataset({ runtime_mode: 'general' })
render(<OperationsDropdown {...defaultProps} dataset={dataset} />)
const triggerButton = document.querySelector('[class*="cursor-pointer"]')
if (triggerButton)
fireEvent.click(triggerButton)
fireEvent.click(screen.getByLabelText('Dataset operations'))
expect(screen.queryByText('datasetPipeline.operations.exportPipeline')).not.toBeInTheDocument()
})
})

View File

@ -11,7 +11,6 @@ import Operations from '../operations'
type OperationsDropdownProps = {
dataset: DataSet
isCurrentWorkspaceDatasetOperator: boolean
openRenameModal: () => void
handleExportPipeline: (include?: boolean) => void
detectIsUsedByApp: () => void
@ -20,7 +19,6 @@ type OperationsDropdownProps = {
const OperationsDropdown = ({
dataset,
isCurrentWorkspaceDatasetOperator,
openRenameModal,
handleExportPipeline,
detectIsUsedByApp,
@ -65,7 +63,7 @@ const OperationsDropdown = ({
>
<Operations
showEdit={datasetACLCapabilities.canEdit}
showDelete={!isCurrentWorkspaceDatasetOperator && datasetACLCapabilities.canDelete}
showDelete={datasetACLCapabilities.canDelete}
showExportPipeline={dataset.runtime_mode === 'rag_pipeline' && datasetACLCapabilities.canImportExportDSL}
showAccessConfig={datasetACLCapabilities.canAccessConfig}
openRenameModal={openRenameModal}

View File

@ -1,7 +1,6 @@
'use client'
import type { DataSet } from '@/models/datasets'
import { useMemo } from 'react'
import { useSelector as useAppContextWithSelector } from '@/context/app-context'
import { DatasetCardTags } from '@/features/tag-management/components/dataset-card-tags'
import { useRouter } from '@/next/navigation'
import { getDatasetACLCapabilities } from '@/utils/permission'
@ -28,8 +27,6 @@ const DatasetCard = ({
}: DatasetCardProps) => {
const { push } = useRouter()
const isCurrentWorkspaceDatasetOperator = useAppContextWithSelector(state => state.isCurrentWorkspaceDatasetOperator)
const datasetCard = useDatasetCardController({ dataset, onSuccess })
const {
modalState,
@ -89,7 +86,6 @@ const DatasetCard = ({
<DatasetCardFooter dataset={dataset} />
<OperationsDropdown
dataset={dataset}
isCurrentWorkspaceDatasetOperator={isCurrentWorkspaceDatasetOperator}
openRenameModal={openRenameModal}
handleExportPipeline={handleExportPipeline}
detectIsUsedByApp={detectIsUsedByApp}