diff --git a/src/content-tags-drawer/ContentTagsDrawer.jsx b/src/content-tags-drawer/ContentTagsDrawer.jsx index 72e41c6fd1..a72f6a839d 100644 --- a/src/content-tags-drawer/ContentTagsDrawer.jsx +++ b/src/content-tags-drawer/ContentTagsDrawer.jsx @@ -76,6 +76,15 @@ const ContentTagsDrawer = ({ id, onClose }) => { } = useContentTaxonomyTagsData(contentId); const { data: taxonomyListData, isSuccess: isTaxonomyListLoaded } = useTaxonomyList(org); + let contentName = ''; + if (isContentDataLoaded) { + if ('displayName' in contentData) { + contentName = contentData.displayName; + } else { + contentName = contentData.courseDisplayNameWithDefault; + } + } + let onCloseDrawer = onClose; if (onCloseDrawer === undefined) { onCloseDrawer = () => { @@ -129,7 +138,7 @@ const ContentTagsDrawer = ({ id, onClose }) => { onCloseDrawer()} data-testid="drawer-close-button" /> {intl.formatMessage(messages.headerSubtitle)} { isContentDataLoaded - ?

{ contentData.displayName }

+ ?

{ contentName }

: (
{ }; export const getContentTaxonomyTagsApiUrl = (contentId) => new URL(`api/content_tagging/v1/object_tags/${contentId}/`, getApiBaseUrl()).href; export const getXBlockContentDataApiURL = (contentId) => new URL(`/xblock/outline/${contentId}`, getApiBaseUrl()).href; +export const getCourseContentDataApiURL = (contentId) => new URL(`/api/contentstore/v1/course_settings/${contentId}`, getApiBaseUrl()).href; export const getLibraryContentDataApiUrl = (contentId) => new URL(`/api/libraries/v2/blocks/${contentId}/`, getApiBaseUrl()).href; export const getContentTaxonomyTagsCountApiUrl = (contentId) => new URL(`api/content_tagging/v1/object_tag_counts/${contentId}/?count_implicit`, getApiBaseUrl()).href; @@ -74,9 +75,14 @@ export async function getContentTaxonomyTagsCount(contentId) { * @returns {Promise} */ export async function getContentData(contentId) { - const url = contentId.startsWith('lb:') - ? getLibraryContentDataApiUrl(contentId) - : getXBlockContentDataApiURL(contentId); + let url; + if (contentId.startsWith('lb:')) { + url = getLibraryContentDataApiUrl(contentId); + } else if (contentId.startsWith('course-v1:')) { + url = getCourseContentDataApiURL(contentId); + } else { + url = getXBlockContentDataApiURL(contentId); + } const { data } = await getAuthenticatedHttpClient().get(url); return camelCaseObject(data); } diff --git a/src/content-tags-drawer/data/apiHooks.jsx b/src/content-tags-drawer/data/apiHooks.jsx index 5c24c0aa6b..0b311d795f 100644 --- a/src/content-tags-drawer/data/apiHooks.jsx +++ b/src/content-tags-drawer/data/apiHooks.jsx @@ -11,7 +11,6 @@ import { getContentTaxonomyTagsData, getContentData, updateContentTaxonomyTags, - getContentTaxonomyTagsCount, } from './api'; /** @typedef {import("../../taxonomy/tag-list/data/types.mjs").TagListData} TagListData */ @@ -106,17 +105,6 @@ export const useContentTaxonomyTagsData = (contentId) => ( }) ); -/** - * Build the query to get the count og taxonomy tags applied to the content object - * @param {string} contentId The ID of the content object to fetch the count of the applied tags for - */ -export const useContentTaxonomyTagsCount = (contentId) => ( - useQuery({ - queryKey: ['contentTaxonomyTagsCount', contentId], - queryFn: () => getContentTaxonomyTagsCount(contentId), - }) -); - /** * Builds the query to get meta data about the content object * @param {string} contentId The id of the content object (unit/component) @@ -150,8 +138,13 @@ export const useContentTaxonomyTagsUpdater = (contentId, taxonomyId) => { onSettled: /* istanbul ignore next */ () => { queryClient.invalidateQueries({ queryKey: ['contentTaxonomyTags', contentId] }); /// Invalidate query with pattern on course outline - queryClient.invalidateQueries({ queryKey: ['unitTagsCount'] }); - queryClient.invalidateQueries({ queryKey: ['contentTaxonomyTagsCount', contentId] }); + let contentPattern; + if (contentId.includes('course-v1')) { + contentPattern = contentId; + } else { + contentPattern = contentId.replace(/\+type@.*$/, '*'); + } + queryClient.invalidateQueries({ queryKey: ['contentTagsCount', contentPattern] }); }, }); }; diff --git a/src/content-tags-drawer/data/apiHooks.test.jsx b/src/content-tags-drawer/data/apiHooks.test.jsx index 127d71cc5b..4e12ef5ea5 100644 --- a/src/content-tags-drawer/data/apiHooks.test.jsx +++ b/src/content-tags-drawer/data/apiHooks.test.jsx @@ -6,7 +6,6 @@ import { useContentTaxonomyTagsData, useContentData, useContentTaxonomyTagsUpdater, - useContentTaxonomyTagsCount, } from './apiHooks'; import { updateContentTaxonomyTags } from './api'; @@ -135,24 +134,6 @@ describe('useContentTaxonomyTagsData', () => { }); }); -describe('useContentTaxonomyTagsCount', () => { - it('should return success response', () => { - useQuery.mockReturnValueOnce({ isSuccess: true, data: 'data' }); - const contentId = '123'; - const result = useContentTaxonomyTagsCount(contentId); - - expect(result).toEqual({ isSuccess: true, data: 'data' }); - }); - - it('should return failure response', () => { - useQuery.mockReturnValueOnce({ isSuccess: false }); - const contentId = '123'; - const result = useContentTaxonomyTagsCount(contentId); - - expect(result).toEqual({ isSuccess: false }); - }); -}); - describe('useContentData', () => { it('should return success response', () => { useQuery.mockReturnValueOnce({ isSuccess: true, data: 'data' }); diff --git a/src/content-tags-drawer/data/types.mjs b/src/content-tags-drawer/data/types.mjs index 3fad73ccf1..1f95977e15 100644 --- a/src/content-tags-drawer/data/types.mjs +++ b/src/content-tags-drawer/data/types.mjs @@ -30,7 +30,7 @@ */ /** - * @typedef {Object} ContentData + * @typedef {Object} XBlockData * @property {string} id * @property {string} displayName * @property {string} category @@ -58,3 +58,12 @@ * @property {boolean} staffOnlyMessage * @property {boolean} hasPartitionGroupComponents */ + +/** + * @typedef {Object} CourseData + * @property {string} courseDisplayNameWithDefault + */ + +/** + * @typedef {XBlockData | CourseData} ContentData + */ diff --git a/src/content-tags-drawer/tags-sidebar-controls/TagsSidebarHeader.jsx b/src/content-tags-drawer/tags-sidebar-controls/TagsSidebarHeader.jsx index e3927deb89..ea154574b0 100644 --- a/src/content-tags-drawer/tags-sidebar-controls/TagsSidebarHeader.jsx +++ b/src/content-tags-drawer/tags-sidebar-controls/TagsSidebarHeader.jsx @@ -4,8 +4,8 @@ import { Stack } from '@openedx/paragon'; import { useParams } from 'react-router-dom'; import { useIntl } from '@edx/frontend-platform/i18n'; +import { useContentTagsCount } from '../../generic/data/apiHooks'; import messages from '../messages'; -import { useContentTaxonomyTagsCount } from '../data/apiHooks'; import TagCount from '../../generic/tag-count'; const TagsSidebarHeader = () => { @@ -13,9 +13,9 @@ const TagsSidebarHeader = () => { const contentId = useParams().blockId; const { - data: contentTaxonomyTagsCount, - isSuccess: isContentTaxonomyTagsCountLoaded, - } = useContentTaxonomyTagsCount(contentId || ''); + data: contentTagsCount, + isSuccess: isContentTagsCountLoaded, + } = useContentTagsCount(contentId || ''); return ( {

{intl.formatMessage(messages.tagsSidebarTitle)}

- { isContentTaxonomyTagsCountLoaded - && } + { isContentTagsCountLoaded + && }
); }; diff --git a/src/content-tags-drawer/tags-sidebar-controls/TagsSidebarHeader.test.jsx b/src/content-tags-drawer/tags-sidebar-controls/TagsSidebarHeader.test.jsx index ab0f9339e8..05188b1a6c 100644 --- a/src/content-tags-drawer/tags-sidebar-controls/TagsSidebarHeader.test.jsx +++ b/src/content-tags-drawer/tags-sidebar-controls/TagsSidebarHeader.test.jsx @@ -1,36 +1,39 @@ import React from 'react'; import { render, screen } from '@testing-library/react'; import { IntlProvider } from '@edx/frontend-platform/i18n'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import TagsSidebarHeader from './TagsSidebarHeader'; -import { useContentTaxonomyTagsCount } from '../data/apiHooks'; -jest.mock('../data/apiHooks', () => ({ - useContentTaxonomyTagsCount: jest.fn(() => ({ - isSuccess: false, - data: 17, - })), +const mockGetTagsCount = jest.fn(); + +jest.mock('../../generic/data/api', () => ({ + ...jest.requireActual('../../generic/data/api'), + getTagsCount: () => mockGetTagsCount(), +})); + +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useParams: () => ({ blockId: '123' }), })); +const queryClient = new QueryClient(); + const RootWrapper = () => ( - + + + ); describe('', () => { - it('should not render count on loading', () => { + it('should render count only after query is complete', async () => { + let resolvePromise; + mockGetTagsCount.mockReturnValueOnce(new Promise((resolve) => { resolvePromise = resolve; })); render(); expect(screen.getByRole('heading', { name: /unit tags/i })).toBeInTheDocument(); expect(screen.queryByText('17')).not.toBeInTheDocument(); - }); - - it('should render count after query is complete', () => { - useContentTaxonomyTagsCount.mockReturnValue({ - isSuccess: true, - data: 17, - }); - render(); - expect(screen.getByRole('heading', { name: /unit tags/i })).toBeInTheDocument(); - expect(screen.getByText('17')).toBeInTheDocument(); + resolvePromise({ 123: 17 }); + expect(await screen.findByText('17')).toBeInTheDocument(); }); }); diff --git a/src/course-outline/CourseOutline.jsx b/src/course-outline/CourseOutline.jsx index 82af8687cc..e046b78a54 100644 --- a/src/course-outline/CourseOutline.jsx +++ b/src/course-outline/CourseOutline.jsx @@ -1,4 +1,5 @@ -import { useState, useEffect, useMemo } from 'react'; +// @ts-check +import React, { useState, useEffect } from 'react'; import PropTypes from 'prop-types'; import { useIntl } from '@edx/frontend-platform/i18n'; import { @@ -52,7 +53,6 @@ import { } from './drag-helper/utils'; import { useCourseOutline } from './hooks'; import messages from './messages'; -import useUnitTagsCount from './data/apiHooks'; const CourseOutline = ({ courseId }) => { const intl = useIntl(); @@ -113,7 +113,6 @@ const CourseOutline = ({ courseId }) => { mfeProctoredExamSettingsUrl, handleDismissNotification, advanceSettingsUrl, - prevContainerInfo, handleSectionDragAndDrop, handleSubsectionDragAndDrop, handleUnitDragAndDrop, @@ -133,27 +132,6 @@ const CourseOutline = ({ courseId }) => { const { category } = useSelector(getCurrentItem); const deleteCategory = COURSE_BLOCK_NAMES[category]?.name.toLowerCase(); - const unitsIdPattern = useMemo(() => { - let pattern = ''; - sections.forEach((section) => { - section.childInfo.children.forEach((subsection) => { - subsection.childInfo.children.forEach((unit) => { - if (pattern !== '') { - pattern += `,${unit.id}`; - } else { - pattern += unit.id; - } - }); - }); - }); - return pattern; - }, [sections]); - - const { - data: unitsTagCounts, - isSuccess: isUnitsTagCountsLoaded, - } = useUnitTagsCount(unitsIdPattern); - /** * Move section to new index * @param {any} currentIndex @@ -268,7 +246,6 @@ const CourseOutline = ({ courseId }) => { ) : null} { items={sections} setSections={setSections} restoreSectionList={restoreSectionList} - prevContainerInfo={prevContainerInfo} handleSectionDragAndDrop={handleSectionDragAndDrop} handleSubsectionDragAndDrop={handleSubsectionDragAndDrop} handleUnitDragAndDrop={handleUnitDragAndDrop} @@ -319,7 +295,6 @@ const CourseOutline = ({ courseId }) => { > {sections.map((section, sectionIndex) => ( { onOrderChange={updateUnitOrderByIndex} onCopyToClipboardClick={handleCopyToClipboardClick} discussionsSettings={discussionsSettings} - tagsCount={isUnitsTagCountsLoaded ? unitsTagCounts[unit.id] : 0} /> ))} @@ -482,6 +456,7 @@ const CourseOutline = ({ courseId }) => { variant="danger" icon={WarningIcon} title={intl.formatMessage(messages.alertErrorTitle)} + description="" aria-hidden="true" /> )} diff --git a/src/course-outline/CourseOutline.test.jsx b/src/course-outline/CourseOutline.test.jsx index 1f3070c784..44f1f004ea 100644 --- a/src/course-outline/CourseOutline.test.jsx +++ b/src/course-outline/CourseOutline.test.jsx @@ -6,6 +6,7 @@ import { AppProvider } from '@edx/frontend-platform/react'; import { initializeMockApp } from '@edx/frontend-platform'; import MockAdapter from 'axios-mock-adapter'; import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { cloneDeep } from 'lodash'; import { closestCorners } from '@dnd-kit/core'; @@ -85,11 +86,13 @@ jest.mock('@edx/frontend-platform/i18n', () => ({ }), })); -jest.mock('./data/apiHooks', () => () => ({ - data: {}, - isSuccess: true, +jest.mock('./data/api', () => ({ + ...jest.requireActual('./data/api'), + getTagsCount: () => jest.fn().mockResolvedValue({}), })); +const queryClient = new QueryClient(); + jest.mock('@dnd-kit/core', () => ({ ...jest.requireActual('@dnd-kit/core'), // Since jsdom (used by jest) does not support getBoundingClientRect function @@ -104,9 +107,11 @@ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); const RootWrapper = () => ( - - - + + + + + ); diff --git a/src/course-outline/__mocks__/index.js b/src/course-outline/__mocks__/index.js index c2e4997d34..15c6504cb1 100644 --- a/src/course-outline/__mocks__/index.js +++ b/src/course-outline/__mocks__/index.js @@ -4,4 +4,3 @@ export { default as courseBestPracticesMock } from './courseBestPractices'; export { default as courseLaunchMock } from './courseLaunch'; export { default as courseSectionMock } from './courseSection'; export { default as courseSubsectionMock } from './courseSubsection'; -export { default as contentTagsCountMock } from './contentTagsCount'; diff --git a/src/course-outline/card-header/CardHeader.jsx b/src/course-outline/card-header/CardHeader.jsx index e1424a119f..065707b243 100644 --- a/src/course-outline/card-header/CardHeader.jsx +++ b/src/course-outline/card-header/CardHeader.jsx @@ -1,5 +1,7 @@ +// @ts-check import React, { useEffect, useRef, useState } from 'react'; import PropTypes from 'prop-types'; +import { getConfig } from '@edx/frontend-platform'; import { useIntl } from '@edx/frontend-platform/i18n'; import { useSearchParams } from 'react-router-dom'; import { @@ -8,18 +10,22 @@ import { Hyperlink, Icon, IconButton, + Sheet, + useToggle, } from '@openedx/paragon'; import { MoreVert as MoveVertIcon, EditOutline as EditIcon, } from '@openedx/paragon/icons'; +import { useContentTagsCount } from '../../generic/data/apiHooks'; +import { ContentTagsDrawer } from '../../content-tags-drawer'; +import TagCount from '../../generic/tag-count'; import { useEscapeClick } from '../../hooks'; import { ITEM_BADGE_STATUS } from '../constants'; import { scrollToElement } from '../utils'; import CardStatus from './CardStatus'; import messages from './messages'; -import TagCount from '../../generic/tag-count'; const CardHeader = ({ title, @@ -28,7 +34,6 @@ const CardHeader = ({ hasChanges, onClickPublish, onClickConfigure, - onClickManageTags, onClickMenuButton, onClickEdit, isFormOpen, @@ -50,16 +55,18 @@ const CardHeader = ({ discussionEnabled, discussionsSettings, parentInfo, - tagsCount, }) => { const intl = useIntl(); const [searchParams] = useSearchParams(); const [titleValue, setTitleValue] = useState(title); const cardHeaderRef = useRef(null); + const [isManageTagsDrawerOpen, openManageTagsDrawer, closeManageTagsDrawer] = useToggle(false); const isDisabledPublish = (status === ITEM_BADGE_STATUS.live || status === ITEM_BADGE_STATUS.publishedNotLive) && !hasChanges; + const { data: contentTagCount } = useContentTagsCount(cardId); + useEffect(() => { const locatorId = searchParams.get('show'); if (!locatorId) { @@ -91,134 +98,148 @@ const CardHeader = ({ }); return ( -
- {isFormOpen ? ( - - e && e.focus()} - value={titleValue} - name="displayName" - onChange={(e) => setTitleValue(e.target.value)} - aria-label="edit field" - onBlur={() => onEditSubmit(titleValue)} - onKeyDown={(e) => { - if (e.key === 'Enter') { - onEditSubmit(titleValue); - } - }} - disabled={isDisabledEditField} - /> - - ) : ( - <> - {titleComponent} - - - )} -
- {(isVertical || isSequential) && ( - + <> +
+ {isFormOpen ? ( + + e && e.focus()} + value={titleValue} + name="displayName" + onChange={(e) => setTitleValue(e.target.value)} + aria-label="edit field" + onBlur={() => onEditSubmit(titleValue)} + onKeyDown={(e) => { + if (e.key === 'Enter') { + onEditSubmit(titleValue); + } + }} + disabled={isDisabledEditField} + /> + + ) : ( + <> + {titleComponent} + + )} - { tagsCount > 0 && } - - - - {isSequential && proctoringExamConfigurationLink && ( +
+ {(isVertical || isSequential) && ( + + )} + { getConfig().ENABLE_TAGGING_TAXONOMY_PAGES === 'true' && contentTagCount > 0 && ( + + )} + + + + {isSequential && proctoringExamConfigurationLink && ( + + {intl.formatMessage(messages.menuProctoringLinkText)} + + )} - {intl.formatMessage(messages.menuProctoringLinkText)} + {intl.formatMessage(messages.menuPublish)} - )} - - {intl.formatMessage(messages.menuPublish)} - - - {intl.formatMessage(messages.menuConfigure)} - - {onClickManageTags && ( - {intl.formatMessage(messages.menuManageTags)} + {intl.formatMessage(messages.menuConfigure)} - )} + {getConfig().ENABLE_TAGGING_TAXONOMY_PAGES === 'true' && ( + + {intl.formatMessage(messages.menuManageTags)} + + )} - {isVertical && enableCopyPasteUnits && ( - - {intl.formatMessage(messages.menuCopy)} - - )} - {actions.duplicable && ( - - {intl.formatMessage(messages.menuDuplicate)} - - )} - {actions.draggable && ( - <> + {isVertical && enableCopyPasteUnits && ( + + {intl.formatMessage(messages.menuCopy)} + + )} + {actions.duplicable && ( - {intl.formatMessage(messages.menuMoveUp)} + {intl.formatMessage(messages.menuDuplicate)} + )} + {actions.draggable && ( + <> + + {intl.formatMessage(messages.menuMoveUp)} + + + {intl.formatMessage(messages.menuMoveDown)} + + + )} + {actions.deletable && ( - {intl.formatMessage(messages.menuMoveDown)} + {intl.formatMessage(messages.menuDelete)} - - )} - {actions.deletable && ( - - {intl.formatMessage(messages.menuDelete)} - - )} - - + )} + + +
-
+ closeManageTagsDrawer()} + > + closeManageTagsDrawer()} + /> + + ); }; @@ -231,8 +252,6 @@ CardHeader.defaultProps = { discussionEnabled: false, discussionsSettings: {}, parentInfo: {}, - onClickManageTags: null, - tagsCount: undefined, cardId: '', }; @@ -243,7 +262,6 @@ CardHeader.propTypes = { hasChanges: PropTypes.bool.isRequired, onClickPublish: PropTypes.func.isRequired, onClickConfigure: PropTypes.func.isRequired, - onClickManageTags: PropTypes.func, onClickMenuButton: PropTypes.func.isRequired, onClickEdit: PropTypes.func.isRequired, isFormOpen: PropTypes.bool.isRequired, @@ -278,7 +296,6 @@ CardHeader.propTypes = { isTimeLimited: PropTypes.bool, graded: PropTypes.bool, }), - tagsCount: PropTypes.number, }; export default CardHeader; diff --git a/src/course-outline/card-header/CardHeader.test.jsx b/src/course-outline/card-header/CardHeader.test.jsx index d6bd769200..0e40058c3f 100644 --- a/src/course-outline/card-header/CardHeader.test.jsx +++ b/src/course-outline/card-header/CardHeader.test.jsx @@ -2,7 +2,9 @@ import { MemoryRouter } from 'react-router-dom'; import { act, render, fireEvent, waitFor, screen, } from '@testing-library/react'; +import { setConfig, getConfig } from '@edx/frontend-platform'; import { IntlProvider } from '@edx/frontend-platform/i18n'; +import { QueryClientProvider, QueryClient } from '@tanstack/react-query'; import { ITEM_BADGE_STATUS } from '../constants'; import CardHeader from './CardHeader'; @@ -18,9 +20,15 @@ const onClickDuplicateMock = jest.fn(); const onClickConfigureMock = jest.fn(); const onClickMoveUpMock = jest.fn(); const onClickMoveDownMock = jest.fn(); -const onClickManageTagsMock = jest.fn(); const closeFormMock = jest.fn(); +const mockGetTagsCount = jest.fn(); + +jest.mock('../../generic/data/api', () => ({ + ...jest.requireActual('../../generic/data/api'), + getTagsCount: () => mockGetTagsCount(), +})); + const cardHeaderProps = { title: 'Some title', status: ITEM_BADGE_STATUS.live, @@ -29,7 +37,6 @@ const cardHeaderProps = { onClickMenuButton: onClickMenuButtonMock, onClickPublish: onClickPublishMock, onClickEdit: onClickEditMock, - onClickManageTags: onClickManageTagsMock, isFormOpen: false, onEditSubmit: jest.fn(), closeForm: closeFormMock, @@ -49,6 +56,8 @@ const cardHeaderProps = { }, }; +const queryClient = new QueryClient(); + const renderComponent = (props, entry = '/') => { const titleComponent = ( { return render( - - - , + + + + + , ); }; @@ -170,14 +181,32 @@ describe('', () => { expect(onClickPublishMock).toHaveBeenCalled(); }); - it('calls onClickManageTags when the menu is clicked', async () => { + it('only shows Manage tags menu if the waffle flag is enabled', async () => { + setConfig({ + ...getConfig(), + ENABLE_TAGGING_TAXONOMY_PAGES: 'false', + }); + renderComponent(); + const menuButton = await screen.findByTestId('subsection-card-header__menu-button'); + fireEvent.click(menuButton); + + expect(screen.queryByText(messages.menuManageTags.defaultMessage)).not.toBeInTheDocument(); + }); + + it('shows ContentTagsDrawer when the menu is clicked', async () => { + setConfig({ + ...getConfig(), + ENABLE_TAGGING_TAXONOMY_PAGES: 'true', + }); renderComponent(); const menuButton = await screen.findByTestId('subsection-card-header__menu-button'); fireEvent.click(menuButton); const manageTagsMenuItem = await screen.findByText(messages.menuManageTags.defaultMessage); - await act(async () => fireEvent.click(manageTagsMenuItem)); - expect(onClickManageTagsMock).toHaveBeenCalled(); + fireEvent.click(manageTagsMenuItem); + + // Check if the drawer is open + expect(screen.getByTestId('drawer-close-button')).toBeInTheDocument(); }); it('calls onClickEdit when the button is clicked', async () => { @@ -264,19 +293,33 @@ describe('', () => { expect(queryByText(messages.discussionEnabledBadgeText.defaultMessage)).toBeInTheDocument(); }); - it('should render tag count if is not zero', () => { - renderComponent({ - ...cardHeaderProps, - tagsCount: 17, + it('should render tag count if is not zero and the waffle flag is enabled', async () => { + setConfig({ + ...getConfig(), + ENABLE_TAGGING_TAXONOMY_PAGES: 'true', + }); + mockGetTagsCount.mockResolvedValue({ 12345: 17 }); + renderComponent(); + expect(await screen.findByText('17')).toBeInTheDocument(); + }); + + it('shouldn render tag count if the waffle flag is disabled', async () => { + setConfig({ + ...getConfig(), + ENABLE_TAGGING_TAXONOMY_PAGES: 'false', }); - expect(screen.getByText('17')).toBeInTheDocument(); + mockGetTagsCount.mockResolvedValue({ 12345: 17 }); + renderComponent(); + expect(screen.queryByText('17')).not.toBeInTheDocument(); }); it('should not render tag count if is zero', () => { - renderComponent({ - ...cardHeaderProps, - tagsCount: 0, + setConfig({ + ...getConfig(), + ENABLE_TAGGING_TAXONOMY_PAGES: 'true', }); + mockGetTagsCount.mockResolvedValue({ 12345: 0 }); + renderComponent(); expect(screen.queryByText('0')).not.toBeInTheDocument(); }); }); diff --git a/src/course-outline/data/api.js b/src/course-outline/data/api.js index be053668f2..6b12cf62d1 100644 --- a/src/course-outline/data/api.js +++ b/src/course-outline/data/api.js @@ -29,7 +29,6 @@ export const getXBlockBaseApiUrl = () => `${getApiBaseUrl()}/xblock/`; export const getCourseItemApiUrl = (itemId) => `${getXBlockBaseApiUrl()}${itemId}`; export const getXBlockApiUrl = (blockId) => `${getXBlockBaseApiUrl()}outline/${blockId}`; export const getClipboardUrl = () => `${getApiBaseUrl()}/api/content-staging/v1/clipboard/`; -export const getTagsCountApiUrl = (contentPattern) => new URL(`api/content_tagging/v1/object_tag_counts/${contentPattern}/?count_implicit`, getApiBaseUrl()).href; /** * @typedef {Object} courseOutline @@ -473,18 +472,3 @@ export async function dismissNotification(url) { await getAuthenticatedHttpClient() .delete(url); } - -/** - * Gets the tags count of multiple content by id separated by commas. - * @param {string} contentPattern - * @returns {Promise} -*/ -export async function getTagsCount(contentPattern) { - if (contentPattern) { - const { data } = await getAuthenticatedHttpClient() - .get(getTagsCountApiUrl(contentPattern)); - - return data; - } - return null; -} diff --git a/src/course-outline/data/api.test.js b/src/course-outline/data/api.test.js deleted file mode 100644 index 2c7ef9d7d0..0000000000 --- a/src/course-outline/data/api.test.js +++ /dev/null @@ -1,40 +0,0 @@ -import MockAdapter from 'axios-mock-adapter'; -import { initializeMockApp } from '@edx/frontend-platform'; -import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; -import { contentTagsCountMock } from '../__mocks__'; -import { getTagsCountApiUrl, getTagsCount } from './api'; - -let axiosMock; - -describe('course outline api calls', () => { - beforeEach(() => { - initializeMockApp({ - authenticatedUser: { - userId: 3, - username: 'abc123', - administrator: true, - roles: [], - }, - }); - axiosMock = new MockAdapter(getAuthenticatedHttpClient()); - }); - - afterEach(() => { - jest.clearAllMocks(); - }); - - it('should get tags count', async () => { - const pattern = 'this,is,a,pattern'; - const contentId = 'block-v1:SampleTaxonomyOrg1+STC1+2023_1+type@vertical+block@aaf8b8eb86b54281aeeab12499d2cb06'; - axiosMock.onGet().reply(200, contentTagsCountMock); - const result = await getTagsCount(pattern); - expect(axiosMock.history.get[0].url).toEqual(getTagsCountApiUrl(pattern)); - expect(result).toEqual(contentTagsCountMock); - expect(contentTagsCountMock[contentId]).toEqual(15); - }); - - it('should get null on empty pattenr', async () => { - const result = await getTagsCount(''); - expect(result).toEqual(null); - }); -}); diff --git a/src/course-outline/data/apiHooks.jsx b/src/course-outline/data/apiHooks.jsx deleted file mode 100644 index ec1207fdd3..0000000000 --- a/src/course-outline/data/apiHooks.jsx +++ /dev/null @@ -1,16 +0,0 @@ -// @ts-check -import { useQuery } from '@tanstack/react-query'; -import { getTagsCount } from './api'; - -/** - * Builds the query to get tags count of a group of units. - * @param {string} contentPattern The IDs of units separated by commas. - */ -const useUnitTagsCount = (contentPattern) => ( - useQuery({ - queryKey: ['unitTagsCount', contentPattern], - queryFn: /* istanbul ignore next */ () => getTagsCount(contentPattern), - }) -); - -export default useUnitTagsCount; diff --git a/src/course-outline/drag-helper/DragContextProvider.jsx b/src/course-outline/drag-helper/DragContextProvider.jsx index d03cfa4cba..48a497d7ef 100644 --- a/src/course-outline/drag-helper/DragContextProvider.jsx +++ b/src/course-outline/drag-helper/DragContextProvider.jsx @@ -1,7 +1,7 @@ import React from 'react'; import PropTypes from 'prop-types'; -export const DragContext = React.createContext({}); +export const DragContext = React.createContext({ activeId: '', overId: '', children: undefined }); const DragContextProvider = ({ activeId, overId, children }) => { const contextValue = React.useMemo(() => ({ diff --git a/src/course-outline/section-card/SectionCard.jsx b/src/course-outline/section-card/SectionCard.jsx index 6e3f617f7c..6d3f3f490b 100644 --- a/src/course-outline/section-card/SectionCard.jsx +++ b/src/course-outline/section-card/SectionCard.jsx @@ -1,3 +1,4 @@ +// @ts-check import React, { useContext, useEffect, useState, useRef, } from 'react'; diff --git a/src/course-outline/section-card/SectionCard.test.jsx b/src/course-outline/section-card/SectionCard.test.jsx index cea2a0fa6b..5c99814c00 100644 --- a/src/course-outline/section-card/SectionCard.test.jsx +++ b/src/course-outline/section-card/SectionCard.test.jsx @@ -7,6 +7,7 @@ import { AppProvider } from '@edx/frontend-platform/react'; import { initializeMockApp } from '@edx/frontend-platform'; import MockAdapter from 'axios-mock-adapter'; import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import initializeStore from '../../store'; import SectionCard from './SectionCard'; @@ -34,30 +35,34 @@ const section = { const onEditSectionSubmit = jest.fn(); +const queryClient = new QueryClient(); + const renderComponent = (props) => render( - - - children - - , + + + + children + + + , ); diff --git a/src/course-outline/status-bar/StatusBar.jsx b/src/course-outline/status-bar/StatusBar.jsx index 8d2c2f05b1..97c4b93538 100644 --- a/src/course-outline/status-bar/StatusBar.jsx +++ b/src/course-outline/status-bar/StatusBar.jsx @@ -2,16 +2,38 @@ import React, { useContext } from 'react'; import moment from 'moment/moment'; import PropTypes from 'prop-types'; import { FormattedDate, useIntl } from '@edx/frontend-platform/i18n'; +import { getConfig } from '@edx/frontend-platform/config'; import { - Button, Hyperlink, Form, Stack, + Button, Hyperlink, Form, Sheet, Stack, useToggle, } from '@openedx/paragon'; import { AppContext } from '@edx/frontend-platform/react'; +import { ContentTagsDrawer } from '../../content-tags-drawer'; +import TagCount from '../../generic/tag-count'; import { useHelpUrls } from '../../help-urls/hooks'; import { VIDEO_SHARING_OPTIONS } from '../constants'; +import { useContentTagsCount } from '../../generic/data/apiHooks'; import messages from './messages'; import { getVideoSharingOptionText } from '../utils'; +const StatusBarItem = ({ title, children }) => ( +
+
{title}
+
+ {children} +
+
+); + +StatusBarItem.propTypes = { + title: PropTypes.string.isRequired, + children: PropTypes.node, +}; + +StatusBarItem.defaultProps = { + children: null, +}; + const StatusBar = ({ statusBarData, isLoading, @@ -48,109 +70,135 @@ const StatusBar = ({ socialSharing: socialSharingUrl, } = useHelpUrls(['contentHighlights', 'socialSharing']); + const { data: courseTagCount } = useContentTagsCount(courseId); + + const [isManageTagsDrawerOpen, openManageTagsDrawer, closeManageTagsDrawer] = useToggle(false); + if (isLoading) { - // eslint-disable-next-line react/jsx-no-useless-fragment - return <>; + return null; } return ( - -
-
{intl.formatMessage(messages.startDateTitle)}
- - {courseReleaseDateObj.isValid() ? ( - - ) : courseReleaseDate} - -
-
-
{intl.formatMessage(messages.pacingTypeTitle)}
- - {isSelfPaced - ? intl.formatMessage(messages.pacingTypeSelfPaced) - : intl.formatMessage(messages.pacingTypeInstructorPaced)} - -
-
-
{intl.formatMessage(messages.checklistTitle)}
- - {checkListTitle} {intl.formatMessage(messages.checklistCompleted)} - -
-
-
{intl.formatMessage(messages.highlightEmailsTitle)}
-
- {highlightsEnabledForMessaging ? ( - - {intl.formatMessage(messages.highlightEmailsEnabled)} - - ) : ( - - )} + <> + + - {intl.formatMessage(messages.highlightEmailsLink)} + {courseReleaseDateObj.isValid() ? ( + + ) : courseReleaseDate} -
-
- {videoSharingEnabled && ( - - {intl.formatMessage(messages.videoSharingTitle)} - + + + + {isSelfPaced + ? intl.formatMessage(messages.pacingTypeSelfPaced) + : intl.formatMessage(messages.pacingTypeInstructorPaced)} + + + + + {checkListTitle} {intl.formatMessage(messages.checklistCompleted)} + + +
- handleVideoSharingOptionChange(e.target.value)} - > - {Object.values(VIDEO_SHARING_OPTIONS).map((option) => ( - - ))} - + {highlightsEnabledForMessaging ? ( + + {intl.formatMessage(messages.highlightEmailsEnabled)} + + ) : ( + + )} - {intl.formatMessage(messages.videoSharingLink)} + {intl.formatMessage(messages.highlightEmailsLink)}
-
+ + {getConfig().ENABLE_TAGGING_TAXONOMY_PAGES === 'true' && ( + +
+ + { /* eslint-disable-next-line jsx-a11y/anchor-is-valid */ } + + {intl.formatMessage(messages.courseManageTagsLink)} + +
+
+ )} + {videoSharingEnabled && ( + + {intl.formatMessage(messages.videoSharingTitle)} + +
+ handleVideoSharingOptionChange(e.target.value)} + > + {Object.values(VIDEO_SHARING_OPTIONS).map((option) => ( + + ))} + + + {intl.formatMessage(messages.videoSharingLink)} + +
+
- )} -
+ )} + + closeManageTagsDrawer()} + > + closeManageTagsDrawer()} + /> + + ); }; diff --git a/src/course-outline/status-bar/StatusBar.test.jsx b/src/course-outline/status-bar/StatusBar.test.jsx index 5891720be4..c57613ae04 100644 --- a/src/course-outline/status-bar/StatusBar.test.jsx +++ b/src/course-outline/status-bar/StatusBar.test.jsx @@ -3,6 +3,8 @@ import { render, fireEvent } from '@testing-library/react'; import { IntlProvider } from '@edx/frontend-platform/i18n'; import { AppProvider } from '@edx/frontend-platform/react'; import { initializeMockApp } from '@edx/frontend-platform'; +import { getConfig, setConfig } from '@edx/frontend-platform/config'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import StatusBar from './StatusBar'; import messages from './messages'; @@ -11,7 +13,7 @@ import { VIDEO_SHARING_OPTIONS } from '../constants'; let store; const mockPathname = '/foo-bar'; -const courseId = '123'; +const courseId = 'course-v1:123'; const isLoading = false; const openEnableHighlightsModalMock = jest.fn(); const handleVideoSharingOptionChange = jest.fn(); @@ -23,6 +25,11 @@ jest.mock('react-router-dom', () => ({ }), })); +jest.mock('../../generic/data/api', () => ({ + ...jest.requireActual('../../generic/data/api'), + getTagsCount: jest.fn().mockResolvedValue({ 'course-v1:123': 17 }), +})); + jest.mock('../../help-urls/hooks', () => ({ useHelpUrls: () => ({ contentHighlights: 'content-highlights-link', @@ -45,18 +52,22 @@ const statusBarData = { videoSharingOptions: VIDEO_SHARING_OPTIONS.allOn, }; +const queryClient = new QueryClient(); + const renderComponent = (props) => render( - - - + + + + + , ); @@ -133,4 +144,23 @@ describe('', () => { expect(queryByTestId('video-sharing-wrapper')).not.toBeInTheDocument(); }); + + it('renders the tag count if the waffle flag is enabled', async () => { + setConfig({ + ...getConfig(), + ENABLE_TAGGING_TAXONOMY_PAGES: 'true', + }); + const { findByText } = renderComponent(); + + expect(await findByText('17')).toBeInTheDocument(); + }); + it('doesnt renders the tag count if the waffle flag is disabled', () => { + setConfig({ + ...getConfig(), + ENABLE_TAGGING_TAXONOMY_PAGES: 'false', + }); + const { queryByText } = renderComponent(); + + expect(queryByText('17')).not.toBeInTheDocument(); + }); }); diff --git a/src/course-outline/status-bar/messages.js b/src/course-outline/status-bar/messages.js index 7c8b75ae4d..e7c752f92b 100644 --- a/src/course-outline/status-bar/messages.js +++ b/src/course-outline/status-bar/messages.js @@ -41,6 +41,16 @@ const messages = defineMessages({ id: 'course-authoring.course-outline.status-bar.highlight-emails.link', defaultMessage: 'Learn more', }, + courseTagsTitle: { + id: 'course-authoring.course-outline.status-bar.course-tags', + defaultMessage: 'Course tags', + description: 'Course tags header in course outline', + }, + courseManageTagsLink: { + id: 'course-authoring.course-outline.status-bar.course-manage-tags-link', + defaultMessage: 'Manage tags', + description: 'Opens the drawer to edit content tags', + }, videoSharingTitle: { id: 'course-authoring.course-outline.status-bar.video-sharing.title', defaultMessage: 'Video Sharing', diff --git a/src/course-outline/subsection-card/SubsectionCard.jsx b/src/course-outline/subsection-card/SubsectionCard.jsx index 8019042e5b..441a4e34f3 100644 --- a/src/course-outline/subsection-card/SubsectionCard.jsx +++ b/src/course-outline/subsection-card/SubsectionCard.jsx @@ -1,4 +1,5 @@ -import { +// @ts-check +import React, { useContext, useEffect, useState, useRef, } from 'react'; import PropTypes from 'prop-types'; @@ -165,6 +166,7 @@ const SubsectionCard = ({ render( - - - - children - - , - , + + + + + children + + + + , ); diff --git a/src/course-outline/unit-card/UnitCard.jsx b/src/course-outline/unit-card/UnitCard.jsx index 11fe255bde..2f01de7854 100644 --- a/src/course-outline/unit-card/UnitCard.jsx +++ b/src/course-outline/unit-card/UnitCard.jsx @@ -1,7 +1,8 @@ -import { useEffect, useRef, useState } from 'react'; +// @ts-check +import React, { useEffect, useRef } from 'react'; import PropTypes from 'prop-types'; import { useDispatch } from 'react-redux'; -import { useToggle, Sheet } from '@openedx/paragon'; +import { useToggle } from '@openedx/paragon'; import { isEmpty } from 'lodash'; import { setCurrentItem, setCurrentSection, setCurrentSubsection } from '../data/slice'; @@ -11,7 +12,6 @@ import SortableItem from '../drag-helper/SortableItem'; import TitleLink from '../card-header/TitleLink'; import XBlockStatus from '../xblock-status/XBlockStatus'; import { getItemStatus, getItemStatusBorder, scrollToElement } from '../utils'; -import { ContentTagsDrawer } from '../../content-tags-drawer'; const UnitCard = ({ unit, @@ -31,13 +31,11 @@ const UnitCard = ({ onOrderChange, onCopyToClipboardClick, discussionsSettings, - tagsCount, }) => { const currentRef = useRef(null); const dispatch = useDispatch(); const [isFormOpen, openForm, closeForm] = useToggle(false); const namePrefix = 'unit'; - const [showManageTags, setShowManageTags] = useState(false); const { id, @@ -129,77 +127,63 @@ const UnitCard = ({ const isDraggable = actions.draggable && (actions.allowMoveUp || actions.allowMoveDown); return ( - <> - +
-
- setShowManageTags(true)} - onClickEdit={openForm} - onClickDelete={onOpenDeleteModal} - onClickMoveUp={handleUnitMoveUp} - onClickMoveDown={handleUnitMoveDown} - isFormOpen={isFormOpen} - closeForm={closeForm} - onEditSubmit={handleEditSubmit} - isDisabledEditField={savingStatus === RequestStatus.IN_PROGRESS} - onClickDuplicate={onDuplicateSubmit} - titleComponent={titleComponent} - namePrefix={namePrefix} - actions={actions} - isVertical - enableCopyPasteUnits={enableCopyPasteUnits} - onClickCopy={handleCopyClick} - discussionEnabled={discussionEnabled} - discussionsSettings={discussionsSettings} - parentInfo={parentInfo} - tagsCount={tagsCount} + +
+ -
- -
- - setShowManageTags(false)} - > - setShowManageTags(false)} - /> - - +
+ ); }; UnitCard.defaultProps = { discussionsSettings: {}, - tagsCount: undefined, }; UnitCard.propTypes = { @@ -256,7 +240,6 @@ UnitCard.propTypes = { providerType: PropTypes.string, enableGradedUnits: PropTypes.bool, }), - tagsCount: PropTypes.number, }; export default UnitCard; diff --git a/src/course-outline/unit-card/UnitCard.test.jsx b/src/course-outline/unit-card/UnitCard.test.jsx index 6468e553fb..ada25836cb 100644 --- a/src/course-outline/unit-card/UnitCard.test.jsx +++ b/src/course-outline/unit-card/UnitCard.test.jsx @@ -7,6 +7,7 @@ import { AppProvider } from '@edx/frontend-platform/react'; import { initializeMockApp } from '@edx/frontend-platform'; import MockAdapter from 'axios-mock-adapter'; import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import initializeStore from '../../store'; import UnitCard from './UnitCard'; @@ -49,29 +50,33 @@ const unit = { isHeaderVisible: true, }; +const queryClient = new QueryClient(); + const renderComponent = (props) => render( - - `/some/${id}`} - isSelfPaced={false} - isCustomRelativeDatesActive={false} - {...props} - /> - , + + + `/some/${id}`} + isSelfPaced={false} + isCustomRelativeDatesActive={false} + {...props} + /> + + , ); diff --git a/src/course-unit/CourseUnit.test.jsx b/src/course-unit/CourseUnit.test.jsx index 5a9685aea3..78b616ad7a 100644 --- a/src/course-unit/CourseUnit.test.jsx +++ b/src/course-unit/CourseUnit.test.jsx @@ -69,7 +69,7 @@ jest.mock('@tanstack/react-query', () => ({ }, isSuccess: true, }; - } if (queryKey[0] === 'contentTaxonomyTagsCount') { + } if (queryKey[0] === 'contentTagsCount') { return { data: 17, isSuccess: true, diff --git a/src/course-outline/__mocks__/contentTagsCount.js b/src/generic/__mocks__/contentTagsCount.js similarity index 97% rename from src/course-outline/__mocks__/contentTagsCount.js rename to src/generic/__mocks__/contentTagsCount.js index b2fa2e8cd4..218e91e150 100644 --- a/src/course-outline/__mocks__/contentTagsCount.js +++ b/src/generic/__mocks__/contentTagsCount.js @@ -1,4 +1,4 @@ -module.exports = { +export default { 'block-v1:SampleTaxonomyOrg1+STC1+2023_1+type@vertical+block@aaf8b8eb86b54281aeeab12499d2cb01': 10, 'block-v1:SampleTaxonomyOrg1+STC1+2023_1+type@vertical+block@aaf8b8eb86b54281aeeab12499d2cb02': 11, 'block-v1:SampleTaxonomyOrg1+STC1+2023_1+type@vertical+block@aaf8b8eb86b54281aeeab12499d2cb03': 12, diff --git a/src/generic/__mocks__/index.js b/src/generic/__mocks__/index.js new file mode 100644 index 0000000000..0436efae7e --- /dev/null +++ b/src/generic/__mocks__/index.js @@ -0,0 +1,2 @@ +/* eslint-disable import/prefer-default-export */ +export { default as contentTagsCountMock } from './contentTagsCount'; diff --git a/src/generic/data/api.js b/src/generic/data/api.js index c00e302efd..6cec7b9159 100644 --- a/src/generic/data/api.js +++ b/src/generic/data/api.js @@ -8,6 +8,7 @@ export const getApiBaseUrl = () => getConfig().STUDIO_BASE_URL; export const getCreateOrRerunCourseUrl = () => new URL('course/', getApiBaseUrl()).href; export const getCourseRerunUrl = (courseId) => new URL(`/api/contentstore/v1/course_rerun/${courseId}`, getApiBaseUrl()).href; export const getOrganizationsUrl = () => new URL('organizations', getApiBaseUrl()).href; +export const getTagsCountApiUrl = (contentPattern) => new URL(`api/content_tagging/v1/object_tag_counts/${contentPattern}/?count_implicit`, getApiBaseUrl()).href; /** * Get's organizations data. Returns list of organization names. @@ -43,3 +44,18 @@ export async function createOrRerunCourse(courseData) { ); return camelCaseObject(data); } + +/** + * Gets the tags count of multiple content by id separated by commas or a pattern using a '*' wildcard. + * @param {string} contentPattern + * @returns {Promise} +*/ +export async function getTagsCount(contentPattern) { + if (contentPattern) { + const { data } = await getAuthenticatedHttpClient() + .get(getTagsCountApiUrl(contentPattern)); + + return data; + } + return null; +} diff --git a/src/generic/data/api.test.js b/src/generic/data/api.test.js index 7abd517945..96b6634d54 100644 --- a/src/generic/data/api.test.js +++ b/src/generic/data/api.test.js @@ -2,6 +2,7 @@ import MockAdapter from 'axios-mock-adapter'; import { initializeMockApp } from '@edx/frontend-platform'; import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; +import { contentTagsCountMock } from '../__mocks__'; import { createOrRerunCourse, getApiBaseUrl, @@ -9,6 +10,8 @@ import { getCreateOrRerunCourseUrl, getCourseRerunUrl, getCourseRerun, + getTagsCount, + getTagsCountApiUrl, } from './api'; let axiosMock; @@ -72,4 +75,19 @@ describe('generic api calls', () => { expect(axiosMock.history.post[0].url).toEqual(getCreateOrRerunCourseUrl()); expect(result).toEqual(courseRerunData); }); + + it('should get tags count', async () => { + const pattern = 'this,is,a,pattern'; + const contentId = 'block-v1:SampleTaxonomyOrg1+STC1+2023_1+type@vertical+block@aaf8b8eb86b54281aeeab12499d2cb06'; + axiosMock.onGet().reply(200, contentTagsCountMock); + const result = await getTagsCount(pattern); + expect(axiosMock.history.get[0].url).toEqual(getTagsCountApiUrl(pattern)); + expect(result).toEqual(contentTagsCountMock); + expect(contentTagsCountMock[contentId]).toEqual(15); + }); + + it('should get null on empty pattern', async () => { + const result = await getTagsCount(''); + expect(result).toEqual(null); + }); }); diff --git a/src/generic/data/apiHooks.js b/src/generic/data/apiHooks.js index 5640878b20..e15ebc12ad 100644 --- a/src/generic/data/apiHooks.js +++ b/src/generic/data/apiHooks.js @@ -1,6 +1,6 @@ // @ts-check import { useQuery } from '@tanstack/react-query'; -import { getOrganizations } from './api'; +import { getOrganizations, getTagsCount } from './api'; /** * Builds the query to get a list of available organizations @@ -12,4 +12,23 @@ export const useOrganizationListData = () => ( }) ); -export default useOrganizationListData; +/** + * Builds the query to get tags count of the whole contentId course and + * returns the tags count of the specific contentId. + * @param {string} contentId + */ +export const useContentTagsCount = (contentId) => { + let contentPattern; + if (contentId.includes('course-v1')) { + // If the contentId is a course, we want to get the tags count only for the course + contentPattern = contentId; + } else { + // If the contentId is not a course, we want to get the tags count for all the content of the course + contentPattern = contentId.replace(/\+type@.*$/, '*'); + } + return useQuery({ + queryKey: ['contentTagsCount', contentPattern], + queryFn: /* istanbul ignore next */ () => getTagsCount(contentPattern), + select: (data) => data[contentId] || 0, // Return the tags count of the specific contentId + }); +}; diff --git a/src/course-outline/data/apiHooks.test.jsx b/src/generic/data/apiHooks.test.js similarity index 75% rename from src/course-outline/data/apiHooks.test.jsx rename to src/generic/data/apiHooks.test.js index 0c9bf506bb..feb6f7fd59 100644 --- a/src/course-outline/data/apiHooks.test.jsx +++ b/src/generic/data/apiHooks.test.js @@ -1,5 +1,5 @@ import { useQuery } from '@tanstack/react-query'; -import useUnitTagsCount from './apiHooks'; +import { useContentTagsCount } from './apiHooks'; jest.mock('@tanstack/react-query', () => ({ useQuery: jest.fn(), @@ -9,11 +9,11 @@ jest.mock('./api', () => ({ getTagsCount: jest.fn(), })); -describe('useUnitTagsCount', () => { +describe('useContentTagsCount', () => { it('should return success response', () => { useQuery.mockReturnValueOnce({ isSuccess: true, data: 'data' }); const pattern = '123'; - const result = useUnitTagsCount(pattern); + const result = useContentTagsCount(pattern); expect(result).toEqual({ isSuccess: true, data: 'data' }); }); @@ -21,7 +21,7 @@ describe('useUnitTagsCount', () => { it('should return failure response', () => { useQuery.mockReturnValueOnce({ isSuccess: false }); const pattern = '123'; - const result = useUnitTagsCount(pattern); + const result = useContentTagsCount(pattern); expect(result).toEqual({ isSuccess: false }); });