diff --git a/src/content-tags-drawer/ContentTagsDrawer.jsx b/src/content-tags-drawer/ContentTagsDrawer.jsx
deleted file mode 100644
index 117ffc3947..0000000000
--- a/src/content-tags-drawer/ContentTagsDrawer.jsx
+++ /dev/null
@@ -1,263 +0,0 @@
-// @ts-check
-import React, { useContext, useEffect } from 'react';
-import PropTypes from 'prop-types';
-import {
- Container,
- Spinner,
- Stack,
- Button,
- Toast,
-} from '@openedx/paragon';
-import { useIntl, FormattedMessage } from '@edx/frontend-platform/i18n';
-import { useParams, useNavigate } from 'react-router-dom';
-import messages from './messages';
-import ContentTagsCollapsible from './ContentTagsCollapsible';
-import Loading from '../generic/Loading';
-import useContentTagsDrawerContext from './ContentTagsDrawerHelper';
-import { ContentTagsDrawerContext, ContentTagsDrawerSheetContext } from './common/context';
-
-const TaxonomyList = ({ contentId }) => {
- const navigate = useNavigate();
- const intl = useIntl();
-
- const {
- isTaxonomyListLoaded,
- isContentTaxonomyTagsLoaded,
- tagsByTaxonomy,
- stagedContentTags,
- collapsibleStates,
- } = React.useContext(ContentTagsDrawerContext);
-
- if (isTaxonomyListLoaded && isContentTaxonomyTagsLoaded) {
- if (tagsByTaxonomy.length !== 0) {
- return (
-
- { tagsByTaxonomy.map((data) => (
-
-
-
-
- ))}
-
- );
- }
-
- return (
- navigate('/taxonomies')}
- >
- { intl.formatMessage(messages.emptyDrawerContentLink) }
-
- ),
- }}
- />
- );
- }
-
- return ;
-};
-
-TaxonomyList.propTypes = {
- contentId: PropTypes.string.isRequired,
-};
-
-/**
- * Drawer with the functionality to show and manage tags in a certain content.
- * It is used both in interfaces of this MFE and in edx-platform interfaces such as iframe.
- * - If you want to use it as an iframe, the component obtains the `contentId` from the url parameters.
- * Functions to close the drawer are handled internally.
- * TODO: We can delete this method when is no longer used on edx-platform.
- * - If you want to use it as react component, you need to pass the content id and the close functions
- * through the component parameters.
- */
-const ContentTagsDrawer = ({ id, onClose }) => {
- const intl = useIntl();
- // TODO: We can delete 'params' when the iframe is no longer used on edx-platform
- const params = useParams();
- const contentId = id ?? params.contentId;
-
- const context = useContentTagsDrawerContext(contentId);
- const { blockingSheet } = useContext(ContentTagsDrawerSheetContext);
-
- const {
- showToastAfterSave,
- toReadMode,
- commitGlobalStagedTagsStatus,
- isContentDataLoaded,
- contentName,
- isTaxonomyListLoaded,
- isContentTaxonomyTagsLoaded,
- stagedContentTags,
- collapsibleStates,
- isEditMode,
- commitGlobalStagedTags,
- toEditMode,
- toastMessage,
- closeToast,
- setCollapsibleToInitalState,
- otherTaxonomies,
- } = context;
-
- let onCloseDrawer = onClose;
- if (onCloseDrawer === undefined) {
- onCloseDrawer = () => {
- // "*" allows communication with any origin
- window.parent.postMessage('closeManageTagsDrawer', '*');
- };
- }
-
- useEffect(() => {
- const handleEsc = (event) => {
- /* Close drawer when ESC-key is pressed and selectable dropdown box not open */
- const selectableBoxOpen = document.querySelector('[data-selectable-box="taxonomy-tags"]');
- if (event.key === 'Escape' && !selectableBoxOpen && !blockingSheet) {
- onCloseDrawer();
- }
- };
- document.addEventListener('keydown', handleEsc);
-
- return () => {
- document.removeEventListener('keydown', handleEsc);
- };
- }, [blockingSheet]);
-
- useEffect(() => {
- /* istanbul ignore next */
- if (commitGlobalStagedTagsStatus === 'success') {
- showToastAfterSave();
- toReadMode();
- }
- }, [commitGlobalStagedTagsStatus]);
-
- // First call of the initial collapsible states
- React.useEffect(() => {
- setCollapsibleToInitalState();
- }, [isTaxonomyListLoaded, isContentTaxonomyTagsLoaded]);
-
- return (
-
-
-
- { isContentDataLoaded
- ? { contentName }
- : (
-
-
-
- )}
-
-
-
- {intl.formatMessage(messages.headerSubtitle)}
-
-
- {otherTaxonomies.length !== 0 && (
-
-
- {intl.formatMessage(messages.otherTagsHeader)}
-
-
- {intl.formatMessage(messages.otherTagsDescription)}
-
- { isTaxonomyListLoaded && isContentTaxonomyTagsLoaded && (
- otherTaxonomies.map((data) => (
-
-
-
-
- ))
- )}
-
- )}
-
-
-
- { isTaxonomyListLoaded && isContentTaxonomyTagsLoaded && (
-
-
- { commitGlobalStagedTagsStatus !== 'loading' ? (
-
-
- { intl.formatMessage(isEditMode
- ? messages.tagsDrawerCancelButtonText
- : messages.tagsDrawerCloseButtonText)}
-
-
- { intl.formatMessage(isEditMode
- ? messages.tagsDrawerSaveButtonText
- : messages.tagsDrawerEditTagsButtonText)}
-
-
- )
- : (
-
- )}
-
-
- )}
- {/* istanbul ignore next */
- toastMessage && (
-
- {toastMessage}
-
- )
- }
-
-
- );
-};
-
-ContentTagsDrawer.propTypes = {
- id: PropTypes.string,
- onClose: PropTypes.func,
-};
-
-ContentTagsDrawer.defaultProps = {
- id: undefined,
- onClose: undefined,
-};
-
-export default ContentTagsDrawer;
diff --git a/src/content-tags-drawer/ContentTagsDrawer.test.jsx b/src/content-tags-drawer/ContentTagsDrawer.test.jsx
index 8f2e517c35..8abd78e1fb 100644
--- a/src/content-tags-drawer/ContentTagsDrawer.test.jsx
+++ b/src/content-tags-drawer/ContentTagsDrawer.test.jsx
@@ -1,589 +1,141 @@
-import React from 'react';
-import { IntlProvider } from '@edx/frontend-platform/i18n';
-import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import {
act,
fireEvent,
+ initializeMocks,
render,
waitFor,
screen,
within,
-} from '@testing-library/react';
-
+} from '../testUtils';
import ContentTagsDrawer from './ContentTagsDrawer';
-import {
- useContentTaxonomyTagsData,
- useContentData,
- useTaxonomyTagsData,
- useContentTaxonomyTagsUpdater,
-} from './data/apiHooks';
-import { getTaxonomyListData } from '../taxonomy/data/api';
import messages from './messages';
import { ContentTagsDrawerSheetContext } from './common/context';
-import { languageExportId } from './utils';
-
-const contentId = 'block-v1:SampleTaxonomyOrg1+STC1+2023_1+type@vertical+block@7f47fe2dbcaf47c5a071671c741fe1ab';
+import {
+ mockContentData,
+ mockContentTaxonomyTagsData,
+ mockTaxonomyListData,
+ mockTaxonomyTagsData,
+} from './data/api.mocks';
+import { getContentTaxonomyTagsApiUrl } from './data/api';
+
+const path = '/content/:contentId/*';
const mockOnClose = jest.fn();
-const mockMutate = jest.fn();
const mockSetBlockingSheet = jest.fn();
const mockNavigate = jest.fn();
+mockContentTaxonomyTagsData.applyMock();
+mockTaxonomyListData.applyMock();
+mockTaxonomyTagsData.applyMock();
+mockContentData.applyMock();
+
+const {
+ stagedTagsId,
+ otherTagsId,
+ languageWithTagsId,
+ languageWithoutTagsId,
+ largeTagsId,
+ emptyTagsId,
+} = mockContentTaxonomyTagsData;
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
- useParams: () => ({
- contentId,
- }),
useNavigate: () => mockNavigate,
}));
-// FIXME: replace these mocks with API mocks
-jest.mock('./data/apiHooks', () => ({
- useContentTaxonomyTagsData: jest.fn(() => {}),
- useContentData: jest.fn(() => ({
- isSuccess: false,
- data: {},
- })),
- useContentTaxonomyTagsUpdater: jest.fn(() => ({
- isError: false,
- mutate: mockMutate,
- })),
- useTaxonomyTagsData: jest.fn(() => ({
- hasMorePages: false,
- tagPages: {
- isLoading: true,
- isError: false,
- canAddTag: false,
- data: [],
- },
- })),
-}));
-
-jest.mock('../taxonomy/data/api', () => ({
- // By default, the mock taxonomy list will never load (promise never resolves):
- getTaxonomyListData: jest.fn(),
-}));
-
-const queryClient = new QueryClient();
-
-const RootWrapper = (params) => (
-
-
-
-
-
-
-
+const renderDrawer = (contentId, drawerParams = {}) => (
+ render(
+
+
+ ,
+ { path, params: { contentId } },
+ )
);
describe(' ', () => {
beforeEach(async () => {
- jest.clearAllMocks();
- await queryClient.resetQueries();
- // By default, we mock the API call with a promise that never resolves.
- // You can override this in specific test.
- getTaxonomyListData.mockReturnValue(new Promise(() => {}));
- useContentTaxonomyTagsUpdater.mockReturnValue({
- isError: false,
- mutate: mockMutate,
- });
+ initializeMocks();
});
- const setupMockDataForStagedTagsTesting = () => {
- useContentTaxonomyTagsData.mockReturnValue({
- isSuccess: true,
- data: {
- taxonomies: [
- {
- name: 'Taxonomy 1',
- taxonomyId: 123,
- canTagObject: true,
- tags: [
- {
- value: 'Tag 1',
- lineage: ['Tag 1'],
- canDeleteObjecttag: true,
- },
- {
- value: 'Tag 2',
- lineage: ['Tag 2'],
- canDeleteObjecttag: true,
- },
- ],
- },
- ],
- },
- });
- getTaxonomyListData.mockResolvedValue({
- results: [
- {
- id: 123,
- name: 'Taxonomy 1',
- description: 'This is a description 1',
- canTagObject: true,
- },
- ],
- });
-
- useTaxonomyTagsData.mockReturnValue({
- hasMorePages: false,
- canAddTag: false,
- tagPages: {
- isLoading: false,
- isError: false,
- data: [{
- value: 'Tag 1',
- externalId: null,
- childCount: 0,
- depth: 0,
- parentValue: null,
- id: 12345,
- subTagsUrl: null,
- canChangeTag: false,
- canDeleteTag: false,
- }, {
- value: 'Tag 2',
- externalId: null,
- childCount: 0,
- depth: 0,
- parentValue: null,
- id: 12346,
- subTagsUrl: null,
- canChangeTag: false,
- canDeleteTag: false,
- }, {
- value: 'Tag 3',
- externalId: null,
- childCount: 0,
- depth: 0,
- parentValue: null,
- id: 12347,
- subTagsUrl: null,
- canChangeTag: false,
- canDeleteTag: false,
- }],
- },
- });
- };
-
- const setupMockDataWithOtherTagsTestings = () => {
- useContentTaxonomyTagsData.mockReturnValue({
- isSuccess: true,
- data: {
- taxonomies: [
- {
- name: 'Taxonomy 1',
- taxonomyId: 123,
- canTagObject: true,
- tags: [
- {
- value: 'Tag 1',
- lineage: ['Tag 1'],
- canDeleteObjecttag: true,
- },
- {
- value: 'Tag 2',
- lineage: ['Tag 2'],
- canDeleteObjecttag: true,
- },
- ],
- },
- {
- name: 'Taxonomy 2',
- taxonomyId: 1234,
- canTagObject: false,
- tags: [
- {
- value: 'Tag 3',
- lineage: ['Tag 3'],
- canDeleteObjecttag: true,
- },
- {
- value: 'Tag 4',
- lineage: ['Tag 4'],
- canDeleteObjecttag: true,
- },
- ],
- },
- ],
- },
- });
- getTaxonomyListData.mockResolvedValue({
- results: [
- {
- id: 123,
- name: 'Taxonomy 1',
- description: 'This is a description 1',
- canTagObject: true,
- },
- ],
- });
-
- useTaxonomyTagsData.mockReturnValue({
- hasMorePages: false,
- canAddTag: false,
- tagPages: {
- isLoading: false,
- isError: false,
- data: [{
- value: 'Tag 1',
- externalId: null,
- childCount: 0,
- depth: 0,
- parentValue: null,
- id: 12345,
- subTagsUrl: null,
- canChangeTag: false,
- canDeleteTag: false,
- }, {
- value: 'Tag 2',
- externalId: null,
- childCount: 0,
- depth: 0,
- parentValue: null,
- id: 12346,
- subTagsUrl: null,
- canChangeTag: false,
- canDeleteTag: false,
- }, {
- value: 'Tag 3',
- externalId: null,
- childCount: 0,
- depth: 0,
- parentValue: null,
- id: 12347,
- subTagsUrl: null,
- canChangeTag: false,
- canDeleteTag: false,
- }],
- },
- });
- };
-
- const setupMockDataLanguageTaxonomyTestings = (hasTags) => {
- useContentTaxonomyTagsData.mockReturnValue({
- isSuccess: true,
- data: {
- taxonomies: [
- {
- name: 'Languages',
- taxonomyId: 123,
- exportId: languageExportId,
- canTagObject: true,
- tags: hasTags ? [
- {
- value: 'Tag 1',
- lineage: ['Tag 1'],
- canDeleteObjecttag: true,
- },
- ] : [],
- },
- {
- name: 'Taxonomy 1',
- taxonomyId: 1234,
- canTagObject: true,
- tags: [
- {
- value: 'Tag 1',
- lineage: ['Tag 1'],
- canDeleteObjecttag: true,
- },
- {
- value: 'Tag 2',
- lineage: ['Tag 2'],
- canDeleteObjecttag: true,
- },
- ],
- },
- ],
- },
- });
- getTaxonomyListData.mockResolvedValue({
- results: [
- {
- id: 123,
- name: 'Languages',
- description: 'This is a description 1',
- exportId: languageExportId,
- canTagObject: true,
- },
- {
- id: 1234,
- name: 'Taxonomy 1',
- description: 'This is a description 2',
- canTagObject: true,
- },
- ],
- });
-
- useTaxonomyTagsData.mockReturnValue({
- hasMorePages: false,
- canAddTag: false,
- tagPages: {
- isLoading: false,
- isError: false,
- data: [{
- value: 'Tag 1',
- externalId: null,
- childCount: 0,
- depth: 0,
- parentValue: null,
- id: 12345,
- subTagsUrl: null,
- canChangeTag: false,
- canDeleteTag: false,
- }],
- },
- });
- };
-
- const setupLargeMockDataForStagedTagsTesting = () => {
- useContentTaxonomyTagsData.mockReturnValue({
- isSuccess: true,
- data: {
- taxonomies: [
- {
- name: 'Taxonomy 1',
- taxonomyId: 123,
- canTagObject: true,
- tags: [
- {
- value: 'Tag 1',
- lineage: ['Tag 1'],
- canDeleteObjecttag: true,
- },
- {
- value: 'Tag 2',
- lineage: ['Tag 2'],
- canDeleteObjecttag: true,
- },
- ],
- },
- {
- name: 'Taxonomy 2',
- taxonomyId: 124,
- canTagObject: true,
- tags: [
- {
- value: 'Tag 1',
- lineage: ['Tag 1'],
- canDeleteObjecttag: true,
- },
- ],
- },
- {
- name: 'Taxonomy 3',
- taxonomyId: 125,
- canTagObject: true,
- tags: [
- {
- value: 'Tag 1.1.1',
- lineage: ['Tag 1', 'Tag 1.1', 'Tag 1.1.1'],
- canDeleteObjecttag: true,
- },
- ],
- },
- {
- name: '(B) Taxonomy 4',
- taxonomyId: 126,
- canTagObject: true,
- tags: [],
- },
- {
- name: '(A) Taxonomy 5',
- taxonomyId: 127,
- canTagObject: true,
- tags: [],
- },
- ],
- },
- });
- getTaxonomyListData.mockResolvedValue({
- results: [
- {
- id: 123,
- name: 'Taxonomy 1',
- description: 'This is a description 1',
- canTagObject: true,
- },
- {
- id: 124,
- name: 'Taxonomy 2',
- description: 'This is a description 2',
- canTagObject: true,
- },
- {
- id: 125,
- name: 'Taxonomy 3',
- description: 'This is a description 3',
- canTagObject: true,
- },
- {
- id: 127,
- name: '(A) Taxonomy 5',
- description: 'This is a description 5',
- canTagObject: true,
- },
- {
- id: 126,
- name: '(B) Taxonomy 4',
- description: 'This is a description 4',
- canTagObject: true,
- },
- ],
- });
-
- useTaxonomyTagsData.mockReturnValue({
- hasMorePages: false,
- canAddTag: false,
- tagPages: {
- isLoading: false,
- isError: false,
- data: [{
- value: 'Tag 1',
- externalId: null,
- childCount: 0,
- depth: 0,
- parentValue: null,
- id: 12345,
- subTagsUrl: null,
- canChangeTag: false,
- canDeleteTag: false,
- }, {
- value: 'Tag 2',
- externalId: null,
- childCount: 0,
- depth: 0,
- parentValue: null,
- id: 12346,
- subTagsUrl: null,
- canChangeTag: false,
- canDeleteTag: false,
- }, {
- value: 'Tag 3',
- externalId: null,
- childCount: 0,
- depth: 0,
- parentValue: null,
- id: 12347,
- subTagsUrl: null,
- canChangeTag: false,
- canDeleteTag: false,
- }],
- },
- });
- };
+ afterEach(() => {
+ jest.clearAllMocks();
+ });
it('should render page and page title correctly', () => {
- setupMockDataForStagedTagsTesting();
- const { getByText } = render( );
- expect(getByText('Manage tags')).toBeInTheDocument();
+ renderDrawer(stagedTagsId);
+ expect(screen.getByText('Manage tags')).toBeInTheDocument();
});
it('shows spinner before the content data query is complete', async () => {
await act(async () => {
- const { getAllByRole } = render( );
- const spinner = getAllByRole('status')[0];
+ renderDrawer(stagedTagsId);
+ const spinner = screen.getAllByRole('status')[0];
expect(spinner.textContent).toEqual('Loading'); // Uses
});
});
it('shows spinner before the taxonomy tags query is complete', async () => {
await act(async () => {
- const { getAllByRole } = render( );
- const spinner = getAllByRole('status')[1];
+ renderDrawer(stagedTagsId);
+ const spinner = screen.getAllByRole('status')[1];
expect(spinner.textContent).toEqual('Loading...'); // Uses
});
});
- it('shows the content display name after the query is complete', async () => {
- useContentData.mockReturnValue({
- isSuccess: true,
- data: {
- displayName: 'Unit 1',
- },
- });
- await act(async () => {
- const { getByText } = render( );
- expect(getByText('Unit 1')).toBeInTheDocument();
- });
+ it('shows the content display name after the query is complete in drawer variant', async () => {
+ renderDrawer('test');
+ expect(await screen.findByText('Loading...')).toBeInTheDocument();
+ expect(await screen.findByText('Unit 1')).toBeInTheDocument();
+ expect(await screen.findByText('Manage tags')).toBeInTheDocument();
+ });
+
+ it('shows the content display name after the query is complete in component variant', async () => {
+ renderDrawer('test', { variant: 'component' });
+ expect(await screen.findByText('Loading...')).toBeInTheDocument();
+ expect(screen.queryByText('Unit 1')).not.toBeInTheDocument();
+ expect(screen.queryByText('Manage tags')).not.toBeInTheDocument();
});
it('shows content using params', async () => {
- useContentData.mockReturnValue({
- isSuccess: true,
- data: {
- displayName: 'Unit 1',
- },
- });
- render( );
- expect(screen.getByText('Unit 1')).toBeInTheDocument();
+ renderDrawer(undefined, { id: 'test' });
+ expect(await screen.findByText('Loading...')).toBeInTheDocument();
+ expect(await screen.findByText('Unit 1')).toBeInTheDocument();
+ expect(await screen.findByText('Manage tags')).toBeInTheDocument();
});
it('shows the taxonomies data including tag numbers after the query is complete', async () => {
- useContentTaxonomyTagsData.mockReturnValue({
- isSuccess: true,
- data: {
- taxonomies: [
- {
- name: 'Taxonomy 1',
- taxonomyId: 123,
- canTagObject: true,
- tags: [
- {
- value: 'Tag 1',
- lineage: ['Tag 1'],
- canDeleteObjecttag: true,
- },
- {
- value: 'Tag 2',
- lineage: ['Tag 2'],
- canDeleteObjecttag: true,
- },
- ],
- },
- {
- name: 'Taxonomy 2',
- taxonomyId: 124,
- canTagObject: true,
- tags: [
- {
- value: 'Tag 3',
- lineage: ['Tag 3'],
- canDeleteObjecttag: true,
- },
- ],
- },
- ],
- },
- });
- getTaxonomyListData.mockResolvedValue({
- results: [{
- id: 123,
- name: 'Taxonomy 1',
- description: 'This is a description 1',
- canTagObject: false,
- }, {
- id: 124,
- name: 'Taxonomy 2',
- description: 'This is a description 2',
- canTagObject: false,
- }],
- });
await act(async () => {
- const { container, getByText } = render( );
- await waitFor(() => { expect(getByText('Taxonomy 1')).toBeInTheDocument(); });
- expect(getByText('Taxonomy 1')).toBeInTheDocument();
- expect(getByText('Taxonomy 2')).toBeInTheDocument();
+ const { container } = renderDrawer(largeTagsId);
+ await waitFor(() => { expect(screen.getByText('Taxonomy 1')).toBeInTheDocument(); });
+ expect(screen.getByText('Taxonomy 1')).toBeInTheDocument();
+ expect(screen.getByText('Taxonomy 2')).toBeInTheDocument();
const tagCountBadges = container.getElementsByClassName('taxonomy-tags-count-chip');
- expect(tagCountBadges[0].textContent).toBe('2');
- expect(tagCountBadges[1].textContent).toBe('1');
+ expect(tagCountBadges[0].textContent).toBe('3');
+ expect(tagCountBadges[1].textContent).toBe('2');
});
});
- it('should be read only on first render', async () => {
- setupMockDataForStagedTagsTesting();
- render( );
+ it('should be read only on first render on drawer variant', async () => {
+ renderDrawer(stagedTagsId);
+ expect(await screen.findByText('Taxonomy 1')).toBeInTheDocument();
+ expect(screen.getByRole('button', { name: /close/i }));
+ expect(screen.getByRole('button', { name: /edit tags/i }));
+
+ // Not show delete tag buttons
+ expect(screen.queryByRole('button', { name: /delete/i })).not.toBeInTheDocument();
+
+ // Not show add a tag select
+ expect(screen.queryByText(/add a tag/i)).not.toBeInTheDocument();
+
+ // Not show cancel button
+ expect(screen.queryByRole('button', { name: /cancel/i })).not.toBeInTheDocument();
+
+ // Not show save button
+ expect(screen.queryByRole('button', { name: /save/i })).not.toBeInTheDocument();
+ });
+
+ it('should be read only on first render on component variant', async () => {
+ renderDrawer(stagedTagsId, { variant: 'component' });
expect(await screen.findByText('Taxonomy 1')).toBeInTheDocument();
+ expect(screen.getByRole('button', { name: /manage tags/i }));
// Not show delete tag buttons
expect(screen.queryByRole('button', { name: /delete/i })).not.toBeInTheDocument();
@@ -598,9 +150,8 @@ describe(' ', () => {
expect(screen.queryByRole('button', { name: /save/i })).not.toBeInTheDocument();
});
- it('should change to edit mode when click on `Edit tags`', async () => {
- setupMockDataForStagedTagsTesting();
- render( );
+ it('should change to edit mode when click on `Edit tags` on drawer variant', async () => {
+ renderDrawer(stagedTagsId);
expect(await screen.findByText('Taxonomy 1')).toBeInTheDocument();
const editTagsButton = screen.getByRole('button', {
name: /edit tags/i,
@@ -622,9 +173,31 @@ describe(' ', () => {
expect(screen.getByRole('button', { name: /save/i })).toBeInTheDocument();
});
- it('should change to read mode when click on `Cancel`', async () => {
- setupMockDataForStagedTagsTesting();
- render( );
+ it('should change to edit mode when click on `Manage tags` on component variant', async () => {
+ renderDrawer(stagedTagsId, { variant: 'component' });
+ expect(await screen.findByText('Taxonomy 1')).toBeInTheDocument();
+ const manageTagsButton = screen.getByRole('button', {
+ name: /manage tags/i,
+ });
+ fireEvent.click(manageTagsButton);
+
+ // Show delete tag buttons
+ expect(screen.getAllByRole('button', {
+ name: /delete/i,
+ }).length).toBe(2);
+
+ // Show add a tag select
+ expect(screen.getByText(/add a tag/i)).toBeInTheDocument();
+
+ // Show cancel button
+ expect(screen.getByRole('button', { name: /cancel/i })).toBeInTheDocument();
+
+ // Show save button
+ expect(screen.getByRole('button', { name: /save/i })).toBeInTheDocument();
+ });
+
+ it('should change to read mode when click on `Cancel` on drawer variant', async () => {
+ renderDrawer(stagedTagsId);
expect(await screen.findByText('Taxonomy 1')).toBeInTheDocument();
const editTagsButton = screen.getByRole('button', {
name: /edit tags/i,
@@ -649,21 +222,34 @@ describe(' ', () => {
expect(screen.queryByRole('button', { name: /save/i })).not.toBeInTheDocument();
});
- it('shows spinner when loading commit tags', async () => {
- setupMockDataForStagedTagsTesting();
- useContentTaxonomyTagsUpdater.mockReturnValue({
- status: 'loading',
- isError: false,
- mutate: mockMutate,
- });
- render( );
+ it('should change to read mode when click on `Cancel` on component variant', async () => {
+ renderDrawer(stagedTagsId, { variant: 'component' });
expect(await screen.findByText('Taxonomy 1')).toBeInTheDocument();
- expect(screen.getByRole('status')).toBeInTheDocument();
+ const manageTagsButton = screen.getByRole('button', {
+ name: /manage tags/i,
+ });
+ fireEvent.click(manageTagsButton);
+
+ const cancelButton = screen.getByRole('button', {
+ name: /cancel/i,
+ });
+ fireEvent.click(cancelButton);
+
+ // Not show delete tag buttons
+ expect(screen.queryByRole('button', { name: /delete/i })).not.toBeInTheDocument();
+
+ // Not show add a tag select
+ expect(screen.queryByText(/add a tag/i)).not.toBeInTheDocument();
+
+ // Not show cancel button
+ expect(screen.queryByRole('button', { name: /cancel/i })).not.toBeInTheDocument();
+
+ // Not show save button
+ expect(screen.queryByRole('button', { name: /save/i })).not.toBeInTheDocument();
});
it('should test adding a content tag to the staged tags for a taxonomy', async () => {
- setupMockDataForStagedTagsTesting();
- render( );
+ renderDrawer(stagedTagsId);
expect(await screen.findByText('Taxonomy 1')).toBeInTheDocument();
// To edit mode
@@ -678,7 +264,7 @@ describe(' ', () => {
fireEvent.mouseDown(addTagsButton);
// Tag 3 should only appear in dropdown selector, (i.e. the dropdown is open, since Tag 3 is not applied)
- expect(screen.getAllByText('Tag 3').length).toBe(1);
+ expect((await screen.findAllByText('Tag 3')).length).toBe(1);
// Click to check Tag 3
const tag3 = screen.getByText('Tag 3');
@@ -689,8 +275,7 @@ describe(' ', () => {
});
it('should test removing a staged content from a taxonomy', async () => {
- setupMockDataForStagedTagsTesting();
- render( );
+ renderDrawer(stagedTagsId);
expect(await screen.findByText('Taxonomy 1')).toBeInTheDocument();
// To edit mode
@@ -705,7 +290,7 @@ describe(' ', () => {
fireEvent.mouseDown(addTagsButton);
// Tag 3 should only appear in dropdown selector, (i.e. the dropdown is open, since Tag 3 is not applied)
- expect(screen.getAllByText('Tag 3').length).toBe(1);
+ expect((await screen.findAllByText('Tag 3')).length).toBe(1);
// Click to check Tag 3
const tag3 = screen.getByText('Tag 3');
@@ -720,11 +305,9 @@ describe(' ', () => {
});
it('should test clearing staged tags for a taxonomy', async () => {
- setupMockDataForStagedTagsTesting();
-
const {
container,
- } = render( );
+ } = renderDrawer(stagedTagsId);
expect(await screen.findByText('Taxonomy 1')).toBeInTheDocument();
// To edit mode
@@ -739,7 +322,7 @@ describe(' ', () => {
fireEvent.mouseDown(addTagsButton);
// Tag 3 should only appear in dropdown selector, (i.e. the dropdown is open, since Tag 3 is not applied)
- expect(screen.getAllByText('Tag 3').length).toBe(1);
+ expect((await screen.findAllByText('Tag 3')).length).toBe(1);
// Click to check Tag 3
const tag3 = screen.getByText('Tag 3');
@@ -758,8 +341,7 @@ describe(' ', () => {
});
it('should test adding global staged tags and cancel', async () => {
- setupMockDataForStagedTagsTesting();
- render( );
+ renderDrawer(stagedTagsId);
expect(await screen.findByText('Taxonomy 1')).toBeInTheDocument();
// To edit mode
@@ -774,7 +356,7 @@ describe(' ', () => {
fireEvent.mouseDown(addTagsButton);
// Click to check Tag 3
- const tag3 = screen.getByText(/tag 3/i);
+ const tag3 = await screen.findByText(/tag 3/i);
fireEvent.click(tag3);
// Click "Add tags" to save to global staged tags
@@ -791,8 +373,7 @@ describe(' ', () => {
});
it('should test delete feched tags and cancel', async () => {
- setupMockDataForStagedTagsTesting();
- render( );
+ renderDrawer(stagedTagsId);
expect(await screen.findByText('Taxonomy 1')).toBeInTheDocument();
// To edit mode
@@ -802,7 +383,7 @@ describe(' ', () => {
fireEvent.click(editTagsButton);
// Delete the tag
- const tag = screen.getByText(/tag 2/i);
+ const tag = await screen.findByText(/tag 2/i);
const deleteButton = within(tag).getByRole('button', {
name: /delete/i,
});
@@ -818,8 +399,7 @@ describe(' ', () => {
});
it('should test delete global staged tags and cancel', async () => {
- setupMockDataForStagedTagsTesting();
- render( );
+ renderDrawer(stagedTagsId);
expect(await screen.findByText('Taxonomy 1')).toBeInTheDocument();
// To edit mode
@@ -834,7 +414,7 @@ describe(' ', () => {
fireEvent.mouseDown(addTagsButton);
// Click to check Tag 3
- const tag3 = screen.getByText(/tag 3/i);
+ const tag3 = await screen.findByText(/tag 3/i);
fireEvent.click(tag3);
// Click "Add tags" to save to global staged tags
@@ -860,8 +440,7 @@ describe(' ', () => {
});
it('should test add removed feched tags and cancel', async () => {
- setupMockDataForStagedTagsTesting();
- render( );
+ renderDrawer(stagedTagsId);
expect(await screen.findByText('Taxonomy 1')).toBeInTheDocument();
// To edit mode
@@ -871,7 +450,7 @@ describe(' ', () => {
fireEvent.click(editTagsButton);
// Delete the tag
- const tag = screen.getByText(/tag 2/i);
+ const tag = await screen.findByText(/tag 2/i);
const deleteButton = within(tag).getByRole('button', {
name: /delete/i,
});
@@ -885,7 +464,7 @@ describe(' ', () => {
fireEvent.mouseDown(addTagsButton);
// Click to check Tag 2
- const tag2 = screen.getByText(/tag 2/i);
+ const tag2 = await screen.findByText(/tag 2/i);
fireEvent.click(tag2);
// Click "Add tags" to save to global staged tags
@@ -902,8 +481,7 @@ describe(' ', () => {
});
it('should call onClose when cancel is clicked', async () => {
- setupMockDataForStagedTagsTesting();
- render( );
+ renderDrawer(stagedTagsId, { onClose: mockOnClose });
const cancelButton = await screen.findByRole('button', {
name: /close/i,
@@ -917,7 +495,7 @@ describe(' ', () => {
it('should call closeManageTagsDrawer when Escape key is pressed and no selectable box is active', () => {
const postMessageSpy = jest.spyOn(window.parent, 'postMessage');
- const { container } = render( );
+ const { container } = renderDrawer(stagedTagsId);
fireEvent.keyDown(container, {
key: 'Escape',
@@ -929,7 +507,7 @@ describe(' ', () => {
});
it('should call `onClose` when Escape key is pressed and no selectable box is active', () => {
- const { container } = render( );
+ const { container } = renderDrawer(stagedTagsId, { onClose: mockOnClose });
fireEvent.keyDown(container, {
key: 'Escape',
@@ -941,7 +519,7 @@ describe(' ', () => {
it('should not call closeManageTagsDrawer when Escape key is pressed and a selectable box is active', () => {
const postMessageSpy = jest.spyOn(window.parent, 'postMessage');
- const { container } = render( );
+ const { container } = renderDrawer(stagedTagsId);
// Simulate that the selectable box is open by adding an element with the data attribute
const selectableBox = document.createElement('div');
@@ -961,7 +539,7 @@ describe(' ', () => {
});
it('should not call `onClose` when Escape key is pressed and a selectable box is active', () => {
- const { container } = render( );
+ const { container } = renderDrawer(stagedTagsId, { onClose: mockOnClose });
// Simulate that the selectable box is open by adding an element with the data attribute
const selectableBox = document.createElement('div');
@@ -980,8 +558,7 @@ describe(' ', () => {
it('should not call closeManageTagsDrawer when Escape key is pressed and container is blocked', () => {
const postMessageSpy = jest.spyOn(window.parent, 'postMessage');
-
- const { container } = render( );
+ const { container } = renderDrawer(stagedTagsId, { blockingSheet: true });
fireEvent.keyDown(container, {
key: 'Escape',
});
@@ -992,7 +569,10 @@ describe(' ', () => {
});
it('should not call `onClose` when Escape key is pressed and container is blocked', () => {
- const { container } = render( );
+ const { container } = renderDrawer(stagedTagsId, {
+ blockingSheet: true,
+ onClose: mockOnClose,
+ });
fireEvent.keyDown(container, {
key: 'Escape',
});
@@ -1001,8 +581,10 @@ describe(' ', () => {
});
it('should call `setBlockingSheet` on add a tag', async () => {
- setupMockDataForStagedTagsTesting();
- render( );
+ renderDrawer(stagedTagsId, {
+ blockingSheet: true,
+ setBlockingSheet: mockSetBlockingSheet,
+ });
expect(await screen.findByText('Taxonomy 1')).toBeInTheDocument();
expect(mockSetBlockingSheet).toHaveBeenCalledWith(false);
@@ -1019,7 +601,7 @@ describe(' ', () => {
fireEvent.mouseDown(addTagsButton);
// Click to check Tag 3
- const tag3 = screen.getByText(/tag 3/i);
+ const tag3 = await screen.findByText(/tag 3/i);
fireEvent.click(tag3);
// Click "Add tags" to save to global staged tags
@@ -1030,8 +612,10 @@ describe(' ', () => {
});
it('should call `setBlockingSheet` on delete a tag', async () => {
- setupMockDataForStagedTagsTesting();
- render( );
+ renderDrawer(stagedTagsId, {
+ blockingSheet: true,
+ setBlockingSheet: mockSetBlockingSheet,
+ });
expect(await screen.findByText('Taxonomy 1')).toBeInTheDocument();
expect(mockSetBlockingSheet).toHaveBeenCalledWith(false);
@@ -1053,8 +637,10 @@ describe(' ', () => {
});
it('should call `updateTags` mutation on save', async () => {
- setupMockDataForStagedTagsTesting();
- render( );
+ const { axiosMock } = initializeMocks();
+ const url = getContentTaxonomyTagsApiUrl(stagedTagsId);
+ axiosMock.onPut(url).reply(200);
+ renderDrawer(stagedTagsId);
expect(await screen.findByText('Taxonomy 1')).toBeInTheDocument();
const editTagsButton = screen.getByRole('button', {
name: /edit tags/i,
@@ -1066,12 +652,11 @@ describe(' ', () => {
});
fireEvent.click(saveButton);
- expect(mockMutate).toHaveBeenCalled();
+ await waitFor(() => expect(axiosMock.history.put[0].url).toEqual(url));
});
it('should taxonomies must be ordered', async () => {
- setupLargeMockDataForStagedTagsTesting();
- render( );
+ renderDrawer(largeTagsId);
expect(await screen.findByText('Taxonomy 1')).toBeInTheDocument();
// First, taxonomies with content sorted by count implicit
@@ -1091,18 +676,14 @@ describe(' ', () => {
});
it('should not show "Other tags" section', async () => {
- setupMockDataForStagedTagsTesting();
-
- render( );
+ renderDrawer(stagedTagsId);
expect(await screen.findByText('Taxonomy 1')).toBeInTheDocument();
expect(screen.queryByText('Other tags')).not.toBeInTheDocument();
});
it('should show "Other tags" section', async () => {
- setupMockDataWithOtherTagsTestings();
-
- render( );
+ renderDrawer(otherTagsId);
expect(await screen.findByText('Taxonomy 1')).toBeInTheDocument();
expect(screen.getByText('Other tags')).toBeInTheDocument();
@@ -1112,8 +693,7 @@ describe(' ', () => {
});
it('should test delete "Other tags" and cancel', async () => {
- setupMockDataWithOtherTagsTestings();
- render( );
+ renderDrawer(otherTagsId);
expect(await screen.findByText('Taxonomy 2')).toBeInTheDocument();
// To edit mode
@@ -1139,40 +719,18 @@ describe(' ', () => {
});
it('should show Language Taxonomy', async () => {
- setupMockDataLanguageTaxonomyTestings(true);
- render( );
+ renderDrawer(languageWithTagsId);
expect(await screen.findByText('Languages')).toBeInTheDocument();
});
it('should hide Language Taxonomy', async () => {
- setupMockDataLanguageTaxonomyTestings(false);
- render( );
+ renderDrawer(languageWithoutTagsId);
expect(await screen.findByText('Taxonomy 1')).toBeInTheDocument();
-
expect(screen.queryByText('Languages')).not.toBeInTheDocument();
});
it('should show empty drawer message', async () => {
- useContentTaxonomyTagsData.mockReturnValue({
- isSuccess: true,
- data: {
- taxonomies: [],
- },
- });
- getTaxonomyListData.mockResolvedValue({
- results: [],
- });
- useTaxonomyTagsData.mockReturnValue({
- hasMorePages: false,
- canAddTag: false,
- tagPages: {
- isLoading: false,
- isError: false,
- data: [],
- },
- });
-
- render( );
+ renderDrawer(emptyTagsId);
expect(await screen.findByText(/to use tags, please or contact your administrator\./i)).toBeInTheDocument();
const enableButton = screen.getByRole('button', {
name: /enable a taxonomy/i,
diff --git a/src/content-tags-drawer/ContentTagsDrawer.tsx b/src/content-tags-drawer/ContentTagsDrawer.tsx
new file mode 100644
index 0000000000..28ab128d94
--- /dev/null
+++ b/src/content-tags-drawer/ContentTagsDrawer.tsx
@@ -0,0 +1,390 @@
+import React, { useContext, useEffect } from 'react';
+import {
+ Container,
+ Spinner,
+ Stack,
+ Button,
+ Toast,
+} from '@openedx/paragon';
+import { useIntl, FormattedMessage } from '@edx/frontend-platform/i18n';
+import { useParams, useNavigate } from 'react-router-dom';
+import classNames from 'classnames';
+import messages from './messages';
+import ContentTagsCollapsible from './ContentTagsCollapsible';
+import Loading from '../generic/Loading';
+import useContentTagsDrawerContext from './ContentTagsDrawerHelper';
+import { ContentTagsDrawerContext, ContentTagsDrawerSheetContext } from './common/context';
+
+interface TaxonomyListProps {
+ contentId: string;
+}
+
+const TaxonomyList = ({ contentId }: TaxonomyListProps) => {
+ const navigate = useNavigate();
+ const intl = useIntl();
+
+ const {
+ isTaxonomyListLoaded,
+ isContentTaxonomyTagsLoaded,
+ tagsByTaxonomy,
+ stagedContentTags,
+ collapsibleStates,
+ } = React.useContext(ContentTagsDrawerContext);
+
+ if (isTaxonomyListLoaded && isContentTaxonomyTagsLoaded) {
+ if (tagsByTaxonomy.length !== 0) {
+ return (
+
+ { tagsByTaxonomy.map((data) => (
+
+
+
+
+ ))}
+
+ );
+ }
+
+ return (
+ navigate('/taxonomies')}
+ >
+ { intl.formatMessage(messages.emptyDrawerContentLink) }
+
+ ),
+ }}
+ />
+ );
+ }
+
+ return ;
+};
+
+const ContentTagsDrawerTittle = () => {
+ const intl = useIntl();
+ const {
+ isContentDataLoaded,
+ contentName,
+ } = useContext(ContentTagsDrawerContext);
+
+ return (
+ <>
+ { isContentDataLoaded
+ ? { contentName }
+ : (
+
+
+
+ )}
+
+ >
+ );
+};
+
+interface ContentTagsDrawerVariantFooterProps {
+ onClose: () => void,
+}
+
+const ContentTagsDrawerVariantFooter = ({ onClose }: ContentTagsDrawerVariantFooterProps) => {
+ const intl = useIntl();
+ const {
+ commitGlobalStagedTagsStatus,
+ commitGlobalStagedTags,
+ isEditMode,
+ toReadMode,
+ toEditMode,
+ } = useContext(ContentTagsDrawerContext);
+
+ return (
+
+
+ { commitGlobalStagedTagsStatus !== 'loading' ? (
+
+
+ { intl.formatMessage(isEditMode
+ ? messages.tagsDrawerCancelButtonText
+ : messages.tagsDrawerCloseButtonText)}
+
+
+ { intl.formatMessage(isEditMode
+ ? messages.tagsDrawerSaveButtonText
+ : messages.tagsDrawerEditTagsButtonText)}
+
+
+ )
+ : (
+
+ )}
+
+
+ );
+};
+
+const ContentTagsComponentVariantFooter = () => {
+ const intl = useIntl();
+ const {
+ commitGlobalStagedTagsStatus,
+ commitGlobalStagedTags,
+ isEditMode,
+ toReadMode,
+ toEditMode,
+ } = useContext(ContentTagsDrawerContext);
+
+ return (
+
+ {isEditMode ? (
+
+ { commitGlobalStagedTagsStatus !== 'loading' ? (
+
+
+ {intl.formatMessage(messages.tagsDrawerCancelButtonText)}
+
+
+ {intl.formatMessage(messages.tagsDrawerSaveButtonText)}
+
+
+ ) : (
+
+
+
+ )}
+
+ ) : (
+
+ {intl.formatMessage(messages.manageTagsButton)}
+
+ )}
+
+ );
+};
+
+interface ContentTagsDrawerProps {
+ id?: string;
+ onClose?: () => void;
+ variant?: 'drawer' | 'component';
+}
+
+/**
+ * Drawer with the functionality to show and manage tags in a certain content.
+ * It is used both in interfaces of this MFE and in edx-platform interfaces such as iframe.
+ * - If you want to use it as an iframe, the component obtains the `contentId` from the url parameters.
+ * Functions to close the drawer are handled internally.
+ * TODO: We can delete this method when is no longer used on edx-platform.
+ * - If you want to use it as react component, you need to pass the content id and the close functions
+ * through the component parameters.
+ */
+const ContentTagsDrawer = ({
+ id,
+ onClose,
+ variant = 'drawer',
+}: ContentTagsDrawerProps) => {
+ const intl = useIntl();
+ // TODO: We can delete 'params' when the iframe is no longer used on edx-platform
+ const params = useParams();
+ const contentId = id ?? params.contentId;
+
+ if (contentId === undefined) {
+ throw new Error('Error: contentId cannot be null.');
+ }
+
+ const context = useContentTagsDrawerContext(contentId);
+ const { blockingSheet } = useContext(ContentTagsDrawerSheetContext);
+
+ const {
+ showToastAfterSave,
+ toReadMode,
+ commitGlobalStagedTagsStatus,
+ isTaxonomyListLoaded,
+ isContentTaxonomyTagsLoaded,
+ stagedContentTags,
+ collapsibleStates,
+ toastMessage,
+ closeToast,
+ setCollapsibleToInitalState,
+ otherTaxonomies,
+ } = context;
+
+ let onCloseDrawer: () => void;
+ if (variant === 'drawer') {
+ if (onClose === undefined) {
+ onCloseDrawer = () => {
+ // "*" allows communication with any origin
+ window.parent.postMessage('closeManageTagsDrawer', '*');
+ };
+ } else {
+ onCloseDrawer = onClose;
+ }
+ }
+
+ useEffect(() => {
+ if (variant === 'drawer') {
+ const handleEsc = (event) => {
+ /* Close drawer when ESC-key is pressed and selectable dropdown box not open */
+ const selectableBoxOpen = document.querySelector('[data-selectable-box="taxonomy-tags"]');
+ if (event.key === 'Escape' && !selectableBoxOpen && !blockingSheet) {
+ onCloseDrawer();
+ }
+ };
+ document.addEventListener('keydown', handleEsc);
+
+ return () => {
+ document.removeEventListener('keydown', handleEsc);
+ };
+ }
+ return () => {};
+ }, [blockingSheet]);
+
+ useEffect(() => {
+ /* istanbul ignore next */
+ if (commitGlobalStagedTagsStatus === 'success') {
+ showToastAfterSave();
+ toReadMode();
+ }
+ }, [commitGlobalStagedTagsStatus]);
+
+ // First call of the initial collapsible states
+ React.useEffect(() => {
+ setCollapsibleToInitalState();
+ }, [isTaxonomyListLoaded, isContentTaxonomyTagsLoaded]);
+
+ const renderFooter = () => {
+ if (isTaxonomyListLoaded && isContentTaxonomyTagsLoaded) {
+ switch (variant) {
+ case 'drawer':
+ return ;
+ case 'component':
+ return ;
+ default:
+ return null;
+ }
+ }
+ return null;
+ };
+
+ return (
+
+
+
+ {variant === 'drawer' && (
+
+ )}
+
+ {variant === 'drawer' && (
+
+ {intl.formatMessage(messages.headerSubtitle)}
+
+ )}
+
+ {otherTaxonomies.length !== 0 && (
+
+
+ {intl.formatMessage(messages.otherTagsHeader)}
+
+
+ {intl.formatMessage(messages.otherTagsDescription)}
+
+ { isTaxonomyListLoaded && isContentTaxonomyTagsLoaded && (
+ otherTaxonomies.map((data) => (
+
+
+
+
+ ))
+ )}
+
+ )}
+
+
+ {renderFooter()}
+ {/* istanbul ignore next */
+ toastMessage && (
+
+ {toastMessage}
+
+ )
+ }
+
+
+ );
+};
+
+export default ContentTagsDrawer;
diff --git a/src/content-tags-drawer/data/api.mocks.ts b/src/content-tags-drawer/data/api.mocks.ts
new file mode 100644
index 0000000000..ce1d50c05c
--- /dev/null
+++ b/src/content-tags-drawer/data/api.mocks.ts
@@ -0,0 +1,378 @@
+import * as api from './api';
+import * as taxonomyApi from '../../taxonomy/data/api';
+import { languageExportId } from '../utils';
+
+/**
+ * Mock for `getContentTaxonomyTagsData()`
+ */
+export async function mockContentTaxonomyTagsData(contentId: string): Promise {
+ const thisMock = mockContentTaxonomyTagsData;
+ switch (contentId) {
+ case thisMock.stagedTagsId: return thisMock.stagedTags;
+ case thisMock.otherTagsId: return thisMock.otherTags;
+ case thisMock.languageWithTagsId: return thisMock.languageWithTags;
+ case thisMock.languageWithoutTagsId: return thisMock.languageWithoutTags;
+ case thisMock.largeTagsId: return thisMock.largeTags;
+ case thisMock.emptyTagsId: return thisMock.emptyTags;
+ default: throw new Error(`No mock has been set up for contentId "${contentId}"`);
+ }
+}
+mockContentTaxonomyTagsData.stagedTagsId = 'block-v1:StagedTagsOrg+STC1+2023_1+type@vertical+block@stagedTagsId';
+mockContentTaxonomyTagsData.stagedTags = {
+ taxonomies: [
+ {
+ name: 'Taxonomy 1',
+ taxonomyId: 123,
+ canTagObject: true,
+ tags: [
+ {
+ value: 'Tag 1',
+ lineage: ['Tag 1'],
+ canDeleteObjecttag: true,
+ },
+ {
+ value: 'Tag 2',
+ lineage: ['Tag 2'],
+ canDeleteObjecttag: true,
+ },
+ ],
+ },
+ ],
+};
+mockContentTaxonomyTagsData.otherTagsId = 'block-v1:StagedTagsOrg+STC1+2023_1+type@vertical+block@otherTagsId';
+mockContentTaxonomyTagsData.otherTags = {
+ taxonomies: [
+ {
+ name: 'Taxonomy 1',
+ taxonomyId: 123,
+ canTagObject: true,
+ tags: [
+ {
+ value: 'Tag 1',
+ lineage: ['Tag 1'],
+ canDeleteObjecttag: true,
+ },
+ {
+ value: 'Tag 2',
+ lineage: ['Tag 2'],
+ canDeleteObjecttag: true,
+ },
+ ],
+ },
+ {
+ name: 'Taxonomy 2',
+ taxonomyId: 1234,
+ canTagObject: false,
+ tags: [
+ {
+ value: 'Tag 3',
+ lineage: ['Tag 3'],
+ canDeleteObjecttag: true,
+ },
+ {
+ value: 'Tag 4',
+ lineage: ['Tag 4'],
+ canDeleteObjecttag: true,
+ },
+ ],
+ },
+ ],
+};
+mockContentTaxonomyTagsData.languageWithTagsId = 'block-v1:LanguageTagsOrg+STC1+2023_1+type@vertical+block@languageWithTagsId';
+mockContentTaxonomyTagsData.languageWithTags = {
+ taxonomies: [
+ {
+ name: 'Languages',
+ taxonomyId: 1234,
+ exportId: languageExportId,
+ canTagObject: true,
+ tags: [
+ {
+ value: 'Tag 1',
+ lineage: ['Tag 1'],
+ canDeleteObjecttag: true,
+ },
+ ],
+ },
+ {
+ name: 'Taxonomy 1',
+ taxonomyId: 12345,
+ canTagObject: true,
+ tags: [
+ {
+ value: 'Tag 1',
+ lineage: ['Tag 1'],
+ canDeleteObjecttag: true,
+ },
+ {
+ value: 'Tag 2',
+ lineage: ['Tag 2'],
+ canDeleteObjecttag: true,
+ },
+ ],
+ },
+ ],
+};
+mockContentTaxonomyTagsData.languageWithoutTagsId = 'block-v1:LanguageTagsOrg+STC1+2023_1+type@vertical+block@languageWithoutTagsId';
+mockContentTaxonomyTagsData.languageWithoutTags = {
+ taxonomies: [
+ {
+ name: 'Languages',
+ taxonomyId: 1234,
+ exportId: languageExportId,
+ canTagObject: true,
+ tags: [],
+ },
+ {
+ name: 'Taxonomy 1',
+ taxonomyId: 12345,
+ canTagObject: true,
+ tags: [
+ {
+ value: 'Tag 1',
+ lineage: ['Tag 1'],
+ canDeleteObjecttag: true,
+ },
+ {
+ value: 'Tag 2',
+ lineage: ['Tag 2'],
+ canDeleteObjecttag: true,
+ },
+ ],
+ },
+ ],
+};
+mockContentTaxonomyTagsData.largeTagsId = 'block-v1:LargeTagsOrg+STC1+2023_1+type@vertical+block@largeTagsId';
+mockContentTaxonomyTagsData.largeTags = {
+ taxonomies: [
+ {
+ name: 'Taxonomy 1',
+ taxonomyId: 123,
+ canTagObject: true,
+ tags: [
+ {
+ value: 'Tag 1',
+ lineage: ['Tag 1'],
+ canDeleteObjecttag: true,
+ },
+ {
+ value: 'Tag 2',
+ lineage: ['Tag 2'],
+ canDeleteObjecttag: true,
+ },
+ ],
+ },
+ {
+ name: 'Taxonomy 2',
+ taxonomyId: 124,
+ canTagObject: true,
+ tags: [
+ {
+ value: 'Tag 1',
+ lineage: ['Tag 1'],
+ canDeleteObjecttag: true,
+ },
+ ],
+ },
+ {
+ name: 'Taxonomy 3',
+ taxonomyId: 125,
+ canTagObject: true,
+ tags: [
+ {
+ value: 'Tag 1.1.1',
+ lineage: ['Tag 1', 'Tag 1.1', 'Tag 1.1.1'],
+ canDeleteObjecttag: true,
+ },
+ ],
+ },
+ {
+ name: '(B) Taxonomy 4',
+ taxonomyId: 126,
+ canTagObject: true,
+ tags: [],
+ },
+ {
+ name: '(A) Taxonomy 5',
+ taxonomyId: 127,
+ canTagObject: true,
+ tags: [],
+ },
+ ],
+};
+mockContentTaxonomyTagsData.emptyTagsId = 'block-v1:EmptyTagsOrg+STC1+2023_1+type@vertical+block@emptyTagsId';
+mockContentTaxonomyTagsData.emptyTags = {
+ taxonomies: [],
+};
+mockContentTaxonomyTagsData.applyMock = () => jest.spyOn(api, 'getContentTaxonomyTagsData').mockImplementation(mockContentTaxonomyTagsData);
+
+/**
+ * Mock for `getTaxonomyListData()`
+ */
+export async function mockTaxonomyListData(org: string): Promise {
+ const thisMock = mockTaxonomyListData;
+ switch (org) {
+ case thisMock.stagedTagsOrg: return thisMock.stagedTags;
+ case thisMock.languageTagsOrg: return thisMock.languageTags;
+ case thisMock.largeTagsOrg: return thisMock.largeTags;
+ case thisMock.emptyTagsOrg: return thisMock.emptyTags;
+ default: throw new Error(`No mock has been set up for org "${org}"`);
+ }
+}
+mockTaxonomyListData.stagedTagsOrg = 'StagedTagsOrg';
+mockTaxonomyListData.stagedTags = {
+ results: [
+ {
+ id: 123,
+ name: 'Taxonomy 1',
+ description: 'This is a description 1',
+ canTagObject: true,
+ },
+ ],
+};
+mockTaxonomyListData.languageTagsOrg = 'LanguageTagsOrg';
+mockTaxonomyListData.languageTags = {
+ results: [
+ {
+ id: 1234,
+ name: 'Languages',
+ description: 'This is a description 1',
+ exportId: languageExportId,
+ canTagObject: true,
+ },
+ {
+ id: 12345,
+ name: 'Taxonomy 1',
+ description: 'This is a description 2',
+ canTagObject: true,
+ },
+ ],
+};
+mockTaxonomyListData.largeTagsOrg = 'LargeTagsOrg';
+mockTaxonomyListData.largeTags = {
+ results: [
+ {
+ id: 123,
+ name: 'Taxonomy 1',
+ description: 'This is a description 1',
+ canTagObject: true,
+ },
+ {
+ id: 124,
+ name: 'Taxonomy 2',
+ description: 'This is a description 2',
+ canTagObject: true,
+ },
+ {
+ id: 125,
+ name: 'Taxonomy 3',
+ description: 'This is a description 3',
+ canTagObject: true,
+ },
+ {
+ id: 127,
+ name: '(A) Taxonomy 5',
+ description: 'This is a description 5',
+ canTagObject: true,
+ },
+ {
+ id: 126,
+ name: '(B) Taxonomy 4',
+ description: 'This is a description 4',
+ canTagObject: true,
+ },
+ ],
+};
+mockTaxonomyListData.emptyTagsOrg = 'EmptyTagsOrg';
+mockTaxonomyListData.emptyTags = {
+ results: [],
+};
+mockTaxonomyListData.applyMock = () => jest.spyOn(taxonomyApi, 'getTaxonomyListData').mockImplementation(mockTaxonomyListData);
+
+/**
+ * Mock for `getTaxonomyTagsData()`
+ */
+export async function mockTaxonomyTagsData(taxonomyId: number): Promise {
+ const thisMock = mockTaxonomyTagsData;
+ switch (taxonomyId) {
+ case thisMock.stagedTagsTaxonomy: return thisMock.stagedTags;
+ case thisMock.languageTagsTaxonomy: return thisMock.languageTags;
+ default: throw new Error(`No mock has been set up for taxonomyId "${taxonomyId}"`);
+ }
+}
+mockTaxonomyTagsData.stagedTagsTaxonomy = 123;
+mockTaxonomyTagsData.stagedTags = {
+ count: 3,
+ currentPage: 1,
+ next: null,
+ numPages: 1,
+ previous: null,
+ start: 1,
+ results: [
+ {
+ value: 'Tag 1',
+ externalId: null,
+ childCount: 0,
+ depth: 0,
+ parentValue: null,
+ id: 12345,
+ subTagsUrl: null,
+ canChangeTag: false,
+ canDeleteTag: false,
+ },
+ {
+ value: 'Tag 2',
+ externalId: null,
+ childCount: 0,
+ depth: 0,
+ parentValue: null,
+ id: 12346,
+ subTagsUrl: null,
+ canChangeTag: false,
+ canDeleteTag: false,
+ },
+ {
+ value: 'Tag 3',
+ externalId: null,
+ childCount: 0,
+ depth: 0,
+ parentValue: null,
+ id: 12347,
+ subTagsUrl: null,
+ canChangeTag: false,
+ canDeleteTag: false,
+ },
+ ],
+};
+mockTaxonomyTagsData.languageTagsTaxonomy = 1234;
+mockTaxonomyTagsData.languageTags = {
+ count: 1,
+ currentPage: 1,
+ next: null,
+ numPages: 1,
+ previous: null,
+ start: 1,
+ results: [{
+ value: 'Tag 1',
+ externalId: null,
+ childCount: 0,
+ depth: 0,
+ parentValue: null,
+ id: 12345,
+ subTagsUrl: null,
+ canChangeTag: false,
+ canDeleteTag: false,
+ }],
+};
+mockTaxonomyTagsData.applyMock = () => jest.spyOn(api, 'getTaxonomyTagsData').mockImplementation(mockTaxonomyTagsData);
+
+/**
+ * Mock for `getContentData()`
+ */
+export async function mockContentData(): Promise {
+ return mockContentData.data;
+}
+mockContentData.data = {
+ displayName: 'Unit 1',
+};
+mockContentData.applyMock = () => jest.spyOn(api, 'getContentData').mockImplementation(mockContentData);
diff --git a/src/content-tags-drawer/data/apiHooks.jsx b/src/content-tags-drawer/data/apiHooks.jsx
index a9e09ae85e..34f70bb3f4 100644
--- a/src/content-tags-drawer/data/apiHooks.jsx
+++ b/src/content-tags-drawer/data/apiHooks.jsx
@@ -14,6 +14,7 @@ import {
updateContentTaxonomyTags,
getContentTaxonomyTagsCount,
} from './api';
+import { libraryQueryPredicate, xblockQueryKeys } from '../../library-authoring/data/apiHooks';
/** @typedef {import("../../taxonomy/tag-list/data/types.mjs").TagListData} TagListData */
/** @typedef {import("../../taxonomy/tag-list/data/types.mjs").TagData} TagData */
@@ -146,6 +147,14 @@ export const useContentTaxonomyTagsUpdater = (contentId) => {
contentPattern = contentId.replace(/\+type@.*$/, '*');
}
queryClient.invalidateQueries({ queryKey: ['contentTagsCount', contentPattern] });
+ if (contentId.includes('lb:')) {
+ // Obtain library id from contentId
+ const libraryId = ['lib', ...contentId.split(':').slice(1, 3)].join(':');
+ // Invalidate component metadata to update tags count
+ queryClient.invalidateQueries(xblockQueryKeys.componentMetadata(contentId));
+ // Invalidate content search to update tags count
+ queryClient.invalidateQueries(['content_search'], { predicate: (query) => libraryQueryPredicate(query, libraryId) });
+ }
},
onSuccess: /* istanbul ignore next */ () => {
/* istanbul ignore next */
diff --git a/src/library-authoring/component-info/ComponentManagement.test.tsx b/src/library-authoring/component-info/ComponentManagement.test.tsx
index 314c36de2a..96a12ec5dd 100644
--- a/src/library-authoring/component-info/ComponentManagement.test.tsx
+++ b/src/library-authoring/component-info/ComponentManagement.test.tsx
@@ -7,6 +7,11 @@ import {
} from '../../testUtils';
import { mockLibraryBlockMetadata } from '../data/api.mocks';
import ComponentManagement from './ComponentManagement';
+import { mockContentTaxonomyTagsData } from '../../content-tags-drawer/data/api.mocks';
+
+jest.mock('../../content-tags-drawer', () => ({
+ ContentTagsDrawer: () => Mocked ContentTagsDrawer
,
+}));
/*
* This function is used to get the inner text of an element.
@@ -51,9 +56,8 @@ describe(' ', () => {
initializeMocks();
mockLibraryBlockMetadata.applyMock();
render( );
- expect(await screen.findByText('Tags')).toBeInTheDocument();
- // TODO: replace with actual data when implement tag list
- expect(screen.queryByText('Tags placeholder')).toBeInTheDocument();
+ expect(await screen.findByText('Tags (0)')).toBeInTheDocument();
+ expect(screen.queryByText('Mocked ContentTagsDrawer')).toBeInTheDocument();
});
it('should not render draft status', async () => {
@@ -67,4 +71,16 @@ describe(' ', () => {
expect(await screen.findByText('Draft')).toBeInTheDocument();
expect(screen.queryByText('Tags')).not.toBeInTheDocument();
});
+
+ it('should render tag count in tagging info', async () => {
+ setConfig({
+ ...getConfig(),
+ ENABLE_TAGGING_TAXONOMY_PAGES: 'true',
+ });
+ initializeMocks();
+ mockLibraryBlockMetadata.applyMock();
+ mockContentTaxonomyTagsData.applyMock();
+ render( );
+ expect(await screen.findByText('Tags (6)')).toBeInTheDocument();
+ });
});
diff --git a/src/library-authoring/component-info/ComponentManagement.tsx b/src/library-authoring/component-info/ComponentManagement.tsx
index 12a9cea75c..92adb33107 100644
--- a/src/library-authoring/component-info/ComponentManagement.tsx
+++ b/src/library-authoring/component-info/ComponentManagement.tsx
@@ -1,3 +1,4 @@
+import React from 'react';
import { getConfig } from '@edx/frontend-platform';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Collapsible, Icon, Stack } from '@openedx/paragon';
@@ -6,6 +7,8 @@ import { Tag } from '@openedx/paragon/icons';
import { useLibraryBlockMetadata } from '../data/apiHooks';
import StatusWidget from '../generic/status-widget';
import messages from './messages';
+import { ContentTagsDrawer } from '../../content-tags-drawer';
+import { useContentTaxonomyTagsData } from '../../content-tags-drawer/data/apiHooks';
interface ComponentManagementProps {
usageKey: string;
@@ -13,6 +16,26 @@ interface ComponentManagementProps {
const ComponentManagement = ({ usageKey }: ComponentManagementProps) => {
const intl = useIntl();
const { data: componentMetadata } = useLibraryBlockMetadata(usageKey);
+ const { data: componentTags } = useContentTaxonomyTagsData(usageKey);
+
+ const tagsCount = React.useMemo(() => {
+ if (!componentTags) {
+ return 0;
+ }
+ let result = 0;
+ componentTags.taxonomies.forEach((taxonomy) => {
+ const countedTags : string[] = [];
+ taxonomy.tags.forEach((tagData) => {
+ tagData.lineage.forEach((tag) => {
+ if (!countedTags.includes(tag)) {
+ result += 1;
+ countedTags.push(tag);
+ }
+ });
+ });
+ });
+ return result;
+ }, [componentTags]);
// istanbul ignore if: this should never happen
if (!componentMetadata) {
@@ -31,12 +54,15 @@ const ComponentManagement = ({ usageKey }: ComponentManagementProps) => {
title={(
- {intl.formatMessage(messages.manageTabTagsTitle)}
+ {intl.formatMessage(messages.manageTabTagsTitle, { count: tagsCount })}
)}
className="border-0"
>
- Tags placeholder
+
)}
jest.spyOn(api, 'getLibraryBlockMetadata').mockImplementation(mockLibraryBlockMetadata);
diff --git a/src/library-authoring/data/apiHooks.ts b/src/library-authoring/data/apiHooks.ts
index 96b7122af8..cb8cdd2fba 100644
--- a/src/library-authoring/data/apiHooks.ts
+++ b/src/library-authoring/data/apiHooks.ts
@@ -30,7 +30,7 @@ import {
type CreateLibraryCollectionDataRequest,
} from './api';
-const libraryQueryPredicate = (query: Query, libraryId: string): boolean => {
+export const libraryQueryPredicate = (query: Query, libraryId: string): boolean => {
// Invalidate all content queries related to this library.
// If we allow searching "all courses and libraries" in the future,
// then we'd have to invalidate all `["content_search", "results"]`