fix: non-blocking UI bugs for video settings (#140)

This commit is contained in:
Kristin Aoki
2022-11-09 12:32:36 -05:00
committed by GitHub
parent b4b31794af
commit af6e21e1fe
14 changed files with 295 additions and 115 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 = {