Skip to content

Commit

Permalink
feat: video sharing option dropdown (openedx#779)
Browse files Browse the repository at this point in the history
* feat: video sharing option dropdown

* test: video sharing option

* fix: lint issues

* refactor: messages for video sharing options

* test: add failure test for video sharing

* refactor: rename course block api url
  • Loading branch information
navinkarkera authored Jan 16, 2024
1 parent b59ecaf commit 008d619
Show file tree
Hide file tree
Showing 12 changed files with 236 additions and 16 deletions.
2 changes: 2 additions & 0 deletions src/course-outline/CourseOutline.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ const CourseOutline = ({ courseId }) => {
handleNewUnitSubmit,
getUnitUrl,
handleDragNDrop,
handleVideoSharingOptionChange,
} = useCourseOutline({ courseId });

const [sections, setSections] = useState(sectionsList);
Expand Down Expand Up @@ -177,6 +178,7 @@ const CourseOutline = ({ courseId }) => {
isLoading={isLoading}
statusBarData={statusBarData}
openEnableHighlightsModal={openEnableHighlightsModal}
handleVideoSharingOptionChange={handleVideoSharingOptionChange}
/>
<div className="pt-4">
{sections.length ? (
Expand Down
65 changes: 60 additions & 5 deletions src/course-outline/CourseOutline.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
getCourseOutlineIndexApiUrl,
getCourseReindexApiUrl,
getXBlockApiUrl,
getEnableHighlightsEmailsApiUrl,
getCourseBlockApiUrl,
getCourseItemApiUrl,
getXBlockBaseApiUrl,
} from './data/api';
Expand All @@ -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;
Expand Down Expand Up @@ -114,6 +115,60 @@ describe('<CourseOutline />', () => {
expect(await findByText(messages.alertSuccessDescription.defaultMessage)).toBeInTheDocument();
});

it('check video sharing option udpates correctly', async () => {
const { findByTestId } = render(<RootWrapper />);

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(<RootWrapper />);

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(<RootWrapper />);

Expand Down Expand Up @@ -235,7 +290,7 @@ describe('<CourseOutline />', () => {

axiosMock.reset();
axiosMock
.onPost(getEnableHighlightsEmailsApiUrl(courseId), {
.onPost(getCourseBlockApiUrl(courseId), {
publish: 'republish',
metadata: {
highlights_enabled_for_messaging: true,
Expand Down Expand Up @@ -641,7 +696,7 @@ describe('<CourseOutline />', () => {
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);
Expand All @@ -662,7 +717,7 @@ describe('<CourseOutline />', () => {
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);
Expand Down
2 changes: 2 additions & 0 deletions src/course-outline/__mocks__/courseOutlineIndex.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ module.exports = {
'Homework',
'Exam',
],
videoSharingEnabled: true,
videoSharingOptions: 'per-video',
hasChanges: false,
actions: {
deletable: true,
Expand Down
6 changes: 6 additions & 0 deletions src/course-outline/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
});
23 changes: 20 additions & 3 deletions src/course-outline/data/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -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`;
};
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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<Object>}
*/
export async function setVideoSharingOption(courseId, videoSharingOption) {
const { data } = await getAuthenticatedHttpClient()
.post(getCourseBlockApiUrl(courseId), {
metadata: {
video_sharing_options: videoSharingOption,
},
});

return data;
}
3 changes: 3 additions & 0 deletions src/course-outline/data/slice.js
Original file line number Diff line number Diff line change
@@ -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({
Expand All @@ -23,6 +24,8 @@ const slice = createSlice({
totalCourseBestPracticesChecks: 0,
completedCourseBestPracticesChecks: 0,
},
videoSharingEnabled: false,
videoSharingOptions: VIDEO_SHARING_OPTIONS.perVideo,
},
sectionsList: [],
currentSection: {},
Expand Down
35 changes: 33 additions & 2 deletions src/course-outline/data/thunk.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
restartIndexingOnCourse,
updateCourseSectionHighlights,
setSectionOrderList,
setVideoSharingOption,
} from './api';
import {
addSection,
Expand All @@ -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) {
Expand Down Expand Up @@ -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 }));
Expand Down
6 changes: 6 additions & 0 deletions src/course-outline/hooks.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import {
updateCourseSectionHighlightsQuery,
configureCourseSectionQuery,
setSectionOrderListQuery,
setVideoSharingOptionQuery,
} from './data/thunk';

const useCourseOutline = ({ courseId }) => {
Expand Down Expand Up @@ -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 }));
Expand Down Expand Up @@ -246,6 +251,7 @@ const useCourseOutline = ({ courseId }) => {
openUnitPage,
handleNewUnitSubmit,
handleDragNDrop,
handleVideoSharingOptionChange,
};
};

Expand Down
45 changes: 43 additions & 2 deletions src/course-outline/status-bar/StatusBar.jsx
Original file line number Diff line number Diff line change
@@ -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);
Expand All @@ -21,6 +26,8 @@ const StatusBar = ({
highlightsEnabledForMessaging,
checklist,
isSelfPaced,
videoSharingEnabled,
videoSharingOptions,
} = statusBarData;

const {
Expand All @@ -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
Expand Down Expand Up @@ -95,6 +103,36 @@ const StatusBar = ({
</Hyperlink>
</div>
</div>
{videoSharingEnabled && (
<div
data-testid="video-sharing-wrapper"
className="outline-status-bar__item ml-2"
>
<h5>{intl.formatMessage(messages.videoSharingTitle)}</h5>
<div className="d-flex align-items-end">
<SelectMenu variant="sm btn-outline-primary">
{Object.values(VIDEO_SHARING_OPTIONS).map((option) => (
<MenuItem
key={option}
value={option}
defaultSelected={option === videoSharingOptions}
onClick={() => handleVideoSharingOptionChange(option)}
>
{getVideoSharingOptionText(option, messages, intl)}
</MenuItem>
))}
</SelectMenu>
<Hyperlink
className="small ml-2"
destination={socialSharingUrl}
target="_blank"
showLaunchIcon={false}
>
{intl.formatMessage(messages.videoSharingLink)}
</Hyperlink>
</div>
</div>
)}
</Stack>
);
};
Expand All @@ -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,
Expand All @@ -113,6 +152,8 @@ StatusBar.propTypes = {
completedCourseBestPracticesChecks: PropTypes.number.isRequired,
}),
highlightsEnabledForMessaging: PropTypes.bool.isRequired,
videoSharingEnabled: PropTypes.bool.isRequired,
videoSharingOptions: PropTypes.string.isRequired,
}).isRequired,
};

Expand Down
Loading

0 comments on commit 008d619

Please sign in to comment.