diff --git a/src/taxonomy/__mocks__/taxonomyListMock.js b/src/taxonomy/__mocks__/taxonomyListMock.js
index 5eab0af023..9ea13d6ab0 100644
--- a/src/taxonomy/__mocks__/taxonomyListMock.js
+++ b/src/taxonomy/__mocks__/taxonomyListMock.js
@@ -15,6 +15,12 @@ module.exports = {
allowFreeText: false,
systemDefined: true,
visibleToAuthors: false,
+ userPermissions: {
+ canAdd: true,
+ canView: true,
+ canChange: false,
+ canDelete: false,
+ },
},
{
id: -1,
@@ -25,6 +31,12 @@ module.exports = {
allowFreeText: false,
systemDefined: true,
visibleToAuthors: true,
+ userPermissions: {
+ canAdd: true,
+ canView: true,
+ canChange: false,
+ canDelete: false,
+ },
},
{
id: 1,
@@ -35,6 +47,12 @@ module.exports = {
allowFreeText: false,
systemDefined: false,
visibleToAuthors: true,
+ userPermissions: {
+ canAdd: true,
+ canView: true,
+ canChange: true,
+ canDelete: true,
+ },
},
{
id: 2,
@@ -45,6 +63,12 @@ module.exports = {
allowFreeText: false,
systemDefined: false,
visibleToAuthors: true,
+ userPermissions: {
+ canAdd: true,
+ canView: true,
+ canChange: true,
+ canDelete: true,
+ },
},
],
};
diff --git a/src/taxonomy/data/types.mjs b/src/taxonomy/data/types.mjs
index b892592a5e..6348470f0e 100644
--- a/src/taxonomy/data/types.mjs
+++ b/src/taxonomy/data/types.mjs
@@ -1,5 +1,13 @@
// @ts-check
+/**
+ * @typedef {Object} UserPermissions
+ * @property {boolean} canAdd
+ * @property {boolean} canView
+ * @property {boolean} canChange
+ * @property {boolean} canDelete
+ **/
+
/**
* @typedef {Object} TaxonomyData
* @property {number} id
@@ -12,6 +20,7 @@
* @property {boolean} visibleToAuthors
* @property {number} tagsCount
* @property {string[]} orgs
+ * @property {UserPermissions} userPermissions
*/
/**
diff --git a/src/taxonomy/taxonomy-card/TaxonomyCard.test.jsx b/src/taxonomy/taxonomy-card/TaxonomyCard.test.jsx
index 6cbbf4c630..33d6ed8aac 100644
--- a/src/taxonomy/taxonomy-card/TaxonomyCard.test.jsx
+++ b/src/taxonomy/taxonomy-card/TaxonomyCard.test.jsx
@@ -3,7 +3,7 @@ import { IntlProvider } from '@edx/frontend-platform/i18n';
import { initializeMockApp } from '@edx/frontend-platform';
import { AppProvider } from '@edx/frontend-platform/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
-import { render } from '@testing-library/react';
+import { fireEvent, render } from '@testing-library/react';
import PropTypes from 'prop-types';
import initializeStore from '../../store';
@@ -16,6 +16,12 @@ const data = {
id: taxonomyId,
name: 'Taxonomy 1',
description: 'This is a description',
+ userPermissions: {
+ canAdd: true,
+ canView: true,
+ canChange: true,
+ canDelete: true,
+ },
};
const queryClient = new QueryClient();
@@ -40,6 +46,12 @@ TaxonomyCardComponent.propTypes = {
systemDefined: PropTypes.bool,
orgsCount: PropTypes.number,
onDeleteTaxonomy: PropTypes.func,
+ userPermissions: PropTypes.shape({
+ canAdd: PropTypes.bool,
+ canView: PropTypes.bool,
+ canChange: PropTypes.bool,
+ canDelete: PropTypes.bool,
+ }),
}).isRequired,
};
@@ -62,6 +74,33 @@ describe('', async () => {
expect(getByText(data.description)).toBeInTheDocument();
});
+ it('should show the ⋮ menu', () => {
+ const { getByTestId, queryByTestId } = render();
+
+ // Menu closed/doesn't exist yet
+ expect(queryByTestId('taxonomy-menu')).not.toBeInTheDocument();
+
+ // Click on the menu button to open
+ fireEvent.click(getByTestId('taxonomy-menu-button'));
+
+ // Menu opened
+ expect(getByTestId('taxonomy-menu')).toBeVisible();
+ expect(getByTestId('taxonomy-menu-import')).toBeVisible();
+ expect(getByTestId('taxonomy-menu-export')).toBeVisible();
+ expect(getByTestId('taxonomy-menu-delete')).toBeVisible();
+
+ // Click on button again to close the menu
+ fireEvent.click(getByTestId('taxonomy-menu-button'));
+
+ // Menu closed
+ // Jest bug: toBeVisible() isn't checking opacity correctly
+ // expect(getByTestId('taxonomy-menu')).not.toBeVisible();
+ expect(getByTestId('taxonomy-menu').style.opacity).toEqual('0');
+
+ // Menu button still visible
+ expect(getByTestId('taxonomy-menu-button')).toBeVisible();
+ });
+
it('not show the system-defined badge with normal taxonomies', () => {
const { queryByText } = render();
expect(queryByText('System-level')).not.toBeInTheDocument();
diff --git a/src/taxonomy/taxonomy-card/index.jsx b/src/taxonomy/taxonomy-card/index.jsx
index 1449730ae5..3d6ac2b2c6 100644
--- a/src/taxonomy/taxonomy-card/index.jsx
+++ b/src/taxonomy/taxonomy-card/index.jsx
@@ -149,6 +149,12 @@ TaxonomyCard.propTypes = {
systemDefined: PropTypes.bool,
orgsCount: PropTypes.number,
tagsCount: PropTypes.number,
+ userPermissions: PropTypes.shape({
+ canAdd: PropTypes.bool.isRequired,
+ canView: PropTypes.bool.isRequired,
+ canChange: PropTypes.bool.isRequired,
+ canDelete: PropTypes.bool.isRequired,
+ }).isRequired,
}).isRequired,
};
diff --git a/src/taxonomy/taxonomy-detail/TaxonomyDetailPage.test.jsx b/src/taxonomy/taxonomy-detail/TaxonomyDetailPage.test.jsx
index 1f245356b6..2c9bc0913f 100644
--- a/src/taxonomy/taxonomy-detail/TaxonomyDetailPage.test.jsx
+++ b/src/taxonomy/taxonomy-detail/TaxonomyDetailPage.test.jsx
@@ -2,7 +2,7 @@ import React, { useMemo } from 'react';
import { initializeMockApp } from '@edx/frontend-platform';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import { AppProvider } from '@edx/frontend-platform/react';
-import { render } from '@testing-library/react';
+import { fireEvent, render } from '@testing-library/react';
import { useTaxonomyDetailData } from './data/api';
import initializeStore from '../../store';
@@ -89,12 +89,62 @@ describe('', async () => {
name: 'Test taxonomy',
description: 'This is a description',
systemDefined: true,
+ userPreferences: {
+ canAdd: true,
+ canView: true,
+ canChange: true,
+ canDelete: true,
+ },
},
});
const { getByRole } = render();
expect(getByRole('heading')).toHaveTextContent('Test taxonomy');
});
+ it('should show the Action menu with options', () => {
+ useTaxonomyDetailData.mockReturnValue({
+ isSuccess: true,
+ isFetched: true,
+ isError: false,
+ data: {
+ id: 1,
+ name: 'Test taxonomy',
+ description: 'This is a description',
+ systemDefined: true,
+ userPreferences: {
+ canAdd: true,
+ canView: true,
+ canChange: true,
+ canDelete: true,
+ },
+ },
+ });
+ const { getByTestId, queryByTestId } = render();
+
+ // Menu closed/doesn't exist yet
+ expect(queryByTestId('taxonomy-menu')).not.toBeInTheDocument();
+
+ // Click on the menu button to open
+ fireEvent.click(getByTestId('taxonomy-menu-button'));
+
+ // Menu opened
+ expect(getByTestId('taxonomy-menu')).toBeVisible();
+ expect(getByTestId('taxonomy-menu-import')).toBeVisible();
+ expect(getByTestId('taxonomy-menu-export')).toBeVisible();
+ expect(getByTestId('taxonomy-menu-delete')).toBeVisible();
+
+ // Click on button again to close the menu
+ fireEvent.click(getByTestId('taxonomy-menu-button'));
+
+ // Menu closed
+ // Jest bug: toBeVisible() isn't checking opacity correctly
+ // expect(getByTestId('taxonomy-menu')).not.toBeVisible();
+ expect(getByTestId('taxonomy-menu').style.opacity).toEqual('0');
+
+ // Menu button still visible
+ expect(getByTestId('taxonomy-menu-button')).toBeVisible();
+ });
+
it('should show system defined badge', async () => {
useTaxonomyDetailData.mockReturnValue({
isSuccess: true,
@@ -105,6 +155,7 @@ describe('', async () => {
name: 'Test taxonomy',
description: 'This is a description',
systemDefined: true,
+ userPreferences: {},
},
});
const { getByText } = render();
@@ -121,6 +172,7 @@ describe('', async () => {
name: 'Test taxonomy',
description: 'This is a description',
systemDefined: false,
+ userPreferences: {},
},
});
const { queryByText } = render();
diff --git a/src/taxonomy/taxonomy-menu/TaxonomyMenu.jsx b/src/taxonomy/taxonomy-menu/TaxonomyMenu.jsx
index 0db273cf46..27442b736a 100644
--- a/src/taxonomy/taxonomy-menu/TaxonomyMenu.jsx
+++ b/src/taxonomy/taxonomy-menu/TaxonomyMenu.jsx
@@ -9,7 +9,7 @@ import {
IconButton,
} from '@edx/paragon';
import { MoreVert } from '@edx/paragon/icons';
-import { omitBy } from 'lodash';
+import { pickBy } from 'lodash';
import PropTypes from 'prop-types';
import { useNavigate } from 'react-router-dom';
@@ -50,7 +50,7 @@ const TaxonomyMenu = ({
* @typedef {Object} MenuItem
* @property {string} title - The title of the menu item
* @property {() => void} action - The action to perform when the menu item is clicked
- * @property {boolean} [hide] - Whether or not to hide the menu item
+ * @property {boolean} [show] - Whether or not to show the menu item
*
* @constant
* @type {Record}
@@ -59,23 +59,22 @@ const TaxonomyMenu = ({
import: {
title: intl.formatMessage(messages.importMenu),
action: () => importTaxonomyTags(taxonomy.id, intl),
- // Hide import menu item if taxonomy is system defined or allows free text
- hide: taxonomy.systemDefined || taxonomy.allowFreeText,
+ show: taxonomy.userPermissions.canChange,
},
export: {
title: intl.formatMessage(messages.exportMenu),
action: exportModalOpen,
+ show: taxonomy.userPermissions.canView,
},
delete: {
title: intl.formatMessage(messages.deleteMenu),
action: deleteDialogOpen,
- // Hide delete menu item if taxonomy is system defined
- hide: taxonomy.systemDefined,
+ show: taxonomy.userPermissions.canDelete,
},
};
// Remove hidden menu items
- menuItems = omitBy(menuItems, (value) => value.hide);
+ menuItems = pickBy(menuItems, (value) => value.show);
const renderModals = () => (
<>
@@ -136,9 +135,13 @@ TaxonomyMenu.propTypes = {
taxonomy: PropTypes.shape({
id: PropTypes.number.isRequired,
name: PropTypes.string.isRequired,
- systemDefined: PropTypes.bool.isRequired,
- allowFreeText: PropTypes.bool.isRequired,
tagsCount: PropTypes.number.isRequired,
+ userPermissions: PropTypes.shape({
+ canAdd: PropTypes.bool.isRequired,
+ canView: PropTypes.bool.isRequired,
+ canChange: PropTypes.bool.isRequired,
+ canDelete: PropTypes.bool.isRequired,
+ }).isRequired,
}).isRequired,
iconMenu: PropTypes.bool,
};
diff --git a/src/taxonomy/taxonomy-menu/TaxonomyMenu.test.jsx b/src/taxonomy/taxonomy-menu/TaxonomyMenu.test.jsx
index c8f398aa94..03ac3b73c1 100644
--- a/src/taxonomy/taxonomy-menu/TaxonomyMenu.test.jsx
+++ b/src/taxonomy/taxonomy-menu/TaxonomyMenu.test.jsx
@@ -32,9 +32,11 @@ const queryClient = new QueryClient();
const mockSetToastMessage = jest.fn();
const TaxonomyMenuComponent = ({
- systemDefined,
- allowFreeText,
iconMenu,
+ canAdd,
+ canView,
+ canChange,
+ canDelete,
}) => {
const context = useMemo(() => ({
toastMessage: null,
@@ -50,9 +52,10 @@ const TaxonomyMenuComponent = ({
taxonomy={{
id: taxonomyId,
name: taxonomyName,
- systemDefined,
- allowFreeText,
tagsCount: 0,
+ userPermissions: {
+ canAdd, canView, canChange, canDelete,
+ },
}}
iconMenu={iconMenu}
/>
@@ -65,13 +68,17 @@ const TaxonomyMenuComponent = ({
TaxonomyMenuComponent.propTypes = {
iconMenu: PropTypes.bool.isRequired,
- systemDefined: PropTypes.bool,
- allowFreeText: PropTypes.bool,
+ canAdd: PropTypes.bool,
+ canView: PropTypes.bool,
+ canChange: PropTypes.bool,
+ canDelete: PropTypes.bool,
};
TaxonomyMenuComponent.defaultProps = {
- systemDefined: false,
- allowFreeText: false,
+ canAdd: true,
+ canView: true,
+ canChange: true,
+ canDelete: true,
};
each([true, false]).describe('', async (iconMenu) => {
@@ -115,36 +122,36 @@ each([true, false]).describe('', async (iconMenu) =>
expect(getByTestId('taxonomy-menu-button')).toBeVisible();
});
- test('doesnt show systemDefined taxonomies disabled menus', () => {
- const { getByTestId, queryByTestId } = render();
-
- // Menu closed/doesn't exist yet
- expect(queryByTestId('taxonomy-menu')).not.toBeInTheDocument();
+ test('Shows menu actions that user is permitted', () => {
+ const { getByTestId, queryByTestId } = render();
// Click on the menu button to open
fireEvent.click(getByTestId('taxonomy-menu-button'));
- // Menu opened
- expect(getByTestId('taxonomy-menu')).toBeVisible();
-
- // Check that the import menu is not show
- expect(queryByTestId('taxonomy-menu-import')).not.toBeInTheDocument();
+ // Ensure that the menu items are present
+ expect(queryByTestId('taxonomy-menu-export')).toBeInTheDocument();
+ expect(queryByTestId('taxonomy-menu-import')).toBeInTheDocument();
+ expect(queryByTestId('taxonomy-menu-delete')).toBeInTheDocument();
});
- test('doesnt show freeText taxonomies disabled menus', () => {
- const { getByTestId, queryByTestId } = render();
-
- // Menu closed/doesn't exist yet
- expect(queryByTestId('taxonomy-menu')).not.toBeInTheDocument();
+ test('Hides menu actions that user is not permitted', () => {
+ const { getByTestId, queryByTestId } = render(
+ ,
+ );
// Click on the menu button to open
fireEvent.click(getByTestId('taxonomy-menu-button'));
- // Menu opened
- expect(getByTestId('taxonomy-menu')).toBeVisible();
-
- // Check that the import menu is not show
+ // Ensure no menu items are found
+ expect(queryByTestId('taxonomy-menu-export')).not.toBeInTheDocument();
expect(queryByTestId('taxonomy-menu-import')).not.toBeInTheDocument();
+ expect(queryByTestId('taxonomy-menu-delete')).not.toBeInTheDocument();
});
test('should open export modal on export menu click', () => {