diff --git a/src/content-tags-drawer/ContentTagsDrawer.jsx b/src/content-tags-drawer/ContentTagsDrawer.jsx index e5f2e47dea..f8596e8959 100644 --- a/src/content-tags-drawer/ContentTagsDrawer.jsx +++ b/src/content-tags-drawer/ContentTagsDrawer.jsx @@ -8,14 +8,72 @@ import { Button, Toast, } from '@openedx/paragon'; -import { useIntl } from '@edx/frontend-platform/i18n'; -import { useParams } from 'react-router-dom'; +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. @@ -42,7 +100,6 @@ const ContentTagsDrawer = ({ id, onClose }) => { contentName, isTaxonomyListLoaded, isContentTaxonomyTagsLoaded, - tagsByTaxonomy, stagedContentTags, collapsibleStates, isEditMode, @@ -110,19 +167,7 @@ const ContentTagsDrawer = ({ id, onClose }) => {

{intl.formatMessage(messages.headerSubtitle)}

- { isTaxonomyListLoaded && isContentTaxonomyTagsLoaded - ? tagsByTaxonomy.map((data) => ( -
- -
-
- )) - : } + {otherTaxonomies.length !== 0 && (

diff --git a/src/content-tags-drawer/ContentTagsDrawer.scss b/src/content-tags-drawer/ContentTagsDrawer.scss index 6124ff9332..ce73c6b959 100644 --- a/src/content-tags-drawer/ContentTagsDrawer.scss +++ b/src/content-tags-drawer/ContentTagsDrawer.scss @@ -22,6 +22,11 @@ .other-description { font-size: .9rem; } + + .enable-taxonomies-button:not([disabled]):hover { + background-color: transparent; + color: $info-900 !important; + } } // Apply styles to sheet only if it has a child with a .tags-drawer class diff --git a/src/content-tags-drawer/ContentTagsDrawer.test.jsx b/src/content-tags-drawer/ContentTagsDrawer.test.jsx index b9891fde25..8f2e517c35 100644 --- a/src/content-tags-drawer/ContentTagsDrawer.test.jsx +++ b/src/content-tags-drawer/ContentTagsDrawer.test.jsx @@ -20,17 +20,20 @@ import { 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'; const mockOnClose = jest.fn(); const mockMutate = jest.fn(); const mockSetBlockingSheet = jest.fn(); +const mockNavigate = jest.fn(); jest.mock('react-router-dom', () => ({ ...jest.requireActual('react-router-dom'), useParams: () => ({ contentId, }), + useNavigate: () => mockNavigate, })); // FIXME: replace these mocks with API mocks @@ -256,6 +259,83 @@ describe('', () => { }); }; + 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, @@ -1057,4 +1137,47 @@ describe('', () => { expect(screen.getByText(/tag 3/i)).toBeInTheDocument(); }); + + it('should show Language Taxonomy', async () => { + setupMockDataLanguageTaxonomyTestings(true); + render(); + expect(await screen.findByText('Languages')).toBeInTheDocument(); + }); + + it('should hide Language Taxonomy', async () => { + setupMockDataLanguageTaxonomyTestings(false); + render(); + 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(); + expect(await screen.findByText(/to use tags, please or contact your administrator\./i)).toBeInTheDocument(); + const enableButton = screen.getByRole('button', { + name: /enable a taxonomy/i, + }); + fireEvent.click(enableButton); + expect(mockNavigate).toHaveBeenCalledWith('/taxonomies'); + }); }); diff --git a/src/content-tags-drawer/ContentTagsDrawerHelper.jsx b/src/content-tags-drawer/ContentTagsDrawerHelper.jsx index 8eb3e0e578..16ec9f6d94 100644 --- a/src/content-tags-drawer/ContentTagsDrawerHelper.jsx +++ b/src/content-tags-drawer/ContentTagsDrawerHelper.jsx @@ -4,7 +4,7 @@ import { useIntl } from '@edx/frontend-platform/i18n'; import { cloneDeep } from 'lodash'; import { useContentData, useContentTaxonomyTagsData, useContentTaxonomyTagsUpdater } from './data/apiHooks'; import { useTaxonomyList } from '../taxonomy/data/apiHooks'; -import { extractOrgFromContentId } from './utils'; +import { extractOrgFromContentId, languageExportId } from './utils'; import messages from './messages'; import { ContentTagsDrawerSheetContext } from './common/context'; @@ -142,8 +142,14 @@ const useContentTagsDrawerContext = (contentId) => { } }); + // Delete Language taxonomy if is empty + const filteredTaxonomies = taxonomiesList.filter( + (taxonomy) => taxonomy.exportId !== languageExportId + || taxonomy.contentTags.length !== 0, + ); + return { - fechedTaxonomies: sortTaxonomies(taxonomiesList), + fechedTaxonomies: sortTaxonomies(filteredTaxonomies), fechedOtherTaxonomies: otherTaxonomiesList, }; } diff --git a/src/content-tags-drawer/ContentTagsDropDownSelector.jsx b/src/content-tags-drawer/ContentTagsDropDownSelector.jsx index 4960e30912..491d442ed2 100644 --- a/src/content-tags-drawer/ContentTagsDropDownSelector.jsx +++ b/src/content-tags-drawer/ContentTagsDropDownSelector.jsx @@ -323,7 +323,9 @@ const ContentTagsDropDownSelector = ({ { tagPages.data.length === 0 && !tagPages.isLoading && (

- + { searchTerm + ? + : }
)} diff --git a/src/content-tags-drawer/ContentTagsDropDownSelector.test.jsx b/src/content-tags-drawer/ContentTagsDropDownSelector.test.jsx index 70d95187f2..08cc6e9bd5 100644 --- a/src/content-tags-drawer/ContentTagsDropDownSelector.test.jsx +++ b/src/content-tags-drawer/ContentTagsDropDownSelector.test.jsx @@ -282,4 +282,28 @@ describe('', () => { expect(getByText(message)).toBeInTheDocument(); }); }); + + it('should render "noTagsInTaxonomy" message if taxonomy is empty', async () => { + useTaxonomyTagsData.mockReturnValueOnce({ + hasMorePages: false, + tagPages: { + isLoading: false, + isError: false, + isSuccess: true, + data: [], + }, + }); + + const searchTerm = ''; + await act(async () => { + const { getByText } = await getComponent({ ...data, searchTerm }); + + await waitFor(() => { + expect(useTaxonomyTagsData).toBeCalledWith(data.taxonomyId, null, 1, searchTerm); + }); + + const message = 'No tags in this taxonomy yet'; + expect(getByText(message)).toBeInTheDocument(); + }); + }); }); diff --git a/src/content-tags-drawer/messages.js b/src/content-tags-drawer/messages.js index ca9c7896c5..4b69010e58 100644 --- a/src/content-tags-drawer/messages.js +++ b/src/content-tags-drawer/messages.js @@ -25,6 +25,11 @@ const messages = defineMessages({ id: 'course-authoring.content-tags-drawer.tags-dropdown-selector.no-tags-found', defaultMessage: 'No tags found with the search term "{searchTerm}"', }, + noTagsInTaxonomyMessage: { + id: 'course-authoring.content-tags-drawer.tags-dropdown-selector.no-tags-in-taxonomy', + defaultMessage: 'No tags in this taxonomy yet', + description: 'Message when the user uses the tags dropdown selector of an empty taxonomy', + }, taxonomyTagChecked: { id: 'course-authoring.content-tags-drawer.tags-dropdown-selector.tag-checked', defaultMessage: 'Checked', @@ -124,6 +129,16 @@ const messages = defineMessages({ defaultMessage: 'These tags are already applied, but you can\'t add new ones as you don\'t have access to their taxonomies.', description: 'Description of "Other tags" subsection in tags drawer', }, + emptyDrawerContent: { + id: 'course-authoring.content-tags-drawer.empty', + defaultMessage: 'To use tags, please {link} or contact your administrator.', + description: 'Message when there are no taxonomies.', + }, + emptyDrawerContentLink: { + id: 'course-authoring.content-tags-drawer.empty-link', + defaultMessage: 'enable a taxonomy', + description: 'Message of the link used in empty drawer message.', + }, }); export default messages; diff --git a/src/content-tags-drawer/utils.js b/src/content-tags-drawer/utils.js index ac81f8dc48..06dd9f5784 100644 --- a/src/content-tags-drawer/utils.js +++ b/src/content-tags-drawer/utils.js @@ -1,2 +1,3 @@ // eslint-disable-next-line import/prefer-default-export export const extractOrgFromContentId = (contentId) => contentId.split('+')[0].split(':')[1]; +export const languageExportId = 'languages-v1';