From 76f69c2dd23a8a3733515ca884dbaa03205e912a Mon Sep 17 00:00:00 2001 From: Braden MacDonald Date: Wed, 29 Jul 2026 18:15:40 -0700 Subject: [PATCH] feat: remove "system defined" taxonomy badge, replace w/ read-only --- src/taxonomy/TaxonomyListPage.test.tsx | 6 +- src/taxonomy/TaxonomyListPage.tsx | 4 +- src/taxonomy/__mocks__/taxonomyListMock.js | 8 +-- src/taxonomy/data/types.ts | 2 +- src/taxonomy/index.scss | 2 +- .../read-only-badge/ReadOnlyBadge.scss | 3 + src/taxonomy/read-only-badge/index.tsx | 13 +++++ src/taxonomy/read-only-badge/messages.ts | 10 ++++ .../SystemDefinedBadge.scss | 3 - src/taxonomy/system-defined-badge/index.jsx | 41 ------------- src/taxonomy/system-defined-badge/messages.ts | 18 ------ src/taxonomy/tag-list/messages.ts | 4 +- .../taxonomy-card/TaxonomyCard.test.jsx | 14 +++-- src/taxonomy/taxonomy-card/index.jsx | 24 ++++---- .../TaxonomyDetailPage.test.jsx | 16 ++--- ...yDetailPage.jsx => TaxonomyDetailPage.tsx} | 12 ++-- .../taxonomy-menu/TaxonomyMenu.test.tsx | 14 ++--- .../{TaxonomyMenu.jsx => TaxonomyMenu.tsx} | 58 +++++++------------ 18 files changed, 98 insertions(+), 154 deletions(-) create mode 100644 src/taxonomy/read-only-badge/ReadOnlyBadge.scss create mode 100644 src/taxonomy/read-only-badge/index.tsx create mode 100644 src/taxonomy/read-only-badge/messages.ts delete mode 100644 src/taxonomy/system-defined-badge/SystemDefinedBadge.scss delete mode 100644 src/taxonomy/system-defined-badge/index.jsx delete mode 100644 src/taxonomy/system-defined-badge/messages.ts rename src/taxonomy/taxonomy-detail/{TaxonomyDetailPage.jsx => TaxonomyDetailPage.tsx} (90%) rename src/taxonomy/taxonomy-menu/{TaxonomyMenu.jsx => TaxonomyMenu.tsx} (78%) diff --git a/src/taxonomy/TaxonomyListPage.test.tsx b/src/taxonomy/TaxonomyListPage.test.tsx index 27c7babd39..9ea7a8dd58 100644 --- a/src/taxonomy/TaxonomyListPage.test.tsx +++ b/src/taxonomy/TaxonomyListPage.test.tsx @@ -14,7 +14,7 @@ const taxonomies = [{ id: 1, name: 'Taxonomy', description: 'This is a description', - showSystemBadge: false, + showReadOnlyBadge: false, canChangeTaxonomy: true, canDeleteTaxonomy: true, tagsCount: 0, @@ -115,7 +115,7 @@ describe('', () => { id: 1, name: 'Taxonomy', description: 'This is a description', - showSystemBadge: false, + showReadOnlyBadge: false, canChangeTaxonomy: false, canDeleteTaxonomy: false, tagsCount: 0, @@ -157,7 +157,7 @@ describe('', () => { axiosMock.onGet(listTaxonomiesUrl).reply(200, { results: taxonomies, canAddTaxonomy: false }); const defaults = { id: 1, - showSystemBadge: false, + showReadOnlyBadge: false, canChangeTaxonomy: true, canDeleteTaxonomy: true, tagsCount: 0, diff --git a/src/taxonomy/TaxonomyListPage.tsx b/src/taxonomy/TaxonomyListPage.tsx index 00b74f2e54..a5982cc2c3 100644 --- a/src/taxonomy/TaxonomyListPage.tsx +++ b/src/taxonomy/TaxonomyListPage.tsx @@ -215,8 +215,8 @@ export const TaxonomyListPage = () => { accessor: 'description', }, { - Header: 'systemDefined', - accessor: 'systemDefined', + Header: 'readOnly', + accessor: 'readOnly', }, { Header: '', diff --git a/src/taxonomy/__mocks__/taxonomyListMock.js b/src/taxonomy/__mocks__/taxonomyListMock.js index 0e7b24e4f7..78dc39ef0d 100644 --- a/src/taxonomy/__mocks__/taxonomyListMock.js +++ b/src/taxonomy/__mocks__/taxonomyListMock.js @@ -14,7 +14,7 @@ module.exports = { enabled: true, allowMultiple: false, allowFreeText: false, - systemDefined: true, + readOnly: true, visibleToAuthors: false, canChangeTaxonomy: false, canDeleteTaxonomy: false, @@ -26,7 +26,7 @@ module.exports = { enabled: true, allowMultiple: false, allowFreeText: false, - systemDefined: true, + readOnly: true, visibleToAuthors: true, canChangeTaxonomy: false, canDeleteTaxonomy: false, @@ -38,7 +38,7 @@ module.exports = { enabled: true, allowMultiple: false, allowFreeText: false, - systemDefined: false, + readOnly: false, visibleToAuthors: true, canChangeTaxonomy: true, canDeleteTaxonomy: true, @@ -50,7 +50,7 @@ module.exports = { enabled: true, allowMultiple: false, allowFreeText: false, - systemDefined: false, + readOnly: false, visibleToAuthors: true, canChangeTaxonomy: true, canDeleteTaxonomy: true, diff --git a/src/taxonomy/data/types.ts b/src/taxonomy/data/types.ts index d2f7df8639..5a91cfdbf8 100644 --- a/src/taxonomy/data/types.ts +++ b/src/taxonomy/data/types.ts @@ -7,7 +7,7 @@ export interface TaxonomyData { enabled: boolean; allowMultiple: boolean; allowFreeText: boolean; - systemDefined: boolean; + readOnly: boolean; visibleToAuthors: boolean; tagsCount: number; orgs: string[]; diff --git a/src/taxonomy/index.scss b/src/taxonomy/index.scss index 873c0f5495..9f35a11e58 100644 --- a/src/taxonomy/index.scss +++ b/src/taxonomy/index.scss @@ -1,6 +1,6 @@ @import "taxonomy/TaxonomyListPage"; @import "taxonomy/taxonomy-card/TaxonomyCard"; @import "taxonomy/delete-dialog/DeleteDialog"; -@import "taxonomy/system-defined-badge/SystemDefinedBadge"; +@import "taxonomy/read-only-badge/ReadOnlyBadge"; @import "taxonomy/export-modal/ExportModal"; @import "taxonomy/tag-list/TagListTable"; diff --git a/src/taxonomy/read-only-badge/ReadOnlyBadge.scss b/src/taxonomy/read-only-badge/ReadOnlyBadge.scss new file mode 100644 index 0000000000..695565f93d --- /dev/null +++ b/src/taxonomy/read-only-badge/ReadOnlyBadge.scss @@ -0,0 +1,3 @@ +.read-only-badge { + font-size: 12px; +} diff --git a/src/taxonomy/read-only-badge/index.tsx b/src/taxonomy/read-only-badge/index.tsx new file mode 100644 index 0000000000..2ff6327053 --- /dev/null +++ b/src/taxonomy/read-only-badge/index.tsx @@ -0,0 +1,13 @@ +import { FormattedMessage } from '@edx/frontend-platform/i18n'; +import { + Badge, +} from '@openedx/paragon'; +import messages from './messages'; + +export const ReadOnlyBadge = () => { + return ( + + + + ); +}; diff --git a/src/taxonomy/read-only-badge/messages.ts b/src/taxonomy/read-only-badge/messages.ts new file mode 100644 index 0000000000..2e15f68d10 --- /dev/null +++ b/src/taxonomy/read-only-badge/messages.ts @@ -0,0 +1,10 @@ +import { defineMessages } from '@edx/frontend-platform/i18n'; + +const messages = defineMessages({ + readOnlyBadge: { + id: 'course-authoring.taxonomy-list.badge.read-only.label', + defaultMessage: 'Read only', + }, +}); + +export default messages; diff --git a/src/taxonomy/system-defined-badge/SystemDefinedBadge.scss b/src/taxonomy/system-defined-badge/SystemDefinedBadge.scss deleted file mode 100644 index dfab7827f4..0000000000 --- a/src/taxonomy/system-defined-badge/SystemDefinedBadge.scss +++ /dev/null @@ -1,3 +0,0 @@ -.system-defined-badge { - font-size: 12px; -} diff --git a/src/taxonomy/system-defined-badge/index.jsx b/src/taxonomy/system-defined-badge/index.jsx deleted file mode 100644 index ed18bdef2a..0000000000 --- a/src/taxonomy/system-defined-badge/index.jsx +++ /dev/null @@ -1,41 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { useIntl } from '@edx/frontend-platform/i18n'; -import { - Badge, - OverlayTrigger, - Popover, -} from '@openedx/paragon'; -import messages from './messages'; - -const SystemDefinedBadge = ({ taxonomyId }) => { - const intl = useIntl(); - const getToolTip = () => ( - - - {intl.formatMessage(messages.systemTaxonomyPopoverTitle)} - - - {intl.formatMessage(messages.systemTaxonomyPopoverBody)} - - - ); - - return ( - - - {intl.formatMessage(messages.systemDefinedBadge)} - - - ); -}; - -SystemDefinedBadge.propTypes = { - taxonomyId: PropTypes.number.isRequired, -}; - -export default SystemDefinedBadge; diff --git a/src/taxonomy/system-defined-badge/messages.ts b/src/taxonomy/system-defined-badge/messages.ts deleted file mode 100644 index 59642d1e55..0000000000 --- a/src/taxonomy/system-defined-badge/messages.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { defineMessages } from '@edx/frontend-platform/i18n'; - -const messages = defineMessages({ - systemTaxonomyPopoverTitle: { - id: 'course-authoring.taxonomy-list.popover.system-defined.title', - defaultMessage: 'System taxonomy', - }, - systemTaxonomyPopoverBody: { - id: 'course-authoring.taxonomy-list.popover.system-defined.body', - defaultMessage: 'This is a system-level taxonomy and is enabled by default.', - }, - systemDefinedBadge: { - id: 'course-authoring.taxonomy-list.badge.system-defined.label', - defaultMessage: 'System-level', - }, -}); - -export default messages; diff --git a/src/taxonomy/tag-list/messages.ts b/src/taxonomy/tag-list/messages.ts index b448e30519..d9d7978a44 100644 --- a/src/taxonomy/tag-list/messages.ts +++ b/src/taxonomy/tag-list/messages.ts @@ -110,11 +110,11 @@ const messages = defineMessages({ defaultMessage: 'This tag does not allow deletion', }, tagEditForbidden: { - id: 'course-authoring.tag-list.system-defined-tag-edit-disabled', + id: 'course-authoring.tag-list.read-only-tag-edit-disabled', defaultMessage: 'Disabled because this is not allowed to be changed', }, tagDeleteForbidden: { - id: 'course-authoring.tag-list.system-defined-tag-delete-disabled', + id: 'course-authoring.tag-list.read-only-tag-delete-disabled', defaultMessage: 'Disabled because this is not allowed to be deleted', }, hasOpenDraft: { diff --git a/src/taxonomy/taxonomy-card/TaxonomyCard.test.jsx b/src/taxonomy/taxonomy-card/TaxonomyCard.test.jsx index 413623e5bc..756ce5333c 100644 --- a/src/taxonomy/taxonomy-card/TaxonomyCard.test.jsx +++ b/src/taxonomy/taxonomy-card/TaxonomyCard.test.jsx @@ -15,7 +15,7 @@ const data = { id: taxonomyId, name: 'Taxonomy 1', description: 'This is a description', - systemDefined: false, + readOnly: false, canChangeTaxonomy: true, canDeleteTaxonomy: true, tagsCount: 0, @@ -83,17 +83,19 @@ describe('', () => { expect(getByTestId('taxonomy-menu-button')).toBeVisible(); }); - it('not show the system-defined badge with normal taxonomies', () => { + const readOnlyBadgeText = 'Read only'; + + it('not show the read-only badge with normal taxonomies', () => { const { queryByText } = render(); - expect(queryByText('System-level')).not.toBeInTheDocument(); + expect(queryByText(readOnlyBadgeText)).not.toBeInTheDocument(); }); - it('shows the system-defined badge with system taxonomies', () => { + it('shows the read-only badge with system taxonomies', () => { const cardData = { ...data }; - cardData.systemDefined = true; + cardData.readOnly = true; const { getByText } = render(); - expect(getByText('System-level')).toBeInTheDocument(); + expect(getByText(readOnlyBadgeText)).toBeInTheDocument(); }); it('not show org count with taxonomies without orgs', () => { diff --git a/src/taxonomy/taxonomy-card/index.jsx b/src/taxonomy/taxonomy-card/index.jsx index 6f3a58e231..53640e160d 100644 --- a/src/taxonomy/taxonomy-card/index.jsx +++ b/src/taxonomy/taxonomy-card/index.jsx @@ -11,20 +11,19 @@ import { useIntl } from '@edx/frontend-platform/i18n'; import { TaxonomyMenu } from '../taxonomy-menu'; import messages from './messages'; -import SystemDefinedBadge from '../system-defined-badge'; +import { ReadOnlyBadge } from '../read-only-badge'; const orgsCountEnabled = (orgsCount) => orgsCount !== undefined && orgsCount !== 0; const HeaderSubtitle = ({ - id, - showSystemBadge, + showReadOnlyBadge, orgsCount, }) => { const intl = useIntl(); // Show system defined badge - if (showSystemBadge) { - return ; + if (showReadOnlyBadge) { + return ; } // Or show orgs count @@ -46,7 +45,7 @@ HeaderSubtitle.defaultProps = { HeaderSubtitle.propTypes = { id: PropTypes.number.isRequired, - showSystemBadge: PropTypes.bool.isRequired, + showReadOnlyBadge: PropTypes.bool.isRequired, orgsCount: PropTypes.number, }; @@ -96,12 +95,10 @@ const TaxonomyCard = ({ className, original }) => { id, name, description, - systemDefined, + readOnly, orgsCount, } = original; - const intl = useIntl(); - const getHeaderActions = () => ( { subtitle={ } actions={getHeaderActions()} /> @@ -153,7 +149,7 @@ TaxonomyCard.propTypes = { id: PropTypes.number, name: PropTypes.string, description: PropTypes.string, - systemDefined: PropTypes.bool, + readOnly: PropTypes.bool, orgsCount: PropTypes.number, tagsCount: PropTypes.number, canChangeTaxonomy: PropTypes.bool, diff --git a/src/taxonomy/taxonomy-detail/TaxonomyDetailPage.test.jsx b/src/taxonomy/taxonomy-detail/TaxonomyDetailPage.test.jsx index 49df3d8a7e..66256492bb 100644 --- a/src/taxonomy/taxonomy-detail/TaxonomyDetailPage.test.jsx +++ b/src/taxonomy/taxonomy-detail/TaxonomyDetailPage.test.jsx @@ -85,7 +85,7 @@ describe('', () => { id: 1, name: 'Test taxonomy', description: 'This is a description', - system_defined: false, + read_only: false, can_change_taxonomy: true, can_delete_taxonomy: true, tagsCount: 0, @@ -108,12 +108,14 @@ describe('', () => { expect(getByTestId('taxonomy-menu-delete')).toBeVisible(); }); - it('should show system defined badge', async () => { + const readOnlyBadgeText = 'Read only'; + + it('should show "read-only" badge if the taxonomy is read-only', async () => { axiosMock.onGet(apiUrls.taxonomy(1)).replyOnce(200, { id: 1, name: 'Test taxonomy', description: 'This is a description', - system_defined: true, + read_only: true, can_change_taxonomy: false, can_delete_taxonomy: false, }); @@ -121,15 +123,15 @@ describe('', () => { const { findByRole, getByText } = render(); expect(await findByRole('heading')).toHaveTextContent('Test taxonomy'); - expect(getByText('System-level')).toBeInTheDocument(); + expect(getByText(readOnlyBadgeText)).toBeInTheDocument(); }); - it('should not show system defined badge', async () => { + it('should not show "read-only" badge if the taxonomy is not read-only', async () => { axiosMock.onGet(apiUrls.taxonomy(1)).replyOnce(200, { id: 1, name: 'Test taxonomy', description: 'This is a description', - system_defined: false, + read_only: false, can_change_taxonomy: false, can_delete_taxonomy: false, }); @@ -137,6 +139,6 @@ describe('', () => { const { findByRole, queryByText } = render(); expect(await findByRole('heading')).toHaveTextContent('Test taxonomy'); - expect(queryByText('System-level')).not.toBeInTheDocument(); + expect(queryByText(readOnlyBadgeText)).not.toBeInTheDocument(); }); }); diff --git a/src/taxonomy/taxonomy-detail/TaxonomyDetailPage.jsx b/src/taxonomy/taxonomy-detail/TaxonomyDetailPage.tsx similarity index 90% rename from src/taxonomy/taxonomy-detail/TaxonomyDetailPage.jsx rename to src/taxonomy/taxonomy-detail/TaxonomyDetailPage.tsx index 07c15ee0f2..175696339f 100644 --- a/src/taxonomy/taxonomy-detail/TaxonomyDetailPage.jsx +++ b/src/taxonomy/taxonomy-detail/TaxonomyDetailPage.tsx @@ -1,5 +1,3 @@ -// @ts-check -import React from 'react'; import { useIntl } from '@edx/frontend-platform/i18n'; import { Breadcrumb, @@ -18,7 +16,7 @@ import { TagListTable } from '../tag-list'; import { TaxonomyMenu } from '../taxonomy-menu'; import TaxonomyDetailSideCard from './TaxonomyDetailSideCard'; import { useTaxonomyDetails } from '../data/apiHooks'; -import SystemDefinedBadge from '../system-defined-badge'; +import { ReadOnlyBadge } from '../read-only-badge'; import { TAXONOMY_MAX_DEPTH } from './constants'; const TaxonomyDetailPage = () => { @@ -46,9 +44,9 @@ const TaxonomyDetailPage = () => { /> ); - const getSystemDefinedBadge = () => { - if (taxonomy.systemDefined) { - return ; + const getReadOnlyBadge = () => { + if (taxonomy.readOnly) { + return ; } return null; }; @@ -69,7 +67,7 @@ const TaxonomyDetailPage = () => { /> diff --git a/src/taxonomy/taxonomy-menu/TaxonomyMenu.test.tsx b/src/taxonomy/taxonomy-menu/TaxonomyMenu.test.tsx index a1ddf4df4b..ba9e6951eb 100644 --- a/src/taxonomy/taxonomy-menu/TaxonomyMenu.test.tsx +++ b/src/taxonomy/taxonomy-menu/TaxonomyMenu.test.tsx @@ -27,12 +27,12 @@ const mockSetToastMessage = jest.fn(); */ const TaxonomyMenuComponent: React.FC<{ iconMenu: boolean; - systemDefined?: boolean; + readOnly?: boolean; canChangeTaxonomy?: boolean; canDeleteTaxonomy?: boolean; }> = ({ iconMenu, - systemDefined = false, + readOnly = false, canChangeTaxonomy = true, canDeleteTaxonomy = true, }) => { @@ -50,7 +50,7 @@ const TaxonomyMenuComponent: React.FC<{ id: taxonomyId, name: taxonomyName, tagsCount: 0, - systemDefined, + readOnly, canChangeTaxonomy, canDeleteTaxonomy, }} @@ -120,7 +120,7 @@ describe.each([true, false])('', (iconMenu) => { const { findByTestId, queryByTestId } = render( , @@ -136,12 +136,12 @@ describe.each([true, false])('', (iconMenu) => { expect(queryByTestId('taxonomy-menu-delete')).not.toBeInTheDocument(); }); - test('Hides import/delete actions for system-defined taxonomies', () => { - const systemDefined = true; + test('Hides import/delete actions for read-only taxonomies', () => { + const readOnly = true; const { getByTestId, queryByTestId } = render( , ); diff --git a/src/taxonomy/taxonomy-menu/TaxonomyMenu.jsx b/src/taxonomy/taxonomy-menu/TaxonomyMenu.tsx similarity index 78% rename from src/taxonomy/taxonomy-menu/TaxonomyMenu.jsx rename to src/taxonomy/taxonomy-menu/TaxonomyMenu.tsx index 2714f290dd..0b1d552494 100644 --- a/src/taxonomy/taxonomy-menu/TaxonomyMenu.jsx +++ b/src/taxonomy/taxonomy-menu/TaxonomyMenu.tsx @@ -1,4 +1,3 @@ -// @ts-check import React, { useCallback, useContext } from 'react'; import { useIntl } from '@edx/frontend-platform/i18n'; import { @@ -9,8 +8,7 @@ import { IconButton, } from '@openedx/paragon'; import { MoreVert } from '@openedx/paragon/icons'; -import { pickBy } from 'lodash'; -import PropTypes from 'prop-types'; +import pickBy from 'lodash/pickBy'; import { useNavigate } from 'react-router-dom'; import ExportModal from '../export-modal'; @@ -20,21 +18,30 @@ import DeleteDialog from '../delete-dialog'; import { ImportTagsWizard } from '../import-tags'; import { ManageOrgsModal } from '../manage-orgs'; import messages from './messages'; +import type { TaxonomyData } from '../data/types'; -/** @typedef {import('../data/types.js').TaxonomyData} TaxonomyData */ // Note: to make mocking easier for tests, the types below only specify the subset of TaxonomyData that we actually use. +interface Props { + taxonomy: Pick; + iconMenu?: boolean; +} + +interface MenuItem { + /** The title of the menu item */ + title: string; + /** The action to perform when the menu item is clicked */ + action: () => void; + /** Whether or not to show the menu item */ + show?: boolean; +} /** * A menu that provides actions for editing a specific taxonomy. - * @type {React.FC<{ - * taxonomy: Pick, - * iconMenu?: boolean - * }>} */ const TaxonomyMenu = ({ taxonomy, - iconMenu, -}) => { + iconMenu = false, +}: Props) => { const intl = useIntl(); const navigate = useNavigate(); @@ -61,20 +68,11 @@ const TaxonomyMenu = ({ const [isImportModalOpen, importModalOpen, importModalClose] = useToggle(false); const [isManageOrgsModalOpen, manageOrgsModalOpen, manageOrgsModalClose] = useToggle(false); - /** - * @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} [show] - Whether or not to show the menu item - * - * @constant - * @type {Record} - */ - let menuItems = { + let menuItems: Record = { import: { title: intl.formatMessage(messages.importMenu), action: importModalOpen, - show: taxonomy.canChangeTaxonomy && !taxonomy.systemDefined, + show: taxonomy.canChangeTaxonomy && !taxonomy.readOnly, }, export: { title: intl.formatMessage(messages.exportMenu), @@ -84,7 +82,7 @@ const TaxonomyMenu = ({ delete: { title: intl.formatMessage(messages.deleteMenu), action: deleteDialogOpen, - show: taxonomy.canDeleteTaxonomy && !taxonomy.systemDefined, + show: taxonomy.canDeleteTaxonomy && !taxonomy.readOnly, }, manageOrgs: { title: intl.formatMessage(messages.manageOrgsMenu), @@ -174,20 +172,4 @@ const TaxonomyMenu = ({ ); }; -TaxonomyMenu.propTypes = { - taxonomy: PropTypes.shape({ - id: PropTypes.number.isRequired, - name: PropTypes.string.isRequired, - tagsCount: PropTypes.number.isRequired, - systemDefined: PropTypes.bool.isRequired, - canChangeTaxonomy: PropTypes.bool.isRequired, - canDeleteTaxonomy: PropTypes.bool.isRequired, - }).isRequired, - iconMenu: PropTypes.bool, -}; - -TaxonomyMenu.defaultProps = { - iconMenu: false, -}; - export default TaxonomyMenu;