Merge pull request #277 from jansenk/jkantor/video-public-allowed-gate

feat: don't show video sharing checkbox unless waffle from studio is active
This commit is contained in:
kenclary
2023-03-15 16:21:03 -04:00
committed by GitHub
16 changed files with 292 additions and 74 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

@@ -26,4 +26,5 @@ export const RequestKeys = StrictDict({
importTranscript: 'importTranscript',
uploadImage: 'uploadImage',
fetchAdvanceSettings: 'fetchAdvanceSettings',
fetchVideoFeatures: 'fetchVideoFeatures',
});

View File

@@ -18,6 +18,7 @@ const initialState = {
[RequestKeys.fetchAssets]: { status: RequestStates.inactive },
[RequestKeys.checkTranscriptsForImport]: { status: RequestStates.inactive },
[RequestKeys.importTranscript]: { status: RequestStates.inactive },
[RequestKeys.fetchVideoFeatures]: { status: RequestStates.inactive },
};
// eslint-disable-next-line no-unused-vars

View File

@@ -272,6 +272,17 @@ export const fetchAdvanceSettings = ({ ...rest }) => (dispatch, getState) => {
}));
};
export const fetchVideoFeatures = ({ ...rest }) => (dispatch, getState) => {
dispatch(module.networkRequest({
requestKey: RequestKeys.fetchVideoFeatures,
promise: api.fetchVideoFeatures({
studioEndpointUrl: selectors.app.studioEndpointUrl(getState()),
learningContextId: selectors.app.learningContextId(getState()),
}),
...rest,
}));
};
export default StrictDict({
fetchBlock,
fetchStudioView,
@@ -289,4 +300,5 @@ export default StrictDict({
checkTranscriptsForImport,
importTranscript,
fetchAdvanceSettings,
fetchVideoFeatures,
});

View File

@@ -29,13 +29,13 @@ jest.mock('../../services/cms/api', () => ({
fetchAssets: ({ id, url }) => ({ id, url }),
uploadAsset: (args) => args,
loadImages: jest.fn(),
allowThumbnailUpload: (args) => args,
uploadThumbnail: (args) => args,
uploadTranscript: (args) => args,
deleteTranscript: (args) => args,
getTranscript: (args) => args,
checkTranscriptsForImport: (args) => args,
importTranscript: (args) => args,
fetchVideoFeatures: (args) => args,
}));
const apiKeys = keyStore(api);
@@ -302,20 +302,6 @@ describe('requests thunkActions module', () => {
},
});
});
describe('allowThumbnailUpload', () => {
testNetworkRequestAction({
action: requests.allowThumbnailUpload,
args: { ...fetchParams },
expectedString: 'with allowThumbnailUpload promise',
expectedData: {
...fetchParams,
requestKey: RequestKeys.allowThumbnailUpload,
promise: api.allowThumbnailUpload({
studioEndpointUrl: selectors.app.studioEndpointUrl(testState),
}),
},
});
});
describe('uploadThumbnail', () => {
const thumbnail = 'SoME tHumbNAil CoNtent As String';
const videoId = 'SoME VidEOid CoNtent As String';
@@ -462,5 +448,20 @@ describe('requests thunkActions module', () => {
},
});
});
describe('fetchVideoFeatures', () => {
testNetworkRequestAction({
action: requests.fetchVideoFeatures,
args: { ...fetchParams },
expectedString: 'with fetchVideoFeatures promise',
expectedData: {
...fetchParams,
requestKey: RequestKeys.fetchVideoFeatures,
promise: api.fetchVideoFeatures({
studioEndpointUrl: selectors.app.studioEndpointUrl(testState),
learningContextId: selectors.app.learningContextId(testState),
}),
},
});
});
});
});

View File

@@ -57,9 +57,10 @@ export const loadVideoData = () => (dispatch, getState) => {
},
thumbnail: rawVideoData.thumbnail,
}));
dispatch(requests.allowThumbnailUpload({
dispatch(requests.fetchVideoFeatures({
onSuccess: (response) => dispatch(actions.video.updateField({
allowThumbnailUpload: response.data.allowThumbnailUpload,
videoSharingEnabledForCourse: response.data.videoSharingEnabled,
})),
}));
const youTubeId = parseYoutubeId(videoUrl);

View File

@@ -30,6 +30,7 @@ jest.mock('./requests', () => ({
updateTranscriptLanguage: (args) => ({ updateTranscriptLanguage: args }),
checkTranscriptsForImport: (args) => ({ checkTranscriptsForImport: args }),
importTranscript: (args) => ({ importTranscript: args }),
fetchVideoFeatures: (args) => ({ fetchVideoFeatures: args }),
}));
jest.mock('../../../utils', () => ({
@@ -48,8 +49,13 @@ const mockFilename = 'soMEtRANscRipT.srt';
const mockThumbnail = 'sOMefILE';
const mockThumbnailResponse = { data: { image_url: 'soMEimAGEUrL' } };
const thumbnailUrl = 'soMEimAGEUrL';
const mockAllowThumbnailUpload = { data: { allowThumbnailUpload: 'soMEbOolEAn' } };
const mockAllowTranscriptImport = { data: { command: 'import' } };
const mockVideoFeatures = {
data: {
allowThumbnailUpload: 'soMEbOolEAn',
videoSharingEnabled: 'someBOoOoOlean',
},
};
const testMetadata = {
download_track: 'dOWNlOAdTraCK',
@@ -117,14 +123,18 @@ describe('video thunkActions', () => {
testMetadata.transcripts,
);
thunkActions.loadVideoData()(dispatch, getState);
[[dispatchedLoad], [dispatchedAction1], [dispatchedAction2]] = dispatch.mock.calls;
[
[dispatchedLoad],
[dispatchedAction1],
[dispatchedAction2],
] = dispatch.mock.calls;
});
afterEach(() => {
jest.restoreAllMocks();
});
it('dispatches allowThumbnailUpload action', () => {
it('dispatches fetchVideoFeatures action', () => {
expect(dispatchedLoad).not.toEqual(undefined);
expect(dispatchedAction1.allowThumbnailUpload).not.toEqual(undefined);
expect(dispatchedAction1.fetchVideoFeatures).not.toEqual(undefined);
});
it('dispatches checkTranscriptsForImport action', () => {
expect(dispatchedLoad).not.toEqual(undefined);
@@ -164,9 +174,10 @@ describe('video thunkActions', () => {
});
it('dispatches actions.video.updateField on success', () => {
dispatch.mockClear();
dispatchedAction1.allowThumbnailUpload.onSuccess(mockAllowThumbnailUpload);
dispatchedAction1.fetchVideoFeatures.onSuccess(mockVideoFeatures);
expect(dispatch).toHaveBeenCalledWith(actions.video.updateField({
allowThumbnailUpload: mockAllowThumbnailUpload.data.allowThumbnailUpload,
allowThumbnailUpload: mockVideoFeatures.data.allowThumbnailUpload,
videoSharingEnabledForCourse: mockVideoFeatures.data.videoSharingEnabled,
}));
dispatch.mockClear();

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,11 +36,6 @@ export const apiMethods = {
data,
);
},
allowThumbnailUpload: ({
studioEndpointUrl,
}) => get(
urls.allowThumbnailUpload({ studioEndpointUrl }),
),
uploadThumbnail: ({
studioEndpointUrl,
learningContextId,
@@ -205,6 +200,12 @@ export const apiMethods = {
title,
}),
),
fetchVideoFeatures: ({
studioEndpointUrl,
learningContextId,
}) => get(
urls.videoFeatures({ studioEndpointUrl, learningContextId }),
),
};
export const loadImage = (imageData) => ({

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'),
videoFeatures: jest.fn().mockName('urls.videoFeatures'),
}));
jest.mock('./utils', () => ({
@@ -245,12 +246,6 @@ describe('cms api', () => {
);
});
});
describe('allowThumbnailUpload', () => {
it('should call get with url.allowThumbnailUpload', () => {
apiMethods.allowThumbnailUpload({ studioEndpointUrl });
expect(get).toHaveBeenCalledWith(urls.allowThumbnailUpload({ studioEndpointUrl }));
});
});
});
describe('videoTranscripts', () => {
const language = 'la';
@@ -506,4 +501,11 @@ describe('cms api', () => {
expect(api.processLicense(licenseType, licenseDetails)).toEqual('all-rights-reserved');
});
});
describe('fetchVideoFeatures', () => {
it('should call get with url.videoFeatures', () => {
const args = { studioEndpointUrl, learningContextId };
apiMethods.fetchVideoFeatures({ ...args });
expect(get).toHaveBeenCalledWith(urls.videoFeatures({ ...args }));
});
});
});

View File

@@ -123,10 +123,6 @@ export const fetchCourseDetails = ({ studioEndpointUrl, learningContextId }) =>
},
});
// eslint-disable-next-line
export const allowThumbnailUpload = ({ studioEndpointUrl }) => mockPromise({
data: true,
});
// eslint-disable-next-line
export const checkTranscripts = ({youTubeId, studioEndpointUrl, blockId, videoId}) => mockPromise({
data: {
command: 'import',
@@ -142,6 +138,13 @@ export const importTranscript = ({youTubeId, studioEndpointUrl, blockId}) => moc
export const fetchAdvanceSettings = ({ studioEndpointUrl, learningContextId }) => mockPromise({
data: { allow_unsupported_xblocks: { value: true } },
});
// eslint-disable-next-line
export const fetchVideoFeatures = ({ studioEndpointUrl, learningContextId }) => mockPromise({
data: {
allowThumbnailUpload: true,
videoSharingEnabledForCourse: true,
},
});
export const normalizeContent = ({
blockId,

View File

@@ -31,10 +31,6 @@ export const courseAssets = ({ studioEndpointUrl, learningContextId }) => (
`${studioEndpointUrl}/assets/${learningContextId}/?page_size=500`
);
export const allowThumbnailUpload = ({ studioEndpointUrl }) => (
`${studioEndpointUrl}/video_images_upload_enabled`
);
export const thumbnailUpload = ({ studioEndpointUrl, learningContextId, videoId }) => (
`${studioEndpointUrl}/video_images/${learningContextId}/${videoId}`
);
@@ -66,3 +62,7 @@ export const replaceTranscript = ({ studioEndpointUrl, parameters }) => (
export const courseAdvanceSettings = ({ studioEndpointUrl, learningContextId }) => (
`${studioEndpointUrl}/api/contentstore/v0/advanced_settings/${learningContextId}`
);
export const videoFeatures = ({ studioEndpointUrl, learningContextId }) => (
`${studioEndpointUrl}/video_features/${learningContextId}`
);

View File

@@ -6,7 +6,6 @@ import {
blockAncestor,
blockStudioView,
courseAssets,
allowThumbnailUpload,
thumbnailUpload,
downloadVideoTranscriptURL,
videoTranscripts,
@@ -14,6 +13,7 @@ import {
courseDetailsUrl,
checkTranscriptsForImport,
replaceTranscript,
videoFeatures,
} from './urls';
describe('cms url methods', () => {
@@ -83,12 +83,6 @@ describe('cms url methods', () => {
.toEqual(`${studioEndpointUrl}/assets/${learningContextId}/?page_size=500`);
});
});
describe('allowThumbnailUpload', () => {
it('returns url with studioEndpointUrl', () => {
expect(allowThumbnailUpload({ studioEndpointUrl }))
.toEqual(`${studioEndpointUrl}/video_images_upload_enabled`);
});
});
describe('thumbnailUpload', () => {
it('returns url with studioEndpointUrl, learningContextId, and videoId', () => {
expect(thumbnailUpload({ studioEndpointUrl, learningContextId, videoId }))
@@ -131,4 +125,10 @@ describe('cms url methods', () => {
.toEqual(`${studioEndpointUrl}/transcripts/replace?data=${parameters}`);
});
});
describe('videoFeatures', () => {
it('returns url with studioEndpointUrl and learningContextId', () => {
expect(videoFeatures({ studioEndpointUrl, learningContextId }))
.toEqual(`${studioEndpointUrl}/video_features/${learningContextId}`);
});
});
});