feat: update UI for video source widget (#120)

This commit is contained in:
Kristin Aoki
2022-09-28 11:34:56 -04:00
committed by GitHub
parent 45215ba504
commit 79ceaca8cc
9 changed files with 405 additions and 89 deletions

View File

@@ -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}
>

View File

@@ -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;

View File

@@ -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)>
`;

View File

@@ -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 };

View File

@@ -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));

View File

@@ -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));
});
});
});

View File

@@ -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;

View File

@@ -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}
>

View File

@@ -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',