From d8618139f27de6781ebddea70dd7f82c2c74256f Mon Sep 17 00:00:00 2001 From: XnpioChV Date: Tue, 27 Feb 2024 11:31:28 -0500 Subject: [PATCH 1/9] feat: TagCount component --- src/generic/tag-count/TagCount.scss | 3 +++ src/generic/tag-count/TagCount.test.jsx | 17 +++++++++++++++++ src/generic/tag-count/index.jsx | 20 ++++++++++++++++++++ 3 files changed, 40 insertions(+) create mode 100644 src/generic/tag-count/TagCount.scss create mode 100644 src/generic/tag-count/TagCount.test.jsx create mode 100644 src/generic/tag-count/index.jsx diff --git a/src/generic/tag-count/TagCount.scss b/src/generic/tag-count/TagCount.scss new file mode 100644 index 0000000000..2002e2e8e5 --- /dev/null +++ b/src/generic/tag-count/TagCount.scss @@ -0,0 +1,3 @@ +.generic-tag-count.zero-count { + opacity: .4; +} diff --git a/src/generic/tag-count/TagCount.test.jsx b/src/generic/tag-count/TagCount.test.jsx new file mode 100644 index 0000000000..15be3efa9f --- /dev/null +++ b/src/generic/tag-count/TagCount.test.jsx @@ -0,0 +1,17 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import TagCount from '.'; + +describe('', () => { + it('should render the component', () => { + const count = 17; + render(); + expect(screen.getByText('17')).toBeInTheDocument(); + }); + + it('should render the component with zero', () => { + const count = 0; + render(); + expect(screen.getByText('0')).toBeInTheDocument(); + }); +}); diff --git a/src/generic/tag-count/index.jsx b/src/generic/tag-count/index.jsx new file mode 100644 index 0000000000..85430c369f --- /dev/null +++ b/src/generic/tag-count/index.jsx @@ -0,0 +1,20 @@ +import PropTypes from 'prop-types'; +import { Icon } from '@openedx/paragon'; +import { Tag } from '@openedx/paragon/icons'; +import classNames from 'classnames'; + +const TagCount = ({ count }) => ( +
+ + {count} +
+); + +TagCount.propTypes = { + count: PropTypes.number.isRequired, +}; + +export default TagCount; From a6c5e5f8490d67de1e7bedafb177c2b8e91f49c0 Mon Sep 17 00:00:00 2001 From: XnpioChV Date: Tue, 27 Feb 2024 12:59:53 -0500 Subject: [PATCH 2/9] feat: Update ContentTagsDrawer to use it in the MFE --- src/content-tags-drawer/ContentTagsDrawer.jsx | 35 ++++++++++++---- .../ContentTagsDrawer.test.jsx | 40 +++++++++++++++---- 2 files changed, 60 insertions(+), 15 deletions(-) diff --git a/src/content-tags-drawer/ContentTagsDrawer.jsx b/src/content-tags-drawer/ContentTagsDrawer.jsx index 9429a65f38..1b1bb15f97 100644 --- a/src/content-tags-drawer/ContentTagsDrawer.jsx +++ b/src/content-tags-drawer/ContentTagsDrawer.jsx @@ -1,5 +1,6 @@ // @ts-check import React, { useMemo, useEffect } from 'react'; +import PropTypes from 'prop-types'; import { Container, CloseButton, @@ -20,9 +21,14 @@ import Loading from '../generic/Loading'; /** @typedef {import("../taxonomy/data/types.mjs").TaxonomyData} TaxonomyData */ /** @typedef {import("./data/types.mjs").Tag} ContentTagData */ -const ContentTagsDrawer = () => { +const ContentTagsDrawer = ({ id, onClose }) => { const intl = useIntl(); - const { contentId } = /** @type {{contentId: string}} */(useParams()); + const params = useParams(); + let contentId = id; + + if (contentId === undefined) { + contentId = params.contentId; + } const org = extractOrgFromContentId(contentId); @@ -39,17 +45,20 @@ const ContentTagsDrawer = () => { } = useContentTaxonomyTagsData(contentId); const { taxonomyListData, isTaxonomyListLoaded } = useTaxonomyListData(); - const closeContentTagsDrawer = () => { - // "*" allows communication with any origin - window.parent.postMessage('closeManageTagsDrawer', '*'); - }; + 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) { - closeContentTagsDrawer(); + onCloseDrawer(); } }; document.addEventListener('keydown', handleEsc); @@ -86,7 +95,7 @@ const ContentTagsDrawer = () => {
- closeContentTagsDrawer()} data-testid="drawer-close-button" /> + onCloseDrawer()} data-testid="drawer-close-button" /> {intl.formatMessage(messages.headerSubtitle)} { isContentDataLoaded ?

{ contentData.displayName }

@@ -116,4 +125,14 @@ const ContentTagsDrawer = () => { ); }; +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 b8fe58c3b8..0f7f1815af 100644 --- a/src/content-tags-drawer/ContentTagsDrawer.test.jsx +++ b/src/content-tags-drawer/ContentTagsDrawer.test.jsx @@ -1,6 +1,8 @@ import React from 'react'; import { IntlProvider } from '@edx/frontend-platform/i18n'; -import { act, render, fireEvent } from '@testing-library/react'; +import { + act, render, fireEvent, screen, +} from '@testing-library/react'; import ContentTagsDrawer from './ContentTagsDrawer'; import { @@ -9,10 +11,13 @@ import { } from './data/apiHooks'; import { useTaxonomyListDataResponse, useIsTaxonomyListDataLoaded } from '../taxonomy/data/apiHooks'; +const contentId = 'block-v1:SampleTaxonomyOrg1+STC1+2023_1+type@vertical+block@7f47fe2dbcaf47c5a071671c741fe1ab'; +const mockOnClose = jest.fn(); + jest.mock('react-router-dom', () => ({ ...jest.requireActual('react-router-dom'), useParams: () => ({ - contentId: 'block-v1:SampleTaxonomyOrg1+STC1+2023_1+type@vertical+block@7f47fe2dbcaf47c5a071671c741fe1ab', + contentId, }), })); @@ -35,9 +40,9 @@ jest.mock('../taxonomy/data/apiHooks', () => ({ useIsTaxonomyListDataLoaded: jest.fn(), })); -const RootWrapper = () => ( +const RootWrapper = (params) => ( - + ); @@ -77,6 +82,17 @@ describe('', () => { }); }); + it('shows content using params', async () => { + useContentData.mockReturnValue({ + isSuccess: true, + data: { + displayName: 'Unit 1', + }, + }); + render(); + expect(screen.getByText('Unit 1')).toBeInTheDocument(); + }); + it('shows the taxonomies data including tag numbers after the query is complete', async () => { useIsTaxonomyListDataLoaded.mockReturnValue(true); useContentTaxonomyTagsData.mockReturnValue({ @@ -138,7 +154,7 @@ describe('', () => { }); }); - it('should call closeContentTagsDrawer when CloseButton is clicked', async () => { + it('should call closeManageTagsDrawer when CloseButton is clicked', async () => { const postMessageSpy = jest.spyOn(window.parent, 'postMessage'); const { getByTestId } = render(); @@ -152,7 +168,17 @@ describe('', () => { postMessageSpy.mockRestore(); }); - it('should call closeContentTagsDrawer when Escape key is pressed and no selectable box is active', () => { + it('should call onClose param when CloseButton is clicked', async () => { + render(); + + // Find the CloseButton element by its test ID and trigger a click event + const closeButton = screen.getByTestId('drawer-close-button'); + fireEvent.click(closeButton); + + expect(mockOnClose).toHaveBeenCalled(); + }); + + 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(); @@ -166,7 +192,7 @@ describe('', () => { postMessageSpy.mockRestore(); }); - it('should not call closeContentTagsDrawer when Escape key is pressed and a selectable box is active', () => { + 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(); From ac4785f9724e0fef85d1fb27ce0570405dc03e2d Mon Sep 17 00:00:00 2001 From: XnpioChV Date: Tue, 27 Feb 2024 13:27:36 -0500 Subject: [PATCH 3/9] feat: Manage tags menu added on units --- src/course-outline/card-header/CardHeader.jsx | 12 ++ .../card-header/CardHeader.test.jsx | 14 ++- src/course-outline/card-header/messages.js | 4 + src/course-outline/unit-card/UnitCard.jsx | 111 ++++++++++-------- 4 files changed, 92 insertions(+), 49 deletions(-) diff --git a/src/course-outline/card-header/CardHeader.jsx b/src/course-outline/card-header/CardHeader.jsx index 1524b3fb17..4e7cae4e43 100644 --- a/src/course-outline/card-header/CardHeader.jsx +++ b/src/course-outline/card-header/CardHeader.jsx @@ -27,6 +27,7 @@ const CardHeader = ({ hasChanges, onClickPublish, onClickConfigure, + onClickManageTags, onClickMenuButton, onClickEdit, isFormOpen, @@ -162,6 +163,15 @@ const CardHeader = ({ > {intl.formatMessage(messages.menuConfigure)} + {onClickManageTags && ( + + {intl.formatMessage(messages.menuManageTags)} + + )} + {isVertical && enableCopyPasteUnits && ( {intl.formatMessage(messages.menuCopy)} @@ -218,6 +228,7 @@ CardHeader.defaultProps = { discussionEnabled: false, discussionsSettings: {}, parentInfo: {}, + onClickManageTags: null, }; CardHeader.propTypes = { @@ -227,6 +238,7 @@ 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, diff --git a/src/course-outline/card-header/CardHeader.test.jsx b/src/course-outline/card-header/CardHeader.test.jsx index 1a666b6614..35ce66d599 100644 --- a/src/course-outline/card-header/CardHeader.test.jsx +++ b/src/course-outline/card-header/CardHeader.test.jsx @@ -1,6 +1,6 @@ import { MemoryRouter } from 'react-router-dom'; import { - act, render, fireEvent, waitFor, + act, render, fireEvent, waitFor, screen, } from '@testing-library/react'; import { IntlProvider } from '@edx/frontend-platform/i18n'; @@ -18,6 +18,7 @@ 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 cardHeaderProps = { @@ -28,6 +29,7 @@ const cardHeaderProps = { onClickMenuButton: onClickMenuButtonMock, onClickPublish: onClickPublishMock, onClickEdit: onClickEditMock, + onClickManageTags: onClickManageTagsMock, isFormOpen: false, onEditSubmit: jest.fn(), closeForm: closeFormMock, @@ -168,6 +170,16 @@ describe('', () => { expect(onClickPublishMock).toHaveBeenCalled(); }); + it('calls onClickManageTags when the menu is clicked', async () => { + 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(); + }); + it('calls onClickEdit when the button is clicked', async () => { const { findByTestId } = renderComponent(); diff --git a/src/course-outline/card-header/messages.js b/src/course-outline/card-header/messages.js index d9f250970d..410443d695 100644 --- a/src/course-outline/card-header/messages.js +++ b/src/course-outline/card-header/messages.js @@ -73,6 +73,10 @@ const messages = defineMessages({ id: 'course-authoring.course-outline.card.badge.discussionEnabled', defaultMessage: 'Discussions enabled', }, + menuManageTags: { + id: 'course-authoring.course-outline.card.menu.manageTags', + defaultMessage: 'Manage tags', + }, }); export default messages; diff --git a/src/course-outline/unit-card/UnitCard.jsx b/src/course-outline/unit-card/UnitCard.jsx index f2cb4ac3e9..1ab5a97a5c 100644 --- a/src/course-outline/unit-card/UnitCard.jsx +++ b/src/course-outline/unit-card/UnitCard.jsx @@ -1,7 +1,7 @@ -import { useEffect, useRef } from 'react'; +import { useEffect, useRef, useState } from 'react'; import PropTypes from 'prop-types'; import { useDispatch } from 'react-redux'; -import { useToggle } from '@openedx/paragon'; +import { useToggle, Sheet } from '@openedx/paragon'; import { setCurrentItem, setCurrentSection, setCurrentSubsection } from '../data/slice'; import { RequestStatus } from '../../data/constants'; @@ -10,6 +10,7 @@ import ConditionalSortableElement from '../drag-helper/ConditionalSortableElemen 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, @@ -34,6 +35,7 @@ const UnitCard = ({ const dispatch = useDispatch(); const [isFormOpen, openForm, closeForm] = useToggle(false); const namePrefix = 'unit'; + const [showManageTags, setShowManageTags] = useState(false); const { id, @@ -122,55 +124,68 @@ 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} /> +
+ +
-
-
+ + setShowManageTags(false)} + > + setShowManageTags(false)} + /> + + ); }; From 0050596938c185494926930685d091b230156aef Mon Sep 17 00:00:00 2001 From: XnpioChV Date: Wed, 28 Feb 2024 11:43:43 -0500 Subject: [PATCH 4/9] feat: Tag count added on unit --- src/content-tags-drawer/data/apiHooks.jsx | 2 ++ src/course-outline/CourseOutline.jsx | 25 ++++++++++++++++- src/course-outline/CourseOutline.test.jsx | 5 ++++ src/course-outline/card-header/CardHeader.jsx | 5 ++++ .../card-header/CardHeader.scss | 4 +++ .../card-header/CardHeader.test.jsx | 16 +++++++++++ src/course-outline/data/api.js | 16 +++++++++++ src/course-outline/data/apiHooks.jsx | 16 +++++++++++ src/course-outline/data/apiHooks.test.jsx | 28 +++++++++++++++++++ src/course-outline/unit-card/UnitCard.jsx | 4 +++ 10 files changed, 120 insertions(+), 1 deletion(-) create mode 100644 src/course-outline/data/apiHooks.jsx create mode 100644 src/course-outline/data/apiHooks.test.jsx diff --git a/src/content-tags-drawer/data/apiHooks.jsx b/src/content-tags-drawer/data/apiHooks.jsx index 1b95f60936..bfa97d507c 100644 --- a/src/content-tags-drawer/data/apiHooks.jsx +++ b/src/content-tags-drawer/data/apiHooks.jsx @@ -137,6 +137,8 @@ export const useContentTaxonomyTagsUpdater = (contentId, taxonomyId) => { mutationFn: ({ tags }) => updateContentTaxonomyTags(contentId, taxonomyId, tags), onSettled: () => { queryClient.invalidateQueries({ queryKey: ['contentTaxonomyTags', contentId] }); + /// Invalidate query with pattern on course outline + queryClient.invalidateQueries({ queryKey: ['unitTagsCount'] }); }, }); }; diff --git a/src/course-outline/CourseOutline.jsx b/src/course-outline/CourseOutline.jsx index 986969ee56..0c3dabc3d0 100644 --- a/src/course-outline/CourseOutline.jsx +++ b/src/course-outline/CourseOutline.jsx @@ -1,4 +1,4 @@ -import { useState, useEffect } from 'react'; +import { useState, useEffect, useMemo } from 'react'; import PropTypes from 'prop-types'; import { useIntl } from '@edx/frontend-platform/i18n'; import { @@ -41,6 +41,7 @@ import DeleteModal from './delete-modal/DeleteModal'; import PageAlerts from './page-alerts/PageAlerts'; import { useCourseOutline } from './hooks'; import messages from './messages'; +import useUnitTagsCount from './data/apiHooks'; const CourseOutline = ({ courseId }) => { const intl = useIntl(); @@ -157,6 +158,27 @@ const CourseOutline = ({ courseId }) => { }); }; + 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); + /** * Check if item can be moved by given step. * Inner function returns false if the new index after moving by given step @@ -400,6 +422,7 @@ const CourseOutline = ({ courseId }) => { )} onCopyToClipboardClick={handleCopyToClipboardClick} discussionsSettings={discussionsSettings} + tagsCount={isUnitsTagCountsLoaded ? unitsTagCounts[unit.id] : 0} /> ))} diff --git a/src/course-outline/CourseOutline.test.jsx b/src/course-outline/CourseOutline.test.jsx index 3c2dc76499..55d260af32 100644 --- a/src/course-outline/CourseOutline.test.jsx +++ b/src/course-outline/CourseOutline.test.jsx @@ -78,6 +78,11 @@ jest.mock('@edx/frontend-platform/i18n', () => ({ }), })); +jest.mock('./data/apiHooks', () => () => ({ + data: {}, + isSuccess: true, +})); + const RootWrapper = () => ( diff --git a/src/course-outline/card-header/CardHeader.jsx b/src/course-outline/card-header/CardHeader.jsx index 4e7cae4e43..0b659e1c2d 100644 --- a/src/course-outline/card-header/CardHeader.jsx +++ b/src/course-outline/card-header/CardHeader.jsx @@ -19,6 +19,7 @@ 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, @@ -49,6 +50,7 @@ const CardHeader = ({ discussionEnabled, discussionsSettings, parentInfo, + tagsCount, }) => { const intl = useIntl(); const [searchParams] = useSearchParams(); @@ -128,6 +130,7 @@ const CardHeader = ({ {(isVertical || isSequential) && ( )} + { tagsCount !== undefined && tagsCount !== 0 && } ', () => { expect(queryByText(messages.discussionEnabledBadgeText.defaultMessage)).toBeInTheDocument(); }); + + it('should render tag count if is not zero', () => { + renderComponent({ + ...cardHeaderProps, + tagsCount: 17, + }); + expect(screen.getByText('17')).toBeInTheDocument(); + }); + + it('should not render tag count if is zero', () => { + renderComponent({ + ...cardHeaderProps, + tagsCount: 0, + }); + expect(screen.queryByText('0')).not.toBeInTheDocument(); + }); }); diff --git a/src/course-outline/data/api.js b/src/course-outline/data/api.js index 3c2e038088..79e01ee512 100644 --- a/src/course-outline/data/api.js +++ b/src/course-outline/data/api.js @@ -29,6 +29,7 @@ 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 @@ -472,3 +473,18 @@ 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/apiHooks.jsx b/src/course-outline/data/apiHooks.jsx new file mode 100644 index 0000000000..b4520327e9 --- /dev/null +++ b/src/course-outline/data/apiHooks.jsx @@ -0,0 +1,16 @@ +// @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: () => getTagsCount(contentPattern), + }) +); + +export default useUnitTagsCount; diff --git a/src/course-outline/data/apiHooks.test.jsx b/src/course-outline/data/apiHooks.test.jsx new file mode 100644 index 0000000000..0c9bf506bb --- /dev/null +++ b/src/course-outline/data/apiHooks.test.jsx @@ -0,0 +1,28 @@ +import { useQuery } from '@tanstack/react-query'; +import useUnitTagsCount from './apiHooks'; + +jest.mock('@tanstack/react-query', () => ({ + useQuery: jest.fn(), +})); + +jest.mock('./api', () => ({ + getTagsCount: jest.fn(), +})); + +describe('useUnitTagsCount', () => { + it('should return success response', () => { + useQuery.mockReturnValueOnce({ isSuccess: true, data: 'data' }); + const pattern = '123'; + const result = useUnitTagsCount(pattern); + + expect(result).toEqual({ isSuccess: true, data: 'data' }); + }); + + it('should return failure response', () => { + useQuery.mockReturnValueOnce({ isSuccess: false }); + const pattern = '123'; + const result = useUnitTagsCount(pattern); + + expect(result).toEqual({ isSuccess: false }); + }); +}); diff --git a/src/course-outline/unit-card/UnitCard.jsx b/src/course-outline/unit-card/UnitCard.jsx index 1ab5a97a5c..3a687f2614 100644 --- a/src/course-outline/unit-card/UnitCard.jsx +++ b/src/course-outline/unit-card/UnitCard.jsx @@ -30,6 +30,7 @@ const UnitCard = ({ onOrderChange, onCopyToClipboardClick, discussionsSettings, + tagsCount, }) => { const currentRef = useRef(null); const dispatch = useDispatch(); @@ -165,6 +166,7 @@ const UnitCard = ({ discussionEnabled={discussionEnabled} discussionsSettings={discussionsSettings} parentInfo={parentInfo} + tagsCount={tagsCount} />
Date: Wed, 28 Feb 2024 12:08:42 -0500 Subject: [PATCH 5/9] feat: Add button feat to Tag count --- src/course-outline/card-header/CardHeader.jsx | 2 +- .../card-header/CardHeader.scss | 4 -- src/generic/tag-count/TagCount.test.jsx | 13 +++++-- src/generic/tag-count/index.jsx | 38 ++++++++++++++----- 4 files changed, 38 insertions(+), 19 deletions(-) diff --git a/src/course-outline/card-header/CardHeader.jsx b/src/course-outline/card-header/CardHeader.jsx index 0b659e1c2d..11814f8884 100644 --- a/src/course-outline/card-header/CardHeader.jsx +++ b/src/course-outline/card-header/CardHeader.jsx @@ -130,7 +130,7 @@ const CardHeader = ({ {(isVertical || isSequential) && ( )} - { tagsCount !== undefined && tagsCount !== 0 && } + { tagsCount !== undefined && tagsCount !== 0 && } ', () => { it('should render the component', () => { - const count = 17; - render(); + render(); expect(screen.getByText('17')).toBeInTheDocument(); }); it('should render the component with zero', () => { - const count = 0; - render(); + render(); expect(screen.getByText('0')).toBeInTheDocument(); }); + + it('should render a button with onClick', () => { + render( {}} />); + expect(screen.getByRole('button', { + name: /17/i, + })); + }); }); diff --git a/src/generic/tag-count/index.jsx b/src/generic/tag-count/index.jsx index 85430c369f..bb6dada9d7 100644 --- a/src/generic/tag-count/index.jsx +++ b/src/generic/tag-count/index.jsx @@ -1,20 +1,38 @@ import PropTypes from 'prop-types'; -import { Icon } from '@openedx/paragon'; +import { Icon, Button } from '@openedx/paragon'; import { Tag } from '@openedx/paragon/icons'; import classNames from 'classnames'; -const TagCount = ({ count }) => ( -
- - {count} -
-); +const TagCount = ({ count, onClick }) => { + const renderContent = () => ( + <> + + {count} + + ); + + return ( +
+ { onClick ? ( + + ) + : renderContent()} +
+ ); +}; + +TagCount.defaultProps = { + onClick: undefined, +}; TagCount.propTypes = { count: PropTypes.number.isRequired, + onClick: PropTypes.func, }; export default TagCount; From 191289067efb2cedb7e30624e35209f76ebe083c Mon Sep 17 00:00:00 2001 From: XnpioChV Date: Thu, 29 Feb 2024 09:46:29 -0500 Subject: [PATCH 6/9] test: Course Outline api tests --- .../__mocks__/contentTagsCount.js | 8 ++++ src/course-outline/__mocks__/index.js | 1 + src/course-outline/data/api.test.js | 40 +++++++++++++++++++ 3 files changed, 49 insertions(+) create mode 100644 src/course-outline/__mocks__/contentTagsCount.js create mode 100644 src/course-outline/data/api.test.js diff --git a/src/course-outline/__mocks__/contentTagsCount.js b/src/course-outline/__mocks__/contentTagsCount.js new file mode 100644 index 0000000000..b2fa2e8cd4 --- /dev/null +++ b/src/course-outline/__mocks__/contentTagsCount.js @@ -0,0 +1,8 @@ +module.exports = { + '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, + 'block-v1:SampleTaxonomyOrg1+STC1+2023_1+type@vertical+block@aaf8b8eb86b54281aeeab12499d2cb04': 13, + 'block-v1:SampleTaxonomyOrg1+STC1+2023_1+type@vertical+block@aaf8b8eb86b54281aeeab12499d2cb05': 14, + 'block-v1:SampleTaxonomyOrg1+STC1+2023_1+type@vertical+block@aaf8b8eb86b54281aeeab12499d2cb06': 15, +}; diff --git a/src/course-outline/__mocks__/index.js b/src/course-outline/__mocks__/index.js index 15c6504cb1..c2e4997d34 100644 --- a/src/course-outline/__mocks__/index.js +++ b/src/course-outline/__mocks__/index.js @@ -4,3 +4,4 @@ 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/data/api.test.js b/src/course-outline/data/api.test.js new file mode 100644 index 0000000000..2c7ef9d7d0 --- /dev/null +++ b/src/course-outline/data/api.test.js @@ -0,0 +1,40 @@ +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); + }); +}); From 54803ccba27787a86ab473554375c9759da1a2d4 Mon Sep 17 00:00:00 2001 From: XnpioChV Date: Thu, 29 Feb 2024 10:18:27 -0500 Subject: [PATCH 7/9] test: Ignore lines that can not be tested --- src/content-tags-drawer/data/apiHooks.jsx | 2 +- src/course-outline/data/apiHooks.jsx | 2 +- src/course-outline/unit-card/UnitCard.jsx | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/content-tags-drawer/data/apiHooks.jsx b/src/content-tags-drawer/data/apiHooks.jsx index bfa97d507c..82e9a700ca 100644 --- a/src/content-tags-drawer/data/apiHooks.jsx +++ b/src/content-tags-drawer/data/apiHooks.jsx @@ -135,7 +135,7 @@ export const useContentTaxonomyTagsUpdater = (contentId, taxonomyId) => { * >} */ mutationFn: ({ tags }) => updateContentTaxonomyTags(contentId, taxonomyId, tags), - onSettled: () => { + onSettled: /* istanbul ignore next */ () => { queryClient.invalidateQueries({ queryKey: ['contentTaxonomyTags', contentId] }); /// Invalidate query with pattern on course outline queryClient.invalidateQueries({ queryKey: ['unitTagsCount'] }); diff --git a/src/course-outline/data/apiHooks.jsx b/src/course-outline/data/apiHooks.jsx index b4520327e9..ec1207fdd3 100644 --- a/src/course-outline/data/apiHooks.jsx +++ b/src/course-outline/data/apiHooks.jsx @@ -9,7 +9,7 @@ import { getTagsCount } from './api'; const useUnitTagsCount = (contentPattern) => ( useQuery({ queryKey: ['unitTagsCount', contentPattern], - queryFn: () => getTagsCount(contentPattern), + queryFn: /* istanbul ignore next */ () => getTagsCount(contentPattern), }) ); diff --git a/src/course-outline/unit-card/UnitCard.jsx b/src/course-outline/unit-card/UnitCard.jsx index 3a687f2614..3c4f5ef8a8 100644 --- a/src/course-outline/unit-card/UnitCard.jsx +++ b/src/course-outline/unit-card/UnitCard.jsx @@ -147,7 +147,7 @@ const UnitCard = ({ onClickMenuButton={handleClickMenuButton} onClickPublish={onOpenPublishModal} onClickConfigure={onOpenConfigureModal} - onClickManageTags={() => setShowManageTags(true)} + onClickManageTags={/* istanbul ignore next */ () => setShowManageTags(true)} onClickEdit={openForm} onClickDelete={onOpenDeleteModal} onClickMoveUp={handleUnitMoveUp} @@ -180,11 +180,11 @@ const UnitCard = ({ setShowManageTags(false)} + onClose={/* istanbul ignore next */ () => setShowManageTags(false)} > setShowManageTags(false)} + onClose={/* istanbul ignore next */ () => setShowManageTags(false)} /> From 71de03df44b78480a363e3c26f409790dd212471 Mon Sep 17 00:00:00 2001 From: XnpioChV Date: Mon, 4 Mar 2024 14:56:30 -0500 Subject: [PATCH 8/9] style: Comment added on ContentTagsDrawer --- src/content-tags-drawer/ContentTagsDrawer.jsx | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/content-tags-drawer/ContentTagsDrawer.jsx b/src/content-tags-drawer/ContentTagsDrawer.jsx index 1b1bb15f97..c117f6fd29 100644 --- a/src/content-tags-drawer/ContentTagsDrawer.jsx +++ b/src/content-tags-drawer/ContentTagsDrawer.jsx @@ -21,6 +21,14 @@ import Loading from '../generic/Loading'; /** @typedef {import("../taxonomy/data/types.mjs").TaxonomyData} TaxonomyData */ /** @typedef {import("./data/types.mjs").Tag} ContentTagData */ +/** + * 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. + * - 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(); const params = useParams(); From a6019e50a09441fdbe2f89885731189da943b3da Mon Sep 17 00:00:00 2001 From: XnpioChV Date: Thu, 7 Mar 2024 11:13:46 -0500 Subject: [PATCH 9/9] style: Nits on CardHeader --- src/course-outline/card-header/CardHeader.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/course-outline/card-header/CardHeader.jsx b/src/course-outline/card-header/CardHeader.jsx index 11814f8884..cb43c5cd1e 100644 --- a/src/course-outline/card-header/CardHeader.jsx +++ b/src/course-outline/card-header/CardHeader.jsx @@ -130,7 +130,7 @@ const CardHeader = ({ {(isVertical || isSequential) && ( )} - { tagsCount !== undefined && tagsCount !== 0 && } + { tagsCount > 0 && }