diff --git a/src/course-outline/CourseOutline.jsx b/src/course-outline/CourseOutline.jsx index d2d2efa91..f361a2b04 100644 --- a/src/course-outline/CourseOutline.jsx +++ b/src/course-outline/CourseOutline.jsx @@ -92,6 +92,7 @@ const CourseOutline = ({ courseId }) => { handleNewUnitSubmit, getUnitUrl, handleDragNDrop, + handleVideoSharingOptionChange, } = useCourseOutline({ courseId }); const [sections, setSections] = useState(sectionsList); @@ -177,6 +178,7 @@ const CourseOutline = ({ courseId }) => { isLoading={isLoading} statusBarData={statusBarData} openEnableHighlightsModal={openEnableHighlightsModal} + handleVideoSharingOptionChange={handleVideoSharingOptionChange} />
{sections.length ? ( diff --git a/src/course-outline/CourseOutline.test.jsx b/src/course-outline/CourseOutline.test.jsx index 003ffd0e3..fdf0f70c3 100644 --- a/src/course-outline/CourseOutline.test.jsx +++ b/src/course-outline/CourseOutline.test.jsx @@ -14,7 +14,7 @@ import { getCourseOutlineIndexApiUrl, getCourseReindexApiUrl, getXBlockApiUrl, - getEnableHighlightsEmailsApiUrl, + getCourseBlockApiUrl, getCourseItemApiUrl, getXBlockBaseApiUrl, } from './data/api'; @@ -36,12 +36,13 @@ import { courseSubsectionMock, } from './__mocks__'; import { executeThunk } from '../utils'; -import { COURSE_BLOCK_NAMES } from './constants'; +import { COURSE_BLOCK_NAMES, VIDEO_SHARING_OPTIONS } from './constants'; import CourseOutline from './CourseOutline'; import messages from './messages'; import headerMessages from './header-navigations/messages'; import cardHeaderMessages from './card-header/messages'; import enableHighlightsModalMessages from './enable-highlights-modal/messages'; +import statusBarMessages from './status-bar/messages'; let axiosMock; let store; @@ -114,6 +115,60 @@ describe('', () => { expect(await findByText(messages.alertSuccessDescription.defaultMessage)).toBeInTheDocument(); }); + it('check video sharing option udpates correctly', async () => { + const { findByTestId } = render(); + + axiosMock + .onPost(getCourseBlockApiUrl(courseId), { + metadata: { + video_sharing_options: VIDEO_SHARING_OPTIONS.allOff, + }, + }) + .reply(200); + const optionDropdownWrapper = await findByTestId('video-sharing-wrapper'); + const optionDropdown = await within(optionDropdownWrapper).findByRole('button'); + await act(async () => fireEvent.click(optionDropdown)); + const allOffOption = await within(optionDropdownWrapper).findByText( + statusBarMessages.videoSharingAllOffText.defaultMessage, + ); + await act(async () => fireEvent.click(allOffOption)); + + expect(axiosMock.history.post.length).toBe(1); + expect(axiosMock.history.post[0].data).toBe(JSON.stringify({ + metadata: { + video_sharing_options: VIDEO_SHARING_OPTIONS.allOff, + }, + })); + }); + + it('check video sharing option shows error on failure', async () => { + const { findByTestId, queryByRole } = render(); + + axiosMock + .onPost(getCourseBlockApiUrl(courseId), { + metadata: { + video_sharing_options: VIDEO_SHARING_OPTIONS.allOff, + }, + }) + .reply(500); + const optionDropdownWrapper = await findByTestId('video-sharing-wrapper'); + const optionDropdown = await within(optionDropdownWrapper).findByRole('button'); + await act(async () => fireEvent.click(optionDropdown)); + const allOffOption = await within(optionDropdownWrapper).findByText( + statusBarMessages.videoSharingAllOffText.defaultMessage, + ); + await act(async () => fireEvent.click(allOffOption)); + + expect(axiosMock.history.post.length).toBe(1); + expect(axiosMock.history.post[0].data).toBe(JSON.stringify({ + metadata: { + video_sharing_options: VIDEO_SHARING_OPTIONS.allOff, + }, + })); + + expect(queryByRole('alert')).toBeInTheDocument(); + }); + it('render error alert after failed reindex correctly', async () => { const { findByText, findByTestId } = render(); @@ -235,7 +290,7 @@ describe('', () => { axiosMock.reset(); axiosMock - .onPost(getEnableHighlightsEmailsApiUrl(courseId), { + .onPost(getCourseBlockApiUrl(courseId), { publish: 'republish', metadata: { highlights_enabled_for_messaging: true, @@ -641,7 +696,7 @@ describe('', () => { children = children.splice(2, 0, children.splice(0, 1)[0]); axiosMock - .onPut(getEnableHighlightsEmailsApiUrl(courseBlockId), { children }) + .onPut(getCourseBlockApiUrl(courseBlockId), { children }) .reply(200, { dummy: 'value' }); await executeThunk(setSectionOrderListQuery(courseBlockId, children, () => {}), store.dispatch); @@ -662,7 +717,7 @@ describe('', () => { const newChildren = children.splice(2, 0, children.splice(0, 1)[0]); axiosMock - .onPut(getEnableHighlightsEmailsApiUrl(courseBlockId), { children }) + .onPut(getCourseBlockApiUrl(courseBlockId), { children }) .reply(500); await executeThunk(setSectionOrderListQuery(courseBlockId, undefined, () => children), store.dispatch); diff --git a/src/course-outline/__mocks__/courseOutlineIndex.js b/src/course-outline/__mocks__/courseOutlineIndex.js index 54d7d2df7..8f206fc68 100644 --- a/src/course-outline/__mocks__/courseOutlineIndex.js +++ b/src/course-outline/__mocks__/courseOutlineIndex.js @@ -24,6 +24,8 @@ module.exports = { 'Homework', 'Exam', ], + videoSharingEnabled: true, + videoSharingOptions: 'per-video', hasChanges: false, actions: { deletable: true, diff --git a/src/course-outline/constants.js b/src/course-outline/constants.js index f9a2c8821..bf3ec53ef 100644 --- a/src/course-outline/constants.js +++ b/src/course-outline/constants.js @@ -74,3 +74,9 @@ export const BEST_PRACTICES_CHECKLIST = /** @type {const} */ ({ }, ], }); + +export const VIDEO_SHARING_OPTIONS = /** @type {const} */ ({ + perVideo: 'per-video', + allOn: 'all-on', + allOff: 'all-off', +}); diff --git a/src/course-outline/data/api.js b/src/course-outline/data/api.js index 702715ce8..df2850fbe 100644 --- a/src/course-outline/data/api.js +++ b/src/course-outline/data/api.js @@ -19,7 +19,7 @@ export const getCourseLaunchApiUrl = ({ all, }) => `${getApiBaseUrl()}/api/courses/v1/validation/${courseId}/?graded_only=${gradedOnly}&validate_oras=${validateOras}&all=${all}`; -export const getEnableHighlightsEmailsApiUrl = (courseId) => { +export const getCourseBlockApiUrl = (courseId) => { const formattedCourseId = courseId.split('course-v1:')[1]; return `${getApiBaseUrl()}/xblock/block-v1:${formattedCourseId}+type@course+block@course`; }; @@ -112,7 +112,7 @@ export async function getCourseLaunch({ */ export async function enableCourseHighlightsEmails(courseId) { const { data } = await getAuthenticatedHttpClient() - .post(getEnableHighlightsEmailsApiUrl(courseId), { + .post(getCourseBlockApiUrl(courseId), { publish: 'republish', metadata: { highlights_enabled_for_messaging: true, @@ -305,9 +305,26 @@ export async function addNewCourseItem(parentLocator, category, displayName) { */ export async function setSectionOrderList(courseId, children) { const { data } = await getAuthenticatedHttpClient() - .put(getEnableHighlightsEmailsApiUrl(courseId), { + .put(getCourseBlockApiUrl(courseId), { children, }); return data; } + +/** + * Set video sharing setting + * @param {string} courseId + * @param {string} videoSharingOption + * @returns {Promise} +*/ +export async function setVideoSharingOption(courseId, videoSharingOption) { + const { data } = await getAuthenticatedHttpClient() + .post(getCourseBlockApiUrl(courseId), { + metadata: { + video_sharing_options: videoSharingOption, + }, + }); + + return data; +} diff --git a/src/course-outline/data/slice.js b/src/course-outline/data/slice.js index edb8b1e4a..c87578c0c 100644 --- a/src/course-outline/data/slice.js +++ b/src/course-outline/data/slice.js @@ -1,6 +1,7 @@ /* eslint-disable no-param-reassign */ import { createSlice } from '@reduxjs/toolkit'; +import { VIDEO_SHARING_OPTIONS } from '../constants'; import { RequestStatus } from '../../data/constants'; const slice = createSlice({ @@ -23,6 +24,8 @@ const slice = createSlice({ totalCourseBestPracticesChecks: 0, completedCourseBestPracticesChecks: 0, }, + videoSharingEnabled: false, + videoSharingOptions: VIDEO_SHARING_OPTIONS.perVideo, }, sectionsList: [], currentSection: {}, diff --git a/src/course-outline/data/thunk.js b/src/course-outline/data/thunk.js index 959f25111..4e62793e7 100644 --- a/src/course-outline/data/thunk.js +++ b/src/course-outline/data/thunk.js @@ -24,6 +24,7 @@ import { restartIndexingOnCourse, updateCourseSectionHighlights, setSectionOrderList, + setVideoSharingOption, } from './api'; import { addSection, @@ -50,9 +51,21 @@ export function fetchCourseOutlineIndexQuery(courseId) { try { const outlineIndex = await getCourseOutlineIndex(courseId); - const { courseReleaseDate, courseStructure: { highlightsEnabledForMessaging } } = outlineIndex; + const { + courseReleaseDate, + courseStructure: { + highlightsEnabledForMessaging, + videoSharingEnabled, + videoSharingOptions, + }, + } = outlineIndex; dispatch(fetchOutlineIndexSuccess(outlineIndex)); - dispatch(updateStatusBar({ courseReleaseDate, highlightsEnabledForMessaging })); + dispatch(updateStatusBar({ + courseReleaseDate, + highlightsEnabledForMessaging, + videoSharingOptions, + videoSharingEnabled, + })); dispatch(updateOutlineIndexLoadingStatus({ status: RequestStatus.SUCCESSFUL })); } catch (error) { @@ -116,6 +129,24 @@ export function enableCourseHighlightsEmailsQuery(courseId) { }; } +export function setVideoSharingOptionQuery(courseId, option) { + return async (dispatch) => { + dispatch(updateSavingStatus({ status: RequestStatus.PENDING })); + dispatch(showProcessingNotification(NOTIFICATION_MESSAGES.saving)); + + try { + await setVideoSharingOption(courseId, option); + dispatch(updateStatusBar({ videoSharingOptions: option })); + + dispatch(updateSavingStatus({ status: RequestStatus.SUCCESSFUL })); + dispatch(hideProcessingNotification()); + } catch (error) { + dispatch(updateSavingStatus({ status: RequestStatus.FAILED })); + dispatch(hideProcessingNotification()); + } + }; +} + export function fetchCourseReindexQuery(courseId, reindexLink) { return async (dispatch) => { dispatch(updateReindexLoadingStatus({ status: RequestStatus.IN_PROGRESS })); diff --git a/src/course-outline/hooks.jsx b/src/course-outline/hooks.jsx index 7bc5ab9e5..be2ad5d5a 100644 --- a/src/course-outline/hooks.jsx +++ b/src/course-outline/hooks.jsx @@ -41,6 +41,7 @@ import { updateCourseSectionHighlightsQuery, configureCourseSectionQuery, setSectionOrderListQuery, + setVideoSharingOptionQuery, } from './data/thunk'; const useCourseOutline = ({ courseId }) => { @@ -186,6 +187,10 @@ const useCourseOutline = ({ courseId }) => { dispatch(setSectionOrderListQuery(courseId, newListId, restoreCallback)); }; + const handleVideoSharingOptionChange = (value) => { + dispatch(setVideoSharingOptionQuery(courseId, value)); + }; + useEffect(() => { dispatch(fetchCourseOutlineIndexQuery(courseId)); dispatch(fetchCourseBestPracticesQuery({ courseId })); @@ -246,6 +251,7 @@ const useCourseOutline = ({ courseId }) => { openUnitPage, handleNewUnitSubmit, handleDragNDrop, + handleVideoSharingOptionChange, }; }; diff --git a/src/course-outline/status-bar/StatusBar.jsx b/src/course-outline/status-bar/StatusBar.jsx index 3e08d5b41..4779b3a70 100644 --- a/src/course-outline/status-bar/StatusBar.jsx +++ b/src/course-outline/status-bar/StatusBar.jsx @@ -1,17 +1,22 @@ import React, { useContext } from 'react'; import PropTypes from 'prop-types'; import { useIntl } from '@edx/frontend-platform/i18n'; -import { Button, Hyperlink, Stack } from '@edx/paragon'; +import { + Button, Hyperlink, SelectMenu, MenuItem, Stack, +} from '@edx/paragon'; import { AppContext } from '@edx/frontend-platform/react'; import { useHelpUrls } from '../../help-urls/hooks'; +import { VIDEO_SHARING_OPTIONS } from '../constants'; import messages from './messages'; +import { getVideoSharingOptionText } from '../utils'; const StatusBar = ({ statusBarData, isLoading, courseId, openEnableHighlightsModal, + handleVideoSharingOptionChange, }) => { const intl = useIntl(); const { config } = useContext(AppContext); @@ -21,6 +26,8 @@ const StatusBar = ({ highlightsEnabledForMessaging, checklist, isSelfPaced, + videoSharingEnabled, + videoSharingOptions, } = statusBarData; const { @@ -36,7 +43,8 @@ const StatusBar = ({ const { contentHighlights: contentHighlightsUrl, - } = useHelpUrls(['contentHighlights']); + socialSharing: socialSharingUrl, + } = useHelpUrls(['contentHighlights', 'socialSharing']); if (isLoading) { // eslint-disable-next-line react/jsx-no-useless-fragment @@ -95,6 +103,36 @@ const StatusBar = ({ + {videoSharingEnabled && ( +
+
{intl.formatMessage(messages.videoSharingTitle)}
+
+ + {Object.values(VIDEO_SHARING_OPTIONS).map((option) => ( + handleVideoSharingOptionChange(option)} + > + {getVideoSharingOptionText(option, messages, intl)} + + ))} + + + {intl.formatMessage(messages.videoSharingLink)} + +
+
+ )} ); }; @@ -103,6 +141,7 @@ StatusBar.propTypes = { courseId: PropTypes.string.isRequired, isLoading: PropTypes.bool.isRequired, openEnableHighlightsModal: PropTypes.func.isRequired, + handleVideoSharingOptionChange: PropTypes.func.isRequired, statusBarData: PropTypes.shape({ courseReleaseDate: PropTypes.string.isRequired, isSelfPaced: PropTypes.bool.isRequired, @@ -113,6 +152,8 @@ StatusBar.propTypes = { completedCourseBestPracticesChecks: PropTypes.number.isRequired, }), highlightsEnabledForMessaging: PropTypes.bool.isRequired, + videoSharingEnabled: PropTypes.bool.isRequired, + videoSharingOptions: PropTypes.string.isRequired, }).isRequired, }; diff --git a/src/course-outline/status-bar/StatusBar.test.jsx b/src/course-outline/status-bar/StatusBar.test.jsx index 9745a5c49..9b17be01b 100644 --- a/src/course-outline/status-bar/StatusBar.test.jsx +++ b/src/course-outline/status-bar/StatusBar.test.jsx @@ -7,12 +7,14 @@ import { initializeMockApp } from '@edx/frontend-platform'; import StatusBar from './StatusBar'; import messages from './messages'; import initializeStore from '../../store'; +import { VIDEO_SHARING_OPTIONS } from '../constants'; let store; const mockPathname = '/foo-bar'; const courseId = '123'; const isLoading = false; const openEnableHighlightsModalMock = jest.fn(); +const handleVideoSharingOptionChange = jest.fn(); jest.mock('react-router-dom', () => ({ ...jest.requireActual('react-router-dom'), @@ -23,7 +25,8 @@ jest.mock('react-router-dom', () => ({ jest.mock('../../help-urls/hooks', () => ({ useHelpUrls: () => ({ - contentHighlights: 'some', + contentHighlights: 'content-highlights-link', + socialSharing: 'social-sharing-link', }), })); @@ -38,6 +41,8 @@ const statusBarData = { }, highlightsEnabledForMessaging: true, highlightsDocUrl: 'https://example.com/highlights-doc', + videoSharingEnabled: true, + videoSharingOptions: VIDEO_SHARING_OPTIONS.allOn, }; const renderComponent = (props) => render( @@ -47,6 +52,7 @@ const renderComponent = (props) => render( courseId={courseId} isLoading={isLoading} openEnableHighlightsModal={openEnableHighlightsModalMock} + handleVideoSharingOptionChange={handleVideoSharingOptionChange} statusBarData={statusBarData} {...props} /> @@ -68,7 +74,7 @@ describe('', () => { }); it('renders StatusBar component correctly', () => { - const { getByText } = renderComponent(); + const { queryByTestId, getByText } = renderComponent(); expect(getByText(messages.startDateTitle.defaultMessage)).toBeInTheDocument(); expect(getByText(statusBarData.courseReleaseDate)).toBeInTheDocument(); @@ -80,8 +86,9 @@ describe('', () => { expect(getByText(`2/9 ${messages.checklistCompleted.defaultMessage}`)).toBeInTheDocument(); expect(getByText(messages.highlightEmailsTitle.defaultMessage)).toBeInTheDocument(); - expect(getByText(messages.highlightEmailsLink.defaultMessage)).toBeInTheDocument(); expect(getByText(messages.highlightEmailsEnabled.defaultMessage)).toBeInTheDocument(); + + expect(queryByTestId('video-sharing-wrapper')).toBeInTheDocument(); }); it('renders StatusBar when isSelfPaced is false', () => { @@ -115,4 +122,15 @@ describe('', () => { expect(queryByTestId('outline-status-bar')).not.toBeInTheDocument(); }); + + it('does not render video sharing dropdown if not enabled', () => { + const { queryByTestId } = renderComponent({ + statusBarData: { + ...statusBarData, + videoSharingEnabled: false, + }, + }); + + expect(queryByTestId('video-sharing-wrapper')).not.toBeInTheDocument(); + }); }); diff --git a/src/course-outline/status-bar/messages.js b/src/course-outline/status-bar/messages.js index 58ddb2bef..7c8b75ae4 100644 --- a/src/course-outline/status-bar/messages.js +++ b/src/course-outline/status-bar/messages.js @@ -41,6 +41,26 @@ const messages = defineMessages({ id: 'course-authoring.course-outline.status-bar.highlight-emails.link', defaultMessage: 'Learn more', }, + videoSharingTitle: { + id: 'course-authoring.course-outline.status-bar.video-sharing.title', + defaultMessage: 'Video Sharing', + }, + videoSharingLink: { + id: 'course-authoring.course-outline.status-bar.video-sharing.title', + defaultMessage: 'Learn more', + }, + videoSharingPerVideoText: { + id: 'course-authoring.course-outline.status-bar.video-sharing.perVideo.text', + defaultMessage: 'Per Video', + }, + videoSharingAllOffText: { + id: 'course-authoring.course-outline.status-bar.video-sharing.allOff.text', + defaultMessage: 'No Videos', + }, + videoSharingAllOnText: { + id: 'course-authoring.course-outline.status-bar.video-sharing.allOn.text', + defaultMessage: 'All Videos', + }, }); export default messages; diff --git a/src/course-outline/utils.jsx b/src/course-outline/utils.jsx index 7853eb310..481dd1f72 100644 --- a/src/course-outline/utils.jsx +++ b/src/course-outline/utils.jsx @@ -4,7 +4,7 @@ import { EditOutline as EditOutlineIcon, } from '@edx/paragon/icons'; -import { ITEM_BADGE_STATUS, STAFF_ONLY } from './constants'; +import { ITEM_BADGE_STATUS, STAFF_ONLY, VIDEO_SHARING_OPTIONS } from './constants'; /** * Get section status depended on section info @@ -128,9 +128,28 @@ const scrollToElement = target => { } }; +/** + * Get video sharing dropdown translated options. + * @param {string} id - option id + * @returns {string} - text to display + */ +const getVideoSharingOptionText = (id, messages, intl) => { + switch (id) { + case VIDEO_SHARING_OPTIONS.perVideo: + return intl.formatMessage(messages.videoSharingPerVideoText); + case VIDEO_SHARING_OPTIONS.allOn: + return intl.formatMessage(messages.videoSharingAllOnText); + case VIDEO_SHARING_OPTIONS.allOff: + return intl.formatMessage(messages.videoSharingAllOffText); + default: + return ''; + } +}; + export { getItemStatus, getItemStatusBadgeContent, getHighlightsFormValues, + getVideoSharingOptionText, scrollToElement, };