feat: update UI for video source widget (#120)
This commit is contained in:
@@ -23,7 +23,7 @@ export const CollapsibleFormWidget = ({
|
||||
intl,
|
||||
}) => (
|
||||
<Collapsible.Advanced
|
||||
className="collapsible-card rounded mb-3 px-3 py-2"
|
||||
className="collapsible-card rounded mb-3 mr-4 px-3 py-2"
|
||||
defaultOpen
|
||||
open={isError || undefined}
|
||||
>
|
||||
|
||||
@@ -1,86 +0,0 @@
|
||||
import React from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
// import PropTypes from 'prop-types';
|
||||
|
||||
import {
|
||||
FormCheck,
|
||||
FormControl,
|
||||
FormGroup,
|
||||
FormLabel,
|
||||
IconButton,
|
||||
Icon,
|
||||
} from '@edx/paragon';
|
||||
import { Delete } from '@edx/paragon/icons';
|
||||
|
||||
import hooks from './hooks';
|
||||
import CollapsibleFormWidget from './CollapsibleFormWidget';
|
||||
|
||||
/**
|
||||
* Collapsible Form widget controlling video source as well as fallback sources
|
||||
*/
|
||||
export const VideoSourceWidget = () => {
|
||||
const dispatch = useDispatch();
|
||||
const {
|
||||
videoSource: source,
|
||||
fallbackVideos,
|
||||
allowVideoDownloads: allowDownload,
|
||||
} = hooks.widgetValues({
|
||||
dispatch,
|
||||
fields: {
|
||||
[hooks.selectorKeys.videoSource]: hooks.genericWidget,
|
||||
[hooks.selectorKeys.fallbackVideos]: hooks.arrayWidget,
|
||||
[hooks.selectorKeys.allowVideoDownloads]: hooks.genericWidget,
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<CollapsibleFormWidget
|
||||
title="Video source"
|
||||
>
|
||||
<FormGroup size="sm">
|
||||
<div className="border-primary-100 border-bottom pb-4">
|
||||
<FormLabel size="sm">Video ID or URL</FormLabel>
|
||||
<FormControl
|
||||
onChange={source.onChange}
|
||||
onBlur={source.onBlur}
|
||||
value={source.local}
|
||||
/>
|
||||
</div>
|
||||
<FormLabel>Fallback videos</FormLabel>
|
||||
<FormLabel>
|
||||
{`
|
||||
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.
|
||||
`}
|
||||
</FormLabel>
|
||||
{[0, 1].map((index) => (
|
||||
<div className="mb-1">
|
||||
<FormControl
|
||||
className="d-inline-block"
|
||||
style={{ width: '260px' }}
|
||||
onChange={fallbackVideos.onChange(index)}
|
||||
value={fallbackVideos.local[index]}
|
||||
onBlur={fallbackVideos.onBlur(index)}
|
||||
/>
|
||||
<IconButton
|
||||
className="d-inline-block"
|
||||
iconAs={Icon}
|
||||
src={Delete}
|
||||
alt="Clear fallback video"
|
||||
onClick={fallbackVideos.onClear(index)}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
<FormCheck
|
||||
checked={allowDownload.local}
|
||||
onChange={allowDownload.onCheckedChange}
|
||||
label="Alow video downloads"
|
||||
/>
|
||||
</FormGroup>
|
||||
</CollapsibleFormWidget>
|
||||
);
|
||||
};
|
||||
|
||||
export default VideoSourceWidget;
|
||||
@@ -0,0 +1,111 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`VideoSourceWidget snapshots snapshots: renders as expected with default props 1`] = `
|
||||
<injectIntl(ShimmedIntlComponent)
|
||||
title="Video Source"
|
||||
>
|
||||
<Form.Group>
|
||||
<div
|
||||
className="border-primary-100 border-bottom pb-4"
|
||||
>
|
||||
<Form.Control
|
||||
floatingLabel="Video ID or URL"
|
||||
onBlur={[MockFunction]}
|
||||
onChange={[MockFunction]}
|
||||
value=""
|
||||
/>
|
||||
</div>
|
||||
<Form.Label
|
||||
className="mt-3"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Fallback Videos"
|
||||
description="Title for the fallback videos section"
|
||||
id="authoring.videoeditor.videoSource.fallbackVideo.title"
|
||||
/>
|
||||
</Form.Label>
|
||||
<Form.Text>
|
||||
<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"
|
||||
/>
|
||||
</Form.Text>
|
||||
<Component
|
||||
className="mt-4.5"
|
||||
>
|
||||
<Form.Control
|
||||
floatingLabel="Video URL"
|
||||
/>
|
||||
<OverLayTrigger
|
||||
key="top"
|
||||
overlay={
|
||||
<ToolTip>
|
||||
<FormattedMessage
|
||||
defaultMessage="Delete"
|
||||
description="Message Presented To user for action to delete fallback video"
|
||||
id="authoring.videoeditor.videoSource.deleteFallbackVideo"
|
||||
/>
|
||||
</ToolTip>
|
||||
}
|
||||
placement="top"
|
||||
>
|
||||
<IconButton
|
||||
className="d-inline-block"
|
||||
iconAs="Icon"
|
||||
onClick={[Function]}
|
||||
/>
|
||||
</OverLayTrigger>
|
||||
</Component>
|
||||
<Component
|
||||
className="mt-4"
|
||||
>
|
||||
<Form.Checkbox
|
||||
checked={false}
|
||||
className="decorative-control-label"
|
||||
onChange={[MockFunction]}
|
||||
>
|
||||
<Form.Label>
|
||||
<FormattedMessage
|
||||
defaultMessage="Allow video downloads"
|
||||
description="Label for allow video downloads checkbox"
|
||||
id="authoring.videoeditor.videoSource.allowDownloadCheckboxLabel"
|
||||
/>
|
||||
</Form.Label>
|
||||
</Form.Checkbox>
|
||||
<OverLayTrigger
|
||||
key="top"
|
||||
overlay={
|
||||
<ToolTip>
|
||||
<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.fallbackVideo.allowDownloadTooltipMessage"
|
||||
/>
|
||||
</ToolTip>
|
||||
}
|
||||
placement="top"
|
||||
>
|
||||
<Icon
|
||||
className="d-inline-block mx-3"
|
||||
/>
|
||||
</OverLayTrigger>
|
||||
</Component>
|
||||
</Form.Group>
|
||||
<Button
|
||||
onClick={[Function]}
|
||||
variant="link"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Add a video URL"
|
||||
description="Label for add a video URL button"
|
||||
id="authoring.videoeditor.videoSource.fallbackVideo.allowDownloadTooltipMessage"
|
||||
/>
|
||||
</Button>
|
||||
</injectIntl(ShimmedIntlComponent)>
|
||||
`;
|
||||
@@ -0,0 +1,25 @@
|
||||
import { actions } from '../../../../../../data/redux';
|
||||
|
||||
/**
|
||||
* deleteFallbackVideo({ fallbackVideos, dispatch })(videoUrl)
|
||||
* deleteFallbackVideo takes the current array of fallback videos, string of
|
||||
* deleted video URL and dispatch method, and updates the redux value for
|
||||
* fallbackVideos.
|
||||
* @param {array} fallbackVideos - array of current fallback videos
|
||||
* @param {func} dispatch - redux dispatch method
|
||||
* @param {string} videoUrl - string of the video URL for the fallabck video that needs to be deleted
|
||||
*/
|
||||
export const deleteFallbackVideo = ({ fallbackVideos, dispatch }) => (videoUrl) => {
|
||||
const updatedFallbackVideos = [];
|
||||
let firstOccurence = true;
|
||||
fallbackVideos.forEach(item => {
|
||||
if (item === videoUrl && firstOccurence) {
|
||||
firstOccurence = false;
|
||||
} else {
|
||||
updatedFallbackVideos.push(item);
|
||||
}
|
||||
});
|
||||
dispatch(actions.video.updateField({ fallbackVideos: updatedFallbackVideos }));
|
||||
};
|
||||
|
||||
export default { deleteFallbackVideo };
|
||||
@@ -0,0 +1,145 @@
|
||||
import React from 'react';
|
||||
import { connect, useDispatch } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import {
|
||||
Form,
|
||||
IconButton,
|
||||
Icon,
|
||||
OverlayTrigger,
|
||||
Tooltip,
|
||||
Button,
|
||||
} from '@edx/paragon';
|
||||
import { Delete, Info, Add } from '@edx/paragon/icons';
|
||||
import {
|
||||
FormattedMessage,
|
||||
injectIntl,
|
||||
intlShape,
|
||||
} from '@edx/frontend-platform/i18n';
|
||||
|
||||
import * as widgetHooks from '../hooks';
|
||||
import * as module from './hooks';
|
||||
import messages from './messages';
|
||||
import { actions } from '../../../../../../data/redux';
|
||||
|
||||
import CollapsibleFormWidget from '../CollapsibleFormWidget';
|
||||
|
||||
/**
|
||||
* Collapsible Form widget controlling video source as well as fallback sources
|
||||
*/
|
||||
export const VideoSourceWidget = ({
|
||||
// error,
|
||||
// injected
|
||||
intl,
|
||||
// redux
|
||||
updateField,
|
||||
}) => {
|
||||
const dispatch = useDispatch();
|
||||
const {
|
||||
videoSource: source,
|
||||
fallbackVideos,
|
||||
allowVideoDownloads: allowDownload,
|
||||
} = widgetHooks.widgetValues({
|
||||
dispatch,
|
||||
fields: {
|
||||
[widgetHooks.selectorKeys.videoSource]: widgetHooks.genericWidget,
|
||||
[widgetHooks.selectorKeys.fallbackVideos]: widgetHooks.arrayWidget,
|
||||
[widgetHooks.selectorKeys.allowVideoDownloads]: widgetHooks.genericWidget,
|
||||
},
|
||||
});
|
||||
const deleteFallbackVideo = module.deleteFallbackVideo({ fallbackVideos: fallbackVideos.formValue, dispatch });
|
||||
|
||||
return (
|
||||
<CollapsibleFormWidget
|
||||
title={intl.formatMessage(messages.titleLabel)}
|
||||
>
|
||||
<Form.Group>
|
||||
<div className="border-primary-100 border-bottom pb-4">
|
||||
<Form.Control
|
||||
floatingLabel={intl.formatMessage(messages.videoIdOrUrlLabel)}
|
||||
onChange={source.onChange}
|
||||
onBlur={source.onBlur}
|
||||
value={source.local}
|
||||
/>
|
||||
</div>
|
||||
<Form.Label className="mt-3">
|
||||
<FormattedMessage {...messages.fallbackVideoTitle} />
|
||||
</Form.Label>
|
||||
<Form.Text>
|
||||
<FormattedMessage {...messages.fallbackVideoMessage} />
|
||||
</Form.Text>
|
||||
{fallbackVideos.formValue.map((videoUrl, index) => (
|
||||
<Form.Row className="mt-4.5">
|
||||
<Form.Control
|
||||
floatingLabel={intl.formatMessage(messages.fallbackVideoLabel)}
|
||||
onChange={fallbackVideos.onChange(index)}
|
||||
value={fallbackVideos.local[index]}
|
||||
onBlur={fallbackVideos.onBlur(index)}
|
||||
/>
|
||||
<OverlayTrigger
|
||||
key="top"
|
||||
placement="top"
|
||||
overlay={(
|
||||
<Tooltip>
|
||||
<FormattedMessage {...messages.deleteFallbackVideo} />
|
||||
</Tooltip>
|
||||
)}
|
||||
>
|
||||
<IconButton
|
||||
className="d-inline-block"
|
||||
iconAs={Icon}
|
||||
src={Delete}
|
||||
onClick={() => deleteFallbackVideo(videoUrl)}
|
||||
/>
|
||||
</OverlayTrigger>
|
||||
</Form.Row>
|
||||
))}
|
||||
<Form.Row className="mt-4">
|
||||
<Form.Checkbox
|
||||
checked={allowDownload.local}
|
||||
className="decorative-control-label"
|
||||
onChange={allowDownload.onCheckedChange}
|
||||
>
|
||||
<Form.Label>
|
||||
<FormattedMessage {...messages.allowDownloadCheckboxLabel} />
|
||||
</Form.Label>
|
||||
</Form.Checkbox>
|
||||
<OverlayTrigger
|
||||
key="top"
|
||||
placement="top"
|
||||
overlay={(
|
||||
<Tooltip>
|
||||
<FormattedMessage {...messages.tooltipMessage} />
|
||||
</Tooltip>
|
||||
)}
|
||||
>
|
||||
<Icon className="d-inline-block mx-3" src={Info} />
|
||||
</OverlayTrigger>
|
||||
</Form.Row>
|
||||
</Form.Group>
|
||||
<Button
|
||||
iconBefore={Add}
|
||||
variant="link"
|
||||
onClick={() => updateField({ fallbackVideos: [...fallbackVideos.formValue, ''] })}
|
||||
>
|
||||
<FormattedMessage {...messages.addButtonLabel} />
|
||||
</Button>
|
||||
</CollapsibleFormWidget>
|
||||
);
|
||||
};
|
||||
VideoSourceWidget.defaultProps = {
|
||||
// error: {},
|
||||
};
|
||||
VideoSourceWidget.propTypes = {
|
||||
// error: PropTypes.node,
|
||||
// injected
|
||||
intl: intlShape.isRequired,
|
||||
// redux
|
||||
updateField: PropTypes.func.isRequired,
|
||||
};
|
||||
export const mapStateToProps = () => ({});
|
||||
|
||||
export const mapDispatchToProps = (dispatch) => ({
|
||||
updateField: (stateUpdate) => dispatch(actions.video.updateField(stateUpdate)),
|
||||
});
|
||||
export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(VideoSourceWidget));
|
||||
@@ -0,0 +1,60 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { formatMessage } from '../../../../../../../testUtils';
|
||||
import { actions } from '../../../../../../data/redux';
|
||||
import { VideoSourceWidget, mapDispatchToProps } from '.';
|
||||
|
||||
jest.mock('../../../../../../data/redux', () => ({
|
||||
actions: {
|
||||
video: {
|
||||
updateField: jest.fn().mockName('actions.video.updateField'),
|
||||
},
|
||||
},
|
||||
selectors: {
|
||||
video: {
|
||||
videoSource: jest.fn(state => ({ videoSource: state })),
|
||||
fallbackVideos: jest.fn(state => ({ fallbackVideos: state })),
|
||||
allowVideoDownloads: jest.fn(state => ({ allowVideoDownloads: state })),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('../hooks', () => ({
|
||||
selectorKeys: ['soMEkEy'],
|
||||
widgetValues: jest.fn().mockReturnValue({
|
||||
videoSource: { onChange: jest.fn(), onBlur: jest.fn(), local: '' },
|
||||
fallbackVideos: {
|
||||
formValue: ['somEUrL'],
|
||||
onChange: jest.fn(),
|
||||
onBlur: jest.fn(),
|
||||
local: '',
|
||||
},
|
||||
allowVideoDownloads: { local: false, onCheckedChange: jest.fn() },
|
||||
}),
|
||||
}));
|
||||
|
||||
describe('VideoSourceWidget', () => {
|
||||
const props = {
|
||||
error: {},
|
||||
title: 'tiTLE',
|
||||
// inject
|
||||
intl: { formatMessage },
|
||||
// redux
|
||||
updateField: jest.fn().mockName('args.updateField'),
|
||||
};
|
||||
|
||||
describe('snapshots', () => {
|
||||
test('snapshots: renders as expected with default props', () => {
|
||||
expect(
|
||||
shallow(<VideoSourceWidget {...props} />),
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
describe('mapDispatchToProps', () => {
|
||||
const dispatch = jest.fn();
|
||||
test('updateField from actions.video.updateField', () => {
|
||||
expect(mapDispatchToProps.updateField).toEqual(dispatch(actions.video.updateField));
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,60 @@
|
||||
export const messages = {
|
||||
titleLabel: {
|
||||
id: 'authoring.videoeditor.videoSource.title.label',
|
||||
defaultMessage: 'Video Source',
|
||||
description: 'Title for the video source widget',
|
||||
},
|
||||
videoIdOrUrlLabel: {
|
||||
id: 'authoring.videoeditor.videoSource.videoIdOrUrl.label',
|
||||
defaultMessage: 'Video ID or URL',
|
||||
description: 'Label for video ID or URL field',
|
||||
},
|
||||
videoIdOrUrlFeedback: {
|
||||
id: 'authoring.videoeditor.videoSource.videoIdOrUrl.feedback',
|
||||
defaultMessage: `Your video ID, YouTube URL, or a link to an .mp4, .ogg, or
|
||||
.webm video file hosted elsewhere on the Internet`,
|
||||
description: 'Feedback for video ID or URL field',
|
||||
},
|
||||
fallbackVideoTitle: {
|
||||
id: 'authoring.videoeditor.videoSource.fallbackVideo.title',
|
||||
defaultMessage: 'Fallback Videos',
|
||||
description: 'Title for the fallback videos section',
|
||||
},
|
||||
fallbackVideoMessage: {
|
||||
id: 'authoring.videoeditor.videoSource.fallbackVideo.message',
|
||||
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',
|
||||
},
|
||||
fallbackVideoLabel: {
|
||||
id: 'authoring.videoeditor.videoSource.fallbackVideo.label',
|
||||
defaultMessage: 'Video URL',
|
||||
description: 'Label for fallback video url field',
|
||||
},
|
||||
deleteFallbackVideo: {
|
||||
id: 'authoring.videoeditor.videoSource.deleteFallbackVideo',
|
||||
defaultMessage: 'Delete',
|
||||
description: 'Message Presented To user for action to delete fallback video',
|
||||
},
|
||||
allowDownloadCheckboxLabel: {
|
||||
id: 'authoring.videoeditor.videoSource.allowDownloadCheckboxLabel',
|
||||
defaultMessage: 'Allow video downloads',
|
||||
description: 'Label for allow video downloads checkbox',
|
||||
},
|
||||
tooltipMessage: {
|
||||
id: 'authoring.videoeditor.videoSource.fallbackVideo.allowDownloadTooltipMessage',
|
||||
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',
|
||||
},
|
||||
addButtonLabel: {
|
||||
id: 'authoring.videoeditor.videoSource.fallbackVideo.allowDownloadTooltipMessage',
|
||||
defaultMessage: 'Add a video URL',
|
||||
description: 'Label for add a video URL button',
|
||||
},
|
||||
};
|
||||
|
||||
export default messages;
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
exports[`CollapsibleFormWidget render snapshots: renders as expected with default props 1`] = `
|
||||
<Advanced
|
||||
className="collapsible-card rounded mb-3 px-3 py-2"
|
||||
className="collapsible-card rounded mb-3 mr-4 px-3 py-2"
|
||||
defaultOpen={true}
|
||||
>
|
||||
<Trigger
|
||||
@@ -67,7 +67,7 @@ exports[`CollapsibleFormWidget render snapshots: renders as expected with defaul
|
||||
|
||||
exports[`CollapsibleFormWidget render snapshots: renders with open={true} when there is error 1`] = `
|
||||
<Advanced
|
||||
className="collapsible-card rounded mb-3 px-3 py-2"
|
||||
className="collapsible-card rounded mb-3 mr-4 px-3 py-2"
|
||||
defaultOpen={true}
|
||||
open={true}
|
||||
>
|
||||
|
||||
@@ -101,6 +101,7 @@ jest.mock('@edx/paragon', () => jest.requireActual('testUtils').mockNestedCompon
|
||||
},
|
||||
Group: 'Form.Group',
|
||||
Label: 'Form.Label',
|
||||
Text: 'Form.Text',
|
||||
},
|
||||
FullscreenModal: 'FullscreenModal',
|
||||
Scrollable: 'Scrollable',
|
||||
|
||||
Reference in New Issue
Block a user