diff --git a/src/course-home/data/__snapshots__/redux.test.js.snap b/src/course-home/data/__snapshots__/redux.test.js.snap index e6f2743fd5..9df06d19e6 100644 --- a/src/course-home/data/__snapshots__/redux.test.js.snap +++ b/src/course-home/data/__snapshots__/redux.test.js.snap @@ -499,6 +499,7 @@ Object { "block-v1:edX+DemoX+Demo_Course+type@chapter+block@bcdabcdabcdabcdabcdabcdabcdabcd2": Object { "complete": false, "courseId": "course-v1:edX+DemoX+Demo_Course", + "hideFromTOC": undefined, "id": "block-v1:edX+DemoX+Demo_Course+type@chapter+block@bcdabcdabcdabcdabcdabcdabcdabcd2", "resumeBlock": false, "sequenceIds": Array [ @@ -514,6 +515,7 @@ Object { "due": null, "effortActivities": 2, "effortTime": 15, + "hideFromTOC": undefined, "icon": null, "id": "block-v1:edX+DemoX+Demo_Course+type@sequential+block@bcdabcdabcdabcdabcdabcdabcdabcd1", "sectionId": "block-v1:edX+DemoX+Demo_Course+type@chapter+block@bcdabcdabcdabcdabcdabcdabcdabcd2", diff --git a/src/course-home/data/api.js b/src/course-home/data/api.js index 963bdefc76..7ce2525ffe 100644 --- a/src/course-home/data/api.js +++ b/src/course-home/data/api.js @@ -136,6 +136,7 @@ export function normalizeOutlineBlocks(courseId, blocks) { title: block.display_name, resumeBlock: block.resume_block, sequenceIds: block.children || [], + hideFromTOC: block.hide_from_toc, }; break; @@ -152,6 +153,7 @@ export function normalizeOutlineBlocks(courseId, blocks) { // link in the outline (even though we ignore the given url and use an internal to ourselves). showLink: !!block.lms_web_url, title: block.display_name, + hideFromTOC: block.hide_from_toc, }; break; diff --git a/src/course-home/outline-tab/OutlineTab.test.jsx b/src/course-home/outline-tab/OutlineTab.test.jsx index bcb219d49e..ba536adaa0 100644 --- a/src/course-home/outline-tab/OutlineTab.test.jsx +++ b/src/course-home/outline-tab/OutlineTab.test.jsx @@ -1269,5 +1269,97 @@ describe('Outline Tab', () => { await waitFor(() => expect(axiosMock.history.post).toHaveLength(1)); expect(axiosMock.history.post[0].url).toEqual(resendEmailUrl); }); + + it('section should show hidden from toc message when hide_from_toc is true', async () => { + const { courseBlocks } = await buildMinimalCourseBlocks(courseId, 'Title', { resumeBlock: true }); + const courseBlocksIds = Object.keys(courseBlocks.blocks); + const newCourseBlocks = courseBlocksIds.reduce((blocks, blockId) => ({ + ...blocks, + [blockId]: { + ...courseBlocks.blocks[blockId], + hide_from_toc: true, + }, + }), {}); + + setTabData({ + course_blocks: { blocks: newCourseBlocks }, + }); + await fetchAndRender(); + + const iconHiddenFromTocSectionNode = screen.getByTestId('hide-from-toc-section-icon'); + const textHiddenFromTocSectionNode = screen.getByTestId('hide-from-toc-section-text'); + expect(iconHiddenFromTocSectionNode).toBeInTheDocument(); + expect(textHiddenFromTocSectionNode).toBeInTheDocument(); + expect(textHiddenFromTocSectionNode.textContent).toBe('Hidden in Course Outline, accessible via link'); + }); + + it('section should not show hidden from toc message when hide_from_toc is false', async () => { + const { courseBlocks } = await buildMinimalCourseBlocks(courseId, 'Title', { resumeBlock: true }); + const courseBlocksIds = Object.keys(courseBlocks.blocks); + const newCourseBlocks = courseBlocksIds.reduce((blocks, blockId) => ({ + ...blocks, + [blockId]: { + ...courseBlocks.blocks[blockId], + hide_from_toc: false, + }, + }), {}); + + setTabData({ + course_blocks: { blocks: newCourseBlocks }, + }); + await fetchAndRender(); + + const iconHiddenFromTocSectionNode = screen.queryByTestId('hide-from-toc-section-icon'); + const textHiddenFromTocSectionNode = screen.queryByTestId('hide-from-toc-section-text'); + + expect(iconHiddenFromTocSectionNode).not.toBeInTheDocument(); + expect(textHiddenFromTocSectionNode).not.toBeInTheDocument(); + }); + + it('sequence link should show hidden from toc message when hide_from_toc is true', async () => { + const { courseBlocks } = await buildMinimalCourseBlocks(courseId, 'Title', { resumeBlock: true }); + const courseBlocksIds = Object.keys(courseBlocks.blocks); + const newCourseBlocks = courseBlocksIds.reduce((blocks, blockId) => ({ + ...blocks, + [blockId]: { + ...courseBlocks.blocks[blockId], + hide_from_toc: true, + }, + }), {}); + + setTabData({ + course_blocks: { blocks: newCourseBlocks }, + }); + await fetchAndRender(); + + const iconHiddenFromTocSequenceLinkNode = screen.getByTestId('hide-from-toc-sequence-link-icon'); + const textHiddenFromTocSequenceLink = screen.getByTestId('hide-from-toc-sequence-link-text'); + expect(iconHiddenFromTocSequenceLinkNode).toBeInTheDocument(); + expect(textHiddenFromTocSequenceLink).toBeInTheDocument(); + expect(textHiddenFromTocSequenceLink.textContent).toBe('Subsections are not navigable between each other, they can only be accessed through their link.'); + }); + + it('sequence link not show hidden from toc message when hide_from_toc is false', async () => { + const { courseBlocks } = await buildMinimalCourseBlocks(courseId, 'Title', { resumeBlock: true }); + const courseBlocksIds = Object.keys(courseBlocks.blocks); + const newCourseBlocks = courseBlocksIds.reduce((blocks, blockId) => ({ + ...blocks, + [blockId]: { + ...courseBlocks.blocks[blockId], + hide_from_toc: false, + }, + }), {}); + + setTabData({ + course_blocks: { blocks: newCourseBlocks }, + }); + await fetchAndRender(); + + const iconHiddenFromTocSequenceLink = screen.queryByTestId('hide-from-toc-sequence-link-icon'); + const textHiddenFromTocSequenceLink = screen.queryByTestId('hide-from-toc-sequence-link-text'); + + expect(iconHiddenFromTocSequenceLink).not.toBeInTheDocument(); + expect(textHiddenFromTocSequenceLink).not.toBeInTheDocument(); + }); }); }); diff --git a/src/course-home/outline-tab/Section.jsx b/src/course-home/outline-tab/Section.jsx index bd8b3d781f..4e013c73cb 100644 --- a/src/course-home/outline-tab/Section.jsx +++ b/src/course-home/outline-tab/Section.jsx @@ -1,11 +1,12 @@ import React, { useEffect, useState } from 'react'; import PropTypes from 'prop-types'; import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; -import { Collapsible, IconButton } from '@openedx/paragon'; +import { Collapsible, IconButton, Icon } from '@openedx/paragon'; import { faCheckCircle as fasCheckCircle, faMinus, faPlus } from '@fortawesome/free-solid-svg-icons'; import { faCheckCircle as farCheckCircle } from '@fortawesome/free-regular-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { DisabledVisible } from '@openedx/paragon/icons'; import SequenceLink from './SequenceLink'; import { useModel } from '../../generic/model-store'; @@ -23,6 +24,7 @@ const Section = ({ complete, sequenceIds, title, + hideFromTOC, } = section; const { courseBlocks: { @@ -42,7 +44,7 @@ const Section = ({ }, []); const sectionTitle = ( -
+
{complete ? ( )}
-
- {title} +
+ {title} , {intl.formatMessage(complete ? messages.completedSection : messages.incompleteSection)}
+ {hideFromTOC && ( +
+ {hideFromTOC && ( + + + + {intl.formatMessage(messages.hiddenSection)} + + + )} +
+ )}
); diff --git a/src/course-home/outline-tab/SequenceLink.jsx b/src/course-home/outline-tab/SequenceLink.jsx index 0530d53e66..c83e254bc1 100644 --- a/src/course-home/outline-tab/SequenceLink.jsx +++ b/src/course-home/outline-tab/SequenceLink.jsx @@ -12,6 +12,8 @@ import { faCheckCircle as fasCheckCircle } from '@fortawesome/free-solid-svg-ico import { faCheckCircle as farCheckCircle } from '@fortawesome/free-regular-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { Icon } from '@openedx/paragon'; +import { Block } from '@openedx/paragon/icons'; import EffortEstimate from '../../shared/effort-estimate'; import { useModel } from '../../generic/model-store'; import messages from './messages'; @@ -29,6 +31,7 @@ const SequenceLink = ({ due, showLink, title, + hideFromTOC, } = sequence; const { userTimezone, @@ -114,6 +117,16 @@ const SequenceLink = ({
+ {hideFromTOC && ( +
+ + + + {intl.formatMessage(messages.hiddenSequenceLink)} + + +
+ )}
{due ? dueDateMessage : noDueDateMessage} diff --git a/src/course-home/outline-tab/messages.js b/src/course-home/outline-tab/messages.js index 8d3204594f..46ac3bd2f0 100644 --- a/src/course-home/outline-tab/messages.js +++ b/src/course-home/outline-tab/messages.js @@ -36,6 +36,16 @@ const messages = defineMessages({ defaultMessage: 'Completed section', description: 'Text used to describe the green checkmark icon in front of a section title', }, + hiddenSection: { + id: 'learning.outline.hiddenSection', + defaultMessage: 'Hidden in Course Outline, accessible via link', + description: 'Label for hidden section in course outline', + }, + hiddenSequenceLink: { + id: 'learning.outline.hiddenSequenceLink', + defaultMessage: 'Subsections are not navigable between each other, they can only be accessed through their link.', + description: 'Label for hidden sequence in course outline', + }, dates: { id: 'learning.outline.dates', defaultMessage: 'Important dates', diff --git a/webpack.dev-tutor.config.js b/webpack.dev-tutor.config.js new file mode 100755 index 0000000000..e69de29bb2