diff --git a/web/__tests__/app-sidebar/dataset-info-flow.test.tsx b/web/__tests__/app-sidebar/dataset-info-flow.test.tsx index 3093b2809d..bff6f5a29c 100644 --- a/web/__tests__/app-sidebar/dataset-info-flow.test.tsx +++ b/web/__tests__/app-sidebar/dataset-info-flow.test.tsx @@ -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 => ({ enable_api: false, is_multimodal: false, is_published: true, + permission_keys: [ + DatasetACLPermission.Edit, + DatasetACLPermission.Delete, + DatasetACLPermission.ImportExportDSL, + ], ...overrides, }) diff --git a/web/app/components/app-sidebar/dataset-info/__tests__/dropdown-callbacks.spec.tsx b/web/app/components/app-sidebar/dataset-info/__tests__/dropdown-callbacks.spec.tsx index 05a06f2f77..e1bfb008a7 100644 --- a/web/app/components/app-sidebar/dataset-info/__tests__/dropdown-callbacks.spec.tsx +++ b/web/app/components/app-sidebar/dataset-info/__tests__/dropdown-callbacks.spec.tsx @@ -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 => ({ 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({}) diff --git a/web/app/components/app-sidebar/dataset-info/__tests__/index.spec.tsx b/web/app/components/app-sidebar/dataset-info/__tests__/index.spec.tsx index e6d3f94e2a..422598f63d 100644 --- a/web/app/components/app-sidebar/dataset-info/__tests__/index.spec.tsx +++ b/web/app/components/app-sidebar/dataset-info/__tests__/index.spec.tsx @@ -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 => ({ 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() // Act diff --git a/web/app/components/app-sidebar/dataset-info/dropdown.tsx b/web/app/components/app-sidebar/dataset-info/dropdown.tsx index b24be66e03..95da7664b9 100644 --- a/web/app/components/app-sidebar/dataset-info/dropdown.tsx +++ b/web/app/components/app-sidebar/dataset-info/dropdown.tsx @@ -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('') 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 = ({ > ({ 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: () =>
, })) 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: () =>
, + default: (props: Record) => { + renderOperationsDropdown(props) + return
+ }, })) // 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() + + expect(renderOperationsDropdown).toHaveBeenCalledWith(expect.objectContaining({ + dataset, + })) + expect(renderOperationsDropdown).toHaveBeenCalledWith(expect.not.objectContaining({ + isCurrentWorkspaceDatasetOperator: expect.any(Boolean), + })) + }) }) diff --git a/web/app/components/datasets/list/dataset-card/components/__tests__/operations-dropdown.spec.tsx b/web/app/components/datasets/list/dataset-card/components/__tests__/operations-dropdown.spec.tsx index 5151fdcd28..f2255a0caf 100644 --- a/web/app/components/datasets/list/dataset-card/components/__tests__/operations-dropdown.spec.tsx +++ b/web/app/components/datasets/list/dataset-card/components/__tests__/operations-dropdown.spec.tsx @@ -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() + it('should show delete option when dataset has delete ACL permission', () => { + render() - 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() + it('should hide delete option when dataset lacks delete ACL permission', () => { + const dataset = createMockDataset({ + permission_keys: [DatasetACLPermission.Edit], + }) + render() - 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() - 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() - 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() }) }) diff --git a/web/app/components/datasets/list/dataset-card/components/operations-dropdown.tsx b/web/app/components/datasets/list/dataset-card/components/operations-dropdown.tsx index 0698233586..f9888df42e 100644 --- a/web/app/components/datasets/list/dataset-card/components/operations-dropdown.tsx +++ b/web/app/components/datasets/list/dataset-card/components/operations-dropdown.tsx @@ -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 = ({ > { const { push } = useRouter() - const isCurrentWorkspaceDatasetOperator = useAppContextWithSelector(state => state.isCurrentWorkspaceDatasetOperator) - const datasetCard = useDatasetCardController({ dataset, onSuccess }) const { modalState, @@ -89,7 +86,6 @@ const DatasetCard = ({