feat: transcript parent widget component
This commit is contained in:
@@ -11,7 +11,7 @@ import messages from './messages';
|
||||
import BaseModal from '../BaseModal';
|
||||
import AltTextControls from './AltTextControls';
|
||||
import DimensionControls from './DimensionControls';
|
||||
import ErrorAlert from '../ErrorAlerts/ErrorAlert';
|
||||
import ErrorAlert from '../../../../sharedComponents/ErrorAlerts/ErrorAlert';
|
||||
|
||||
/**
|
||||
* Modal display wrapping the dimension and alt-text controls for image tags
|
||||
|
||||
@@ -30,8 +30,24 @@ exports[`SelectImageModal component snapshot 1`] = `
|
||||
isOpen={true}
|
||||
title="Add an image"
|
||||
>
|
||||
<FetchErrorAlert />
|
||||
<UploadErrorAlert />
|
||||
<FetchErrorAlert
|
||||
message={
|
||||
Object {
|
||||
"defaultMessage": "Failed to obtain course images. Please try again.",
|
||||
"description": "Message presented to user when images are not found",
|
||||
"id": "authoring.texteditor.selectimagemodal.error.fetchImagesError",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<UploadErrorAlert
|
||||
message={
|
||||
Object {
|
||||
"defaultMessage": "Failed to upload image. Please try again.",
|
||||
"description": "Message presented to user when image fails to upload",
|
||||
"id": "authoring.texteditor.selectimagemodal.error.uploadImageError",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<ErrorAlert
|
||||
dismissError={[MockFunction]}
|
||||
hideHeading={true}
|
||||
@@ -64,6 +80,7 @@ exports[`SelectImageModal component snapshot 1`] = `
|
||||
gallery="props"
|
||||
/>
|
||||
<FileInput
|
||||
acceptedFiles=".gif,.jpg,.jpeg,.png,.tif,.tiff,.ico"
|
||||
fileInput={
|
||||
Object {
|
||||
"addFile": "imgHooks.fileInput.addFile",
|
||||
@@ -106,8 +123,24 @@ exports[`SelectImageModal component snapshot: uploaded image not loaded, show sp
|
||||
isOpen={true}
|
||||
title="Add an image"
|
||||
>
|
||||
<FetchErrorAlert />
|
||||
<UploadErrorAlert />
|
||||
<FetchErrorAlert
|
||||
message={
|
||||
Object {
|
||||
"defaultMessage": "Failed to obtain course images. Please try again.",
|
||||
"description": "Message presented to user when images are not found",
|
||||
"id": "authoring.texteditor.selectimagemodal.error.fetchImagesError",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<UploadErrorAlert
|
||||
message={
|
||||
Object {
|
||||
"defaultMessage": "Failed to upload image. Please try again.",
|
||||
"description": "Message presented to user when image fails to upload",
|
||||
"id": "authoring.texteditor.selectimagemodal.error.uploadImageError",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<ErrorAlert
|
||||
dismissError={[MockFunction]}
|
||||
hideHeading={true}
|
||||
@@ -142,6 +175,7 @@ exports[`SelectImageModal component snapshot: uploaded image not loaded, show sp
|
||||
screenReaderText="loading..."
|
||||
/>
|
||||
<FileInput
|
||||
acceptedFiles=".gif,.jpg,.jpeg,.png,.tif,.tiff,.ico"
|
||||
fileInput={
|
||||
Object {
|
||||
"addFile": "imgHooks.fileInput.addFile",
|
||||
|
||||
@@ -11,16 +11,17 @@ import {
|
||||
} from '@edx/frontend-platform/i18n';
|
||||
import { selectors } from '../../../../data/redux';
|
||||
import { RequestKeys } from '../../../../data/constants/requests';
|
||||
import { acceptedImgKeys } from './utils';
|
||||
|
||||
import hooks from './hooks';
|
||||
import messages from './messages';
|
||||
import BaseModal from '../BaseModal';
|
||||
import SearchSort from './SearchSort';
|
||||
import Gallery from './Gallery';
|
||||
import FileInput from './FileInput';
|
||||
import FetchErrorAlert from '../ErrorAlerts/FetchErrorAlert';
|
||||
import UploadErrorAlert from '../ErrorAlerts/UploadErrorAlert';
|
||||
import ErrorAlert from '../ErrorAlerts/ErrorAlert';
|
||||
import FileInput from '../../../../sharedComponents/FileInput';
|
||||
import FetchErrorAlert from '../../../../sharedComponents/ErrorAlerts/FetchErrorAlert';
|
||||
import UploadErrorAlert from '../../../../sharedComponents/ErrorAlerts/UploadErrorAlert';
|
||||
import ErrorAlert from '../../../../sharedComponents/ErrorAlerts/ErrorAlert';
|
||||
|
||||
export const SelectImageModal = ({
|
||||
isOpen,
|
||||
@@ -59,8 +60,8 @@ export const SelectImageModal = ({
|
||||
title={intl.formatMessage(messages.titleLabel)}
|
||||
>
|
||||
{/* Error Alerts */}
|
||||
<FetchErrorAlert />
|
||||
<UploadErrorAlert />
|
||||
<FetchErrorAlert message={messages.fetchImagesError} />
|
||||
<UploadErrorAlert message={messages.uploadImageError} />
|
||||
<ErrorAlert
|
||||
dismissError={inputError.dismiss}
|
||||
hideHeading
|
||||
@@ -86,7 +87,7 @@ export const SelectImageModal = ({
|
||||
screenReaderText={intl.formatMessage(messages.loading)}
|
||||
/>
|
||||
)}
|
||||
<FileInput fileInput={fileInput} />
|
||||
<FileInput fileInput={fileInput} acceptedFiles={Object.values(acceptedImgKeys).join()} />
|
||||
</Stack>
|
||||
</BaseModal>
|
||||
);
|
||||
|
||||
@@ -5,19 +5,19 @@ import { formatMessage } from '../../../../../testUtils';
|
||||
import { RequestKeys } from '../../../../data/constants/requests';
|
||||
import { selectors } from '../../../../data/redux';
|
||||
import BaseModal from '../BaseModal';
|
||||
import FileInput from './FileInput';
|
||||
import FileInput from '../../../../sharedComponents/FileInput';
|
||||
import Gallery from './Gallery';
|
||||
import SearchSort from './SearchSort';
|
||||
import hooks from './hooks';
|
||||
import { SelectImageModal, mapStateToProps, mapDispatchToProps } from '.';
|
||||
|
||||
jest.mock('../BaseModal', () => 'BaseModal');
|
||||
jest.mock('./FileInput', () => 'FileInput');
|
||||
jest.mock('../../../../sharedComponents/FileInput', () => 'FileInput');
|
||||
jest.mock('./Gallery', () => 'Gallery');
|
||||
jest.mock('./SearchSort', () => 'SearchSort');
|
||||
jest.mock('../ErrorAlerts/FetchErrorAlert', () => 'FetchErrorAlert');
|
||||
jest.mock('../ErrorAlerts/UploadErrorAlert', () => 'UploadErrorAlert');
|
||||
jest.mock('../ErrorAlerts/ErrorAlert', () => 'ErrorAlert');
|
||||
jest.mock('../../../../sharedComponents/ErrorAlerts/FetchErrorAlert', () => 'FetchErrorAlert');
|
||||
jest.mock('../../../../sharedComponents/ErrorAlerts/UploadErrorAlert', () => 'UploadErrorAlert');
|
||||
jest.mock('../../../../sharedComponents/ErrorAlerts/ErrorAlert', () => 'ErrorAlert');
|
||||
|
||||
jest.mock('./hooks', () => ({
|
||||
imgHooks: jest.fn(() => ({
|
||||
|
||||
@@ -66,11 +66,6 @@ export const messages = {
|
||||
},
|
||||
|
||||
// Errors
|
||||
errorTitle: {
|
||||
id: 'authoring.texteditor.selectimagemodal.error.errorTitle',
|
||||
defaultMessage: 'Error',
|
||||
description: 'Title of message presented to user when something goes wrong',
|
||||
},
|
||||
uploadImageError: {
|
||||
id: 'authoring.texteditor.selectimagemodal.error.uploadImageError',
|
||||
defaultMessage: 'Failed to upload image. Please try again.',
|
||||
|
||||
@@ -0,0 +1,407 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`TranscriptWidget snapshots snapshots: renders as expected with allowTranscriptDownloads true 1`] = `
|
||||
<injectIntl(ShimmedIntlComponent)
|
||||
isError={false}
|
||||
subtitle="english"
|
||||
title="Transcript"
|
||||
>
|
||||
<ErrorAlert
|
||||
dismissError={[Function]}
|
||||
hideHeading={true}
|
||||
isError={true}
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Failed to upload transcript. Please try again."
|
||||
description="Message presented to user when transcript fails to upload"
|
||||
id="authoring.videoeditor.transcript.error.uploadTranscriptError"
|
||||
/>
|
||||
</ErrorAlert>
|
||||
<ErrorAlert
|
||||
dismissError={[Function]}
|
||||
hideHeading={true}
|
||||
isError={true}
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Transcript file size exeeds the maximum. Please try again."
|
||||
description="Message presented to user when transcript file size is too large"
|
||||
id="authoring.videoeditor.transcript.error.fileSizeError"
|
||||
/>
|
||||
</ErrorAlert>
|
||||
<Stack
|
||||
gap={3}
|
||||
>
|
||||
<Form.Group
|
||||
className="mt-4.5"
|
||||
>
|
||||
<b>
|
||||
Transcript widget:
|
||||
</b>
|
||||
<div
|
||||
className="mb-1"
|
||||
>
|
||||
<Form.Checkbox
|
||||
checked={true}
|
||||
className="mt-4.5 decorative-control-label"
|
||||
onChange={[Function]}
|
||||
>
|
||||
<Form.Label>
|
||||
<FormattedMessage
|
||||
defaultMessage="Allow transcript downloads"
|
||||
description="Label for allow transcript downloads checkbox"
|
||||
id="authoring.videoeditor.transcripts.allowDownloadCheckboxLabel"
|
||||
/>
|
||||
</Form.Label>
|
||||
</Form.Checkbox>
|
||||
<Component
|
||||
key="right"
|
||||
overlay={
|
||||
<UNDEFINED>
|
||||
<FormattedMessage
|
||||
defaultMessage="Learners will see a link to download the transcript below the video."
|
||||
description="Message for show by default checkbox"
|
||||
id="authoring.videoeditor.transcripts.upload.allowDownloadTooltipMessage"
|
||||
/>
|
||||
</UNDEFINED>
|
||||
}
|
||||
placement="right"
|
||||
>
|
||||
<Icon
|
||||
className="d-inline-block mx-3"
|
||||
/>
|
||||
</Component>
|
||||
</div>
|
||||
<Form.Checkbox
|
||||
checked={false}
|
||||
className="mt-4.5 decorative-control-label"
|
||||
onChange={[Function]}
|
||||
>
|
||||
<Form.Label
|
||||
size="sm"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Show transcript in the video player by default"
|
||||
description="Label for show by default checkbox"
|
||||
id="authoring.videoeditor.transcripts.upload.showByDefaultCheckboxLabel"
|
||||
/>
|
||||
</Form.Label>
|
||||
</Form.Checkbox>
|
||||
</Form.Group>
|
||||
<FileInput
|
||||
acceptedFiles=".srt"
|
||||
fileInput={
|
||||
Object {
|
||||
"addFile": [Function],
|
||||
"click": [Function],
|
||||
"ref": Object {
|
||||
"current": undefined,
|
||||
},
|
||||
}
|
||||
}
|
||||
/>
|
||||
<Button
|
||||
onClick={[Function]}
|
||||
variant="link"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Upload Transcript"
|
||||
description="Label for upload button"
|
||||
id="authoring.videoeditor.transcripts.upload.label"
|
||||
/>
|
||||
</Button>
|
||||
</Stack>
|
||||
</injectIntl(ShimmedIntlComponent)>
|
||||
`;
|
||||
|
||||
exports[`TranscriptWidget snapshots snapshots: renders as expected with default props 1`] = `
|
||||
<injectIntl(ShimmedIntlComponent)
|
||||
isError={false}
|
||||
subtitle="None"
|
||||
title="Transcript"
|
||||
>
|
||||
<ErrorAlert
|
||||
dismissError={[Function]}
|
||||
hideHeading={true}
|
||||
isError={true}
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Failed to upload transcript. Please try again."
|
||||
description="Message presented to user when transcript fails to upload"
|
||||
id="authoring.videoeditor.transcript.error.uploadTranscriptError"
|
||||
/>
|
||||
</ErrorAlert>
|
||||
<ErrorAlert
|
||||
dismissError={[Function]}
|
||||
hideHeading={true}
|
||||
isError={true}
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Transcript file size exeeds the maximum. Please try again."
|
||||
description="Message presented to user when transcript file size is too large"
|
||||
id="authoring.videoeditor.transcript.error.fileSizeError"
|
||||
/>
|
||||
</ErrorAlert>
|
||||
<Stack
|
||||
gap={3}
|
||||
>
|
||||
<Alert
|
||||
variant="danger"
|
||||
>
|
||||
Only SRT files can be uploaded. Please select a file ending in .srt to upload.
|
||||
</Alert>
|
||||
<FormattedMessage
|
||||
defaultMessage="Add video transcripts for improved accessibility."
|
||||
description="Message for adding first transcript"
|
||||
id="authoring.videoeditor.transcripts.upload.firstTranscriptMessage"
|
||||
/>
|
||||
<FileInput
|
||||
acceptedFiles=".srt"
|
||||
fileInput={
|
||||
Object {
|
||||
"addFile": [Function],
|
||||
"click": [Function],
|
||||
"ref": Object {
|
||||
"current": undefined,
|
||||
},
|
||||
}
|
||||
}
|
||||
/>
|
||||
<Button
|
||||
onClick={[Function]}
|
||||
variant="link"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Upload Transcript"
|
||||
description="Label for upload button"
|
||||
id="authoring.videoeditor.transcripts.upload.label"
|
||||
/>
|
||||
</Button>
|
||||
</Stack>
|
||||
</injectIntl(ShimmedIntlComponent)>
|
||||
`;
|
||||
|
||||
exports[`TranscriptWidget snapshots snapshots: renders as expected with showTranscriptByDefault true 1`] = `
|
||||
<injectIntl(ShimmedIntlComponent)
|
||||
isError={false}
|
||||
subtitle="english"
|
||||
title="Transcript"
|
||||
>
|
||||
<ErrorAlert
|
||||
dismissError={[Function]}
|
||||
hideHeading={true}
|
||||
isError={true}
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Failed to upload transcript. Please try again."
|
||||
description="Message presented to user when transcript fails to upload"
|
||||
id="authoring.videoeditor.transcript.error.uploadTranscriptError"
|
||||
/>
|
||||
</ErrorAlert>
|
||||
<ErrorAlert
|
||||
dismissError={[Function]}
|
||||
hideHeading={true}
|
||||
isError={true}
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Transcript file size exeeds the maximum. Please try again."
|
||||
description="Message presented to user when transcript file size is too large"
|
||||
id="authoring.videoeditor.transcript.error.fileSizeError"
|
||||
/>
|
||||
</ErrorAlert>
|
||||
<Stack
|
||||
gap={3}
|
||||
>
|
||||
<Form.Group
|
||||
className="mt-4.5"
|
||||
>
|
||||
<b>
|
||||
Transcript widget:
|
||||
</b>
|
||||
<div
|
||||
className="mb-1"
|
||||
>
|
||||
<Form.Checkbox
|
||||
checked={false}
|
||||
className="mt-4.5 decorative-control-label"
|
||||
onChange={[Function]}
|
||||
>
|
||||
<Form.Label>
|
||||
<FormattedMessage
|
||||
defaultMessage="Allow transcript downloads"
|
||||
description="Label for allow transcript downloads checkbox"
|
||||
id="authoring.videoeditor.transcripts.allowDownloadCheckboxLabel"
|
||||
/>
|
||||
</Form.Label>
|
||||
</Form.Checkbox>
|
||||
<Component
|
||||
key="right"
|
||||
overlay={
|
||||
<UNDEFINED>
|
||||
<FormattedMessage
|
||||
defaultMessage="Learners will see a link to download the transcript below the video."
|
||||
description="Message for show by default checkbox"
|
||||
id="authoring.videoeditor.transcripts.upload.allowDownloadTooltipMessage"
|
||||
/>
|
||||
</UNDEFINED>
|
||||
}
|
||||
placement="right"
|
||||
>
|
||||
<Icon
|
||||
className="d-inline-block mx-3"
|
||||
/>
|
||||
</Component>
|
||||
</div>
|
||||
<Form.Checkbox
|
||||
checked={true}
|
||||
className="mt-4.5 decorative-control-label"
|
||||
onChange={[Function]}
|
||||
>
|
||||
<Form.Label
|
||||
size="sm"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Show transcript in the video player by default"
|
||||
description="Label for show by default checkbox"
|
||||
id="authoring.videoeditor.transcripts.upload.showByDefaultCheckboxLabel"
|
||||
/>
|
||||
</Form.Label>
|
||||
</Form.Checkbox>
|
||||
</Form.Group>
|
||||
<FileInput
|
||||
acceptedFiles=".srt"
|
||||
fileInput={
|
||||
Object {
|
||||
"addFile": [Function],
|
||||
"click": [Function],
|
||||
"ref": Object {
|
||||
"current": undefined,
|
||||
},
|
||||
}
|
||||
}
|
||||
/>
|
||||
<Button
|
||||
onClick={[Function]}
|
||||
variant="link"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Upload Transcript"
|
||||
description="Label for upload button"
|
||||
id="authoring.videoeditor.transcripts.upload.label"
|
||||
/>
|
||||
</Button>
|
||||
</Stack>
|
||||
</injectIntl(ShimmedIntlComponent)>
|
||||
`;
|
||||
|
||||
exports[`TranscriptWidget snapshots snapshots: renders as expected with transcripts 1`] = `
|
||||
<injectIntl(ShimmedIntlComponent)
|
||||
isError={false}
|
||||
subtitle="english"
|
||||
title="Transcript"
|
||||
>
|
||||
<ErrorAlert
|
||||
dismissError={[Function]}
|
||||
hideHeading={true}
|
||||
isError={true}
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Failed to upload transcript. Please try again."
|
||||
description="Message presented to user when transcript fails to upload"
|
||||
id="authoring.videoeditor.transcript.error.uploadTranscriptError"
|
||||
/>
|
||||
</ErrorAlert>
|
||||
<ErrorAlert
|
||||
dismissError={[Function]}
|
||||
hideHeading={true}
|
||||
isError={true}
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Transcript file size exeeds the maximum. Please try again."
|
||||
description="Message presented to user when transcript file size is too large"
|
||||
id="authoring.videoeditor.transcript.error.fileSizeError"
|
||||
/>
|
||||
</ErrorAlert>
|
||||
<Stack
|
||||
gap={3}
|
||||
>
|
||||
<Form.Group
|
||||
className="mt-4.5"
|
||||
>
|
||||
<b>
|
||||
Transcript widget:
|
||||
</b>
|
||||
<div
|
||||
className="mb-1"
|
||||
>
|
||||
<Form.Checkbox
|
||||
checked={false}
|
||||
className="mt-4.5 decorative-control-label"
|
||||
onChange={[Function]}
|
||||
>
|
||||
<Form.Label>
|
||||
<FormattedMessage
|
||||
defaultMessage="Allow transcript downloads"
|
||||
description="Label for allow transcript downloads checkbox"
|
||||
id="authoring.videoeditor.transcripts.allowDownloadCheckboxLabel"
|
||||
/>
|
||||
</Form.Label>
|
||||
</Form.Checkbox>
|
||||
<Component
|
||||
key="right"
|
||||
overlay={
|
||||
<UNDEFINED>
|
||||
<FormattedMessage
|
||||
defaultMessage="Learners will see a link to download the transcript below the video."
|
||||
description="Message for show by default checkbox"
|
||||
id="authoring.videoeditor.transcripts.upload.allowDownloadTooltipMessage"
|
||||
/>
|
||||
</UNDEFINED>
|
||||
}
|
||||
placement="right"
|
||||
>
|
||||
<Icon
|
||||
className="d-inline-block mx-3"
|
||||
/>
|
||||
</Component>
|
||||
</div>
|
||||
<Form.Checkbox
|
||||
checked={false}
|
||||
className="mt-4.5 decorative-control-label"
|
||||
onChange={[Function]}
|
||||
>
|
||||
<Form.Label
|
||||
size="sm"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Show transcript in the video player by default"
|
||||
description="Label for show by default checkbox"
|
||||
id="authoring.videoeditor.transcripts.upload.showByDefaultCheckboxLabel"
|
||||
/>
|
||||
</Form.Label>
|
||||
</Form.Checkbox>
|
||||
</Form.Group>
|
||||
<FileInput
|
||||
acceptedFiles=".srt"
|
||||
fileInput={
|
||||
Object {
|
||||
"addFile": [Function],
|
||||
"click": [Function],
|
||||
"ref": Object {
|
||||
"current": undefined,
|
||||
},
|
||||
}
|
||||
}
|
||||
/>
|
||||
<Button
|
||||
onClick={[Function]}
|
||||
variant="link"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Upload Transcript"
|
||||
description="Label for upload button"
|
||||
id="authoring.videoeditor.transcripts.upload.label"
|
||||
/>
|
||||
</Button>
|
||||
</Stack>
|
||||
</injectIntl(ShimmedIntlComponent)>
|
||||
`;
|
||||
@@ -0,0 +1,29 @@
|
||||
import React from 'react';
|
||||
|
||||
export const transcriptLanguages = (transcripts) => {
|
||||
const languages = [];
|
||||
if (transcripts) {
|
||||
Object.keys(transcripts).forEach(transcript => {
|
||||
languages.push(transcript);
|
||||
});
|
||||
return languages.join(', ');
|
||||
}
|
||||
return 'None';
|
||||
};
|
||||
|
||||
export const fileInput = () => {
|
||||
const ref = React.useRef();
|
||||
const click = () => ref.current.click();
|
||||
const addFile = (e) => {
|
||||
const selectedFile = e.target.files[0];
|
||||
console.log(selectedFile);
|
||||
};
|
||||
|
||||
return {
|
||||
click,
|
||||
addFile,
|
||||
ref,
|
||||
};
|
||||
};
|
||||
|
||||
export default { transcriptLanguages, fileInput };
|
||||
@@ -0,0 +1,148 @@
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import {
|
||||
Form,
|
||||
Button,
|
||||
Stack,
|
||||
Icon,
|
||||
OverlayTrigger,
|
||||
Tooltip,
|
||||
Alert,
|
||||
} from '@edx/paragon';
|
||||
import { FileUpload, Info } from '@edx/paragon/icons';
|
||||
import {
|
||||
FormattedMessage,
|
||||
injectIntl,
|
||||
} from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { actions, selectors } from '../../../../../../data/redux';
|
||||
import * as hooks from './hooks';
|
||||
import messages from './messages';
|
||||
|
||||
import FileInput from '../../../../../../sharedComponents/FileInput';
|
||||
import ErrorAlert from '../../../../../../sharedComponents/ErrorAlerts/ErrorAlert';
|
||||
import CollapsibleFormWidget from '../CollapsibleFormWidget';
|
||||
|
||||
/**
|
||||
* Collapsible Form widget controlling video transcripts
|
||||
*/
|
||||
export const TranscriptWidget = ({
|
||||
error,
|
||||
// redux
|
||||
transcripts,
|
||||
allowTranscriptDownloads,
|
||||
showTranscriptByDefault,
|
||||
updateField,
|
||||
}) => {
|
||||
const languagesArr = hooks.transcriptLanguages(transcripts);
|
||||
const fileInput = hooks.fileInput();
|
||||
const input = {
|
||||
error: {
|
||||
dismiss: () => { console.log('dismiss'); },
|
||||
show: true,
|
||||
},
|
||||
};
|
||||
const upload = {
|
||||
error: {
|
||||
dismiss: () => { console.log('dismiss'); },
|
||||
show: true,
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<CollapsibleFormWidget
|
||||
isError={Object.keys(error).length !== 0}
|
||||
subtitle={languagesArr}
|
||||
title="Transcript"
|
||||
>
|
||||
<ErrorAlert
|
||||
dismissError={upload.error.dismiss}
|
||||
hideHeading
|
||||
isError={upload.error.show}
|
||||
>
|
||||
<FormattedMessage {...messages.uploadTranscriptError} />
|
||||
</ErrorAlert>
|
||||
<ErrorAlert
|
||||
dismissError={input.error.dismiss}
|
||||
hideHeading
|
||||
isError={input.error.show}
|
||||
>
|
||||
<FormattedMessage {...messages.fileSizeError} />
|
||||
</ErrorAlert>
|
||||
<Stack gap={3}>
|
||||
{transcripts ? (
|
||||
<Form.Group className="mt-4.5">
|
||||
<b>Transcript widget:</b>
|
||||
<div className="mb-1">
|
||||
<Form.Checkbox
|
||||
checked={allowTranscriptDownloads}
|
||||
className="mt-4.5 decorative-control-label"
|
||||
onChange={(e) => updateField({ allowTranscriptDownloads: e.target.checked })}
|
||||
>
|
||||
<Form.Label>
|
||||
<FormattedMessage {...messages.allowDownloadCheckboxLabel} />
|
||||
</Form.Label>
|
||||
</Form.Checkbox>
|
||||
<OverlayTrigger
|
||||
key="right"
|
||||
placement="right"
|
||||
overlay={(
|
||||
<Tooltip>
|
||||
<FormattedMessage {...messages.tooltipMessage} />
|
||||
</Tooltip>
|
||||
)}
|
||||
>
|
||||
<Icon className="d-inline-block mx-3" src={Info} />
|
||||
</OverlayTrigger>
|
||||
</div>
|
||||
<Form.Checkbox
|
||||
checked={showTranscriptByDefault}
|
||||
className="mt-4.5 decorative-control-label"
|
||||
onChange={(e) => updateField({ showTranscriptByDefault: e.target.checked })}
|
||||
>
|
||||
<Form.Label size="sm">
|
||||
<FormattedMessage {...messages.showByDefaultCheckboxLabel} />
|
||||
</Form.Label>
|
||||
</Form.Checkbox>
|
||||
</Form.Group>
|
||||
) : (
|
||||
<>
|
||||
<Alert variant="danger" icon={Info}>
|
||||
Only SRT files can be uploaded. Please select a file ending in .srt to upload.
|
||||
</Alert>
|
||||
<FormattedMessage {...messages.addFirstTranscript} />
|
||||
</>
|
||||
)}
|
||||
<FileInput fileInput={fileInput} acceptedFiles=".srt" />
|
||||
<Button iconBefore={FileUpload} onClick={fileInput.click} variant="link">
|
||||
<FormattedMessage {...messages.uploadButtonLabel} />
|
||||
</Button>
|
||||
</Stack>
|
||||
</CollapsibleFormWidget>
|
||||
);
|
||||
};
|
||||
|
||||
TranscriptWidget.defaultProps = {
|
||||
error: {},
|
||||
};
|
||||
TranscriptWidget.propTypes = {
|
||||
error: PropTypes.node,
|
||||
// redux
|
||||
transcripts: PropTypes.shape({}).isRequired,
|
||||
allowTranscriptDownloads: PropTypes.bool.isRequired,
|
||||
showTranscriptByDefault: PropTypes.bool.isRequired,
|
||||
updateField: PropTypes.func.isRequired,
|
||||
};
|
||||
export const mapStateToProps = (state) => ({
|
||||
transcripts: selectors.video.transcripts(state),
|
||||
allowTranscriptDownloads: selectors.video.allowTranscriptDownloads(state),
|
||||
showTranscriptByDefault: selectors.video.showTranscriptByDefault(state),
|
||||
});
|
||||
|
||||
export const mapDispatchToProps = (dispatch) => ({
|
||||
updateField: (stateUpdate) => dispatch(actions.video.updateField(stateUpdate)),
|
||||
});
|
||||
|
||||
export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(TranscriptWidget));
|
||||
@@ -0,0 +1,83 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { formatMessage } from '../../../../../../../testUtils';
|
||||
import { actions, selectors } from '../../../../../../data/redux';
|
||||
import { TranscriptWidget, mapStateToProps, mapDispatchToProps } from '.';
|
||||
|
||||
jest.mock('../../../../../../data/redux', () => ({
|
||||
actions: {
|
||||
video: {
|
||||
updateField: jest.fn().mockName('actions.video.updateField'),
|
||||
},
|
||||
},
|
||||
selectors: {
|
||||
video: {
|
||||
transcripts: jest.fn(state => ({ transcripts: state })),
|
||||
allowTranscriptDownloads: jest.fn(state => ({ allowTranscriptDownloads: state })),
|
||||
showTranscriptByDefault: jest.fn(state => ({ showTranscriptByDefault: state })),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
describe('TranscriptWidget', () => {
|
||||
const props = {
|
||||
error: {},
|
||||
subtitle: 'SuBTItle',
|
||||
title: 'tiTLE',
|
||||
// inject
|
||||
intl: { formatMessage },
|
||||
// redux
|
||||
transcripts: null,
|
||||
allowTranscriptDownloads: false,
|
||||
showTranscriptByDefault: false,
|
||||
updateField: jest.fn().mockName('args.updateField'),
|
||||
};
|
||||
|
||||
describe('snapshots', () => {
|
||||
test('snapshots: renders as expected with default props', () => {
|
||||
expect(
|
||||
shallow(<TranscriptWidget {...props} />),
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
test('snapshots: renders as expected with transcripts', () => {
|
||||
expect(
|
||||
shallow(<TranscriptWidget {...props} transcripts={{ english: 'sOMeUrl' }} />),
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
test('snapshots: renders as expected with allowTranscriptDownloads true', () => {
|
||||
expect(
|
||||
shallow(<TranscriptWidget {...props} allowTranscriptDownloads transcripts={{ english: 'sOMeUrl' }} />),
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
test('snapshots: renders as expected with showTranscriptByDefault true', () => {
|
||||
expect(
|
||||
shallow(<TranscriptWidget {...props} showTranscriptByDefault transcripts={{ english: 'sOMeUrl' }} />),
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
describe('mapStateToProps', () => {
|
||||
const testState = { A: 'pple', B: 'anana', C: 'ucumber' };
|
||||
test('transcripts from video.transcript', () => {
|
||||
expect(
|
||||
mapStateToProps(testState).transcripts,
|
||||
).toEqual(selectors.video.transcripts(testState));
|
||||
});
|
||||
test('allowTranscriptDownloads from video.allowTranscriptDownloads', () => {
|
||||
expect(
|
||||
mapStateToProps(testState).allowTranscriptDownloads,
|
||||
).toEqual(selectors.video.allowTranscriptDownloads(testState));
|
||||
});
|
||||
test('showTranscriptByDefault from video.showTranscriptByDefault', () => {
|
||||
expect(
|
||||
mapStateToProps(testState).showTranscriptByDefault,
|
||||
).toEqual(selectors.video.showTranscriptByDefault(testState));
|
||||
});
|
||||
});
|
||||
describe('mapDispatchToProps', () => {
|
||||
const dispatch = jest.fn();
|
||||
test('updateField from actions.video.updateField', () => {
|
||||
expect(mapDispatchToProps.updateField).toEqual(dispatch(actions.video.updateField));
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,44 @@
|
||||
export const messages = {
|
||||
uploadButtonLabel: {
|
||||
id: 'authoring.videoeditor.transcripts.upload.label',
|
||||
defaultMessage: 'Upload Transcript',
|
||||
description: 'Label for upload button',
|
||||
},
|
||||
addFirstTranscript: {
|
||||
id: 'authoring.videoeditor.transcripts.upload.firstTranscriptMessage',
|
||||
defaultMessage: 'Add video transcripts for improved accessibility.',
|
||||
description: 'Message for adding first transcript',
|
||||
},
|
||||
allowDownloadCheckboxLabel: {
|
||||
id: 'authoring.videoeditor.transcripts.allowDownloadCheckboxLabel',
|
||||
defaultMessage: 'Allow transcript downloads',
|
||||
description: 'Label for allow transcript downloads checkbox',
|
||||
},
|
||||
showByDefaultCheckboxLabel: {
|
||||
id: 'authoring.videoeditor.transcripts.upload.showByDefaultCheckboxLabel',
|
||||
defaultMessage: 'Show transcript in the video player by default',
|
||||
description: 'Label for show by default checkbox',
|
||||
},
|
||||
tooltipMessage: {
|
||||
id: 'authoring.videoeditor.transcripts.upload.allowDownloadTooltipMessage',
|
||||
defaultMessage: 'Learners will see a link to download the transcript below the video.',
|
||||
description: 'Message for show by default checkbox',
|
||||
},
|
||||
transcriptTypeError: {
|
||||
id: 'authoring.videoeditor.transcript.error.transcriptTypeError',
|
||||
defaultMessage: 'Only SRT file can be uploaded',
|
||||
description: 'Message presented to user when image fails to upload',
|
||||
},
|
||||
uploadTranscriptError: {
|
||||
id: 'authoring.videoeditor.transcript.error.uploadTranscriptError',
|
||||
defaultMessage: 'Failed to upload transcript. Please try again.',
|
||||
description: 'Message presented to user when transcript fails to upload',
|
||||
},
|
||||
fileSizeError: {
|
||||
id: 'authoring.videoeditor.transcript.error.fileSizeError',
|
||||
defaultMessage: 'Transcript file size exeeds the maximum. Please try again.',
|
||||
description: 'Message presented to user when transcript file size is too large',
|
||||
},
|
||||
};
|
||||
|
||||
export default messages;
|
||||
@@ -1,53 +0,0 @@
|
||||
import React from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import hooks from './hooks';
|
||||
import CollapsibleFormWidget from './CollapsibleFormWidget';
|
||||
|
||||
/**
|
||||
* Collapsible Form widget controlling video transcripts
|
||||
*/
|
||||
export const TranscriptWidget = ({
|
||||
error,
|
||||
}) => {
|
||||
const dispatch = useDispatch();
|
||||
const values = hooks.widgetValues({
|
||||
dispatch,
|
||||
fields: {
|
||||
[hooks.selectorKeys.transcripts]: hooks.objectWidget,
|
||||
[hooks.selectorKeys.allowTranscriptDownloads]: hooks.genericWidget,
|
||||
[hooks.selectorKeys.showTranscriptByDefault]: hooks.genericWidget,
|
||||
},
|
||||
});
|
||||
const {
|
||||
transcripts,
|
||||
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
|
||||
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>
|
||||
<p><b>Show by default:</b> {showByDefault.formValue ? 'True' : 'False' }</p>
|
||||
</CollapsibleFormWidget>
|
||||
);
|
||||
};
|
||||
|
||||
TranscriptWidget.defaultProps = {
|
||||
error: {},
|
||||
};
|
||||
TranscriptWidget.propTypes = {
|
||||
error: PropTypes.node,
|
||||
};
|
||||
|
||||
export default TranscriptWidget;
|
||||
@@ -8,7 +8,7 @@ import DurationWidget from './components/DurationWidget';
|
||||
import HandoutWidget from './components/HandoutWidget';
|
||||
import LicenseWidget from './components/LicenseWidget';
|
||||
import ThumbnailWidget from './components/ThumbnailWidget';
|
||||
import TranscriptsWidget from './components/TranscriptsWidget';
|
||||
import TranscriptWidget from './components/TranscriptWidget';
|
||||
import VideoSourceWidget from './components/VideoSourceWidget';
|
||||
import './index.scss';
|
||||
|
||||
@@ -33,7 +33,7 @@ export const VideoSettingsModal = ({
|
||||
<h3>Settings</h3>
|
||||
<VideoSourceWidget error={error.videoSource} />
|
||||
<ThumbnailWidget error={error.thumbnail} />
|
||||
<TranscriptsWidget error={error.transcripts} />
|
||||
<TranscriptWidget error={error.transcripts} />
|
||||
<DurationWidget error={error.duration} />
|
||||
<HandoutWidget error={error.handout} />
|
||||
<LicenseWidget error={error.license} />
|
||||
|
||||
@@ -5,7 +5,7 @@ import { Alert } from '@edx/paragon';
|
||||
import { Info } from '@edx/paragon/icons';
|
||||
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
import messages from '../SelectImageModal/messages';
|
||||
import messages from './messages';
|
||||
|
||||
export const hooks = {
|
||||
state: {
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import * as module from './ErrorAlert';
|
||||
import { MockUseState } from '../../../../../testUtils';
|
||||
import { MockUseState } from '../../../testUtils';
|
||||
|
||||
const { ErrorAlert } = module;
|
||||
|
||||
@@ -3,12 +3,12 @@ import { connect } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import messages from '../SelectImageModal/messages';
|
||||
import ErrorAlert from './ErrorAlert';
|
||||
import { selectors } from '../../../../data/redux';
|
||||
import { RequestKeys } from '../../../../data/constants/requests';
|
||||
import { selectors } from '../../data/redux';
|
||||
import { RequestKeys } from '../../data/constants/requests';
|
||||
|
||||
export const FetchErrorAlert = ({
|
||||
message,
|
||||
// redux
|
||||
isFetchError,
|
||||
// inject
|
||||
@@ -17,14 +17,20 @@ export const FetchErrorAlert = ({
|
||||
isError={isFetchError}
|
||||
>
|
||||
<FormattedMessage
|
||||
{...messages.fetchImagesError}
|
||||
{...message}
|
||||
/>
|
||||
</ErrorAlert>
|
||||
);
|
||||
|
||||
FetchErrorAlert.propTypes = {
|
||||
message: PropTypes.shape({
|
||||
id: PropTypes.string,
|
||||
defaultMessage: PropTypes.string,
|
||||
description: PropTypes.string,
|
||||
}).isRequired,
|
||||
// redux
|
||||
isFetchError: PropTypes.bool.isRequired,
|
||||
|
||||
};
|
||||
export const mapStateToProps = (state) => ({
|
||||
isFetchError: selectors.requests.isFailed(state, { requestKey: RequestKeys.fetchImages }),
|
||||
@@ -1,10 +1,10 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { FetchErrorAlert, mapStateToProps } from './FetchErrorAlert';
|
||||
import { selectors } from '../../../../data/redux';
|
||||
import { RequestKeys } from '../../../../data/constants/requests';
|
||||
import { selectors } from '../../data/redux';
|
||||
import { RequestKeys } from '../../data/constants/requests';
|
||||
|
||||
jest.mock('../../../../data/redux', () => ({
|
||||
jest.mock('../../data/redux', () => ({
|
||||
selectors: {
|
||||
requests: {
|
||||
isFailed: jest.fn((state, params) => ({ isFailed: { state, params } })),
|
||||
@@ -3,12 +3,12 @@ import { connect } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import messages from '../SelectImageModal/messages';
|
||||
import ErrorAlert from './ErrorAlert';
|
||||
import { selectors } from '../../../../data/redux';
|
||||
import { RequestKeys } from '../../../../data/constants/requests';
|
||||
import { selectors } from '../../data/redux';
|
||||
import { RequestKeys } from '../../data/constants/requests';
|
||||
|
||||
export const UploadErrorAlert = ({
|
||||
message,
|
||||
// redux
|
||||
isUploadError,
|
||||
// inject
|
||||
@@ -17,12 +17,17 @@ export const UploadErrorAlert = ({
|
||||
isError={isUploadError}
|
||||
>
|
||||
<FormattedMessage
|
||||
{...messages.uploadImageError}
|
||||
{...message}
|
||||
/>
|
||||
</ErrorAlert>
|
||||
);
|
||||
|
||||
UploadErrorAlert.propTypes = {
|
||||
message: PropTypes.shape({
|
||||
id: PropTypes.string,
|
||||
defaultMessage: PropTypes.string,
|
||||
description: PropTypes.string,
|
||||
}).isRequired,
|
||||
// redux
|
||||
isUploadError: PropTypes.bool.isRequired,
|
||||
};
|
||||
@@ -1,10 +1,10 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { UploadErrorAlert, mapStateToProps } from './UploadErrorAlert';
|
||||
import { selectors } from '../../../../data/redux';
|
||||
import { RequestKeys } from '../../../../data/constants/requests';
|
||||
import { selectors } from '../../data/redux';
|
||||
import { RequestKeys } from '../../data/constants/requests';
|
||||
|
||||
jest.mock('../../../../data/redux', () => ({
|
||||
jest.mock('../../data/redux', () => ({
|
||||
selectors: {
|
||||
requests: {
|
||||
isFailed: jest.fn((state, params) => ({ isFailed: { state, params } })),
|
||||
@@ -6,10 +6,6 @@ exports[`FetchErrorAlert Snapshots snapshot: is ErrorAlert with Message error (
|
||||
hideHeading={false}
|
||||
isError={true}
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Failed to obtain course images. Please try again."
|
||||
description="Message presented to user when images are not found"
|
||||
id="authoring.texteditor.selectimagemodal.error.fetchImagesError"
|
||||
/>
|
||||
<FormattedMessage />
|
||||
</ErrorAlert>
|
||||
`;
|
||||
@@ -6,10 +6,6 @@ exports[`UploadErrorAlert Snapshots snapshot: is ErrorAlert with Message error
|
||||
hideHeading={false}
|
||||
isError={true}
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Failed to upload image. Please try again."
|
||||
description="Message presented to user when image fails to upload"
|
||||
id="authoring.texteditor.selectimagemodal.error.uploadImageError"
|
||||
/>
|
||||
<FormattedMessage />
|
||||
</ErrorAlert>
|
||||
`;
|
||||
9
src/editors/sharedComponents/ErrorAlerts/messages.js
Normal file
9
src/editors/sharedComponents/ErrorAlerts/messages.js
Normal file
@@ -0,0 +1,9 @@
|
||||
export const messages = {
|
||||
errorTitle: {
|
||||
id: 'authoring.texteditor.selectimagemodal.error.errorTitle',
|
||||
defaultMessage: 'Error',
|
||||
description: 'Title of message presented to user when something goes wrong',
|
||||
},
|
||||
};
|
||||
|
||||
export default messages;
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
exports[`FileInput component snapshot 1`] = `
|
||||
<FileInput
|
||||
acceptedFiles=".srt"
|
||||
fileInput={
|
||||
Object {
|
||||
"addFile": [MockFunction props.fileInput.addFile],
|
||||
@@ -10,7 +11,7 @@ exports[`FileInput component snapshot 1`] = `
|
||||
}
|
||||
>
|
||||
<input
|
||||
accept=".gif,.jpg,.jpeg,.png,.tif,.tiff,.ico"
|
||||
accept=".srt"
|
||||
className="upload d-none"
|
||||
onChange={[MockFunction props.fileInput.addFile]}
|
||||
type="file"
|
||||
@@ -1,11 +1,9 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { acceptedImgKeys } from './utils';
|
||||
|
||||
export const FileInput = ({ fileInput }) => (
|
||||
export const FileInput = ({ fileInput, acceptedFiles }) => (
|
||||
<input
|
||||
accept={Object.values(acceptedImgKeys).join()}
|
||||
accept={acceptedFiles}
|
||||
className="upload d-none"
|
||||
onChange={fileInput.addFile}
|
||||
ref={fileInput.ref}
|
||||
@@ -14,6 +12,7 @@ export const FileInput = ({ fileInput }) => (
|
||||
);
|
||||
|
||||
FileInput.propTypes = {
|
||||
acceptedFiles: PropTypes.string.isRequired,
|
||||
fileInput: PropTypes.shape({
|
||||
addFile: PropTypes.func,
|
||||
ref: PropTypes.oneOfType([
|
||||
@@ -1,9 +1,7 @@
|
||||
import React from 'react';
|
||||
import { mount } from 'enzyme';
|
||||
|
||||
import { acceptedImgKeys } from './utils';
|
||||
|
||||
import { FileInput } from './FileInput';
|
||||
import { FileInput } from '.';
|
||||
|
||||
describe('FileInput component', () => {
|
||||
let el;
|
||||
@@ -12,6 +10,7 @@ describe('FileInput component', () => {
|
||||
beforeEach(() => {
|
||||
container = {};
|
||||
props = {
|
||||
acceptedFiles: '.srt',
|
||||
fileInput: {
|
||||
addFile: jest.fn().mockName('props.fileInput.addFile'),
|
||||
ref: (input) => { container.ref = input; },
|
||||
@@ -23,9 +22,7 @@ describe('FileInput component', () => {
|
||||
expect(el).toMatchSnapshot();
|
||||
});
|
||||
test('only accepts allowed file types', () => {
|
||||
expect(el.find('input').props().accept).toEqual(
|
||||
Object.values(acceptedImgKeys).join(),
|
||||
);
|
||||
expect(el.find('input').props().accept).toEqual('.srt');
|
||||
});
|
||||
test('calls fileInput.addFile onChange', () => {
|
||||
expect(el.find('input').props().onChange).toEqual(props.fileInput.addFile);
|
||||
Reference in New Issue
Block a user