feat: hide checkbox when video share not enabled for course

This commit is contained in:
jansenk
2023-03-14 15:24:45 -04:00
parent 2dc42e6a46
commit a895c28c4c
16 changed files with 297 additions and 29 deletions

View File

@@ -122,6 +122,152 @@ exports[`VideoSourceWidget snapshots snapshots: renders as expected with default
/>
</OverlayTrigger>
<ActionRow.Spacer />
</ActionRow>
</Form.Group>
<div
className="my-4 border-primary-100 border-bottom"
/>
<Button
className="text-primary-500 font-weight-bold pl-0"
onClick={[Function]}
size="sm"
variant="link"
>
<FormattedMessage
defaultMessage="Add a video URL"
description="Label for add a video URL button"
id="authoring.videoeditor.videoSource.fallbackVideo.addButtonLabel"
/>
</Button>
</injectIntl(ShimmedIntlComponent)>
`;
exports[`VideoSourceWidget snapshots snapshots: renders as expected with videoSharingEnabledForCourse=true 1`] = `
<injectIntl(ShimmedIntlComponent)
fontSize="x-small"
title="Video source"
>
<Form.Group>
<div
className="border-primary-100 border-bottom pb-4"
>
<Form.Control
floatingLabel="Video ID"
onBlur={[Function]}
onChange={[MockFunction]}
value=""
/>
<Component
className="text-primary-300 mb-4"
>
<FormattedMessage
defaultMessage="If you were assigned a video ID by edX, enter the ID here."
description="Feedback for video ID field"
id="authoring.videoeditor.videoSource.videoId.feedback"
/>
</Component>
<Form.Control
floatingLabel="Video URL"
onBlur={[Function]}
onChange={[MockFunction]}
value=""
/>
<Component
className="text-primary-300"
>
<FormattedMessage
defaultMessage="The URL for your video. This can be a YouTube URL, or a link
to an .mp4, .ogg, or .webm video file hosted elsewhere on the internet."
description="Feedback for video URL field"
id="authoring.videoeditor.videoSource.videoUrl.feedback"
/>
</Component>
</div>
<div
className="mt-4"
>
<FormattedMessage
defaultMessage="Fallback videos"
description="Title for the fallback videos section"
id="authoring.videoeditor.videoSource.fallbackVideo.title"
/>
</div>
<div
className="mt-3"
>
<FormattedMessage
defaultMessage="To be sure all learners can access the video, edX
recommends providing additional videos in both .mp4 and
.webm formats. The first listed video compatible with the
learner's device will play."
description="Test explaining reason for fallback videos"
id="authoring.videoeditor.videoSource.fallbackVideo.message"
/>
</div>
<Form.Row
className="mt-3.5 mx-0 flex-nowrap"
>
<Form.Control
floatingLabel="Video URL"
/>
<IconButtonWithTooltip
alt="Delete"
iconAs="Icon"
key="top-delete-somEUrL"
onClick={[Function]}
tooltipContent="Delete"
tooltipPlacement="top"
/>
</Form.Row>
<ActionRow
className="mt-4.5"
>
<Form.Checkbox
checked={false}
className="decorative-control-label"
onChange={[MockFunction]}
>
<div
className="small text-gray-700"
>
<FormattedMessage
defaultMessage="Allow video downloads"
description="Label for allow video downloads checkbox"
id="authoring.videoeditor.videoSource.allowDownloadCheckboxLabel"
/>
</div>
</Form.Checkbox>
<OverlayTrigger
key="top"
overlay={
<Tooltip
id="tooltip-top"
>
<FormattedMessage
defaultMessage="Allow learners to download versions of this video in
different formats if they cannot use the edX video player or do not have
access to YouTube."
description="Message for allow video downloads checkbox"
id="authoring.videoeditor.videoSource.allowDownloadTooltipMessage"
/>
</Tooltip>
}
placement="top"
>
<Icon
style={
Object {
"height": "16px",
"width": "16px",
}
}
/>
</OverlayTrigger>
<ActionRow.Spacer />
</ActionRow>
<ActionRow
className="mt-4.5"
>
<Form.Checkbox
checked={false}
className="decorative-control-label"
@@ -164,6 +310,7 @@ exports[`VideoSourceWidget snapshots snapshots: renders as expected with default
}
/>
</OverlayTrigger>
<ActionRow.Spacer />
</ActionRow>
</Form.Group>
<div

View File

@@ -1,5 +1,6 @@
import React from 'react';
import { useDispatch } from 'react-redux';
import { connect, useDispatch } from 'react-redux';
import PropTypes from 'prop-types';
import {
Form,
@@ -21,6 +22,7 @@ import {
import * as widgetHooks from '../hooks';
import * as hooks from './hooks';
import messages from './messages';
import { selectors } from '../../../../../../data/redux';
import CollapsibleFormWidget from '../CollapsibleFormWidget';
@@ -30,6 +32,8 @@ import CollapsibleFormWidget from '../CollapsibleFormWidget';
export const VideoSourceWidget = ({
// injected
intl,
// redux
videoSharingEnabledForCourse,
}) => {
const dispatch = useDispatch();
const {
@@ -127,27 +131,32 @@ export const VideoSourceWidget = ({
<Icon src={InfoOutline} style={{ height: '16px', width: '16px' }} />
</OverlayTrigger>
<ActionRow.Spacer />
<Form.Checkbox
checked={allowSharing.local}
className="decorative-control-label"
onChange={allowSharing.onCheckedChange}
>
<div className="small text-gray-700">
<FormattedMessage {...messages.allowVideoSharingCheckboxLabel} />
</div>
</Form.Checkbox>
<OverlayTrigger
key="top-allow-sharing"
placement="top"
overlay={(
<Tooltip id="tooltip-top-allow-sharing">
<FormattedMessage {...messages.allowVideoSharingTooltipMessage} />
</Tooltip>
)}
>
<Icon src={InfoOutline} style={{ height: '16px', width: '16px' }} />
</OverlayTrigger>
</ActionRow>
{videoSharingEnabledForCourse && (
<ActionRow className="mt-4.5">
<Form.Checkbox
checked={allowSharing.local}
className="decorative-control-label"
onChange={allowSharing.onCheckedChange}
>
<div className="small text-gray-700">
<FormattedMessage {...messages.allowVideoSharingCheckboxLabel} />
</div>
</Form.Checkbox>
<OverlayTrigger
key="top-allow-sharing"
placement="top"
overlay={(
<Tooltip id="tooltip-top-allow-sharing">
<FormattedMessage {...messages.allowVideoSharingTooltipMessage} />
</Tooltip>
)}
>
<Icon src={InfoOutline} style={{ height: '16px', width: '16px' }} />
</OverlayTrigger>
<ActionRow.Spacer />
</ActionRow>
)}
</Form.Group>
<div className="my-4 border-primary-100 border-bottom" />
<Button
@@ -165,6 +174,12 @@ export const VideoSourceWidget = ({
VideoSourceWidget.propTypes = {
// injected
intl: intlShape.isRequired,
// redux
videoSharingEnabledForCourse: PropTypes.bool.isRequired,
};
export default injectIntl(VideoSourceWidget);
export const mapStateToProps = (state) => ({
videoSharingEnabledForCourse: selectors.video.videoSharingEnabledForCourse(state),
});
export default injectIntl(connect(mapStateToProps, {})(VideoSourceWidget));

View File

@@ -42,17 +42,38 @@ jest.mock('./hooks', () => ({
}),
}));
jest.mock('../../../../../../data/redux', () => ({
selectors: {
video: {
allow: jest.fn(state => ({ allowTranscriptImport: state })),
},
requests: {
isFailed: jest.fn(state => ({ isFailed: state })),
},
},
}));
describe('VideoSourceWidget', () => {
const props = {
// inject
intl: { formatMessage },
// redux
videoSharingEnabledForCourse: false,
};
describe('snapshots', () => {
test('snapshots: renders as expected with default props', () => {
expect(
shallow(<VideoSourceWidget {...props} />),
).toMatchSnapshot();
describe('snapshots: renders as expected with', () => {
it('default props', () => {
expect(
shallow(<VideoSourceWidget {...props} />),
).toMatchSnapshot();
});
it('videoSharingEnabledForCourse=true', () => {
const newProps = { ...props, videoSharingEnabledForCourse: true };
expect(
shallow(<VideoSourceWidget {...newProps} />),
).toMatchSnapshot();
});
});
});

View File

@@ -16,6 +16,7 @@ export const RequestKeys = StrictDict({
saveBlock: 'saveBlock',
uploadAsset: 'uploadAsset',
allowThumbnailUpload: 'allowThumbnailUpload',
videoSharingEnabledForCourse: 'videoSharingEnabledForCourse',
uploadThumbnail: 'uploadThumbnail',
uploadTranscript: 'uploadTranscript',
deleteTranscript: 'deleteTranscript',

View File

@@ -11,6 +11,7 @@ const initialState = {
[RequestKeys.saveBlock]: { status: RequestStates.inactive },
[RequestKeys.uploadAsset]: { status: RequestStates.inactive },
[RequestKeys.allowThumbnailUpload]: { status: RequestStates.inactive },
[RequestKeys.videoSharingEnabledForCourse]: { status: RequestStates.inactive },
[RequestKeys.uploadThumbnail]: { status: RequestStates.inactive },
[RequestKeys.uploadTranscript]: { status: RequestStates.inactive },
[RequestKeys.deleteTranscript]: { status: RequestStates.inactive },

View File

@@ -136,6 +136,17 @@ export const fetchAssets = ({ ...rest }) => (dispatch, getState) => {
}));
};
export const videoSharingEnabledForCourse = ({ ...rest }) => (dispatch, getState) => {
dispatch(module.networkRequest({
requestKey: RequestKeys.videoSharingEnabledForCourse,
promise: api.videoSharingEnabledForCourse({
studioEndpointUrl: selectors.app.studioEndpointUrl(getState()),
learningContextId: selectors.app.learningContextId(getState()),
}),
...rest,
}));
};
export const allowThumbnailUpload = ({ ...rest }) => (dispatch, getState) => {
dispatch(module.networkRequest({
requestKey: RequestKeys.allowThumbnailUpload,
@@ -279,6 +290,7 @@ export default StrictDict({
saveBlock,
fetchAssets,
uploadAsset,
videoSharingEnabledForCourse,
allowThumbnailUpload,
uploadThumbnail,
deleteTranscript,

View File

@@ -29,6 +29,7 @@ jest.mock('../../services/cms/api', () => ({
fetchAssets: ({ id, url }) => ({ id, url }),
uploadAsset: (args) => args,
loadImages: jest.fn(),
videoSharingEnabledForCourse: (args) => args,
allowThumbnailUpload: (args) => args,
uploadThumbnail: (args) => args,
uploadTranscript: (args) => args,
@@ -302,6 +303,21 @@ describe('requests thunkActions module', () => {
},
});
});
describe('videoSharingEnabledForCourse', () => {
testNetworkRequestAction({
action: requests.videoSharingEnabledForCourse,
args: { ...fetchParams },
expectedString: 'with videoSharingEnabledForCourse promise',
expectedData: {
...fetchParams,
requestKey: RequestKeys.videoSharingEnabledForCourse,
promise: api.videoSharingEnabledForCourse({
studioEndpointUrl: selectors.app.studioEndpointUrl(testState),
learningContextId: selectors.app.learningContextId(testState),
}),
},
});
});
describe('allowThumbnailUpload', () => {
testNetworkRequestAction({
action: requests.allowThumbnailUpload,

View File

@@ -62,6 +62,11 @@ export const loadVideoData = () => (dispatch, getState) => {
allowThumbnailUpload: response.data.allowThumbnailUpload,
})),
}));
dispatch(requests.videoSharingEnabledForCourse({
onSuccess: (response) => dispatch(actions.video.updateField({
videoSharingEnabledForCourse: response.data.videoSharingEnabled,
})),
}));
const youTubeId = parseYoutubeId(videoUrl);
if (youTubeId) {
dispatch(requests.checkTranscriptsForImport({

View File

@@ -22,6 +22,7 @@ jest.mock('..', () => ({
}));
jest.mock('./requests', () => ({
uploadAsset: (args) => ({ uploadAsset: args }),
videoSharingEnabledForCourse: (args) => ({ videoSharingEnabledForCourse: args }),
allowThumbnailUpload: (args) => ({ allowThumbnailUpload: args }),
uploadThumbnail: (args) => ({ uploadThumbnail: args }),
deleteTranscript: (args) => ({ deleteTranscript: args }),
@@ -50,6 +51,7 @@ const mockThumbnailResponse = { data: { image_url: 'soMEimAGEUrL' } };
const thumbnailUrl = 'soMEimAGEUrL';
const mockAllowThumbnailUpload = { data: { allowThumbnailUpload: 'soMEbOolEAn' } };
const mockAllowTranscriptImport = { data: { command: 'import' } };
const mockVideoSharingEnabledForCourse = { data: { videoSharingEnabled: 'someBOoOoOlean' } };
const testMetadata = {
download_track: 'dOWNlOAdTraCK',
@@ -98,6 +100,7 @@ describe('video thunkActions', () => {
let dispatchedLoad;
let dispatchedAction1;
let dispatchedAction2;
let dispatchedAction3;
beforeEach(() => {
jest.spyOn(thunkActions, thunkActionsKeys.determineVideoSources).mockReturnValue({
videoUrl: 'videOsOurce',
@@ -117,7 +120,12 @@ describe('video thunkActions', () => {
testMetadata.transcripts,
);
thunkActions.loadVideoData()(dispatch, getState);
[[dispatchedLoad], [dispatchedAction1], [dispatchedAction2]] = dispatch.mock.calls;
[
[dispatchedLoad],
[dispatchedAction1],
[dispatchedAction2],
[dispatchedAction3],
] = dispatch.mock.calls;
});
afterEach(() => {
jest.restoreAllMocks();
@@ -126,9 +134,13 @@ describe('video thunkActions', () => {
expect(dispatchedLoad).not.toEqual(undefined);
expect(dispatchedAction1.allowThumbnailUpload).not.toEqual(undefined);
});
it('dispatches videoSharingEnabledForCourse action', () => {
expect(dispatchedLoad).not.toEqual(undefined);
expect(dispatchedAction2.videoSharingEnabledForCourse).not.toEqual(undefined);
});
it('dispatches checkTranscriptsForImport action', () => {
expect(dispatchedLoad).not.toEqual(undefined);
expect(dispatchedAction2.checkTranscriptsForImport).not.toEqual(undefined);
expect(dispatchedAction3.checkTranscriptsForImport).not.toEqual(undefined);
});
it('dispatches actions.video.load', () => {
expect(dispatchedLoad.load).toEqual({
@@ -170,7 +182,12 @@ describe('video thunkActions', () => {
}));
dispatch.mockClear();
dispatchedAction2.checkTranscriptsForImport.onSuccess(mockAllowTranscriptImport);
dispatchedAction2.videoSharingEnabledForCourse.onSuccess(mockVideoSharingEnabledForCourse);
expect(dispatch).toHaveBeenCalledWith(actions.video.updateField({
videoSharingEnabledForCourse: mockVideoSharingEnabledForCourse.data.videoSharingEnabled,
}));
dispatchedAction3.checkTranscriptsForImport.onSuccess(mockAllowTranscriptImport);
expect(dispatch).toHaveBeenCalledWith(actions.video.updateField({
allowTranscriptImport: true,
}));

View File

@@ -11,6 +11,7 @@ const initialState = {
],
allowVideoDownloads: false,
allowVideoSharing: false,
videoSharingEnabledForCourse: false,
thumbnail: null,
transcripts: [],
allowTranscriptDownloads: false,

View File

@@ -17,6 +17,7 @@ export const simpleSelectors = [
stateKeys.videoId,
stateKeys.fallbackVideos,
stateKeys.allowVideoDownloads,
stateKeys.videoSharingEnabledForCourse,
stateKeys.allowVideoSharing,
stateKeys.thumbnail,
stateKeys.transcripts,

View File

@@ -36,6 +36,12 @@ export const apiMethods = {
data,
);
},
videoSharingEnabledForCourse: ({
studioEndpointUrl,
learningContextId,
}) => get(
urls.videoSharingEnabledForCourse({ studioEndpointUrl, learningContextId }),
),
allowThumbnailUpload: ({
studioEndpointUrl,
}) => get(

View File

@@ -22,6 +22,7 @@ jest.mock('./urls', () => ({
thumbnailUpload: jest.fn().mockName('urls.thumbnailUpload'),
checkTranscriptsForImport: jest.fn().mockName('urls.checkTranscriptsForImport'),
replaceTranscript: jest.fn().mockName('urls.replaceTranscript'),
videoSharingEnabledForCourse: jest.fn().mockName('urls.videoSharingEnabledForCourse'),
}));
jest.mock('./utils', () => ({
@@ -252,6 +253,15 @@ describe('cms api', () => {
});
});
});
describe('videoSharing', () => {
describe('videoSharingEnabledForCourse', () => {
it('should call get with url.videoSharingEnabledForCourse', () => {
const args = { studioEndpointUrl, learningContextId };
apiMethods.videoSharingEnabledForCourse({ ...args });
expect(get).toHaveBeenCalledWith(urls.videoSharingEnabledForCourse({ ...args }));
});
});
});
describe('videoTranscripts', () => {
const language = 'la';
const videoId = 'sOmeVIDeoiD';

View File

@@ -123,6 +123,10 @@ export const fetchCourseDetails = ({ studioEndpointUrl, learningContextId }) =>
},
});
// eslint-disable-next-line
export const videoSharingEnabledForCourse = ({ studioEndpointUrl, learningContextId }) => mockPromise({
data: true,
});
// eslint-disable-next-line
export const allowThumbnailUpload = ({ studioEndpointUrl }) => mockPromise({
data: true,
});

View File

@@ -35,6 +35,10 @@ export const allowThumbnailUpload = ({ studioEndpointUrl }) => (
`${studioEndpointUrl}/video_images_upload_enabled`
);
export const videoSharingEnabledForCourse = ({ studioEndpointUrl, learningContextId }) => (
`${studioEndpointUrl}/video_sharing_enabled/${learningContextId}`
);
export const thumbnailUpload = ({ studioEndpointUrl, learningContextId, videoId }) => (
`${studioEndpointUrl}/video_images/${learningContextId}/${videoId}`
);

View File

@@ -6,6 +6,7 @@ import {
blockAncestor,
blockStudioView,
courseAssets,
videoSharingEnabledForCourse,
allowThumbnailUpload,
thumbnailUpload,
downloadVideoTranscriptURL,
@@ -89,6 +90,12 @@ describe('cms url methods', () => {
.toEqual(`${studioEndpointUrl}/video_images_upload_enabled`);
});
});
describe('videoSharingEnabledForCourse', () => {
it('returns url with studioEndpointUrl and learningContextId', () => {
expect(videoSharingEnabledForCourse({ studioEndpointUrl, learningContextId }))
.toEqual(`${studioEndpointUrl}/video_sharing_enabled/${learningContextId}`);
});
});
describe('thumbnailUpload', () => {
it('returns url with studioEndpointUrl, learningContextId, and videoId', () => {
expect(thumbnailUpload({ studioEndpointUrl, learningContextId, videoId }))