fix: non-blocking UI bugs for video settings (#140)
This commit is contained in:
@@ -23,7 +23,7 @@ export const CollapsibleFormWidget = ({
|
||||
intl,
|
||||
}) => (
|
||||
<Collapsible.Advanced
|
||||
className="collapsible-card rounded mb-3 mr-4 px-3 py-2"
|
||||
className="collapsible-card rounded m-4 px-3 py-2"
|
||||
defaultOpen
|
||||
open={isError || undefined}
|
||||
>
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import React from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
// import PropTypes from 'prop-types';
|
||||
|
||||
import {
|
||||
Col,
|
||||
FormControl,
|
||||
FormGroup,
|
||||
Form,
|
||||
Row,
|
||||
} from '@edx/paragon';
|
||||
|
||||
@@ -31,12 +29,15 @@ export const DurationWidget = ({
|
||||
});
|
||||
const timeKeys = keyStore(duration.formValue);
|
||||
|
||||
const getTotalLabel = (startTime, stopTime) => {
|
||||
const getTotalLabel = (startTime, stopTime, subtitle) => {
|
||||
if (!stopTime) {
|
||||
if (!startTime) {
|
||||
return intl.formatMessage(messages.fullVideoLength);
|
||||
}
|
||||
return intl.formatMessage(messages.startsAt, { startTime: durationFromValue(startTime) });
|
||||
if (subtitle) {
|
||||
return intl.formatMessage(messages.startsAt, { startTime: durationFromValue(startTime) });
|
||||
}
|
||||
return null;
|
||||
}
|
||||
const total = stopTime - (startTime || 0);
|
||||
return intl.formatMessage(messages.total, { total: durationFromValue(total) });
|
||||
@@ -45,32 +46,32 @@ export const DurationWidget = ({
|
||||
return (
|
||||
<CollapsibleFormWidget
|
||||
title={intl.formatMessage(messages.durationTitle)}
|
||||
subtitle={getTotalLabel(duration.formValue.startTime, duration.formValue.stopTime)}
|
||||
subtitle={getTotalLabel(duration.formValue.startTime, duration.formValue.stopTime, true)}
|
||||
>
|
||||
<FormattedMessage {...messages.durationDescription} />
|
||||
<Row className="mt-4">
|
||||
<FormGroup as={Col}>
|
||||
<FormControl
|
||||
<Form.Group as={Col}>
|
||||
<Form.Control
|
||||
floatingLabel={intl.formatMessage(messages.startTimeLabel)}
|
||||
value={duration.local.startTime}
|
||||
onBlur={duration.onBlur(timeKeys.startTime)}
|
||||
onChange={duration.onChange(timeKeys.startTime)}
|
||||
/>
|
||||
<FormControl.Feedback>
|
||||
<Form.Control.Feedback>
|
||||
<FormattedMessage {...messages.durationHint} />
|
||||
</FormControl.Feedback>
|
||||
</FormGroup>
|
||||
<FormGroup as={Col}>
|
||||
<FormControl
|
||||
</Form.Control.Feedback>
|
||||
</Form.Group>
|
||||
<Form.Group as={Col}>
|
||||
<Form.Control
|
||||
floatingLabel={intl.formatMessage(messages.stopTimeLabel)}
|
||||
value={duration.local.stopTime}
|
||||
onBlur={duration.onBlur(timeKeys.stopTime)}
|
||||
onChange={duration.onChange(timeKeys.stopTime)}
|
||||
/>
|
||||
<FormControl.Feedback>
|
||||
<Form.Control.Feedback>
|
||||
<FormattedMessage {...messages.durationHint} />
|
||||
</FormControl.Feedback>
|
||||
</FormGroup>
|
||||
</Form.Control.Feedback>
|
||||
</Form.Group>
|
||||
</Row>
|
||||
<div className="mt-4">
|
||||
{getTotalLabel(duration.formValue.startTime, duration.formValue.stopTime)}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { formatMessage } from '../../../../../../testUtils';
|
||||
import { DurationWidget } from './DurationWidget';
|
||||
|
||||
describe('DurationWidget', () => {
|
||||
const props = {
|
||||
isError: false,
|
||||
subtitle: 'SuBTItle',
|
||||
title: 'tiTLE',
|
||||
// inject
|
||||
intl: { formatMessage },
|
||||
};
|
||||
describe('render', () => {
|
||||
test('snapshots: renders as expected with default props', () => {
|
||||
expect(
|
||||
shallow(<DurationWidget {...props} />),
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -97,54 +97,64 @@ exports[`HandoutWidget snapshots snapshots: renders as expected with handout 1`]
|
||||
}
|
||||
}
|
||||
/>
|
||||
<Card>
|
||||
<Card.Header
|
||||
actions={
|
||||
<Dropdown>
|
||||
<Dropdown.Toggle
|
||||
alt="Actions dropdown"
|
||||
as="IconButton"
|
||||
iconAs="Icon"
|
||||
id="dropdown-toggle-with-iconbutton-video-transcript-widget"
|
||||
variant="primary"
|
||||
/>
|
||||
<Dropdown.Menu
|
||||
className="video_handout Action Menu"
|
||||
<Stack
|
||||
gap={3}
|
||||
>
|
||||
<ActionRow
|
||||
className="border border-gray-300 rounded px-3 py-2"
|
||||
>
|
||||
sOMeUrl
|
||||
<ActionRow.Spacer />
|
||||
<Dropdown>
|
||||
<Dropdown.Toggle
|
||||
alt="Actions dropdown"
|
||||
as="IconButton"
|
||||
iconAs="Icon"
|
||||
id="dropdown-toggle-with-iconbutton-video-transcript-widget"
|
||||
variant="primary"
|
||||
/>
|
||||
<Dropdown.Menu
|
||||
className="video_handout Action Menu"
|
||||
>
|
||||
<Dropdown.Item
|
||||
key="handout-actions-replace"
|
||||
onClick={[Function]}
|
||||
>
|
||||
<Dropdown.Item
|
||||
onClick={[Function]}
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Replace"
|
||||
description="Message Presented To user for action to replace handout"
|
||||
id="authoring.videoeditor.handout.replaceHandout"
|
||||
/>
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item
|
||||
target="_blank"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Download"
|
||||
description="Message Presented To user for action to download handout"
|
||||
id="authoring.videoeditor.handout.downloadHandout"
|
||||
/>
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item
|
||||
onClick={[Function]}
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Delete"
|
||||
description="Message Presented To user for action to delete handout"
|
||||
id="authoring.videoeditor.handout.deleteHandout"
|
||||
/>
|
||||
</Dropdown.Item>
|
||||
</Dropdown.Menu>
|
||||
</Dropdown>
|
||||
}
|
||||
className="mt-1"
|
||||
subtitle="sOMeUrl "
|
||||
<FormattedMessage
|
||||
defaultMessage="Replace"
|
||||
description="Message Presented To user for action to replace handout"
|
||||
id="authoring.videoeditor.handout.replaceHandout"
|
||||
/>
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item
|
||||
key="handout-actions-download"
|
||||
target="_blank"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Download"
|
||||
description="Message Presented To user for action to download handout"
|
||||
id="authoring.videoeditor.handout.downloadHandout"
|
||||
/>
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item
|
||||
key="handout-actions-delete"
|
||||
onClick={[Function]}
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Delete"
|
||||
description="Message Presented To user for action to delete handout"
|
||||
id="authoring.videoeditor.handout.deleteHandout"
|
||||
/>
|
||||
</Dropdown.Item>
|
||||
</Dropdown.Menu>
|
||||
</Dropdown>
|
||||
</ActionRow>
|
||||
<FormattedMessage
|
||||
defaultMessage="Learners can download this file by clicking \\"Download Handout\\" below the video."
|
||||
description="Message presented to user when a handout is present"
|
||||
id="authoring.videoeditor.handout.handoutHelpMessage"
|
||||
/>
|
||||
</Card>
|
||||
</Stack>
|
||||
</injectIntl(ShimmedIntlComponent)>
|
||||
`;
|
||||
|
||||
|
||||
@@ -7,8 +7,8 @@ import {
|
||||
Stack,
|
||||
Icon,
|
||||
IconButton,
|
||||
Card,
|
||||
Dropdown,
|
||||
ActionRow,
|
||||
} from '@edx/paragon';
|
||||
import { FileUpload, MoreHoriz } from '@edx/paragon/icons';
|
||||
import {
|
||||
@@ -61,38 +61,37 @@ export const HandoutWidget = ({
|
||||
<UploadErrorAlert message={messages.uploadHandoutError} />
|
||||
<FileInput fileInput={fileInput} />
|
||||
{handout ? (
|
||||
<Card>
|
||||
<Card.Header
|
||||
className="mt-1"
|
||||
subtitle={handoutName}
|
||||
actions={(
|
||||
<Dropdown>
|
||||
<Dropdown.Toggle
|
||||
id="dropdown-toggle-with-iconbutton-video-transcript-widget"
|
||||
as={IconButton}
|
||||
src={MoreHoriz}
|
||||
iconAs={Icon}
|
||||
variant="primary"
|
||||
alt="Actions dropdown"
|
||||
/>
|
||||
<Dropdown.Menu className="video_handout Action Menu">
|
||||
<Dropdown.Item
|
||||
key="handout-actions-replace"
|
||||
onClick={fileInput.click}
|
||||
>
|
||||
<FormattedMessage {...messages.replaceHandout} />
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item key="handout-actions-download" target="_blank" href={downloadLink}>
|
||||
<FormattedMessage {...messages.downloadHandout} />
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item key="handout-actions-delete" onClick={() => updateField({ handout: null })}>
|
||||
<FormattedMessage {...messages.deleteHandout} />
|
||||
</Dropdown.Item>
|
||||
</Dropdown.Menu>
|
||||
</Dropdown>
|
||||
)}
|
||||
/>
|
||||
</Card>
|
||||
<Stack gap={3}>
|
||||
<ActionRow className="border border-gray-300 rounded px-3 py-2">
|
||||
{handoutName}
|
||||
<ActionRow.Spacer />
|
||||
<Dropdown>
|
||||
<Dropdown.Toggle
|
||||
id="dropdown-toggle-with-iconbutton-video-transcript-widget"
|
||||
as={IconButton}
|
||||
src={MoreHoriz}
|
||||
iconAs={Icon}
|
||||
variant="primary"
|
||||
alt="Actions dropdown"
|
||||
/>
|
||||
<Dropdown.Menu className="video_handout Action Menu">
|
||||
<Dropdown.Item
|
||||
key="handout-actions-replace"
|
||||
onClick={fileInput.click}
|
||||
>
|
||||
<FormattedMessage {...messages.replaceHandout} />
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item key="handout-actions-download" target="_blank" href={downloadLink}>
|
||||
<FormattedMessage {...messages.downloadHandout} />
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item key="handout-actions-delete" onClick={() => updateField({ handout: null })}>
|
||||
<FormattedMessage {...messages.deleteHandout} />
|
||||
</Dropdown.Item>
|
||||
</Dropdown.Menu>
|
||||
</Dropdown>
|
||||
</ActionRow>
|
||||
<FormattedMessage {...messages.handoutHelpMessage} />
|
||||
</Stack>
|
||||
) : (
|
||||
<Stack gap={3}>
|
||||
<FormattedMessage {...messages.addHandoutMessage} />
|
||||
|
||||
@@ -44,7 +44,7 @@ exports[`ThumbnailWidget snapshots snapshots: renders as expected where thumbnai
|
||||
exports[`ThumbnailWidget snapshots snapshots: renders as expected where videoType equals edxVideo 1`] = `
|
||||
<injectIntl(ShimmedIntlComponent)
|
||||
isError={true}
|
||||
subtitle={null}
|
||||
subtitle="Yes"
|
||||
title="Thumbnail"
|
||||
>
|
||||
<ErrorAlert
|
||||
@@ -80,10 +80,77 @@ exports[`ThumbnailWidget snapshots snapshots: renders as expected where videoTyp
|
||||
</injectIntl(ShimmedIntlComponent)>
|
||||
`;
|
||||
|
||||
exports[`ThumbnailWidget snapshots snapshots: renders as expected where videoType equals edxVideo and no thumbnail 1`] = `
|
||||
<injectIntl(ShimmedIntlComponent)
|
||||
isError={true}
|
||||
subtitle="None"
|
||||
title="Thumbnail"
|
||||
>
|
||||
<ErrorAlert
|
||||
dismissError={[Function]}
|
||||
hideHeading={true}
|
||||
isError={false}
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="The file size for thumbnails must be larger than 2 KB or less than 2 MB. Please resize image and try again."
|
||||
description=" Message presented to user when file size of image is less than 2 KB or larger than 2 MB"
|
||||
id="authoring.videoeditor.thumbnail.error.fileSizeError"
|
||||
/>
|
||||
</ErrorAlert>
|
||||
<Stack
|
||||
gap={3}
|
||||
>
|
||||
<div>
|
||||
<FormattedMessage
|
||||
defaultMessage="Upload an image for learners to see before playing the video."
|
||||
description="Message for adding thumbnail"
|
||||
id="authoring.videoeditor.thumbnail.upload.message"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"color": "grey",
|
||||
}
|
||||
}
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Images must have an aspect ratio of 16:9 (1280x720 px recommended)"
|
||||
description="Message for thumbnail aspectRequirements"
|
||||
id="authoring.videoeditor.thumbnail.upload.aspectRequirements"
|
||||
/>
|
||||
</div>
|
||||
<FileInput
|
||||
acceptedFiles=".gif,.jpg,.jpeg,.png,.bmp,.bmp2"
|
||||
fileInput={
|
||||
Object {
|
||||
"addFile": [Function],
|
||||
"click": [Function],
|
||||
"ref": Object {
|
||||
"current": undefined,
|
||||
},
|
||||
}
|
||||
}
|
||||
/>
|
||||
<Button
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
variant="link"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Upload Thumbnail"
|
||||
description="Label for upload button"
|
||||
id="authoring.videoeditor.thumbnail.upload.label"
|
||||
/>
|
||||
</Button>
|
||||
</Stack>
|
||||
</injectIntl(ShimmedIntlComponent)>
|
||||
`;
|
||||
|
||||
exports[`ThumbnailWidget snapshots snapshots: renders as expected with a thumbnail provided 1`] = `
|
||||
<injectIntl(ShimmedIntlComponent)
|
||||
isError={true}
|
||||
subtitle={null}
|
||||
subtitle="Yes"
|
||||
title="Thumbnail"
|
||||
>
|
||||
<ErrorAlert
|
||||
|
||||
@@ -49,12 +49,21 @@ export const ThumbnailWidget = ({
|
||||
fileSizeError,
|
||||
});
|
||||
const isEdxVideo = videoType === 'edxVideo';
|
||||
const getSubtitle = () => {
|
||||
if (isEdxVideo) {
|
||||
if (thumbnail) {
|
||||
return intl.formatMessage(messages.yesSubtitle);
|
||||
}
|
||||
return intl.formatMessage(messages.noneSubtitle);
|
||||
}
|
||||
return intl.formatMessage(messages.unavailableSubtitle);
|
||||
};
|
||||
|
||||
return (!isLibrary ? (
|
||||
<CollapsibleFormWidget
|
||||
isError={Object.keys(error).length !== 0}
|
||||
title={intl.formatMessage(messages.title)}
|
||||
subtitle={isEdxVideo ? null : intl.formatMessage(messages.unavailableSubtitle)}
|
||||
subtitle={getSubtitle()}
|
||||
>
|
||||
<ErrorAlert
|
||||
dismissError={fileSizeError.dismiss}
|
||||
|
||||
@@ -65,6 +65,11 @@ describe('ThumbnailWidget', () => {
|
||||
shallow(<ThumbnailWidget {...props} thumbnail="sOMeUrl" allowThumbnailUpload videoType="edxVideo" />),
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
test('snapshots: renders as expected where videoType equals edxVideo and no thumbnail', () => {
|
||||
expect(
|
||||
shallow(<ThumbnailWidget {...props} allowThumbnailUpload videoType="edxVideo" />),
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
describe('mapStateToProps', () => {
|
||||
const testState = { A: 'pple', B: 'anana', C: 'ucumber' };
|
||||
|
||||
@@ -9,6 +9,16 @@ export const messages = {
|
||||
defaultMessage: 'Unavailable',
|
||||
description: 'Subtitle for unavailable thumbnail widget',
|
||||
},
|
||||
noneSubtitle: {
|
||||
id: 'authoring.videoeditor.thumbnail.none.subtitle',
|
||||
defaultMessage: 'None',
|
||||
description: 'Subtitle for when no thumbnail has been uploaded to the widget',
|
||||
},
|
||||
yesSubtitle: {
|
||||
id: 'authoring.videoeditor.thumbnail.yes.subtitle',
|
||||
defaultMessage: 'Yes',
|
||||
description: 'Subtitle for when thumbnail has been uploaded to the widget',
|
||||
},
|
||||
unavailableMessage: {
|
||||
id: 'authoring.videoeditor.thumbnail.unavailable.message',
|
||||
defaultMessage: 'Select a video from your library to enable this feature',
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
exports[`CollapsibleFormWidget render snapshots: renders as expected with default props 1`] = `
|
||||
<Advanced
|
||||
className="collapsible-card rounded mb-3 mr-4 px-3 py-2"
|
||||
className="collapsible-card rounded m-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 mr-4 px-3 py-2"
|
||||
className="collapsible-card rounded m-4 px-3 py-2"
|
||||
defaultOpen={true}
|
||||
open={true}
|
||||
>
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`DurationWidget render snapshots: renders as expected with default props 1`] = `
|
||||
<injectIntl(ShimmedIntlComponent)
|
||||
subtitle="Full video length"
|
||||
title="Duration"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Set a specific section of the video to play."
|
||||
description="Description of Duration widget"
|
||||
id="authoring.videoeditor.duration.description"
|
||||
/>
|
||||
<Component
|
||||
className="mt-4"
|
||||
>
|
||||
<Form.Group>
|
||||
<Form.Control
|
||||
floatingLabel="Start time"
|
||||
onBlur={[Function]}
|
||||
onChange={[Function]}
|
||||
value="00:00:00"
|
||||
/>
|
||||
<Form.Control.Feedback>
|
||||
<FormattedMessage
|
||||
defaultMessage="Enter time as HH:MM:SS"
|
||||
description="Hint text for start and stop time input fields"
|
||||
id="authoring.videoeditor.duration.hint"
|
||||
/>
|
||||
</Form.Control.Feedback>
|
||||
</Form.Group>
|
||||
<Form.Group>
|
||||
<Form.Control
|
||||
floatingLabel="Stop time"
|
||||
onBlur={[Function]}
|
||||
onChange={[Function]}
|
||||
value="00:00:00"
|
||||
/>
|
||||
<Form.Control.Feedback>
|
||||
<FormattedMessage
|
||||
defaultMessage="Enter time as HH:MM:SS"
|
||||
description="Hint text for start and stop time input fields"
|
||||
id="authoring.videoeditor.duration.hint"
|
||||
/>
|
||||
</Form.Control.Feedback>
|
||||
</Form.Group>
|
||||
</Component>
|
||||
<div
|
||||
className="mt-4"
|
||||
>
|
||||
Full video length
|
||||
</div>
|
||||
</injectIntl(ShimmedIntlComponent)>
|
||||
`;
|
||||
@@ -11,18 +11,15 @@ import VideoSourceWidget from './components/VideoSourceWidget';
|
||||
import './index.scss';
|
||||
|
||||
export const VideoSettingsModal = () => (
|
||||
<div className="video-settings-modal row">
|
||||
<div className="video-controls col mx-2">
|
||||
<ErrorSummary />
|
||||
<h3>Settings</h3>
|
||||
<VideoSourceWidget />
|
||||
<ThumbnailWidget />
|
||||
<TranscriptWidget />
|
||||
<DurationWidget />
|
||||
<HandoutWidget />
|
||||
<LicenseWidget />
|
||||
</div>
|
||||
</div>
|
||||
<>
|
||||
<ErrorSummary />
|
||||
<VideoSourceWidget />
|
||||
<ThumbnailWidget />
|
||||
<TranscriptWidget />
|
||||
<DurationWidget />
|
||||
<HandoutWidget />
|
||||
<LicenseWidget />
|
||||
</>
|
||||
);
|
||||
|
||||
export default VideoSettingsModal;
|
||||
|
||||
@@ -175,7 +175,14 @@ export const uploadThumbnail = ({ thumbnail }) => (dispatch, getState) => {
|
||||
thumbnail,
|
||||
videoId,
|
||||
onSuccess: (response) => {
|
||||
const thumbnailUrl = studioEndpointUrl + response.data.image_url;
|
||||
let thumbnailUrl;
|
||||
if (response.data.image_url.startsWith('/')) {
|
||||
// in local environments, image_url is a relative path
|
||||
thumbnailUrl = studioEndpointUrl + response.data.image_url;
|
||||
} else {
|
||||
// in stage and production, image_url is an absolute path to the image
|
||||
thumbnailUrl = response.data.image_url;
|
||||
}
|
||||
dispatch(actions.video.updateField({
|
||||
thumbnail: thumbnailUrl,
|
||||
}));
|
||||
|
||||
@@ -33,7 +33,7 @@ const mockFile = 'soMEtRANscRipT';
|
||||
const mockFilename = 'soMEtRANscRipT.srt';
|
||||
const mockThumbnail = 'sOMefILE';
|
||||
const mockThumbnailResponse = { data: { image_url: 'soMEimAGEUrL' } };
|
||||
const thumbnailUrl = 'soMEeNDPoiNTsoMEimAGEUrL';
|
||||
const thumbnailUrl = 'soMEimAGEUrL';
|
||||
const mockAllowThumbnailUpload = { data: { allowThumbnailUpload: 'soMEbOolEAn' } };
|
||||
|
||||
const testMetadata = {
|
||||
|
||||
Reference in New Issue
Block a user