Feat shared widget componentries and layout (#107)
This commit is contained in:
@@ -41,6 +41,7 @@ exports[`EditorContainer component render snapshot: initialized. enable save and
|
||||
"handleSaveClicked": Object {
|
||||
"dispatch": [MockFunction react-redux.dispatch],
|
||||
"getContent": [MockFunction props.getContent],
|
||||
"validateEntry": [MockFunction props.validateEntry],
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -86,6 +87,7 @@ exports[`EditorContainer component render snapshot: not initialized. disable sav
|
||||
"handleSaveClicked": Object {
|
||||
"dispatch": [MockFunction react-redux.dispatch],
|
||||
"getContent": [MockFunction props.getContent],
|
||||
"validateEntry": [MockFunction props.validateEntry],
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ export const setAssetToStaticUrl = (images, getContent) => {
|
||||
imgsArray.forEach(image => {
|
||||
imageUrls.push({ portableUrl: image.portableUrl, displayName: image.displayName });
|
||||
});
|
||||
const imageSrcs = content.split('src="');
|
||||
const imageSrcs = typeof content === 'string' ? content.split('src="') : [];
|
||||
imageSrcs.forEach(src => {
|
||||
if (src.startsWith('/asset') && imageUrls.length > 0) {
|
||||
const nameFromEditorSrc = src.substring(src.lastIndexOf('@') + 1, src.indexOf('"'));
|
||||
@@ -44,15 +44,16 @@ export const setAssetToStaticUrl = (images, getContent) => {
|
||||
return content;
|
||||
};
|
||||
|
||||
export const handleSaveClicked = ({ getContent, dispatch }) => {
|
||||
export const handleSaveClicked = ({ dispatch, getContent, validateEntry }) => {
|
||||
const destination = useSelector(selectors.app.returnUrl);
|
||||
const analytics = useSelector(selectors.app.analytics);
|
||||
const images = useSelector(selectors.app.images);
|
||||
return () => saveBlock({
|
||||
analytics,
|
||||
content: setAssetToStaticUrl(images, getContent),
|
||||
destination,
|
||||
analytics,
|
||||
dispatch,
|
||||
validateEntry,
|
||||
});
|
||||
};
|
||||
export const handleCancelClicked = ({ onClose }) => {
|
||||
|
||||
@@ -59,12 +59,17 @@ describe('EditorContainer hooks', () => {
|
||||
it('returns callback to saveBlock with dispatch and content from setAssetToStaticUrl', () => {
|
||||
const getContent = () => 'myTestContentValue';
|
||||
const setAssetToStaticUrl = () => 'myTestContentValue';
|
||||
const validateEntry = () => 'vaLIdAteENTry';
|
||||
const output = hooks.handleSaveClicked({
|
||||
getContent,
|
||||
images: { portableUrl: '/static/sOmEuiMAge.jpeg', displayName: 'sOmEuiMAge' },
|
||||
images: {
|
||||
portableUrl: '/static/sOmEuiMAge.jpeg',
|
||||
displayName: 'sOmEuiMAge',
|
||||
},
|
||||
destination: 'testDEsTURL',
|
||||
analytics: 'soMEanALytics',
|
||||
dispatch,
|
||||
validateEntry,
|
||||
});
|
||||
output();
|
||||
expect(appHooks.saveBlock).toHaveBeenCalledWith({
|
||||
@@ -72,6 +77,7 @@ describe('EditorContainer hooks', () => {
|
||||
destination: reactRedux.useSelector(selectors.app.returnUrl),
|
||||
analytics: reactRedux.useSelector(selectors.app.analytics),
|
||||
dispatch,
|
||||
validateEntry,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -13,6 +13,7 @@ export const EditorContainer = ({
|
||||
children,
|
||||
getContent,
|
||||
onClose,
|
||||
validateEntry,
|
||||
}) => {
|
||||
const dispatch = useDispatch();
|
||||
const isInitialized = hooks.isInitialized();
|
||||
@@ -34,7 +35,7 @@ export const EditorContainer = ({
|
||||
{isInitialized && children}
|
||||
<EditorFooter
|
||||
onCancel={handleCancelClicked}
|
||||
onSave={hooks.handleSaveClicked({ getContent, dispatch })}
|
||||
onSave={hooks.handleSaveClicked({ dispatch, getContent, validateEntry })}
|
||||
disableSave={!isInitialized}
|
||||
saveFailed={hooks.saveFailed()}
|
||||
/>
|
||||
@@ -43,11 +44,13 @@ export const EditorContainer = ({
|
||||
};
|
||||
EditorContainer.defaultProps = {
|
||||
onClose: null,
|
||||
validateEntry: null,
|
||||
};
|
||||
EditorContainer.propTypes = {
|
||||
getContent: PropTypes.func.isRequired,
|
||||
children: PropTypes.node.isRequired,
|
||||
getContent: PropTypes.func.isRequired,
|
||||
onClose: PropTypes.func,
|
||||
validateEntry: PropTypes.func,
|
||||
};
|
||||
|
||||
export default EditorContainer;
|
||||
|
||||
@@ -8,6 +8,7 @@ import * as hooks from './hooks';
|
||||
const props = {
|
||||
getContent: jest.fn().mockName('props.getContent'),
|
||||
onClose: jest.fn().mockName('props.onClose'),
|
||||
validateEntry: jest.fn().mockName('props.validateEntry'),
|
||||
};
|
||||
|
||||
jest.mock('./hooks', () => ({
|
||||
@@ -50,8 +51,9 @@ describe('EditorContainer component', () => {
|
||||
});
|
||||
test('save behavior is linked to footer onSave', () => {
|
||||
const expected = hooks.handleSaveClicked({
|
||||
getContent: props.getContent,
|
||||
dispatch: useDispatch(),
|
||||
getContent: props.getContent,
|
||||
validateEntry: props.validateEntry,
|
||||
});
|
||||
expect(el.children().at(2)
|
||||
.props().onSave).toEqual(expected);
|
||||
|
||||
@@ -16,21 +16,31 @@ export const hooks = {
|
||||
};
|
||||
|
||||
const VideoEditorModal = ({
|
||||
isOpen,
|
||||
close,
|
||||
error,
|
||||
isOpen,
|
||||
}) => {
|
||||
const dispatch = useDispatch();
|
||||
module.hooks.initialize(dispatch);
|
||||
return (
|
||||
<VideoSettingsModal {...{ isOpen, close }} />
|
||||
<VideoSettingsModal {...{ close, error, isOpen }} />
|
||||
);
|
||||
// TODO: add logic to show SelectVideoModal if no selection
|
||||
};
|
||||
|
||||
VideoEditorModal.defaultProps = {
|
||||
error: {
|
||||
duration: {},
|
||||
handout: {},
|
||||
license: {},
|
||||
thumbnail: {},
|
||||
transcripts: {},
|
||||
videoSource: {},
|
||||
},
|
||||
};
|
||||
VideoEditorModal.propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
close: PropTypes.func.isRequired,
|
||||
error: PropTypes.node,
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
};
|
||||
export default VideoEditorModal;
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { Collapsible } from '@edx/paragon';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { Collapsible, Icon, IconButton } from '@edx/paragon';
|
||||
import { ExpandLess, ExpandMore, Info } from '@edx/paragon/icons';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
/**
|
||||
* Simple Wrapper for a Form Widget component in the Video Settings modal
|
||||
@@ -11,16 +15,56 @@ import { Collapsible } from '@edx/paragon';
|
||||
* </CollapsibleFormWidget>
|
||||
*/
|
||||
export const CollapsibleFormWidget = ({
|
||||
title,
|
||||
children,
|
||||
isError,
|
||||
subtitle,
|
||||
title,
|
||||
// injected
|
||||
intl,
|
||||
}) => (
|
||||
<Collapsible defaultOpen title={title}>
|
||||
{children}
|
||||
</Collapsible>
|
||||
<Collapsible.Advanced
|
||||
className="collapsible-card rounded mb-3 px-3 py-2"
|
||||
defaultOpen
|
||||
open={isError || undefined}
|
||||
>
|
||||
<Collapsible.Trigger
|
||||
className="collapsible-trigger d-flex border-0 align-items-center"
|
||||
style={{ justifyContent: 'unset' }}
|
||||
>
|
||||
<Collapsible.Visible whenClosed>
|
||||
<div className="d-flex flex-column flex-grow-1">
|
||||
<div className="d-flex flex-grow-1 w-75">{title}</div>
|
||||
{subtitle}
|
||||
</div>
|
||||
<div className="d-flex flex-row align-self-start">
|
||||
{isError && <Icon className="alert-icon" src={Info} />}
|
||||
<IconButton alt={intl.formatMessage(messages.expandAltText)} src={ExpandMore} iconAs={Icon} variant="dark" />
|
||||
</div>
|
||||
</Collapsible.Visible>
|
||||
<Collapsible.Visible whenOpen>
|
||||
<div className="d-flex flex-grow-1 w-75">{title}</div>
|
||||
<div className="align-self-start">
|
||||
<IconButton alt={intl.formatMessage(messages.collapseAltText)} src={ExpandLess} iconAs={Icon} variant="dark" />
|
||||
</div>
|
||||
</Collapsible.Visible>
|
||||
</Collapsible.Trigger>
|
||||
<Collapsible.Body className="collapsible-body rounded px-0">
|
||||
{children}
|
||||
</Collapsible.Body>
|
||||
</Collapsible.Advanced>
|
||||
);
|
||||
CollapsibleFormWidget.propTypes = {
|
||||
title: PropTypes.node.isRequired,
|
||||
children: PropTypes.node.isRequired,
|
||||
|
||||
CollapsibleFormWidget.defaultProps = {
|
||||
subtitle: null,
|
||||
};
|
||||
|
||||
export default CollapsibleFormWidget;
|
||||
CollapsibleFormWidget.propTypes = {
|
||||
children: PropTypes.node.isRequired,
|
||||
isError: PropTypes.bool.isRequired,
|
||||
subtitle: PropTypes.node,
|
||||
title: PropTypes.node.isRequired,
|
||||
// injected
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(CollapsibleFormWidget);
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { formatMessage } from '../../../../../../testUtils';
|
||||
import { CollapsibleFormWidget } from './CollapsibleFormWidget';
|
||||
|
||||
describe('CollapsibleFormWidget', () => {
|
||||
const props = {
|
||||
isError: false,
|
||||
subtitle: 'SuBTItle',
|
||||
title: 'tiTLE',
|
||||
// inject
|
||||
intl: { formatMessage },
|
||||
};
|
||||
describe('render', () => {
|
||||
const testContent = (<p>Some test string</p>);
|
||||
test('snapshots: renders as expected with default props', () => {
|
||||
expect(
|
||||
shallow(<CollapsibleFormWidget {...props}>{testContent}</CollapsibleFormWidget>),
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
test('snapshots: renders with open={true} when there is error', () => {
|
||||
expect(
|
||||
shallow(<CollapsibleFormWidget {...props} isError>{testContent}</CollapsibleFormWidget>),
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,40 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { Alert } from '@edx/paragon';
|
||||
import { Info } from '@edx/paragon/icons';
|
||||
import messages from './messages';
|
||||
|
||||
export const ErrorSummary = ({
|
||||
error,
|
||||
}) => (
|
||||
<Alert
|
||||
icon={Info}
|
||||
show={!Object.values(error).every(val => Object.keys(val).length === 0)}
|
||||
variant="danger"
|
||||
>
|
||||
<Alert.Heading>
|
||||
<FormattedMessage {...messages.validateErrorTitle} />
|
||||
</Alert.Heading>
|
||||
<p>
|
||||
<FormattedMessage {...messages.validateErrorBody} />
|
||||
</p>
|
||||
</Alert>
|
||||
);
|
||||
|
||||
ErrorSummary.defaultProps = {
|
||||
error: {
|
||||
duration: {},
|
||||
handout: {},
|
||||
license: {},
|
||||
thumbnail: {},
|
||||
transcripts: {},
|
||||
videoSource: {},
|
||||
},
|
||||
};
|
||||
ErrorSummary.propTypes = {
|
||||
error: PropTypes.node,
|
||||
};
|
||||
|
||||
export default ErrorSummary;
|
||||
@@ -0,0 +1,17 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { ErrorSummary } from './ErrorSummary';
|
||||
|
||||
describe('ErrorSummary', () => {
|
||||
const props = {
|
||||
error: 'eRrOr',
|
||||
};
|
||||
describe('render', () => {
|
||||
test('snapshots: renders as expected', () => {
|
||||
expect(
|
||||
shallow(<ErrorSummary {...props} />),
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
// import PropTypes from 'prop-types';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import hooks from './hooks';
|
||||
import CollapsibleFormWidget from './CollapsibleFormWidget';
|
||||
@@ -8,7 +8,9 @@ import CollapsibleFormWidget from './CollapsibleFormWidget';
|
||||
/**
|
||||
* Collapsible Form widget controlling video transcripts
|
||||
*/
|
||||
export const TranscriptWidget = () => {
|
||||
export const TranscriptWidget = ({
|
||||
error,
|
||||
}) => {
|
||||
const dispatch = useDispatch();
|
||||
const values = hooks.widgetValues({
|
||||
dispatch,
|
||||
@@ -23,8 +25,16 @@ export const TranscriptWidget = () => {
|
||||
allowTranscriptDownloads: allowDownload,
|
||||
showTranscriptByDefault: showByDefault,
|
||||
} = values;
|
||||
|
||||
// TODO: replace the following sample subtitle input with one managed by hook logic
|
||||
const sampleSubtitle = <div>{transcripts.formValue.english}</div>;
|
||||
|
||||
return (
|
||||
<CollapsibleFormWidget title="Transcript">
|
||||
<CollapsibleFormWidget
|
||||
isError={Object.keys(error).length !== 0}
|
||||
subtitle={sampleSubtitle}
|
||||
title="Transcript"
|
||||
>
|
||||
<b>Transcripts</b>
|
||||
<p>English: {transcripts.formValue.english}</p>
|
||||
<p><b>Allow downloads:</b> {allowDownload.formValue ? 'True' : 'False' }</p>
|
||||
@@ -33,4 +43,11 @@ export const TranscriptWidget = () => {
|
||||
);
|
||||
};
|
||||
|
||||
TranscriptWidget.defaultProps = {
|
||||
error: {},
|
||||
};
|
||||
TranscriptWidget.propTypes = {
|
||||
error: PropTypes.node,
|
||||
};
|
||||
|
||||
export default TranscriptWidget;
|
||||
|
||||
@@ -34,7 +34,9 @@ export const VideoSourceWidget = () => {
|
||||
});
|
||||
|
||||
return (
|
||||
<CollapsibleFormWidget title="Video source">
|
||||
<CollapsibleFormWidget
|
||||
title="Video source"
|
||||
>
|
||||
<FormGroup size="sm">
|
||||
<div className="border-primary-100 border-bottom pb-4">
|
||||
<FormLabel size="sm">Video ID or URL</FormLabel>
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`CollapsibleFormWidget render snapshots: renders as expected with default props 1`] = `
|
||||
<Advanced
|
||||
className="collapsible-card rounded mb-3 px-3 py-2"
|
||||
defaultOpen={true}
|
||||
>
|
||||
<Trigger
|
||||
className="collapsible-trigger d-flex border-0 align-items-center"
|
||||
style={
|
||||
Object {
|
||||
"justifyContent": "unset",
|
||||
}
|
||||
}
|
||||
>
|
||||
<Visible
|
||||
whenClosed={true}
|
||||
>
|
||||
<div
|
||||
className="d-flex flex-column flex-grow-1"
|
||||
>
|
||||
<div
|
||||
className="d-flex flex-grow-1 w-75"
|
||||
>
|
||||
tiTLE
|
||||
</div>
|
||||
SuBTItle
|
||||
</div>
|
||||
<div
|
||||
className="d-flex flex-row align-self-start"
|
||||
>
|
||||
<IconButton
|
||||
alt="Expand"
|
||||
iconAs="Icon"
|
||||
variant="dark"
|
||||
/>
|
||||
</div>
|
||||
</Visible>
|
||||
<Visible
|
||||
whenOpen={true}
|
||||
>
|
||||
<div
|
||||
className="d-flex flex-grow-1 w-75"
|
||||
>
|
||||
tiTLE
|
||||
</div>
|
||||
<div
|
||||
className="align-self-start"
|
||||
>
|
||||
<IconButton
|
||||
alt="Collapse"
|
||||
iconAs="Icon"
|
||||
variant="dark"
|
||||
/>
|
||||
</div>
|
||||
</Visible>
|
||||
</Trigger>
|
||||
<Body
|
||||
className="collapsible-body rounded px-0"
|
||||
>
|
||||
<p>
|
||||
Some test string
|
||||
</p>
|
||||
</Body>
|
||||
</Advanced>
|
||||
`;
|
||||
|
||||
exports[`CollapsibleFormWidget render snapshots: renders with open={true} when there is error 1`] = `
|
||||
<Advanced
|
||||
className="collapsible-card rounded mb-3 px-3 py-2"
|
||||
defaultOpen={true}
|
||||
open={true}
|
||||
>
|
||||
<Trigger
|
||||
className="collapsible-trigger d-flex border-0 align-items-center"
|
||||
style={
|
||||
Object {
|
||||
"justifyContent": "unset",
|
||||
}
|
||||
}
|
||||
>
|
||||
<Visible
|
||||
whenClosed={true}
|
||||
>
|
||||
<div
|
||||
className="d-flex flex-column flex-grow-1"
|
||||
>
|
||||
<div
|
||||
className="d-flex flex-grow-1 w-75"
|
||||
>
|
||||
tiTLE
|
||||
</div>
|
||||
SuBTItle
|
||||
</div>
|
||||
<div
|
||||
className="d-flex flex-row align-self-start"
|
||||
>
|
||||
<Icon
|
||||
className="alert-icon"
|
||||
/>
|
||||
<IconButton
|
||||
alt="Expand"
|
||||
iconAs="Icon"
|
||||
variant="dark"
|
||||
/>
|
||||
</div>
|
||||
</Visible>
|
||||
<Visible
|
||||
whenOpen={true}
|
||||
>
|
||||
<div
|
||||
className="d-flex flex-grow-1 w-75"
|
||||
>
|
||||
tiTLE
|
||||
</div>
|
||||
<div
|
||||
className="align-self-start"
|
||||
>
|
||||
<IconButton
|
||||
alt="Collapse"
|
||||
iconAs="Icon"
|
||||
variant="dark"
|
||||
/>
|
||||
</div>
|
||||
</Visible>
|
||||
</Trigger>
|
||||
<Body
|
||||
className="collapsible-body rounded px-0"
|
||||
>
|
||||
<p>
|
||||
Some test string
|
||||
</p>
|
||||
</Body>
|
||||
</Advanced>
|
||||
`;
|
||||
@@ -0,0 +1,23 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`ErrorSummary render snapshots: renders as expected 1`] = `
|
||||
<Alert
|
||||
show={true}
|
||||
variant="danger"
|
||||
>
|
||||
<Alert.Heading>
|
||||
<FormattedMessage
|
||||
defaultMessage="We couldn't add your video."
|
||||
description="Title of validation error."
|
||||
id="authoring.videoeditor.validate.error.title"
|
||||
/>
|
||||
</Alert.Heading>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
defaultMessage="Please check your entries and try again."
|
||||
description="Body of validation error."
|
||||
id="authoring.videoeditor.validate.error.body"
|
||||
/>
|
||||
</p>
|
||||
</Alert>
|
||||
`;
|
||||
@@ -205,18 +205,18 @@ export const arrayWidget = ({ dispatch, key }) => {
|
||||
const widget = module.valueHooks({ dispatch, key });
|
||||
return {
|
||||
...widget,
|
||||
onChange: handleIndexTransformEvent({
|
||||
handler: onValue,
|
||||
setter: widget.setLocal,
|
||||
transform: module.updatedArray,
|
||||
local: widget.local,
|
||||
}),
|
||||
onBlur: handleIndexTransformEvent({
|
||||
handler: onValue,
|
||||
setter: widget.setAll,
|
||||
transform: module.updatedArray,
|
||||
local: widget.local,
|
||||
}),
|
||||
onChange: handleIndexTransformEvent({
|
||||
handler: onValue,
|
||||
setter: widget.setLocal,
|
||||
transform: module.updatedArray,
|
||||
local: widget.local,
|
||||
}),
|
||||
onClear: (index) => () => widget.setAll(module.updatedArray(widget.local, index, '')),
|
||||
};
|
||||
};
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
export const messages = {
|
||||
expandAltText: {
|
||||
id: 'authoring.videoeditor.expand',
|
||||
defaultMessage: 'Expand',
|
||||
},
|
||||
collapseAltText: {
|
||||
id: 'authoring.videoeditor.collapse',
|
||||
defaultMessage: 'Collapse',
|
||||
},
|
||||
validateErrorTitle: {
|
||||
id: 'authoring.videoeditor.validate.error.title',
|
||||
defaultMessage: 'We couldn\'t add your video.',
|
||||
description: 'Title of validation error.',
|
||||
},
|
||||
validateErrorBody: {
|
||||
id: 'authoring.videoeditor.validate.error.body',
|
||||
defaultMessage: 'Please check your entries and try again.',
|
||||
description: 'Body of validation error.',
|
||||
},
|
||||
};
|
||||
|
||||
export default messages;
|
||||
@@ -1,7 +1,9 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { thunkActions } from '../../../../data/redux';
|
||||
// import VideoPreview from './components/VideoPreview';
|
||||
import ErrorSummary from './components/ErrorSummary';
|
||||
import DurationWidget from './components/DurationWidget';
|
||||
import HandoutWidget from './components/HandoutWidget';
|
||||
import LicenseWidget from './components/LicenseWidget';
|
||||
@@ -18,24 +20,39 @@ export const hooks = {
|
||||
},
|
||||
};
|
||||
|
||||
export const VideoSettingsModal = () => (
|
||||
export const VideoSettingsModal = ({
|
||||
error,
|
||||
}) => (
|
||||
<div className="video-settings-modal row">
|
||||
<div className="video-preview col col-4">
|
||||
Video Preview goes here
|
||||
{/* <VideoPreview /> */}
|
||||
</div>
|
||||
<div className="video-controls col col-8">
|
||||
<VideoSourceWidget />
|
||||
<ThumbnailWidget />
|
||||
<TranscriptsWidget />
|
||||
<DurationWidget />
|
||||
<HandoutWidget />
|
||||
<LicenseWidget />
|
||||
<ErrorSummary {...{ error }} />
|
||||
<h3>Settings</h3>
|
||||
<VideoSourceWidget error={error.videoSource} />
|
||||
<ThumbnailWidget error={error.thumbnail} />
|
||||
<TranscriptsWidget error={error.transcripts} />
|
||||
<DurationWidget error={error.duration} />
|
||||
<HandoutWidget error={error.handout} />
|
||||
<LicenseWidget error={error.license} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
VideoSettingsModal.defaultProps = {
|
||||
error: {
|
||||
duration: {},
|
||||
handout: {},
|
||||
license: {},
|
||||
thumbnail: {},
|
||||
transcripts: {},
|
||||
videoSource: {},
|
||||
},
|
||||
};
|
||||
VideoSettingsModal.propTypes = {
|
||||
error: PropTypes.node,
|
||||
};
|
||||
|
||||
export default VideoSettingsModal;
|
||||
|
||||
80
src/editors/containers/VideoEditor/hooks.js
Normal file
80
src/editors/containers/VideoEditor/hooks.js
Normal file
@@ -0,0 +1,80 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
import { StrictDict } from '../../utils';
|
||||
import * as module from './hooks';
|
||||
|
||||
export const state = StrictDict({
|
||||
durationErrors: (val) => useState(val),
|
||||
handoutErrors: (val) => useState(val),
|
||||
licenseErrors: (val) => useState(val),
|
||||
thumbnailErrors: (val) => useState(val),
|
||||
transcriptsErrors: (val) => useState(val),
|
||||
videoSourceErrors: (val) => useState(val),
|
||||
});
|
||||
|
||||
export const errorsHook = () => {
|
||||
const [durationErrors, setDurationErrors] = module.state.durationErrors({});
|
||||
const [handoutErrors, setHandoutErrors] = module.state.handoutErrors({});
|
||||
const [licenseErrors, setLicenseErrors] = module.state.licenseErrors({});
|
||||
const [thumbnailErrors, setThumbnailErrors] = module.state.thumbnailErrors({});
|
||||
const [transcriptsErrors, setTranscriptsErrors] = module.state.transcriptsErrors({});
|
||||
const [videoSourceErrors, setVideoSourceErrors] = module.state.videoSourceErrors({});
|
||||
|
||||
return {
|
||||
error: {
|
||||
duration: durationErrors,
|
||||
handout: handoutErrors,
|
||||
license: licenseErrors,
|
||||
thumbnail: thumbnailErrors,
|
||||
transcripts: transcriptsErrors,
|
||||
videoSource: videoSourceErrors,
|
||||
},
|
||||
validateEntry: () => {
|
||||
let validated = true;
|
||||
if (!module.validateDuration({ setDurationErrors })) { validated = false; }
|
||||
if (!module.validateHandout({ setHandoutErrors })) { validated = false; }
|
||||
if (!module.validateLicense({ setLicenseErrors })) { validated = false; }
|
||||
if (!module.validateThumbnail({ setThumbnailErrors })) { validated = false; }
|
||||
if (!module.validateTranscripts({ setTranscriptsErrors })) { validated = false; }
|
||||
if (!module.validateVideoSource({ setVideoSourceErrors })) { validated = false; }
|
||||
return validated;
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const validateDuration = ({ setDurationErrors }) => {
|
||||
setDurationErrors({
|
||||
fieldName: 'sample error message',
|
||||
});
|
||||
return false;
|
||||
};
|
||||
export const validateHandout = ({ setHandoutErrors }) => {
|
||||
setHandoutErrors({
|
||||
fieldName: 'sample error message',
|
||||
});
|
||||
return false;
|
||||
};
|
||||
export const validateLicense = ({ setLicenseErrors }) => {
|
||||
setLicenseErrors({
|
||||
fieldName: 'sample error message',
|
||||
});
|
||||
return false;
|
||||
};
|
||||
export const validateThumbnail = ({ setThumbnailErrors }) => {
|
||||
setThumbnailErrors({
|
||||
fieldName: 'sample error message',
|
||||
});
|
||||
return false;
|
||||
};
|
||||
export const validateTranscripts = ({ setTranscriptsErrors }) => {
|
||||
setTranscriptsErrors({
|
||||
fieldName: 'sample error message',
|
||||
});
|
||||
return false;
|
||||
};
|
||||
export const validateVideoSource = ({ setVideoSourceErrors }) => {
|
||||
setVideoSourceErrors({
|
||||
fieldName: 'sample error message',
|
||||
});
|
||||
return false;
|
||||
};
|
||||
72
src/editors/containers/VideoEditor/hooks.test.js
Normal file
72
src/editors/containers/VideoEditor/hooks.test.js
Normal file
@@ -0,0 +1,72 @@
|
||||
import { MockUseState } from '../../../testUtils';
|
||||
|
||||
import { keyStore } from '../../utils';
|
||||
import * as module from './hooks';
|
||||
|
||||
jest.mock('react', () => ({
|
||||
...jest.requireActual('react'),
|
||||
}));
|
||||
|
||||
const state = new MockUseState(module);
|
||||
const moduleKeys = keyStore(module);
|
||||
|
||||
let hook;
|
||||
|
||||
describe('VideoEditorHooks', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
describe('state hooks', () => {
|
||||
state.testGetter(state.keys.durationErrors);
|
||||
state.testGetter(state.keys.handoutErrors);
|
||||
state.testGetter(state.keys.licenseErrors);
|
||||
state.testGetter(state.keys.thumbnailErrors);
|
||||
state.testGetter(state.keys.transcriptsErrors);
|
||||
state.testGetter(state.keys.videoSourceErrors);
|
||||
});
|
||||
|
||||
describe('errors hook', () => {
|
||||
beforeEach(() => {
|
||||
state.mock();
|
||||
});
|
||||
afterEach(() => {
|
||||
state.restore();
|
||||
});
|
||||
|
||||
const mockTrue = () => true;
|
||||
const mockFalse = () => false;
|
||||
test('error: state values', () => {
|
||||
expect(module.errorsHook().error).toEqual({
|
||||
duration: state.stateVals[state.keys.durationErrors],
|
||||
handout: state.stateVals[state.keys.handoutErrors],
|
||||
license: state.stateVals[state.keys.licenseErrors],
|
||||
thumbnail: state.stateVals[state.keys.thumbnailErrors],
|
||||
transcripts: state.stateVals[state.keys.transcriptsErrors],
|
||||
videoSource: state.stateVals[state.keys.videoSourceErrors],
|
||||
});
|
||||
});
|
||||
describe('validateEntry', () => {
|
||||
beforeEach(() => {
|
||||
hook = module.errorsHook();
|
||||
});
|
||||
test('validateEntry: returns true if all validation calls are true', () => {
|
||||
jest.spyOn(module, moduleKeys.validateDuration).mockImplementationOnce(mockTrue);
|
||||
jest.spyOn(module, moduleKeys.validateHandout).mockImplementationOnce(mockTrue);
|
||||
jest.spyOn(module, moduleKeys.validateLicense).mockImplementationOnce(mockTrue);
|
||||
jest.spyOn(module, moduleKeys.validateThumbnail).mockImplementationOnce(mockTrue);
|
||||
jest.spyOn(module, moduleKeys.validateTranscripts).mockImplementationOnce(mockTrue);
|
||||
jest.spyOn(module, moduleKeys.validateVideoSource).mockImplementationOnce(mockTrue);
|
||||
expect(hook.validateEntry()).toEqual(true);
|
||||
});
|
||||
test('validateEntry: returns false if any validation calls are false', () => {
|
||||
jest.spyOn(module, moduleKeys.validateDuration).mockImplementationOnce(mockFalse);
|
||||
jest.spyOn(module, moduleKeys.validateHandout).mockImplementationOnce(mockTrue);
|
||||
jest.spyOn(module, moduleKeys.validateLicense).mockImplementationOnce(mockTrue);
|
||||
jest.spyOn(module, moduleKeys.validateThumbnail).mockImplementationOnce(mockTrue);
|
||||
jest.spyOn(module, moduleKeys.validateTranscripts).mockImplementationOnce(mockTrue);
|
||||
jest.spyOn(module, moduleKeys.validateVideoSource).mockImplementationOnce(mockTrue);
|
||||
expect(hook.validateEntry()).toEqual(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -3,21 +3,29 @@ import PropTypes from 'prop-types';
|
||||
|
||||
import EditorContainer from '../EditorContainer';
|
||||
import VideoEditorModal from './components/VideoEditorModal';
|
||||
import * as hooks from './hooks';
|
||||
|
||||
export default function VideoEditor({
|
||||
onClose,
|
||||
}) {
|
||||
const {
|
||||
error,
|
||||
validateEntry,
|
||||
} = hooks.errorsHook();
|
||||
|
||||
return (
|
||||
<EditorContainer
|
||||
onClose={onClose}
|
||||
getContent={() => ({})}
|
||||
onClose={onClose}
|
||||
validateEntry={validateEntry}
|
||||
>
|
||||
<div className="video-editor">
|
||||
<VideoEditorModal />
|
||||
<VideoEditorModal {...{ error }} />
|
||||
</div>
|
||||
</EditorContainer>
|
||||
);
|
||||
}
|
||||
|
||||
VideoEditor.defaultProps = {
|
||||
onClose: null,
|
||||
};
|
||||
|
||||
@@ -29,17 +29,28 @@ export const navigateCallback = ({
|
||||
export const nullMethod = () => ({});
|
||||
|
||||
export const saveBlock = ({
|
||||
analytics,
|
||||
content,
|
||||
destination,
|
||||
analytics,
|
||||
dispatch,
|
||||
validateEntry,
|
||||
}) => {
|
||||
dispatch(thunkActions.app.saveBlock({
|
||||
returnToUnit: module.navigateCallback({
|
||||
destination,
|
||||
analyticsEvent: analyticsEvt.editorSaveClick,
|
||||
analytics,
|
||||
}),
|
||||
content,
|
||||
}));
|
||||
let attemptSave = false;
|
||||
if (validateEntry) {
|
||||
if (validateEntry()) {
|
||||
attemptSave = true;
|
||||
}
|
||||
} else {
|
||||
attemptSave = true;
|
||||
}
|
||||
if (attemptSave) {
|
||||
dispatch(thunkActions.app.saveBlock({
|
||||
returnToUnit: module.navigateCallback({
|
||||
destination,
|
||||
analyticsEvent: analyticsEvt.editorSaveClick,
|
||||
analytics,
|
||||
}),
|
||||
content,
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -67,6 +67,12 @@ jest.mock('@edx/paragon', () => jest.requireActual('testUtils').mockNestedCompon
|
||||
Spacer: 'ActionRow.Spacer',
|
||||
},
|
||||
Button: 'Button',
|
||||
Collapsible: {
|
||||
Advanced: 'Advanced',
|
||||
Body: 'Body',
|
||||
Trigger: 'Trigger',
|
||||
Visible: 'Visible',
|
||||
},
|
||||
Dropdown: {
|
||||
Item: 'Dropdown.Item',
|
||||
Menu: 'Dropdown.Menu',
|
||||
|
||||
Reference in New Issue
Block a user