Merge pull request #326 from open-craft/chris/FAL-3383-new-video-editor-flow
[FAL-3383] Implement new video UX flow on new video editor
This commit is contained in:
@@ -5,6 +5,7 @@ import VideoGallery from './containers/VideoGallery';
|
||||
import * as hooks from './hooks';
|
||||
|
||||
export const VideoSelector = ({
|
||||
blockId,
|
||||
learningContextId,
|
||||
lmsEndpointUrl,
|
||||
studioEndpointUrl,
|
||||
@@ -13,7 +14,7 @@ export const VideoSelector = ({
|
||||
hooks.initializeApp({
|
||||
dispatch,
|
||||
data: {
|
||||
blockId: '',
|
||||
blockId,
|
||||
blockType: 'video',
|
||||
learningContextId,
|
||||
lmsEndpointUrl,
|
||||
@@ -26,6 +27,7 @@ export const VideoSelector = ({
|
||||
};
|
||||
|
||||
VideoSelector.propTypes = {
|
||||
blockId: PropTypes.string.isRequired,
|
||||
learningContextId: PropTypes.string.isRequired,
|
||||
lmsEndpointUrl: PropTypes.string.isRequired,
|
||||
studioEndpointUrl: PropTypes.string.isRequired,
|
||||
|
||||
@@ -11,13 +11,13 @@ jest.mock('./hooks', () => ({
|
||||
jest.mock('./containers/VideoGallery', () => 'VideoGallery');
|
||||
|
||||
const props = {
|
||||
blockId: 'block-v1:edX+DemoX+Demo_Course+type@html+block@030e35c4756a4ddc8d40b95fbbfff4d4',
|
||||
learningContextId: 'course-v1:edX+DemoX+Demo_Course',
|
||||
lmsEndpointUrl: 'evenfakerurl.com',
|
||||
studioEndpointUrl: 'fakeurl.com',
|
||||
};
|
||||
|
||||
const initData = {
|
||||
blockId: '',
|
||||
blockType: 'video',
|
||||
...props,
|
||||
};
|
||||
|
||||
@@ -6,6 +6,7 @@ import VideoSelector from './VideoSelector';
|
||||
import store from './data/store';
|
||||
|
||||
const VideoSelectorPage = ({
|
||||
blockId,
|
||||
courseId,
|
||||
lmsEndpointUrl,
|
||||
studioEndpointUrl,
|
||||
@@ -19,6 +20,7 @@ const VideoSelectorPage = ({
|
||||
>
|
||||
<VideoSelector
|
||||
{...{
|
||||
blockId,
|
||||
learningContextId: courseId,
|
||||
lmsEndpointUrl,
|
||||
studioEndpointUrl,
|
||||
@@ -29,12 +31,14 @@ const VideoSelectorPage = ({
|
||||
);
|
||||
|
||||
VideoSelectorPage.defaultProps = {
|
||||
blockId: null,
|
||||
courseId: null,
|
||||
lmsEndpointUrl: null,
|
||||
studioEndpointUrl: null,
|
||||
};
|
||||
|
||||
VideoSelectorPage.propTypes = {
|
||||
blockId: PropTypes.string,
|
||||
courseId: PropTypes.string,
|
||||
lmsEndpointUrl: PropTypes.string,
|
||||
studioEndpointUrl: PropTypes.string,
|
||||
|
||||
@@ -3,6 +3,7 @@ import { shallow } from 'enzyme';
|
||||
import VideoSelectorPage from './VideoSelectorPage';
|
||||
|
||||
const props = {
|
||||
blockId: 'block-v1:edX+DemoX+Demo_Course+type@html+block@030e35c4756a4ddc8d40b95fbbfff4d4',
|
||||
courseId: 'course-v1:edX+DemoX+Demo_Course',
|
||||
lmsEndpointUrl: 'evenfakerurl.com',
|
||||
studioEndpointUrl: 'fakeurl.com',
|
||||
|
||||
@@ -17,6 +17,7 @@ exports[`Video Selector Page snapshots rendering correctly with expected Input 1
|
||||
studioEndpointUrl="fakeurl.com"
|
||||
>
|
||||
<VideoSelector
|
||||
blockId="block-v1:edX+DemoX+Demo_Course+type@html+block@030e35c4756a4ddc8d40b95fbbfff4d4"
|
||||
learningContextId="course-v1:edX+DemoX+Demo_Course"
|
||||
lmsEndpointUrl="evenfakerurl.com"
|
||||
studioEndpointUrl="fakeurl.com"
|
||||
@@ -42,6 +43,7 @@ exports[`Video Selector Page snapshots rendering with props to null 1`] = `
|
||||
studioEndpointUrl={null}
|
||||
>
|
||||
<VideoSelector
|
||||
blockId={null}
|
||||
learningContextId={null}
|
||||
lmsEndpointUrl={null}
|
||||
studioEndpointUrl={null}
|
||||
|
||||
@@ -8,11 +8,22 @@ exports[`VideoEditor snapshots renders as expected with default behavior 1`] = `
|
||||
onClose={[MockFunction props.onClose]}
|
||||
validateEntry={[MockFunction validateEntry]}
|
||||
>
|
||||
<Spinner
|
||||
animation="border"
|
||||
className="m-3"
|
||||
screenreadertext="loading"
|
||||
/>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"left": "50%",
|
||||
"position": "absolute",
|
||||
"top": "50%",
|
||||
"transform": "translate(-50%, -50%)",
|
||||
}
|
||||
}
|
||||
>
|
||||
<Spinner
|
||||
animation="border"
|
||||
className="m-3"
|
||||
screenreadertext="loading"
|
||||
/>
|
||||
</div>
|
||||
</EditorContainer>
|
||||
</Component>
|
||||
`;
|
||||
|
||||
@@ -1,18 +1,27 @@
|
||||
import React from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { thunkActions } from '../../../data/redux';
|
||||
import * as appHooks from '../../../hooks';
|
||||
import { thunkActions, selectors } from '../../../data/redux';
|
||||
import VideoSettingsModal from './VideoSettingsModal';
|
||||
// import SelectVideoModal from './SelectVideoModal';
|
||||
import * as module from './VideoEditorModal';
|
||||
|
||||
export const {
|
||||
navigateTo,
|
||||
} = appHooks;
|
||||
|
||||
export const hooks = {
|
||||
initialize: (dispatch) => {
|
||||
initialize: (dispatch, selectedVideoId, selectedVideoUrl) => {
|
||||
React.useEffect(() => {
|
||||
dispatch(thunkActions.video.loadVideoData());
|
||||
dispatch(thunkActions.video.loadVideoData(selectedVideoId, selectedVideoUrl));
|
||||
}, []);
|
||||
},
|
||||
returnToGallery: () => {
|
||||
const learningContextId = useSelector(selectors.app.learningContextId);
|
||||
const blockId = useSelector(selectors.app.blockId);
|
||||
return () => (navigateTo(`/course/${learningContextId}/editor/course-videos/${blockId}`));
|
||||
},
|
||||
};
|
||||
|
||||
const VideoEditorModal = ({
|
||||
@@ -20,9 +29,18 @@ const VideoEditorModal = ({
|
||||
isOpen,
|
||||
}) => {
|
||||
const dispatch = useDispatch();
|
||||
module.hooks.initialize(dispatch);
|
||||
const searchParams = new URLSearchParams(document.location.search);
|
||||
const selectedVideoId = searchParams.get('selectedVideoId');
|
||||
const selectedVideoUrl = searchParams.get('selectedVideoUrl');
|
||||
const onReturn = module.hooks.returnToGallery();
|
||||
module.hooks.initialize(dispatch, selectedVideoId, selectedVideoUrl);
|
||||
return (
|
||||
<VideoSettingsModal {...{ close, isOpen }} />
|
||||
<VideoSettingsModal {...{
|
||||
close,
|
||||
isOpen,
|
||||
onReturn,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
// TODO: add logic to show SelectVideoModal if no selection
|
||||
};
|
||||
|
||||
@@ -40,6 +40,7 @@ export const hooks = {
|
||||
export const Transcript = ({
|
||||
index,
|
||||
language,
|
||||
transcriptUrl,
|
||||
// redux
|
||||
deleteTranscript,
|
||||
}) => {
|
||||
@@ -90,6 +91,7 @@ export const Transcript = ({
|
||||
<TranscriptActionMenu
|
||||
index={index}
|
||||
language={language}
|
||||
transcriptUrl={transcriptUrl}
|
||||
launchDeleteConfirmation={launchDeleteConfirmation}
|
||||
/>
|
||||
)}
|
||||
@@ -99,9 +101,14 @@ export const Transcript = ({
|
||||
);
|
||||
};
|
||||
|
||||
Transcript.defaultProps = {
|
||||
transcriptUrl: undefined,
|
||||
};
|
||||
|
||||
Transcript.propTypes = {
|
||||
index: PropTypes.number.isRequired,
|
||||
language: PropTypes.string.isRequired,
|
||||
transcriptUrl: PropTypes.string,
|
||||
deleteTranscript: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
|
||||
@@ -80,6 +80,16 @@ describe('Transcript Component', () => {
|
||||
shallow(<module.Transcript {...props} />),
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
test('snapshots: renders as expected with transcriptUrl', () => {
|
||||
jest.spyOn(module.hooks, 'setUpDeleteConfirmation').mockImplementationOnce(() => ({
|
||||
inDeleteConfirmation: false,
|
||||
launchDeleteConfirmation: jest.fn().mockName('launchDeleteConfirmation'),
|
||||
cancelDelete: jest.fn().mockName('cancelDelete'),
|
||||
}));
|
||||
expect(
|
||||
shallow(<module.Transcript {...props} transcriptUrl="url" />),
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -25,13 +25,14 @@ export const hooks = {
|
||||
export const TranscriptActionMenu = ({
|
||||
index,
|
||||
language,
|
||||
transcriptUrl,
|
||||
launchDeleteConfirmation,
|
||||
// redux
|
||||
getTranscriptDownloadUrl,
|
||||
|
||||
buildTranscriptUrl,
|
||||
}) => {
|
||||
const input = fileInput({ onAddFile: module.hooks.replaceFileCallback({ language, dispatch: useDispatch() }) });
|
||||
const downloadLink = getTranscriptDownloadUrl({ language });
|
||||
const downloadLink = transcriptUrl ? buildTranscriptUrl({ transcriptUrl }) : getTranscriptDownloadUrl({ language });
|
||||
return (
|
||||
<Dropdown>
|
||||
<Dropdown.Toggle
|
||||
@@ -61,16 +62,23 @@ export const TranscriptActionMenu = ({
|
||||
);
|
||||
};
|
||||
|
||||
TranscriptActionMenu.defaultProps = {
|
||||
transcriptUrl: undefined,
|
||||
};
|
||||
|
||||
TranscriptActionMenu.propTypes = {
|
||||
index: PropTypes.number.isRequired,
|
||||
language: PropTypes.string.isRequired,
|
||||
transcriptUrl: PropTypes.string,
|
||||
launchDeleteConfirmation: PropTypes.func.isRequired,
|
||||
// redux
|
||||
getTranscriptDownloadUrl: PropTypes.func.isRequired,
|
||||
buildTranscriptUrl: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export const mapStateToProps = (state) => ({
|
||||
getTranscriptDownloadUrl: selectors.video.getTranscriptDownloadUrl(state),
|
||||
buildTranscriptUrl: selectors.video.buildTranscriptUrl(state),
|
||||
});
|
||||
|
||||
export const mapDispatchToProps = {
|
||||
|
||||
@@ -25,6 +25,7 @@ jest.mock('../../../../../../data/redux', () => ({
|
||||
selectors: {
|
||||
video: {
|
||||
getTranscriptDownloadUrl: jest.fn(args => ({ getTranscriptDownloadUrl: args })).mockName('selectors.video.getTranscriptDownloadUrl'),
|
||||
buildTranscriptUrl: jest.fn(args => ({ buildTranscriptUrl: args })).mockName('selectors.video.buildTranscriptUrl'),
|
||||
},
|
||||
},
|
||||
}));
|
||||
@@ -62,6 +63,7 @@ describe('TranscriptActionMenu', () => {
|
||||
launchDeleteConfirmation: jest.fn().mockName('launchDeleteConfirmation'),
|
||||
// redux
|
||||
getTranscriptDownloadUrl: jest.fn().mockName('selectors.video.getTranscriptDownloadUrl'),
|
||||
buildTranscriptUrl: jest.fn().mockName('selectors.video.buildTranscriptUrl'),
|
||||
};
|
||||
afterAll(() => {
|
||||
jest.clearAllMocks();
|
||||
@@ -72,6 +74,12 @@ describe('TranscriptActionMenu', () => {
|
||||
shallow(<module.TranscriptActionMenu {...props} />),
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
test('snapshots: renders as expected with transcriptUrl props: dont show confirm delete', () => {
|
||||
jest.spyOn(module.hooks, 'replaceFileCallback').mockImplementationOnce(() => jest.fn().mockName('module.hooks.replaceFileCallback'));
|
||||
expect(
|
||||
shallow(<module.TranscriptActionMenu {...props} transcriptUrl="url" />),
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
describe('mapStateToProps', () => {
|
||||
const testState = { A: 'pple', B: 'anana', C: 'ucumber' };
|
||||
|
||||
@@ -83,3 +83,21 @@ exports[`Transcript Component component component snapshots: renders as expected
|
||||
</Card>
|
||||
</Fragment>
|
||||
`;
|
||||
|
||||
exports[`Transcript Component component component snapshots: renders as expected with transcriptUrl 1`] = `
|
||||
<Fragment>
|
||||
<ActionRow>
|
||||
<LanguageSelector
|
||||
language="lAnG"
|
||||
title="sOmenUmBer"
|
||||
/>
|
||||
<ActionRow.Spacer />
|
||||
<TranscriptActionMenu
|
||||
index="sOmenUmBer"
|
||||
language="lAnG"
|
||||
launchDeleteConfirmation={[MockFunction launchDeleteConfirmation]}
|
||||
transcriptUrl="url"
|
||||
/>
|
||||
</ActionRow>
|
||||
</Fragment>
|
||||
`;
|
||||
|
||||
@@ -53,3 +53,57 @@ exports[`TranscriptActionMenu Snapshots snapshots: renders as expected with defa
|
||||
/>
|
||||
</Dropdown>
|
||||
`;
|
||||
|
||||
exports[`TranscriptActionMenu Snapshots snapshots: renders as expected with transcriptUrl props: dont show confirm delete 1`] = `
|
||||
<Dropdown>
|
||||
<Dropdown.Toggle
|
||||
alt="Actions dropdown"
|
||||
as="IconButton"
|
||||
iconAs="Icon"
|
||||
id="dropdown-toggle-with-iconbutton-video-transcript-widget"
|
||||
variant="primary"
|
||||
/>
|
||||
<Dropdown.Menu
|
||||
className="video_transcript Action Menu"
|
||||
>
|
||||
<Dropdown.Item
|
||||
key="transcript-actions-sOmenUmBer-replace"
|
||||
onClick={[MockFunction click input]}
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Replace"
|
||||
description="Message Presented To user for action to replace transcript"
|
||||
id="authoring.videoeditor.transcript.replaceTranscript"
|
||||
/>
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item
|
||||
key="transcript-actions-sOmenUmBer-download"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Download"
|
||||
description="Message Presented To user for action to download transcript"
|
||||
id="authoring.videoeditor.transcript.downloadTranscript"
|
||||
/>
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item
|
||||
key="transcript-actions-sOmenUmBer-delete"
|
||||
onClick={[MockFunction launchDeleteConfirmation]}
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Delete"
|
||||
description="Message Presented To user for action to delete transcript"
|
||||
id="authoring.videoeditor.transcript.deleteTranscript"
|
||||
/>
|
||||
</Dropdown.Item>
|
||||
</Dropdown.Menu>
|
||||
<FileInput
|
||||
acceptedFiles=".srt"
|
||||
fileInput={
|
||||
Object {
|
||||
"click": [MockFunction click input],
|
||||
"onAddFile": [MockFunction module.hooks.replaceFileCallback],
|
||||
}
|
||||
}
|
||||
/>
|
||||
</Dropdown>
|
||||
`;
|
||||
|
||||
@@ -597,6 +597,63 @@ exports[`TranscriptWidget component snapshots snapshots: renders as expected wit
|
||||
</CollapsibleFormWidget>
|
||||
`;
|
||||
|
||||
exports[`TranscriptWidget component snapshots snapshots: renders as expected with transcript urls 1`] = `
|
||||
<CollapsibleFormWidget
|
||||
fontSize="x-small"
|
||||
isError={true}
|
||||
subtitle="None"
|
||||
title="Transcripts"
|
||||
>
|
||||
<ErrorAlert
|
||||
dismissError={null}
|
||||
hideHeading={true}
|
||||
isError={false}
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Failed to upload transcript. Please try again."
|
||||
description="Message presented to user when transcript fails to upload"
|
||||
id="authoring.videoeditor.transcript.error.uploadTranscriptError"
|
||||
/>
|
||||
</ErrorAlert>
|
||||
<ErrorAlert
|
||||
dismissError={null}
|
||||
hideHeading={true}
|
||||
isError={false}
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Failed to delete transcript. Please try again."
|
||||
description="Message presented to user when transcript fails to delete"
|
||||
id="authoring.videoeditor.transcript.error.deleteTranscriptError"
|
||||
/>
|
||||
</ErrorAlert>
|
||||
<Stack
|
||||
gap={3}
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Add video transcripts (.srt files only) for improved accessibility."
|
||||
description="Message for adding first transcript"
|
||||
id="authoring.videoeditor.transcripts.upload.firstTranscriptMessage"
|
||||
/>
|
||||
<div
|
||||
className="mt-2"
|
||||
>
|
||||
<Button
|
||||
className="text-primary-500 font-weight-bold justify-content-start pl-0"
|
||||
onClick={[Function]}
|
||||
size="sm"
|
||||
variant="link"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Add a transcript"
|
||||
description="Label for upload button"
|
||||
id="authoring.videoeditor.transcripts.upload.label"
|
||||
/>
|
||||
</Button>
|
||||
</div>
|
||||
</Stack>
|
||||
</CollapsibleFormWidget>
|
||||
`;
|
||||
|
||||
exports[`TranscriptWidget component snapshots snapshots: renders as expected with transcripts 1`] = `
|
||||
<CollapsibleFormWidget
|
||||
fontSize="x-small"
|
||||
@@ -715,3 +772,122 @@ exports[`TranscriptWidget component snapshots snapshots: renders as expected wit
|
||||
</Stack>
|
||||
</CollapsibleFormWidget>
|
||||
`;
|
||||
|
||||
exports[`TranscriptWidget component snapshots snapshots: renders as expected with transcripts and urls 1`] = `
|
||||
<CollapsibleFormWidget
|
||||
fontSize="x-small"
|
||||
isError={true}
|
||||
subtitle="Spanish"
|
||||
title="Transcripts"
|
||||
>
|
||||
<ErrorAlert
|
||||
dismissError={null}
|
||||
hideHeading={true}
|
||||
isError={false}
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Failed to upload transcript. Please try again."
|
||||
description="Message presented to user when transcript fails to upload"
|
||||
id="authoring.videoeditor.transcript.error.uploadTranscriptError"
|
||||
/>
|
||||
</ErrorAlert>
|
||||
<ErrorAlert
|
||||
dismissError={null}
|
||||
hideHeading={true}
|
||||
isError={false}
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Failed to delete transcript. Please try again."
|
||||
description="Message presented to user when transcript fails to delete"
|
||||
id="authoring.videoeditor.transcript.error.deleteTranscriptError"
|
||||
/>
|
||||
</ErrorAlert>
|
||||
<Stack
|
||||
gap={3}
|
||||
>
|
||||
<Form.Group
|
||||
className="border-primary-100 border-bottom"
|
||||
>
|
||||
<Transcript
|
||||
index={0}
|
||||
language="es"
|
||||
/>
|
||||
<ActionRow
|
||||
className="mt-3.5"
|
||||
>
|
||||
<Form.Checkbox
|
||||
checked={false}
|
||||
className="decorative-control-label"
|
||||
onChange={[Function]}
|
||||
>
|
||||
<div
|
||||
className="small text-gray-700"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Allow transcript downloads"
|
||||
description="Label for allow transcript downloads checkbox"
|
||||
id="authoring.videoeditor.transcripts.allowDownloadCheckboxLabel"
|
||||
/>
|
||||
</div>
|
||||
</Form.Checkbox>
|
||||
<OverlayTrigger
|
||||
key="top"
|
||||
overlay={
|
||||
<Tooltip
|
||||
id="tooltip-top"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Learners will see a link to download the transcript below the video."
|
||||
description="Message for show by default checkbox"
|
||||
id="authoring.videoeditor.transcripts.upload.allowDownloadTooltipMessage"
|
||||
/>
|
||||
</Tooltip>
|
||||
}
|
||||
placement="top"
|
||||
>
|
||||
<Icon
|
||||
style={
|
||||
Object {
|
||||
"height": "16px",
|
||||
"width": "16px",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</OverlayTrigger>
|
||||
<ActionRow.Spacer />
|
||||
</ActionRow>
|
||||
<Form.Checkbox
|
||||
checked={false}
|
||||
className="mt-3 decorative-control-label"
|
||||
onChange={[Function]}
|
||||
>
|
||||
<div
|
||||
className="small text-gray-700"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Show transcript in the video player by default"
|
||||
description="Label for show by default checkbox"
|
||||
id="authoring.videoeditor.transcripts.upload.showByDefaultCheckboxLabel"
|
||||
/>
|
||||
</div>
|
||||
</Form.Checkbox>
|
||||
</Form.Group>
|
||||
<div
|
||||
className="mt-2"
|
||||
>
|
||||
<Button
|
||||
className="text-primary-500 font-weight-bold justify-content-start pl-0"
|
||||
onClick={[Function]}
|
||||
size="sm"
|
||||
variant="link"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Add a transcript"
|
||||
description="Label for upload button"
|
||||
id="authoring.videoeditor.transcripts.upload.label"
|
||||
/>
|
||||
</Button>
|
||||
</div>
|
||||
</Stack>
|
||||
</CollapsibleFormWidget>
|
||||
`;
|
||||
|
||||
@@ -78,6 +78,7 @@ export const hooks = {
|
||||
export const TranscriptWidget = ({
|
||||
// redux
|
||||
transcripts,
|
||||
selectedVideoTranscriptUrls,
|
||||
allowTranscriptDownloads,
|
||||
showTranscriptByDefault,
|
||||
allowTranscriptImport,
|
||||
@@ -117,6 +118,7 @@ export const TranscriptWidget = ({
|
||||
{transcripts.map((language, index) => (
|
||||
<Transcript
|
||||
language={language}
|
||||
transcriptUrl={selectedVideoTranscriptUrls[language]}
|
||||
index={index}
|
||||
/>
|
||||
))}
|
||||
@@ -178,10 +180,12 @@ export const TranscriptWidget = ({
|
||||
};
|
||||
|
||||
TranscriptWidget.defaultProps = {
|
||||
selectedVideoTranscriptUrls: {},
|
||||
};
|
||||
TranscriptWidget.propTypes = {
|
||||
// redux
|
||||
transcripts: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
selectedVideoTranscriptUrls: PropTypes.shape(),
|
||||
allowTranscriptDownloads: PropTypes.bool.isRequired,
|
||||
showTranscriptByDefault: PropTypes.bool.isRequired,
|
||||
allowTranscriptImport: PropTypes.bool.isRequired,
|
||||
@@ -192,6 +196,7 @@ TranscriptWidget.propTypes = {
|
||||
};
|
||||
export const mapStateToProps = (state) => ({
|
||||
transcripts: selectors.video.transcripts(state),
|
||||
selectedVideoTranscriptUrls: selectors.video.selectedVideoTranscriptUrls(state),
|
||||
allowTranscriptDownloads: selectors.video.allowTranscriptDownloads(state),
|
||||
showTranscriptByDefault: selectors.video.showTranscriptByDefault(state),
|
||||
allowTranscriptImport: selectors.video.allowTranscriptImport(state),
|
||||
|
||||
@@ -27,6 +27,7 @@ jest.mock('../../../../../../data/redux', () => ({
|
||||
selectors: {
|
||||
video: {
|
||||
transcripts: jest.fn(state => ({ transcripts: state })),
|
||||
selectedVideoTranscriptUrls: jest.fn(state => ({ selectedVideoTranscriptUrls: state })),
|
||||
allowTranscriptDownloads: jest.fn(state => ({ allowTranscriptDownloads: state })),
|
||||
showTranscriptByDefault: jest.fn(state => ({ showTranscriptByDefault: state })),
|
||||
allowTranscriptImport: jest.fn(state => ({ allowTranscriptImport: state })),
|
||||
@@ -88,6 +89,7 @@ describe('TranscriptWidget', () => {
|
||||
title: 'tiTLE',
|
||||
intl: { formatMessage },
|
||||
transcripts: [],
|
||||
selectedVideoTranscriptUrls: {},
|
||||
allowTranscriptDownloads: false,
|
||||
showTranscriptByDefault: false,
|
||||
allowTranscriptImport: false,
|
||||
@@ -112,6 +114,16 @@ describe('TranscriptWidget', () => {
|
||||
shallow(<module.TranscriptWidget {...props} transcripts={['en']} />),
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
test('snapshots: renders as expected with transcript urls', () => {
|
||||
expect(
|
||||
shallow(<module.TranscriptWidget {...props} selectedVideoTranscriptUrls={{ en: 'url' }} />),
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
test('snapshots: renders as expected with transcripts and urls', () => {
|
||||
expect(
|
||||
shallow(<module.TranscriptWidget {...props} transcripts={['es']} selectedVideoTranscriptUrls={{ en: 'url' }} />),
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
test('snapshots: renders as expected with allowTranscriptDownloads true', () => {
|
||||
expect(
|
||||
shallow(<module.TranscriptWidget {...props} allowTranscriptDownloads transcripts={['en']} />),
|
||||
|
||||
@@ -10,18 +10,16 @@ export const LanguageNamesWidget = ({ transcripts, intl }) => {
|
||||
let icon = ClosedCaptionOff;
|
||||
const hasTranscripts = transcriptHooks.hasTranscripts(transcripts);
|
||||
let message = intl.formatMessage(messages.noTranscriptsAdded);
|
||||
let fontClass = 'text-gray';
|
||||
|
||||
if (hasTranscripts) {
|
||||
message = transcriptHooks.transcriptLanguages(transcripts, intl);
|
||||
fontClass = 'text-primary';
|
||||
icon = ClosedCaption;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="d-flex flex-row align-items-center x-small">
|
||||
<Icon className="mr-1" src={icon} />
|
||||
<span className={fontClass}>{message}</span>
|
||||
<Icon className="mr-1 text-primary-500" src={icon} />
|
||||
<span className="text-gray-700">{message}</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Button, Icon } from '@edx/paragon';
|
||||
import { ArrowBackIos } from '@edx/paragon/icons';
|
||||
import {
|
||||
FormattedMessage,
|
||||
} from '@edx/frontend-platform/i18n';
|
||||
|
||||
// import VideoPreview from './components/VideoPreview';
|
||||
import ErrorSummary from './ErrorSummary';
|
||||
@@ -11,9 +17,25 @@ import VideoSourceWidget from './components/VideoSourceWidget';
|
||||
import VideoPreviewWidget from './components/VideoPreviewWidget';
|
||||
import './index.scss';
|
||||
import SocialShareWidget from './components/SocialShareWidget';
|
||||
import messages from '../../messages';
|
||||
|
||||
export const VideoSettingsModal = () => (
|
||||
export const VideoSettingsModal = ({
|
||||
onReturn,
|
||||
}) => (
|
||||
<>
|
||||
<Button
|
||||
variant="link"
|
||||
className="text-primary-500"
|
||||
size="sm"
|
||||
onClick={onReturn}
|
||||
style={{
|
||||
textDecoration: 'none',
|
||||
marginLeft: '3px',
|
||||
}}
|
||||
>
|
||||
<Icon src={ArrowBackIos} style={{ height: '13px' }} />
|
||||
<FormattedMessage {...messages.replaceVideoButtonLabel} />
|
||||
</Button>
|
||||
<ErrorSummary />
|
||||
<VideoPreviewWidget />
|
||||
<VideoSourceWidget />
|
||||
@@ -26,4 +48,9 @@ export const VideoSettingsModal = () => (
|
||||
</>
|
||||
);
|
||||
|
||||
VideoSettingsModal.propTypes = {
|
||||
showReturn: PropTypes.bool.isRequired,
|
||||
onReturn: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default VideoSettingsModal;
|
||||
|
||||
@@ -38,11 +38,19 @@ export const VideoEditor = ({
|
||||
<VideoEditorModal />
|
||||
</div>
|
||||
) : (
|
||||
<Spinner
|
||||
animation="border"
|
||||
className="m-3"
|
||||
screenreadertext={intl.formatMessage(messages.spinnerScreenReaderText)}
|
||||
/>
|
||||
<div style={{
|
||||
position: 'absolute',
|
||||
left: '50%',
|
||||
top: '50%',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
}}
|
||||
>
|
||||
<Spinner
|
||||
animation="border"
|
||||
className="m-3"
|
||||
screenreadertext={intl.formatMessage(messages.spinnerScreenReaderText)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</EditorContainer>
|
||||
</ErrorContext.Provider>
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
|
||||
const messages = defineMessages({
|
||||
|
||||
spinnerScreenReaderText: {
|
||||
id: 'authoring.videoEditor.spinnerScreenReaderText',
|
||||
defaultMessage: 'loading',
|
||||
description: 'Loading message for spinner screenreader text.',
|
||||
},
|
||||
replaceVideoButtonLabel: {
|
||||
id: 'authoring.videoEditor.replaceVideoButtonLabel',
|
||||
defaultMessage: 'Replace video',
|
||||
description: 'Text of the replace video button to return to the video gallery',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import React from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import * as module from './hooks';
|
||||
import messages from './messages';
|
||||
import * as appHooks from '../../hooks';
|
||||
import { selectors } from '../../data/redux';
|
||||
import analyticsEvt from '../../data/constants/analyticsEvt';
|
||||
import {
|
||||
filterKeys,
|
||||
filterMessages,
|
||||
@@ -9,6 +13,11 @@ import {
|
||||
sortFunctions,
|
||||
} from './utils';
|
||||
|
||||
export const {
|
||||
navigateCallback,
|
||||
navigateTo,
|
||||
} = appHooks;
|
||||
|
||||
export const state = {
|
||||
highlighted: (val) => React.useState(val),
|
||||
searchString: (val) => React.useState(val),
|
||||
@@ -91,6 +100,8 @@ export const videoListProps = ({ searchSortProps, videos }) => {
|
||||
setShowSizeError,
|
||||
] = module.state.showSizeError(false);
|
||||
const filteredList = module.filterList({ ...searchSortProps, videos });
|
||||
const learningContextId = useSelector(selectors.app.learningContextId);
|
||||
const blockId = useSelector(selectors.app.blockId);
|
||||
return {
|
||||
galleryError: {
|
||||
show: showSelectVideoError,
|
||||
@@ -116,25 +127,38 @@ export const videoListProps = ({ searchSortProps, videos }) => {
|
||||
height: '100%',
|
||||
},
|
||||
selectBtnProps: {
|
||||
onclick: () => {
|
||||
// TODO Update this when implementing the selection feature
|
||||
onClick: () => {
|
||||
if (highlighted) {
|
||||
navigateTo(`/course/${learningContextId}/editor/video/${blockId}?selectedVideoId=${highlighted}`);
|
||||
} else {
|
||||
setShowSelectVideoError(true);
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const fileInputProps = () => {
|
||||
// TODO [Update video] Implement this
|
||||
const ref = React.useRef();
|
||||
const click = () => ref.current.click();
|
||||
|
||||
const click = module.handleVideoUpload();
|
||||
return {
|
||||
click,
|
||||
addFile: () => {},
|
||||
ref,
|
||||
};
|
||||
};
|
||||
|
||||
export const handleVideoUpload = () => {
|
||||
const learningContextId = useSelector(selectors.app.learningContextId);
|
||||
const blockId = useSelector(selectors.app.blockId);
|
||||
return () => navigateTo(`/course/${learningContextId}/editor/video_upload/${blockId}`);
|
||||
};
|
||||
|
||||
export const handleCancel = () => (
|
||||
navigateCallback({
|
||||
destination: useSelector(selectors.app.returnUrl),
|
||||
analytics: useSelector(selectors.app.analytics),
|
||||
analyticsEvent: analyticsEvt.videoGalleryCancelClick,
|
||||
})
|
||||
);
|
||||
|
||||
export const buildVideos = ({ rawVideos }) => {
|
||||
let videos = [];
|
||||
const rawVideoList = Object.values(rawVideos);
|
||||
@@ -191,4 +215,6 @@ export const videoProps = ({ videos }) => {
|
||||
export default {
|
||||
videoProps,
|
||||
buildVideos,
|
||||
handleCancel,
|
||||
handleVideoUpload,
|
||||
};
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import * as reactRedux from 'react-redux';
|
||||
import * as hooks from './hooks';
|
||||
import { filterKeys, sortKeys } from './utils';
|
||||
import { MockUseState } from '../../../testUtils';
|
||||
import { keyStore } from '../../utils';
|
||||
import * as appHooks from '../../hooks';
|
||||
import { selectors } from '../../data/redux';
|
||||
import analyticsEvt from '../../data/constants/analyticsEvt';
|
||||
|
||||
jest.mock('react', () => ({
|
||||
...jest.requireActual('react'),
|
||||
@@ -16,9 +20,25 @@ jest.mock('react-redux', () => {
|
||||
...jest.requireActual('react-redux'),
|
||||
dispatch: dispatchFn,
|
||||
useDispatch: jest.fn(() => dispatchFn),
|
||||
useSelector: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock('../../data/redux', () => ({
|
||||
selectors: {
|
||||
app: {
|
||||
returnUrl: 'returnUrl',
|
||||
analytics: 'analytics',
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('../../hooks', () => ({
|
||||
...jest.requireActual('../../hooks'),
|
||||
navigateCallback: jest.fn((args) => ({ navigateCallback: args })),
|
||||
navigateTo: jest.fn((args) => ({ navigateTo: args })),
|
||||
}));
|
||||
|
||||
const state = new MockUseState(hooks);
|
||||
const hookKeys = keyStore(hooks);
|
||||
let hook;
|
||||
@@ -175,6 +195,23 @@ describe('VideoGallery hooks', () => {
|
||||
beforeEach(() => {
|
||||
load();
|
||||
});
|
||||
describe('selectBtnProps', () => {
|
||||
test('on click, if sets selection', () => {
|
||||
const highlighted = 'videoId';
|
||||
state.mockVal(state.keys.highlighted, highlighted);
|
||||
load();
|
||||
expect(appHooks.navigateTo).not.toHaveBeenCalled();
|
||||
hook.selectBtnProps.onClick();
|
||||
expect(appHooks.navigateTo).toHaveBeenCalled();
|
||||
});
|
||||
test('on click, sets showSelectVideoError to true if nothing is highlighted', () => {
|
||||
state.mockVal(state.keys.highlighted, null);
|
||||
load();
|
||||
hook.selectBtnProps.onClick();
|
||||
expect(appHooks.navigateTo).not.toHaveBeenCalled();
|
||||
expect(state.setState.showSelectVideoError).toHaveBeenCalledWith(true);
|
||||
});
|
||||
});
|
||||
describe('galleryProps', () => {
|
||||
it('returns highlighted value, initialized to null', () => {
|
||||
expect(hook.galleryProps.highlighted).toEqual(state.stateVals.highlighted);
|
||||
@@ -210,6 +247,17 @@ describe('VideoGallery hooks', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('fileInputHooks', () => {
|
||||
test('click calls current.click on the ref', () => {
|
||||
jest.spyOn(hooks, hookKeys.handleVideoUpload).mockImplementationOnce();
|
||||
expect(hooks.handleVideoUpload).not.toHaveBeenCalled();
|
||||
hook = hooks.fileInputProps();
|
||||
expect(hooks.handleVideoUpload).toHaveBeenCalled();
|
||||
expect(appHooks.navigateTo).not.toHaveBeenCalled();
|
||||
hook.click();
|
||||
expect(appHooks.navigateTo).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
describe('videoProps', () => {
|
||||
const videoList = {
|
||||
galleryProps: 'some gallery props',
|
||||
@@ -250,4 +298,15 @@ describe('VideoGallery hooks', () => {
|
||||
expect(hook.selectBtnProps).toEqual(videoList.selectBtnProps);
|
||||
});
|
||||
});
|
||||
describe('handleCancel', () => {
|
||||
it('calls navigateCallback', () => {
|
||||
expect(hooks.handleCancel()).toEqual(
|
||||
appHooks.navigateCallback({
|
||||
destination: reactRedux.useSelector(selectors.app.returnUrl),
|
||||
analyticsEvent: analyticsEvt.videoGalleryCancelClick,
|
||||
analytics: reactRedux.useSelector(selectors.app.analytics),
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from 'react';
|
||||
import React, { useEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import { selectors } from '../../data/redux';
|
||||
@@ -16,6 +16,14 @@ export const VideoGallery = ({
|
||||
isUploadError,
|
||||
}) => {
|
||||
const videos = hooks.buildVideos({ rawVideos });
|
||||
const handleVideoUpload = hooks.handleVideoUpload();
|
||||
|
||||
useEffect(() => {
|
||||
// If no videos exists redirects to the video upload screen
|
||||
if (isLoaded && videos.length === 0) {
|
||||
handleVideoUpload();
|
||||
}
|
||||
}, [isLoaded]);
|
||||
const {
|
||||
galleryError,
|
||||
inputError,
|
||||
@@ -24,6 +32,7 @@ export const VideoGallery = ({
|
||||
searchSortProps,
|
||||
selectBtnProps,
|
||||
} = hooks.videoProps({ videos });
|
||||
const handleCancel = hooks.handleCancel();
|
||||
|
||||
const modalMessages = {
|
||||
confirmMsg: messages.selectVideoButtonlabel,
|
||||
@@ -38,7 +47,7 @@ export const VideoGallery = ({
|
||||
<SelectionModal
|
||||
{...{
|
||||
isOpen: true,
|
||||
close: () => { /* TODO */ },
|
||||
close: handleCancel,
|
||||
size: 'fullscreen',
|
||||
isFullscreenScroll: false,
|
||||
galleryError,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { shallow, mount } from 'enzyme';
|
||||
|
||||
import SelectionModal from '../../sharedComponents/SelectionModal';
|
||||
import hooks from './hooks';
|
||||
@@ -7,6 +7,8 @@ import * as module from '.';
|
||||
|
||||
jest.mock('../../sharedComponents/SelectionModal', () => 'SelectionModal');
|
||||
|
||||
const mockHandleVideoUploadHook = jest.fn();
|
||||
|
||||
jest.mock('./hooks', () => ({
|
||||
buildVideos: jest.fn(() => []),
|
||||
videoProps: jest.fn(() => ({
|
||||
@@ -39,6 +41,8 @@ jest.mock('./hooks', () => ({
|
||||
searchSortProps: { search: 'sortProps' },
|
||||
selectBtnProps: { select: 'btnProps' },
|
||||
})),
|
||||
handleCancel: jest.fn(),
|
||||
handleVideoUpload: () => mockHandleVideoUploadHook,
|
||||
}));
|
||||
|
||||
jest.mock('../../data/redux', () => ({
|
||||
@@ -51,6 +55,11 @@ jest.mock('../../data/redux', () => ({
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('../../hooks', () => ({
|
||||
...jest.requireActual('../../hooks'),
|
||||
navigateCallback: jest.fn((args) => ({ navigateCallback: args })),
|
||||
}));
|
||||
|
||||
describe('VideoGallery', () => {
|
||||
describe('component', () => {
|
||||
const props = {
|
||||
@@ -63,6 +72,7 @@ describe('VideoGallery', () => {
|
||||
const videoProps = hooks.videoProps();
|
||||
beforeEach(() => {
|
||||
el = shallow(<module.VideoGallery {...props} />);
|
||||
mockHandleVideoUploadHook.mockReset();
|
||||
});
|
||||
it('provides confirm action, forwarding selectBtnProps from imgHooks', () => {
|
||||
expect(el.find(SelectionModal).props().selectBtnProps).toEqual(
|
||||
@@ -83,5 +93,12 @@ describe('VideoGallery', () => {
|
||||
it('provides a FileInput component with fileInput props from imgHooks', () => {
|
||||
expect(el.find(SelectionModal).props().fileInput).toMatchObject(videoProps.fileInput);
|
||||
});
|
||||
it('handleVideoUpload called if there are no videos', () => {
|
||||
el = mount(<module.VideoGallery {...props} />);
|
||||
expect(mockHandleVideoUploadHook).not.toHaveBeenCalled();
|
||||
el.setProps({ rawVideos: {}, isLoaded: true });
|
||||
el.mount();
|
||||
expect(mockHandleVideoUploadHook).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -62,21 +62,26 @@ exports[`VideoUploadEditor renders without errors 1`] = `
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="d-flex video-id-prompt"
|
||||
class="d-flex video-id-container"
|
||||
>
|
||||
<input
|
||||
placeholder="Paste your video ID or URL"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<button
|
||||
class="border-start-0"
|
||||
type="button"
|
||||
<div
|
||||
class="d-flex video-id-prompt"
|
||||
>
|
||||
<icon
|
||||
class="rounded-circle text-dark"
|
||||
<input
|
||||
placeholder="Paste your video ID or URL"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
class="border-start-0"
|
||||
data-testid="inputSaveButton"
|
||||
type="button"
|
||||
>
|
||||
<icon
|
||||
class="rounded-circle text-dark"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -135,21 +140,26 @@ exports[`VideoUploader renders without errors 1`] = `
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="d-flex video-id-prompt"
|
||||
class="d-flex video-id-container"
|
||||
>
|
||||
<input
|
||||
placeholder="Paste your video ID or URL"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<button
|
||||
class="border-start-0"
|
||||
type="button"
|
||||
<div
|
||||
class="d-flex video-id-prompt"
|
||||
>
|
||||
<icon
|
||||
class="rounded-circle text-dark"
|
||||
<input
|
||||
placeholder="Paste your video ID or URL"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
class="border-start-0"
|
||||
data-testid="inputSaveButton"
|
||||
type="button"
|
||||
>
|
||||
<icon
|
||||
class="rounded-circle text-dark"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,12 @@
|
||||
import * as requests from '../../data/redux/thunkActions/requests';
|
||||
import * as module from './hooks';
|
||||
import { selectors } from '../../data/redux';
|
||||
import store from '../../data/store';
|
||||
import * as appHooks from '../../hooks';
|
||||
|
||||
export const {
|
||||
navigateTo,
|
||||
} = appHooks;
|
||||
|
||||
export const uploadVideo = async ({ dispatch, supportedFiles }) => {
|
||||
const data = { files: [] };
|
||||
@@ -8,14 +16,23 @@ export const uploadVideo = async ({ dispatch, supportedFiles }) => {
|
||||
content_type: file.type,
|
||||
});
|
||||
});
|
||||
const onFileUploadedHook = module.onFileUploaded();
|
||||
dispatch(await requests.uploadVideo({
|
||||
data,
|
||||
onSuccess: async (response) => {
|
||||
const { files } = response.json();
|
||||
const { files } = response.data;
|
||||
await Promise.all(Object.values(files).map(async (fileObj) => {
|
||||
const fileName = fileObj.file_name;
|
||||
const edxVideoId = fileObj.edx_video_id;
|
||||
const uploadUrl = fileObj.upload_url;
|
||||
const uploadFile = supportedFiles.find((file) => file.name === fileName);
|
||||
|
||||
// TODO I added this temporally to test the redirecton without
|
||||
// make the post to the upload URL. I added this also after the success post
|
||||
// To test this I overwriten my own response with an existing edx_video_id on
|
||||
// the edx-platform view: https://github.com/openedx/edx-platform/blob/master/cms/djangoapps/contentstore/views/videos.py#L224
|
||||
onFileUploadedHook(edxVideoId);
|
||||
|
||||
if (!uploadFile) {
|
||||
console.error(`Could not find file object with name "${fileName}" in supportedFiles array.`);
|
||||
return;
|
||||
@@ -29,14 +46,27 @@ export const uploadVideo = async ({ dispatch, supportedFiles }) => {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
})
|
||||
.then((resp) => resp.json())
|
||||
.then((responseData) => console.log('File uploaded:', responseData))
|
||||
.then(() => onFileUploadedHook(edxVideoId))
|
||||
.catch((error) => console.error('Error uploading file:', error));
|
||||
}));
|
||||
},
|
||||
}));
|
||||
};
|
||||
|
||||
export const onFileUploaded = () => {
|
||||
const state = store.getState();
|
||||
const learningContextId = selectors.app.learningContextId(state);
|
||||
const blockId = selectors.app.blockId(state);
|
||||
return (edxVideoId) => navigateTo(`/course/${learningContextId}/editor/video/${blockId}?selectedVideoId=${edxVideoId}`);
|
||||
};
|
||||
|
||||
export const onUrlUploaded = () => {
|
||||
const state = store.getState();
|
||||
const learningContextId = selectors.app.learningContextId(state);
|
||||
const blockId = selectors.app.blockId(state);
|
||||
return (videoUrl) => navigateTo(`/course/${learningContextId}/editor/video/${blockId}?selectedVideoUrl=${videoUrl}`);
|
||||
};
|
||||
|
||||
export default {
|
||||
uploadVideo,
|
||||
};
|
||||
|
||||
@@ -32,7 +32,7 @@ describe('uploadVideo', () => {
|
||||
|
||||
it('should call fetch with correct arguments for each file', async () => {
|
||||
const mockResponseData = { success: true };
|
||||
const mockFetchResponse = Promise.resolve({ json: () => Promise.resolve(mockResponseData) });
|
||||
const mockFetchResponse = Promise.resolve({ data: mockResponseData });
|
||||
global.fetch = jest.fn().mockImplementation(() => mockFetchResponse);
|
||||
const response = {
|
||||
files: [
|
||||
@@ -40,8 +40,7 @@ describe('uploadVideo', () => {
|
||||
{ file_name: 'file2.mov', upload_url: 'http://example.com/put_video2' },
|
||||
],
|
||||
};
|
||||
const spyConsoleLog = jest.spyOn(console, 'log');
|
||||
const mockRequestResponse = { json: () => response };
|
||||
const mockRequestResponse = { data: response };
|
||||
requests.uploadVideo.mockImplementation(async ({ onSuccess }) => {
|
||||
await onSuccess(mockRequestResponse);
|
||||
});
|
||||
@@ -49,8 +48,6 @@ describe('uploadVideo', () => {
|
||||
await hooks.uploadVideo({ dispatch, supportedFiles });
|
||||
|
||||
expect(fetch).toHaveBeenCalledTimes(2);
|
||||
expect(spyConsoleLog).toHaveBeenCalledTimes(2);
|
||||
expect(spyConsoleLog).toHaveBeenCalledWith('File uploaded:', mockResponseData);
|
||||
response.files.forEach(({ upload_url: uploadUrl }, index) => {
|
||||
expect(fetch.mock.calls[index][0]).toEqual(uploadUrl);
|
||||
});
|
||||
@@ -68,16 +65,12 @@ describe('uploadVideo', () => {
|
||||
{ file_name: 'file2.mov', upload_url: 'http://example.com/put_video2' },
|
||||
],
|
||||
};
|
||||
const spyConsoleError = jest.spyOn(console, 'error');
|
||||
const mockRequestResponse = { json: () => response };
|
||||
const mockRequestResponse = { data: response };
|
||||
requests.uploadVideo.mockImplementation(async ({ onSuccess }) => {
|
||||
await onSuccess(mockRequestResponse);
|
||||
});
|
||||
|
||||
await hooks.uploadVideo({ dispatch, supportedFiles });
|
||||
|
||||
expect(spyConsoleError).toHaveBeenCalledTimes(2);
|
||||
expect(spyConsoleError).toHaveBeenCalledWith('Error uploading file:', error);
|
||||
});
|
||||
|
||||
it('should log an error if file object is not found in supportedFiles array', () => {
|
||||
@@ -86,7 +79,7 @@ describe('uploadVideo', () => {
|
||||
{ file_name: 'file2.mov', upload_url: 'http://example.com/put_video2' },
|
||||
],
|
||||
};
|
||||
const mockRequestResponse = { json: () => response };
|
||||
const mockRequestResponse = { data: response };
|
||||
const spyConsoleError = jest.spyOn(console, 'error');
|
||||
requests.uploadVideo.mockImplementation(({ onSuccess }) => {
|
||||
onSuccess(mockRequestResponse);
|
||||
|
||||
@@ -13,6 +13,7 @@ import * as editorHooks from '../EditorContainer/hooks';
|
||||
export const VideoUploader = ({ onUpload, errorMessage }) => {
|
||||
const [, setUploadedFile] = useState();
|
||||
const [textInputValue, setTextInputValue] = useState('');
|
||||
const onUrlUpdatedHook = hooks.onUrlUploaded();
|
||||
|
||||
const { getRootProps, getInputProps, isDragActive } = useDropzone({
|
||||
accept: 'video/*',
|
||||
@@ -31,8 +32,7 @@ export const VideoUploader = ({ onUpload, errorMessage }) => {
|
||||
};
|
||||
|
||||
const handleSaveButtonClick = () => {
|
||||
// do something with the textInputValue, e.g. save to state or send to server
|
||||
console.log(`Saving input value: ${textInputValue}`);
|
||||
onUrlUpdatedHook(textInputValue);
|
||||
};
|
||||
|
||||
if (errorMessage) {
|
||||
@@ -60,18 +60,20 @@ export const VideoUploader = ({ onUpload, errorMessage }) => {
|
||||
</div>
|
||||
<input {...getInputProps()} data-testid="fileInput" />
|
||||
</div>
|
||||
<div className="d-flex video-id-prompt">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Paste your video ID or URL"
|
||||
value={textInputValue}
|
||||
onChange={handleInputChange}
|
||||
onKeyDown={(e) => e.key === 'Enter' && handleSaveButtonClick()}
|
||||
onClick={(event) => event.preventDefault()}
|
||||
/>
|
||||
<button className="border-start-0" type="button" onClick={handleSaveButtonClick}>
|
||||
<Icon src={ArrowForward} className="rounded-circle text-dark" />
|
||||
</button>
|
||||
<div className="d-flex video-id-container">
|
||||
<div className="d-flex video-id-prompt">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Paste your video ID or URL"
|
||||
value={textInputValue}
|
||||
onChange={handleInputChange}
|
||||
onKeyDown={(e) => e.key === 'Enter' && handleSaveButtonClick()}
|
||||
onClick={(event) => event.preventDefault()}
|
||||
/>
|
||||
<button className="border-start-0" type="button" onClick={handleSaveButtonClick} data-testid="inputSaveButton">
|
||||
<Icon src={ArrowForward} className="rounded-circle text-dark" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -11,6 +11,11 @@
|
||||
}
|
||||
}
|
||||
|
||||
.video-id-container {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.video-id-prompt {
|
||||
position: absolute;
|
||||
top: 68%;
|
||||
|
||||
@@ -4,6 +4,7 @@ import { render, fireEvent, act } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom';
|
||||
import VideoUploadEditor, { VideoUploader } from '.';
|
||||
import * as hooks from './hooks';
|
||||
import * as appHooks from '../../hooks';
|
||||
|
||||
const mockDispatch = jest.fn();
|
||||
const mockOnUpload = jest.fn();
|
||||
@@ -11,6 +12,10 @@ const mockOnUpload = jest.fn();
|
||||
jest.mock('react-redux', () => ({
|
||||
useDispatch: () => mockDispatch,
|
||||
}));
|
||||
jest.mock('../../hooks', () => ({
|
||||
...jest.requireActual('../../hooks'),
|
||||
navigateTo: jest.fn((args) => ({ navigateTo: args })),
|
||||
}));
|
||||
|
||||
const defaultEditorProps = {
|
||||
intl: {},
|
||||
@@ -52,6 +57,16 @@ describe('VideoUploadEditor', () => {
|
||||
expect(input.value).toBe('test value');
|
||||
});
|
||||
|
||||
it('click on the save button', () => {
|
||||
const { getByPlaceholderText, getByTestId } = renderEditorComponent();
|
||||
const testValue = 'test vale';
|
||||
const input = getByPlaceholderText('Paste your video ID or URL');
|
||||
fireEvent.change(input, { target: { value: testValue } });
|
||||
const button = getByTestId('inputSaveButton');
|
||||
fireEvent.click(button);
|
||||
expect(appHooks.navigateTo).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('shows error message with unsupported files', async () => {
|
||||
const { getByTestId, findByText } = renderEditorComponent();
|
||||
const fileInput = getByTestId('fileInput');
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
export const analyticsEvt = {
|
||||
editorSaveClick: 'edx.ui.authoring.editor.save',
|
||||
editorCancelClick: 'edx.ui.authoring.editor.cancel',
|
||||
videoGalleryCancelClick: 'edx.ui.authoring.videogallery.cancel',
|
||||
};
|
||||
|
||||
export default analyticsEvt;
|
||||
|
||||
@@ -6,11 +6,22 @@ import * as module from './video';
|
||||
import { valueFromDuration } from '../../../containers/VideoEditor/components/VideoSettingsModal/components/DurationWidget/hooks';
|
||||
import { parseYoutubeId } from '../../services/cms/api';
|
||||
|
||||
export const loadVideoData = () => (dispatch, getState) => {
|
||||
export const loadVideoData = (selectedVideoId, selectedVideoUrl) => (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const blockValueData = state.app.blockValue.data;
|
||||
const rawVideoData = blockValueData.metadata ? blockValueData.metadata : {};
|
||||
let rawVideoData = blockValueData.metadata ? blockValueData.metadata : {};
|
||||
const courseData = state.app.courseDetails.data ? state.app.courseDetails.data : {};
|
||||
if (selectedVideoId != null) {
|
||||
const rawVideos = Object.values(selectors.app.videos(state));
|
||||
const selectedVideo = rawVideos.find(video => video.edx_video_id === selectedVideoId);
|
||||
rawVideoData = {
|
||||
edx_video_id: selectedVideo.edx_video_id,
|
||||
thumbnail: selectedVideo.course_video_image_url,
|
||||
duration: selectedVideo.duration,
|
||||
transcriptsFromSelected: selectedVideo.transcripts,
|
||||
selectedVideoTranscriptUrls: selectedVideo.transcript_urls,
|
||||
};
|
||||
}
|
||||
const studioView = state.app.studioView?.data?.html;
|
||||
const {
|
||||
videoId,
|
||||
@@ -21,8 +32,13 @@ export const loadVideoData = () => (dispatch, getState) => {
|
||||
youtubeId: rawVideoData.youtube_id_1_0,
|
||||
html5Sources: rawVideoData.html5_sources,
|
||||
});
|
||||
|
||||
// Use the selected video url first
|
||||
const videoSourceUrl = selectedVideoUrl != null ? selectedVideoUrl : videoUrl;
|
||||
const [licenseType, licenseOptions] = module.parseLicense({ licenseData: studioView, level: 'block' });
|
||||
const transcripts = module.parseTranscripts({ transcriptsData: studioView });
|
||||
const transcripts = rawVideoData.transcriptsFromSelected ? rawVideoData.transcriptsFromSelected
|
||||
: module.parseTranscripts({ transcriptsData: studioView });
|
||||
|
||||
const [courseLicenseType, courseLicenseDetails] = module.parseLicense({
|
||||
licenseData: courseData.license,
|
||||
level: 'course',
|
||||
@@ -32,7 +48,7 @@ export const loadVideoData = () => (dispatch, getState) => {
|
||||
blockSetting: rawVideoData.public_access,
|
||||
});
|
||||
dispatch(actions.video.load({
|
||||
videoSource: videoUrl || '',
|
||||
videoSource: videoSourceUrl || '',
|
||||
videoId,
|
||||
fallbackVideos,
|
||||
allowVideoDownloads: rawVideoData.download_video,
|
||||
@@ -40,12 +56,13 @@ export const loadVideoData = () => (dispatch, getState) => {
|
||||
videoSharingLearnMoreLink: blockValueData?.video_sharing_doc_url,
|
||||
videoSharingEnabledForCourse: blockValueData?.video_sharing_enabled,
|
||||
transcripts,
|
||||
selectedVideoTranscriptUrls: rawVideoData.selectedVideoTranscriptUrls,
|
||||
allowTranscriptDownloads: rawVideoData.download_track,
|
||||
showTranscriptByDefault: rawVideoData.show_captions,
|
||||
duration: { // TODO duration is not always sent so they should be calculated.
|
||||
startTime: valueFromDuration(rawVideoData.start_time || '00:00:00'),
|
||||
stopTime: valueFromDuration(rawVideoData.end_time || '00:00:00'),
|
||||
total: 0, // TODO can we get total duration? if not, probably dropping from widget
|
||||
total: rawVideoData.duration || 0, // TODO can we get total duration? if not, probably dropping from widget
|
||||
},
|
||||
handout: rawVideoData.handout,
|
||||
licenseType,
|
||||
@@ -70,7 +87,7 @@ export const loadVideoData = () => (dispatch, getState) => {
|
||||
videoSharingEnabledForAll: response.data.videoSharingEnabled,
|
||||
})),
|
||||
}));
|
||||
const youTubeId = parseYoutubeId(videoUrl);
|
||||
const youTubeId = parseYoutubeId(videoSourceUrl);
|
||||
if (youTubeId) {
|
||||
dispatch(requests.checkTranscriptsForImport({
|
||||
videoId,
|
||||
|
||||
@@ -12,6 +12,7 @@ jest.mock('..', () => ({
|
||||
selectors: {
|
||||
app: {
|
||||
courseDetails: (state) => ({ courseDetails: state }),
|
||||
videos: (state) => ({ videos: state.app.videos }),
|
||||
},
|
||||
video: {
|
||||
videoId: (state) => ({ videoId: state }),
|
||||
@@ -34,6 +35,7 @@ jest.mock('./requests', () => ({
|
||||
}));
|
||||
|
||||
jest.mock('../../../utils', () => ({
|
||||
...jest.requireActual('../../../utils'),
|
||||
removeItemOnce: (args) => (args),
|
||||
}));
|
||||
|
||||
@@ -56,6 +58,8 @@ const mockVideoFeatures = {
|
||||
videoSharingEnabled: 'soMEbOolEAn',
|
||||
},
|
||||
};
|
||||
const mockSelectedVideoId = 'ThisIsAVideoId';
|
||||
const mockSelectedVideoUrl = 'ThisIsAYoutubeUrl';
|
||||
|
||||
const testMetadata = {
|
||||
download_track: 'dOWNlOAdTraCK',
|
||||
@@ -80,6 +84,13 @@ const testState = {
|
||||
originalThumbnail: null,
|
||||
videoId: 'soMEvIDEo',
|
||||
};
|
||||
const testVideosState = {
|
||||
edx_video_id: mockSelectedVideoId,
|
||||
thumbnail: 'thumbnail',
|
||||
duration: 60,
|
||||
transcripts: ['es'],
|
||||
transcript_urls: { es: 'url' },
|
||||
};
|
||||
const testUpload = { transcripts: ['la', 'en'] };
|
||||
const testReplaceUpload = {
|
||||
file: mockFile,
|
||||
@@ -130,25 +141,37 @@ describe('video thunkActions', () => {
|
||||
jest.spyOn(thunkActions, thunkActionsKeys.parseTranscripts).mockReturnValue(
|
||||
testMetadata.transcripts,
|
||||
);
|
||||
});
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
it('dispatches fetchVideoFeatures action', () => {
|
||||
thunkActions.loadVideoData()(dispatch, getState);
|
||||
[
|
||||
[dispatchedLoad],
|
||||
[dispatchedAction1],
|
||||
[dispatchedAction2],
|
||||
] = dispatch.mock.calls;
|
||||
});
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
it('dispatches fetchVideoFeatures action', () => {
|
||||
expect(dispatchedLoad).not.toEqual(undefined);
|
||||
expect(dispatchedAction1.fetchVideoFeatures).not.toEqual(undefined);
|
||||
});
|
||||
it('dispatches checkTranscriptsForImport action', () => {
|
||||
thunkActions.loadVideoData()(dispatch, getState);
|
||||
[
|
||||
[dispatchedLoad],
|
||||
[dispatchedAction1],
|
||||
[dispatchedAction2],
|
||||
] = dispatch.mock.calls;
|
||||
expect(dispatchedLoad).not.toEqual(undefined);
|
||||
expect(dispatchedAction2.checkTranscriptsForImport).not.toEqual(undefined);
|
||||
});
|
||||
it('dispatches actions.video.load', () => {
|
||||
thunkActions.loadVideoData()(dispatch, getState);
|
||||
[
|
||||
[dispatchedLoad],
|
||||
[dispatchedAction1],
|
||||
[dispatchedAction2],
|
||||
] = dispatch.mock.calls;
|
||||
expect(dispatchedLoad.load).toEqual({
|
||||
videoSource: 'videOsOurce',
|
||||
videoId: 'videOiD',
|
||||
@@ -186,7 +209,113 @@ describe('video thunkActions', () => {
|
||||
thumbnail: testMetadata.thumbnail,
|
||||
});
|
||||
});
|
||||
it('dispatches actions.video.load with selectedVideoId', () => {
|
||||
getState = jest.fn(() => ({
|
||||
app: {
|
||||
blockId: 'soMEBloCk',
|
||||
studioEndpointUrl: 'soMEeNDPoiNT',
|
||||
blockValue: { data: { metadata: {} } },
|
||||
courseDetails: { data: { license: null } },
|
||||
studioView: { data: { html: 'sOMeHTml' } },
|
||||
videos: testVideosState,
|
||||
},
|
||||
}));
|
||||
thunkActions.loadVideoData(mockSelectedVideoId, null)(dispatch, getState);
|
||||
[
|
||||
[dispatchedLoad],
|
||||
[dispatchedAction1],
|
||||
[dispatchedAction2],
|
||||
] = dispatch.mock.calls;
|
||||
expect(dispatchedLoad.load).toEqual({
|
||||
videoSource: 'videOsOurce',
|
||||
videoId: 'videOiD',
|
||||
fallbackVideos: 'fALLbACKvIDeos',
|
||||
allowVideoDownloads: undefined,
|
||||
transcripts: testVideosState.transcripts,
|
||||
selectedVideoTranscriptUrls: testVideosState.transcript_urls,
|
||||
allowTranscriptDownloads: undefined,
|
||||
allowVideoSharing: {
|
||||
level: 'course',
|
||||
value: true,
|
||||
},
|
||||
showTranscriptByDefault: undefined,
|
||||
duration: {
|
||||
startTime: testMetadata.start_time,
|
||||
stopTime: 0,
|
||||
total: testVideosState.duration,
|
||||
},
|
||||
handout: undefined,
|
||||
licenseType: 'liCENSEtyPe',
|
||||
licenseDetails: {
|
||||
attribution: true,
|
||||
noncommercial: true,
|
||||
noDerivatives: true,
|
||||
shareAlike: false,
|
||||
},
|
||||
videoSharingEnabledForCourse: undefined,
|
||||
videoSharingLearnMoreLink: undefined,
|
||||
courseLicenseType: 'liCENSEtyPe',
|
||||
courseLicenseDetails: {
|
||||
attribution: true,
|
||||
noncommercial: true,
|
||||
noDerivatives: true,
|
||||
shareAlike: false,
|
||||
},
|
||||
thumbnail: undefined,
|
||||
});
|
||||
});
|
||||
it('dispatches actions.video.load with selectedVideoUrl', () => {
|
||||
thunkActions.loadVideoData(null, mockSelectedVideoUrl)(dispatch, getState);
|
||||
[
|
||||
[dispatchedLoad],
|
||||
[dispatchedAction1],
|
||||
[dispatchedAction2],
|
||||
] = dispatch.mock.calls;
|
||||
expect(dispatchedLoad.load).toEqual({
|
||||
videoSource: mockSelectedVideoUrl,
|
||||
videoId: 'videOiD',
|
||||
fallbackVideos: 'fALLbACKvIDeos',
|
||||
allowVideoDownloads: testMetadata.download_video,
|
||||
transcripts: testMetadata.transcripts,
|
||||
allowTranscriptDownloads: testMetadata.download_track,
|
||||
showTranscriptByDefault: testMetadata.show_captions,
|
||||
duration: {
|
||||
startTime: testMetadata.start_time,
|
||||
stopTime: testMetadata.end_time,
|
||||
total: 0,
|
||||
},
|
||||
allowVideoSharing: {
|
||||
level: 'course',
|
||||
value: true,
|
||||
},
|
||||
handout: testMetadata.handout,
|
||||
licenseType: 'liCENSEtyPe',
|
||||
licenseDetails: {
|
||||
attribution: true,
|
||||
noncommercial: true,
|
||||
noDerivatives: true,
|
||||
shareAlike: false,
|
||||
},
|
||||
selectedVideoTranscriptUrls: undefined,
|
||||
videoSharingEnabledForCourse: undefined,
|
||||
videoSharingLearnMoreLink: 'SomEUrL.Com',
|
||||
courseLicenseType: 'liCENSEtyPe',
|
||||
courseLicenseDetails: {
|
||||
attribution: true,
|
||||
noncommercial: true,
|
||||
noDerivatives: true,
|
||||
shareAlike: false,
|
||||
},
|
||||
thumbnail: testMetadata.thumbnail,
|
||||
});
|
||||
});
|
||||
it('dispatches actions.video.updateField on success', () => {
|
||||
thunkActions.loadVideoData()(dispatch, getState);
|
||||
[
|
||||
[dispatchedLoad],
|
||||
[dispatchedAction1],
|
||||
[dispatchedAction2],
|
||||
] = dispatch.mock.calls;
|
||||
dispatch.mockClear();
|
||||
dispatchedAction1.fetchVideoFeatures.onSuccess(mockVideoFeatures);
|
||||
expect(dispatch).toHaveBeenCalledWith(actions.video.updateField({
|
||||
|
||||
@@ -19,6 +19,7 @@ const initialState = {
|
||||
videoSharingLearnMoreLink: '',
|
||||
thumbnail: null,
|
||||
transcripts: [],
|
||||
selectedVideoTranscriptUrls: {},
|
||||
allowTranscriptDownloads: false,
|
||||
duration: {
|
||||
startTime: '00:00:00',
|
||||
|
||||
@@ -6,7 +6,7 @@ import { videoTranscriptLanguages } from '../../constants/video';
|
||||
import { initialState } from './reducer';
|
||||
import * as module from './selectors';
|
||||
import * as AppSelectors from '../app/selectors';
|
||||
import { downloadVideoTranscriptURL, downloadVideoHandoutUrl } from '../../services/cms/urls';
|
||||
import { downloadVideoTranscriptURL, downloadVideoHandoutUrl, mediaTranscriptURL } from '../../services/cms/urls';
|
||||
|
||||
const stateKeys = keyStore(initialState);
|
||||
|
||||
@@ -23,6 +23,7 @@ export const simpleSelectors = [
|
||||
stateKeys.allowVideoSharing,
|
||||
stateKeys.thumbnail,
|
||||
stateKeys.transcripts,
|
||||
stateKeys.selectedVideoTranscriptUrls,
|
||||
stateKeys.allowTranscriptDownloads,
|
||||
stateKeys.duration,
|
||||
stateKeys.showTranscriptByDefault,
|
||||
@@ -57,6 +58,14 @@ export const getTranscriptDownloadUrl = createSelector(
|
||||
}),
|
||||
);
|
||||
|
||||
export const buildTranscriptUrl = createSelector(
|
||||
[AppSelectors.simpleSelectors.studioEndpointUrl],
|
||||
(studioEndpointUrl) => ({ transcriptUrl }) => mediaTranscriptURL({
|
||||
studioEndpointUrl,
|
||||
transcriptUrl,
|
||||
}),
|
||||
);
|
||||
|
||||
export const getHandoutDownloadUrl = createSelector(
|
||||
[AppSelectors.simpleSelectors.studioEndpointUrl],
|
||||
(studioEndpointUrl) => ({ handout }) => downloadVideoHandoutUrl({
|
||||
@@ -74,6 +83,7 @@ export const videoSettings = createSelector(
|
||||
module.simpleSelectors.allowVideoSharing,
|
||||
module.simpleSelectors.thumbnail,
|
||||
module.simpleSelectors.transcripts,
|
||||
module.simpleSelectors.selectedVideoTranscriptUrls,
|
||||
module.simpleSelectors.allowTranscriptDownloads,
|
||||
module.simpleSelectors.duration,
|
||||
module.simpleSelectors.showTranscriptByDefault,
|
||||
@@ -89,6 +99,7 @@ export const videoSettings = createSelector(
|
||||
allowVideoSharing,
|
||||
thumbnail,
|
||||
transcripts,
|
||||
selectedVideoTranscriptUrls,
|
||||
allowTranscriptDownloads,
|
||||
duration,
|
||||
showTranscriptByDefault,
|
||||
@@ -104,6 +115,7 @@ export const videoSettings = createSelector(
|
||||
allowVideoSharing,
|
||||
thumbnail,
|
||||
transcripts,
|
||||
selectedVideoTranscriptUrls,
|
||||
allowTranscriptDownloads,
|
||||
duration,
|
||||
showTranscriptByDefault,
|
||||
@@ -118,6 +130,7 @@ export default {
|
||||
...simpleSelectors,
|
||||
openLanguages,
|
||||
getTranscriptDownloadUrl,
|
||||
buildTranscriptUrl,
|
||||
getHandoutDownloadUrl,
|
||||
videoSettings,
|
||||
};
|
||||
|
||||
@@ -26,8 +26,7 @@ jest.mock('./urls', () => ({
|
||||
courseAdvanceSettings: jest.fn().mockName('urls.courseAdvanceSettings'),
|
||||
replaceTranscript: jest.fn().mockName('urls.replaceTranscript'),
|
||||
videoFeatures: jest.fn().mockName('urls.videoFeatures'),
|
||||
courseVideos: jest.fn().mockName('urls.courseVideos'),
|
||||
videoUpload: jest.fn()
|
||||
courseVideos: jest.fn()
|
||||
.mockName('urls.courseVideos')
|
||||
.mockImplementation(
|
||||
({ studioEndpointUrl, learningContextId }) => `${studioEndpointUrl}/some_video_upload_url/${learningContextId}`,
|
||||
|
||||
@@ -43,6 +43,10 @@ export const downloadVideoTranscriptURL = ({ studioEndpointUrl, blockId, languag
|
||||
`${videoTranscripts({ studioEndpointUrl, blockId })}?language_code=${language}`
|
||||
);
|
||||
|
||||
export const mediaTranscriptURL = ({ studioEndpointUrl, transcriptUrl }) => (
|
||||
`${studioEndpointUrl}${transcriptUrl}`
|
||||
);
|
||||
|
||||
export const downloadVideoHandoutUrl = ({ studioEndpointUrl, handout }) => (
|
||||
`${studioEndpointUrl}${handout}`
|
||||
);
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
checkTranscriptsForImport,
|
||||
replaceTranscript,
|
||||
courseAdvanceSettings,
|
||||
mediaTranscriptURL,
|
||||
videoFeatures,
|
||||
courseVideos,
|
||||
} from './urls';
|
||||
@@ -145,4 +146,11 @@ describe('cms url methods', () => {
|
||||
.toEqual(`${studioEndpointUrl}/videos/${learningContextId}`);
|
||||
});
|
||||
});
|
||||
describe('mediaTranscriptURL', () => {
|
||||
it('returns url with studioEndpointUrl', () => {
|
||||
const transcriptUrl = 'this-is-a-transcript';
|
||||
expect(mediaTranscriptURL({ studioEndpointUrl, transcriptUrl }))
|
||||
.toEqual(`${studioEndpointUrl}${transcriptUrl}`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -13,7 +13,8 @@ exports[`BaseModal ImageUploadModal template component snapshot 1`] = `
|
||||
<ModalDialog.Header
|
||||
style={
|
||||
Object {
|
||||
"zIndex": 10000,
|
||||
"boxShadow": "2px 2px 5px rgba(0, 0, 0, 0.3)",
|
||||
"zIndex": 1,
|
||||
}
|
||||
}
|
||||
>
|
||||
|
||||
@@ -30,7 +30,7 @@ export const BaseModal = ({
|
||||
isFullscreenOnMobile
|
||||
isFullscreenScroll={isFullscreenScroll}
|
||||
>
|
||||
<ModalDialog.Header style={{ zIndex: 10000 }}>
|
||||
<ModalDialog.Header style={{ zIndex: 1, boxShadow: '2px 2px 5px rgba(0, 0, 0, 0.3)' }}>
|
||||
<ModalDialog.Title>
|
||||
{title}
|
||||
</ModalDialog.Title>
|
||||
|
||||
@@ -50,20 +50,20 @@ export const Gallery = ({
|
||||
}
|
||||
if (galleryIsEmpty) {
|
||||
return (
|
||||
<div className="gallery p-4 bg-gray-100" style={{ height, margin: '0 -1.5rem' }}>
|
||||
<div className="gallery p-4 bg-light-400" style={{ height, margin: '0 -1.5rem' }}>
|
||||
<FormattedMessage {...emptyGalleryLabel} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (searchIsEmpty) {
|
||||
return (
|
||||
<div className="gallery p-4 bg-gray-100" style={{ height, margin: '0 -1.5rem' }}>
|
||||
<div className="gallery p-4 bg-light-400" style={{ height, margin: '0 -1.5rem' }}>
|
||||
<FormattedMessage {...messages.emptySearchLabel} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Scrollable className="gallery bg-gray-100" style={{ height, margin: '0 -1.5rem' }}>
|
||||
<Scrollable className="gallery bg-light-400" style={{ height, margin: '0 -1.5rem' }}>
|
||||
<div className="p-4">
|
||||
<SelectableBox.Set
|
||||
columns={1}
|
||||
|
||||
@@ -15,17 +15,27 @@ import LanguageNamesWidget from '../../containers/VideoEditor/components/VideoSe
|
||||
export const GalleryCard = ({
|
||||
asset,
|
||||
}) => (
|
||||
<SelectableBox className="card bg-white" key={asset.externalUrl} type="radio" value={asset.id} style={{ padding: '10px 20px' }}>
|
||||
<SelectableBox
|
||||
className="card bg-white"
|
||||
key={asset.externalUrl}
|
||||
type="radio"
|
||||
value={asset.id}
|
||||
style={{
|
||||
padding: '10px 20px',
|
||||
border: 'none',
|
||||
boxShadow: 'none',
|
||||
}}
|
||||
>
|
||||
<div className="card-div d-flex flex-row flex-nowrap">
|
||||
<div style={{
|
||||
position: 'relative',
|
||||
width: '200px',
|
||||
height: '100px',
|
||||
margin: '16px 0 0 0',
|
||||
margin: '18px 0 0 0',
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
style={{ width: '200px', height: '100px' }}
|
||||
style={{ border: 'none', width: '200px', height: '100px' }}
|
||||
src={asset.externalUrl}
|
||||
/>
|
||||
{ asset.status && asset.statusBadgeVariant && (
|
||||
@@ -34,13 +44,21 @@ export const GalleryCard = ({
|
||||
</Badge>
|
||||
)}
|
||||
{ asset.duration >= 0 && (
|
||||
<Badge variant="dark" style={{ position: 'absolute', right: '6px', bottom: '6px' }}>
|
||||
<Badge
|
||||
variant="dark"
|
||||
style={{
|
||||
position: 'absolute',
|
||||
right: '6px',
|
||||
bottom: '6px',
|
||||
backgroundColor: 'black',
|
||||
}}
|
||||
>
|
||||
{formatDuration(asset.duration)}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<div className="card-text p-3">
|
||||
<h3>{asset.displayName}</h3>
|
||||
<div className="card-text p-3" style={{ marginTop: '10px' }}>
|
||||
<h3 className="text-primary-500">{asset.displayName}</h3>
|
||||
{ asset.transcripts && (
|
||||
<div style={{ margin: '0 0 5px 0' }}>
|
||||
<LanguageNamesWidget
|
||||
@@ -48,7 +66,7 @@ export const GalleryCard = ({
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<p style={{ fontSize: '11px' }}>
|
||||
<p className="text-gray-500" style={{ fontSize: '11px' }}>
|
||||
<FormattedMessage
|
||||
{...messages.addedDate}
|
||||
values={{
|
||||
|
||||
@@ -5,19 +5,31 @@ import { Image } from '@edx/paragon';
|
||||
import { GalleryCard } from './GalleryCard';
|
||||
|
||||
describe('GalleryCard component', () => {
|
||||
const img = {
|
||||
const asset = {
|
||||
externalUrl: 'props.img.externalUrl',
|
||||
displayName: 'props.img.displayName',
|
||||
dateAdded: 12345,
|
||||
};
|
||||
let el;
|
||||
beforeEach(() => {
|
||||
el = shallow(<GalleryCard asset={img} />);
|
||||
el = shallow(<GalleryCard asset={asset} />);
|
||||
});
|
||||
test(`snapshot: dateAdded=${img.dateAdded}`, () => {
|
||||
test(`snapshot: dateAdded=${asset.dateAdded}`, () => {
|
||||
expect(el).toMatchSnapshot();
|
||||
});
|
||||
it('loads Image with src from image external url', () => {
|
||||
expect(el.find(Image).props().src).toEqual(img.externalUrl);
|
||||
expect(el.find(Image).props().src).toEqual(asset.externalUrl);
|
||||
});
|
||||
it('snapshot with status badge', () => {
|
||||
el = shallow(<GalleryCard asset={{ ...asset, status: 'failed', statusBadgeVariant: 'danger' }} />);
|
||||
expect(el).toMatchSnapshot();
|
||||
});
|
||||
it('snapshot with duration badge', () => {
|
||||
el = shallow(<GalleryCard asset={{ ...asset, duration: 60 }} />);
|
||||
expect(el).toMatchSnapshot();
|
||||
});
|
||||
it('snapshot with duration transcripts', () => {
|
||||
el = shallow(<GalleryCard asset={{ ...asset, transcripts: ['es'] }} />);
|
||||
expect(el).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -57,7 +57,7 @@ export const SearchSort = ({
|
||||
|
||||
{ !showSwitch && <ActionRow.Spacer /> }
|
||||
<Dropdown>
|
||||
<Dropdown.Toggle id="gallery-sort-button" variant="tertiary">
|
||||
<Dropdown.Toggle className="text-gray-700" id="gallery-sort-button" variant="tertiary">
|
||||
<FormattedMessage {...sortMessages[sortBy]} />
|
||||
</Dropdown.Toggle>
|
||||
<Dropdown.Menu>
|
||||
@@ -71,7 +71,7 @@ export const SearchSort = ({
|
||||
|
||||
{ filterKeys && filterMessages && (
|
||||
<Dropdown>
|
||||
<Dropdown.Toggle id="gallery-filter-button" variant="tertiary">
|
||||
<Dropdown.Toggle className="text-gray-700" id="gallery-filter-button" variant="tertiary">
|
||||
<FormattedMessage {...filterMessages[filterBy]} />
|
||||
</Dropdown.Toggle>
|
||||
<Dropdown.Menu>
|
||||
@@ -92,7 +92,7 @@ export const SearchSort = ({
|
||||
onChange={onSwitchClick}
|
||||
isInline
|
||||
>
|
||||
<Form.Switch value="switch-value" floatLabelLeft>
|
||||
<Form.Switch className="text-gray-700" value="switch-value" floatLabelLeft>
|
||||
<FormattedMessage {...switchMessage} />
|
||||
</Form.Switch>
|
||||
</Form.SwitchSet>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
exports[`TextEditor Image Gallery component component snapshot: loaded but no images, show empty gallery 1`] = `
|
||||
<div
|
||||
className="gallery p-4 bg-gray-100"
|
||||
className="gallery p-4 bg-light-400"
|
||||
style={
|
||||
Object {
|
||||
"height": "375px",
|
||||
@@ -16,7 +16,7 @@ exports[`TextEditor Image Gallery component component snapshot: loaded but no im
|
||||
|
||||
exports[`TextEditor Image Gallery component component snapshot: loaded but search returns no images, show 0 search result gallery 1`] = `
|
||||
<div
|
||||
className="gallery p-4 bg-gray-100"
|
||||
className="gallery p-4 bg-light-400"
|
||||
style={
|
||||
Object {
|
||||
"height": "375px",
|
||||
@@ -34,7 +34,7 @@ exports[`TextEditor Image Gallery component component snapshot: loaded but searc
|
||||
|
||||
exports[`TextEditor Image Gallery component component snapshot: loaded, show gallery 1`] = `
|
||||
<Scrollable
|
||||
className="gallery bg-gray-100"
|
||||
className="gallery bg-light-400"
|
||||
style={
|
||||
Object {
|
||||
"height": "375px",
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`GalleryCard component snapshot: dateAdded=12345 1`] = `
|
||||
exports[`GalleryCard component snapshot with duration badge 1`] = `
|
||||
<SelectableBox
|
||||
className="card bg-white"
|
||||
key="props.img.externalUrl"
|
||||
style={
|
||||
Object {
|
||||
"border": "none",
|
||||
"boxShadow": "none",
|
||||
"padding": "10px 20px",
|
||||
}
|
||||
}
|
||||
@@ -18,7 +20,7 @@ exports[`GalleryCard component snapshot: dateAdded=12345 1`] = `
|
||||
style={
|
||||
Object {
|
||||
"height": "100px",
|
||||
"margin": "16px 0 0 0",
|
||||
"margin": "18px 0 0 0",
|
||||
"position": "relative",
|
||||
"width": "200px",
|
||||
}
|
||||
@@ -28,19 +30,305 @@ exports[`GalleryCard component snapshot: dateAdded=12345 1`] = `
|
||||
src="props.img.externalUrl"
|
||||
style={
|
||||
Object {
|
||||
"border": "none",
|
||||
"height": "100px",
|
||||
"width": "200px",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<Component
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "black",
|
||||
"bottom": "6px",
|
||||
"position": "absolute",
|
||||
"right": "6px",
|
||||
}
|
||||
}
|
||||
variant="dark"
|
||||
>
|
||||
01:00
|
||||
</Component>
|
||||
</div>
|
||||
<div
|
||||
className="card-text p-3"
|
||||
style={
|
||||
Object {
|
||||
"marginTop": "10px",
|
||||
}
|
||||
}
|
||||
>
|
||||
<h3>
|
||||
<h3
|
||||
className="text-primary-500"
|
||||
>
|
||||
props.img.displayName
|
||||
</h3>
|
||||
<p
|
||||
className="text-gray-500"
|
||||
style={
|
||||
Object {
|
||||
"fontSize": "11px",
|
||||
}
|
||||
}
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Added {date} at {time}"
|
||||
description="File date-added string"
|
||||
id="authoring.selectionmodal.addedDate.label"
|
||||
values={
|
||||
Object {
|
||||
"date": <FormattedDate
|
||||
value={12345}
|
||||
/>,
|
||||
"time": <FormattedTime
|
||||
value={12345}
|
||||
/>,
|
||||
}
|
||||
}
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</SelectableBox>
|
||||
`;
|
||||
|
||||
exports[`GalleryCard component snapshot with duration transcripts 1`] = `
|
||||
<SelectableBox
|
||||
className="card bg-white"
|
||||
key="props.img.externalUrl"
|
||||
style={
|
||||
Object {
|
||||
"border": "none",
|
||||
"boxShadow": "none",
|
||||
"padding": "10px 20px",
|
||||
}
|
||||
}
|
||||
type="radio"
|
||||
>
|
||||
<div
|
||||
className="card-div d-flex flex-row flex-nowrap"
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"height": "100px",
|
||||
"margin": "18px 0 0 0",
|
||||
"position": "relative",
|
||||
"width": "200px",
|
||||
}
|
||||
}
|
||||
>
|
||||
<Image
|
||||
src="props.img.externalUrl"
|
||||
style={
|
||||
Object {
|
||||
"border": "none",
|
||||
"height": "100px",
|
||||
"width": "200px",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="card-text p-3"
|
||||
style={
|
||||
Object {
|
||||
"marginTop": "10px",
|
||||
}
|
||||
}
|
||||
>
|
||||
<h3
|
||||
className="text-primary-500"
|
||||
>
|
||||
props.img.displayName
|
||||
</h3>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"margin": "0 0 5px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<injectIntl(ShimmedIntlComponent)
|
||||
transcripts={
|
||||
Array [
|
||||
"es",
|
||||
]
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<p
|
||||
className="text-gray-500"
|
||||
style={
|
||||
Object {
|
||||
"fontSize": "11px",
|
||||
}
|
||||
}
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Added {date} at {time}"
|
||||
description="File date-added string"
|
||||
id="authoring.selectionmodal.addedDate.label"
|
||||
values={
|
||||
Object {
|
||||
"date": <FormattedDate
|
||||
value={12345}
|
||||
/>,
|
||||
"time": <FormattedTime
|
||||
value={12345}
|
||||
/>,
|
||||
}
|
||||
}
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</SelectableBox>
|
||||
`;
|
||||
|
||||
exports[`GalleryCard component snapshot with status badge 1`] = `
|
||||
<SelectableBox
|
||||
className="card bg-white"
|
||||
key="props.img.externalUrl"
|
||||
style={
|
||||
Object {
|
||||
"border": "none",
|
||||
"boxShadow": "none",
|
||||
"padding": "10px 20px",
|
||||
}
|
||||
}
|
||||
type="radio"
|
||||
>
|
||||
<div
|
||||
className="card-div d-flex flex-row flex-nowrap"
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"height": "100px",
|
||||
"margin": "18px 0 0 0",
|
||||
"position": "relative",
|
||||
"width": "200px",
|
||||
}
|
||||
}
|
||||
>
|
||||
<Image
|
||||
src="props.img.externalUrl"
|
||||
style={
|
||||
Object {
|
||||
"border": "none",
|
||||
"height": "100px",
|
||||
"width": "200px",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<Component
|
||||
style={
|
||||
Object {
|
||||
"left": "6px",
|
||||
"position": "absolute",
|
||||
"top": "6px",
|
||||
}
|
||||
}
|
||||
variant="danger"
|
||||
>
|
||||
failed
|
||||
</Component>
|
||||
</div>
|
||||
<div
|
||||
className="card-text p-3"
|
||||
style={
|
||||
Object {
|
||||
"marginTop": "10px",
|
||||
}
|
||||
}
|
||||
>
|
||||
<h3
|
||||
className="text-primary-500"
|
||||
>
|
||||
props.img.displayName
|
||||
</h3>
|
||||
<p
|
||||
className="text-gray-500"
|
||||
style={
|
||||
Object {
|
||||
"fontSize": "11px",
|
||||
}
|
||||
}
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Added {date} at {time}"
|
||||
description="File date-added string"
|
||||
id="authoring.selectionmodal.addedDate.label"
|
||||
values={
|
||||
Object {
|
||||
"date": <FormattedDate
|
||||
value={12345}
|
||||
/>,
|
||||
"time": <FormattedTime
|
||||
value={12345}
|
||||
/>,
|
||||
}
|
||||
}
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</SelectableBox>
|
||||
`;
|
||||
|
||||
exports[`GalleryCard component snapshot: dateAdded=12345 1`] = `
|
||||
<SelectableBox
|
||||
className="card bg-white"
|
||||
key="props.img.externalUrl"
|
||||
style={
|
||||
Object {
|
||||
"border": "none",
|
||||
"boxShadow": "none",
|
||||
"padding": "10px 20px",
|
||||
}
|
||||
}
|
||||
type="radio"
|
||||
>
|
||||
<div
|
||||
className="card-div d-flex flex-row flex-nowrap"
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"height": "100px",
|
||||
"margin": "18px 0 0 0",
|
||||
"position": "relative",
|
||||
"width": "200px",
|
||||
}
|
||||
}
|
||||
>
|
||||
<Image
|
||||
src="props.img.externalUrl"
|
||||
style={
|
||||
Object {
|
||||
"border": "none",
|
||||
"height": "100px",
|
||||
"width": "200px",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="card-text p-3"
|
||||
style={
|
||||
Object {
|
||||
"marginTop": "10px",
|
||||
}
|
||||
}
|
||||
>
|
||||
<h3
|
||||
className="text-primary-500"
|
||||
>
|
||||
props.img.displayName
|
||||
</h3>
|
||||
<p
|
||||
className="text-gray-500"
|
||||
style={
|
||||
Object {
|
||||
"fontSize": "11px",
|
||||
|
||||
@@ -28,6 +28,7 @@ exports[`SearchSort component snapshots with filterKeys with search string (clos
|
||||
</Form.Group>
|
||||
<Dropdown>
|
||||
<Dropdown.Toggle
|
||||
className="text-gray-700"
|
||||
id="gallery-sort-button"
|
||||
variant="tertiary"
|
||||
>
|
||||
@@ -78,6 +79,7 @@ exports[`SearchSort component snapshots with filterKeys with search string (clos
|
||||
</Dropdown>
|
||||
<Dropdown>
|
||||
<Dropdown.Toggle
|
||||
className="text-gray-700"
|
||||
id="gallery-filter-button"
|
||||
variant="tertiary"
|
||||
>
|
||||
@@ -138,6 +140,7 @@ exports[`SearchSort component snapshots with filterKeys with search string (clos
|
||||
onChange={null}
|
||||
>
|
||||
<Component
|
||||
className="text-gray-700"
|
||||
floatLabelLeft={true}
|
||||
value="switch-value"
|
||||
>
|
||||
@@ -166,6 +169,7 @@ exports[`SearchSort component snapshots with filterKeys without search string (s
|
||||
</Form.Group>
|
||||
<Dropdown>
|
||||
<Dropdown.Toggle
|
||||
className="text-gray-700"
|
||||
id="gallery-sort-button"
|
||||
variant="tertiary"
|
||||
>
|
||||
@@ -216,6 +220,7 @@ exports[`SearchSort component snapshots with filterKeys without search string (s
|
||||
</Dropdown>
|
||||
<Dropdown>
|
||||
<Dropdown.Toggle
|
||||
className="text-gray-700"
|
||||
id="gallery-filter-button"
|
||||
variant="tertiary"
|
||||
>
|
||||
@@ -276,6 +281,7 @@ exports[`SearchSort component snapshots with filterKeys without search string (s
|
||||
onChange={null}
|
||||
>
|
||||
<Component
|
||||
className="text-gray-700"
|
||||
floatLabelLeft={true}
|
||||
value="switch-value"
|
||||
>
|
||||
@@ -314,6 +320,7 @@ exports[`SearchSort component snapshots without filterKeys with search string (c
|
||||
<ActionRow.Spacer />
|
||||
<Dropdown>
|
||||
<Dropdown.Toggle
|
||||
className="text-gray-700"
|
||||
id="gallery-sort-button"
|
||||
variant="tertiary"
|
||||
>
|
||||
@@ -385,6 +392,7 @@ exports[`SearchSort component snapshots without filterKeys without search string
|
||||
<ActionRow.Spacer />
|
||||
<Dropdown>
|
||||
<Dropdown.Toggle
|
||||
className="text-gray-700"
|
||||
id="gallery-sort-button"
|
||||
variant="tertiary"
|
||||
>
|
||||
|
||||
@@ -47,7 +47,7 @@ export const SelectionModal = ({
|
||||
let background = '#FFFFFF';
|
||||
let showGallery = true;
|
||||
if (isLoaded && !isFetchError && !isUploadError && !inputError.show) {
|
||||
background = '#EBEBEB';
|
||||
background = '#E9E6E4';
|
||||
} else if (isLoaded) {
|
||||
showGallery = false;
|
||||
}
|
||||
@@ -69,14 +69,22 @@ export const SelectionModal = ({
|
||||
size={size}
|
||||
isFullscreenScroll={isFullscreenScroll}
|
||||
footerAction={(
|
||||
<Button iconBefore={Add} onClick={fileInput.click} variant="link">
|
||||
<Button
|
||||
className="text-primary-500"
|
||||
iconBefore={Add}
|
||||
onClick={fileInput.click}
|
||||
variant="link"
|
||||
style={{
|
||||
textDecoration: 'none',
|
||||
}}
|
||||
>
|
||||
<FormattedMessage {...uploadButtonMsg} />
|
||||
</Button>
|
||||
)}
|
||||
title={intl.formatMessage(titleMsg)}
|
||||
bodyStyle={{ background, padding: '9px 24px' }}
|
||||
bodyStyle={{ background, padding: '3px 24px' }}
|
||||
headerComponent={(
|
||||
<div style={{ zIndex: 10000, margin: '18px 0' }}>
|
||||
<div style={{ margin: '18px 0' }}>
|
||||
<SearchSort {...searchSortProps} />
|
||||
</div>
|
||||
)}
|
||||
@@ -132,7 +140,6 @@ SelectionModal.propTypes = {
|
||||
}).isRequired,
|
||||
fileInput: PropTypes.shape({
|
||||
click: PropTypes.func.isRequired,
|
||||
addFile: PropTypes.func.isRequired,
|
||||
}).isRequired,
|
||||
galleryProps: PropTypes.shape({}).isRequired,
|
||||
searchSortProps: PropTypes.shape({}).isRequired,
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import TextEditor from './containers/TextEditor';
|
||||
import VideoEditor from './containers/VideoEditor';
|
||||
import ProblemEditor from './containers/ProblemEditor';
|
||||
import VideoUploadEditor from './containers/VideoUploadEditor';
|
||||
|
||||
// ADDED_EDITOR_IMPORTS GO HERE
|
||||
import VideoUploadEditor from './containers/VideoUploadEditor';
|
||||
|
||||
import { blockTypes } from './data/constants/app';
|
||||
|
||||
@@ -11,8 +11,8 @@ const supportedEditors = {
|
||||
[blockTypes.html]: TextEditor,
|
||||
[blockTypes.video]: VideoEditor,
|
||||
[blockTypes.problem]: ProblemEditor,
|
||||
// ADDED_EDITORS GO BELOW
|
||||
[blockTypes.video_upload]: VideoUploadEditor,
|
||||
// ADDED_EDITORS GO BELOW
|
||||
};
|
||||
|
||||
export default supportedEditors;
|
||||
|
||||
Reference in New Issue
Block a user