Feat: full transcript widget (#117)
This commit is contained in:
@@ -0,0 +1,62 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import {
|
||||
Form,
|
||||
} from '@edx/paragon';
|
||||
import { connect, useDispatch } from 'react-redux';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import hooks from './hooks';
|
||||
import { selectors } from '../../../../../../data/redux';
|
||||
import { videoTranscriptLanguages } from '../../../../../../data/constants/video';
|
||||
import messages from './messages';
|
||||
|
||||
export const LanguageSelect = ({
|
||||
title, // For a unique id for the form control
|
||||
language,
|
||||
// Redux
|
||||
openLanguages, // Only allow those languages not already associated with a transcript to be selected
|
||||
transcripts,
|
||||
// intl
|
||||
intl,
|
||||
|
||||
}) => {
|
||||
const onLanguageChange = hooks.onSelectLanguage({
|
||||
filename: title, dispatch: useDispatch(), transcripts, languageBeforeChange: language,
|
||||
});
|
||||
|
||||
return (
|
||||
<Form.Group controlId={`selectLanguage-form-${title}`} className="mt-2 mx-2">
|
||||
<Form.Control as="select" defaultValue={language} onChange={(e) => onLanguageChange(e)} floatingLabel={intl.formatMessage(messages.languageSelectLabel)}>
|
||||
{Object.entries(videoTranscriptLanguages).map(([lang, text]) => {
|
||||
if (language === lang) { return (<option value={lang} selected>{text}</option>); }
|
||||
if (openLanguages.some(row => row.includes(lang))) {
|
||||
return (<option value={lang}>{text}</option>);
|
||||
}
|
||||
return (<option value={lang} disabled>{text}</option>);
|
||||
})}
|
||||
</Form.Control>
|
||||
</Form.Group>
|
||||
);
|
||||
};
|
||||
|
||||
LanguageSelect.defaultProps = {
|
||||
openLanguages: [],
|
||||
};
|
||||
|
||||
LanguageSelect.propTypes = {
|
||||
openLanguages: PropTypes.arrayOf(PropTypes.string),
|
||||
title: PropTypes.string.isRequired,
|
||||
language: PropTypes.string.isRequired,
|
||||
transcripts: PropTypes.objectOf(PropTypes.string).isRequired,
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
export const mapStateToProps = (state) => ({
|
||||
openLanguages: selectors.video.openLanguages(state),
|
||||
transcripts: selectors.video.transcripts(state),
|
||||
});
|
||||
|
||||
export const mapDispatchToProps = {};
|
||||
|
||||
export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(LanguageSelect));
|
||||
@@ -0,0 +1,44 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { LanguageSelect } from './LanguageSelect';
|
||||
import { formatMessage } from '../../../../../../../testUtils';
|
||||
|
||||
const lang1 = 'kLinGon';
|
||||
const lang1Code = 'kl';
|
||||
const lang2 = 'eLvIsh';
|
||||
const lang2Code = 'el';
|
||||
const lang3 = 'sImLisH';
|
||||
const lang3Code = 'sl';
|
||||
|
||||
jest.mock('../../../../../../data/constants/video', () => ({
|
||||
videoTranscriptLanguages: {
|
||||
[lang1Code]: lang1,
|
||||
[lang2Code]: lang2,
|
||||
[lang3Code]: lang3,
|
||||
},
|
||||
}));
|
||||
|
||||
describe('LanguageSelect', () => {
|
||||
const props = {
|
||||
intl: { formatMessage },
|
||||
onSelect: jest.fn().mockName('props.OnSelect'),
|
||||
title: 'tITle',
|
||||
language: lang1Code,
|
||||
openLanguages: [[lang2Code, lang2], [lang3Code, lang3]],
|
||||
|
||||
};
|
||||
describe('snapshot', () => {
|
||||
test('transcript option', () => {
|
||||
expect(
|
||||
shallow(<LanguageSelect {...props} />),
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
describe('snapshots -- no', () => {
|
||||
test('transcripts no Open Languages, all should be disabled', () => {
|
||||
expect(
|
||||
shallow(<LanguageSelect {...props} openLanguages={[]} />),
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,108 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect, useDispatch } from 'react-redux';
|
||||
import { FormattedMessage, injectIntl } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
Card, Dropdown, Icon, IconButton, Button,
|
||||
} from '@edx/paragon';
|
||||
|
||||
import { MoreVert } from '@edx/paragon/icons';
|
||||
|
||||
import LanguageSelect from './LanguageSelect';
|
||||
import hooks from './hooks';
|
||||
import { thunkActions, selectors } from '../../../../../../data/redux';
|
||||
import FileInput from '../../../../../../sharedComponents/FileInput';
|
||||
import messages from './messages';
|
||||
|
||||
export const TranscriptListItem = ({
|
||||
title,
|
||||
language,
|
||||
// redux
|
||||
deleteTranscript,
|
||||
getTranscriptDownloadUrl,
|
||||
}) => {
|
||||
const fileInput = hooks.fileInput({ onAddFile: hooks.replaceFileCallback({ language, dispatch: useDispatch() }) });
|
||||
const { inDeleteConfirmation, launchDeleteConfirmation, cancelDelete } = hooks.setUpDeleteConfirmation();
|
||||
const downloadLink = getTranscriptDownloadUrl({ language });
|
||||
|
||||
return (
|
||||
<div className="mb-2">
|
||||
<Card>
|
||||
{ inDeleteConfirmation ? (
|
||||
<>
|
||||
<Card.Header title={(<FormattedMessage {...messages.deleteConfirmationHeader} />)} />
|
||||
<Card.Body>
|
||||
<Card.Section>
|
||||
<FormattedMessage {...messages.deleteConfirmationMessage} />
|
||||
</Card.Section>
|
||||
<Card.Footer>
|
||||
<Button variant="tertiary" className="mb-2 mb-sm-0" onClick={cancelDelete}>
|
||||
<FormattedMessage {...messages.cancelDeleteLabel} />
|
||||
</Button>
|
||||
<Button variant="danger" className="mb-2 mb-sm-0" onClick={() => deleteTranscript({ language })}>
|
||||
<FormattedMessage {...messages.confirmDeleteLabel} />
|
||||
</Button>
|
||||
</Card.Footer>
|
||||
</Card.Body>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Card.Header
|
||||
subtitle={title}
|
||||
actions={(
|
||||
<Dropdown>
|
||||
<Dropdown.Toggle
|
||||
id="dropdown-toggle-with-iconbutton-video-transcript-widget"
|
||||
as={IconButton}
|
||||
src={MoreVert}
|
||||
iconAs={Icon}
|
||||
variant="primary"
|
||||
alt="Actions dropdown"
|
||||
/>
|
||||
<Dropdown.Menu className="video_transcript Action Menu">
|
||||
<Dropdown.Item
|
||||
key={`transcript-actions-${title}-replace`}
|
||||
onClick={fileInput.click}
|
||||
>
|
||||
<FormattedMessage {...messages.replaceTranscript} />
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item key={`transcript-actions-${title}-download`} href={downloadLink}>
|
||||
<FormattedMessage {...messages.downloadTranscript} />
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item key={`transcript-actions-${title}-delete`} onClick={launchDeleteConfirmation}>
|
||||
<FormattedMessage {...messages.deleteTranscript} />
|
||||
</Dropdown.Item>
|
||||
</Dropdown.Menu>
|
||||
<FileInput fileInput={fileInput} acceptedFiles=".srt" />
|
||||
</Dropdown>
|
||||
)}
|
||||
/>
|
||||
<LanguageSelect
|
||||
title={title}
|
||||
language={language}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
TranscriptListItem.propTypes = {
|
||||
title: PropTypes.string.isRequired,
|
||||
language: PropTypes.string.isRequired,
|
||||
// redux
|
||||
deleteTranscript: PropTypes.func.isRequired,
|
||||
getTranscriptDownloadUrl: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export const mapStateToProps = (state) => ({
|
||||
getTranscriptDownloadUrl: selectors.video.getTranscriptDownloadUrl(state),
|
||||
});
|
||||
|
||||
export const mapDispatchToProps = {
|
||||
deleteTranscript: thunkActions.video.deleteTranscript,
|
||||
downloadTranscript: thunkActions.video.downloadTranscript,
|
||||
};
|
||||
|
||||
export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(TranscriptListItem));
|
||||
@@ -0,0 +1,82 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { TranscriptListItem, mapDispatchToProps, mapStateToProps } from './TranscriptListItem';
|
||||
import { thunkActions, selectors } from '../../../../../../data/redux';
|
||||
import hooks from './hooks';
|
||||
|
||||
jest.mock('react-redux', () => {
|
||||
const dispatchFn = jest.fn().mockName('mockUseDispatch');
|
||||
return {
|
||||
...jest.requireActual('react-redux'),
|
||||
dispatch: dispatchFn,
|
||||
useDispatch: jest.fn(() => dispatchFn),
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock('../../../../../../data/redux', () => ({
|
||||
thunkActions: {
|
||||
video: {
|
||||
deleteTranscript: jest.fn().mockName('actions.video.deleteTranscript'),
|
||||
},
|
||||
},
|
||||
selectors: {
|
||||
video: {
|
||||
getTranscriptDownloadUrl: jest.fn(args => ({ getTranscriptDownloadUrl: args })).mockName('selectors.video.getTranscriptDownloadUrl'),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('./hooks', () => ({
|
||||
fileInput: jest.fn((args) => ({ fileInput: args, click: jest.fn().mockName('mockInputClick') })),
|
||||
replaceFileCallback: jest.fn((args) => ({ replaceFileCallback: args })),
|
||||
setUpDeleteConfirmation: jest.fn((args) => ({ setUpDeleteConfirmation: args })).mockName('setUpDeleteConfirmation'),
|
||||
}));
|
||||
|
||||
describe('TranscriptListItem', () => {
|
||||
const props = {
|
||||
getTranscriptDownloadUrl: jest.fn().mockName('selectors..video.getTranscriptDownloadUrl'),
|
||||
title: 'sOmeTiTLE',
|
||||
language: 'lAnG',
|
||||
deleteTranscript: jest.fn().mockName('thunkActions.video.deleteTranscript'),
|
||||
};
|
||||
|
||||
describe('Snapshots', () => {
|
||||
afterAll(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
test('snapshots: renders as expected with default props: dont show confirm delete', () => {
|
||||
jest.spyOn(hooks, 'setUpDeleteConfirmation').mockImplementationOnce(() => ({
|
||||
inDeleteConfirmation: false,
|
||||
launchDeleteConfirmation: jest.fn().mockName('launchDeleteConfirmation'),
|
||||
cancelDelete: jest.fn().mockName('cancelDelete'),
|
||||
}));
|
||||
expect(
|
||||
shallow(<TranscriptListItem {...props} />),
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
test('snapshots: renders as expected with default props: show confirm delete', () => {
|
||||
jest.spyOn(hooks, 'setUpDeleteConfirmation').mockImplementationOnce(() => ({
|
||||
inDeleteConfirmation: true,
|
||||
launchDeleteConfirmation: jest.fn().mockName('launchDeleteConfirmation'),
|
||||
cancelDelete: jest.fn().mockName('cancelDelete'),
|
||||
}));
|
||||
expect(
|
||||
shallow(<TranscriptListItem {...props} />),
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
describe('mapStateToProps', () => {
|
||||
const testState = { A: 'pple', B: 'anana', C: 'ucumber' };
|
||||
test('getTranscriptDownloadUrl from video.getTranscriptDownloadUrl', () => {
|
||||
expect(
|
||||
mapStateToProps(testState).getTranscriptDownloadUrl,
|
||||
).toEqual(selectors.video.getTranscriptDownloadUrl(testState));
|
||||
});
|
||||
});
|
||||
describe('mapDispatchToProps', () => {
|
||||
test('deleteTranscript from thunkActions.video.deleteTranscript', () => {
|
||||
expect(mapDispatchToProps.deleteTranscript).toEqual(thunkActions.video.deleteTranscript);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,65 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`LanguageSelect snapshot transcript option 1`] = `
|
||||
<Form.Group
|
||||
className="mt-2 mx-2"
|
||||
controlId="selectLanguage-form-tITle"
|
||||
>
|
||||
<Form.Control
|
||||
as="select"
|
||||
defaultValue="kl"
|
||||
floatingLabel="Languages"
|
||||
onChange={[Function]}
|
||||
>
|
||||
<option
|
||||
selected={true}
|
||||
value="kl"
|
||||
>
|
||||
kLinGon
|
||||
</option>
|
||||
<option
|
||||
value="el"
|
||||
>
|
||||
eLvIsh
|
||||
</option>
|
||||
<option
|
||||
value="sl"
|
||||
>
|
||||
sImLisH
|
||||
</option>
|
||||
</Form.Control>
|
||||
</Form.Group>
|
||||
`;
|
||||
|
||||
exports[`LanguageSelect snapshots -- no transcripts no Open Languages, all should be disabled 1`] = `
|
||||
<Form.Group
|
||||
className="mt-2 mx-2"
|
||||
controlId="selectLanguage-form-tITle"
|
||||
>
|
||||
<Form.Control
|
||||
as="select"
|
||||
defaultValue="kl"
|
||||
floatingLabel="Languages"
|
||||
onChange={[Function]}
|
||||
>
|
||||
<option
|
||||
selected={true}
|
||||
value="kl"
|
||||
>
|
||||
kLinGon
|
||||
</option>
|
||||
<option
|
||||
disabled={true}
|
||||
value="el"
|
||||
>
|
||||
eLvIsh
|
||||
</option>
|
||||
<option
|
||||
disabled={true}
|
||||
value="sl"
|
||||
>
|
||||
sImLisH
|
||||
</option>
|
||||
</Form.Control>
|
||||
</Form.Group>
|
||||
`;
|
||||
@@ -0,0 +1,124 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`TranscriptListItem Snapshots snapshots: renders as expected with default props: dont show confirm delete 1`] = `
|
||||
<div
|
||||
className="mb-2"
|
||||
>
|
||||
<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_transcript Action Menu"
|
||||
>
|
||||
<Dropdown.Item
|
||||
onClick={[MockFunction mockInputClick]}
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Replace"
|
||||
description="Message Presented To user for action to replace transcript"
|
||||
id="authoring.videoeditor.transcript.replaceTranscript"
|
||||
/>
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item>
|
||||
<FormattedMessage
|
||||
defaultMessage="Download"
|
||||
description="Message Presented To user for action to download transcript"
|
||||
id="authoring.videoeditor.transcript.downloadTranscript"
|
||||
/>
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item
|
||||
onClick={[MockFunction launchDeleteConfirmation]}
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Delete"
|
||||
description="Message Presented To user for action to delete transcript"
|
||||
id="authoring.videoeditor.transcript.deleteTranscript"
|
||||
/>
|
||||
</Dropdown.Item>
|
||||
</Dropdown.Menu>
|
||||
<FileInput
|
||||
acceptedFiles=".srt"
|
||||
fileInput={
|
||||
Object {
|
||||
"click": [MockFunction mockInputClick],
|
||||
"fileInput": Object {
|
||||
"onAddFile": Object {
|
||||
"replaceFileCallback": Object {
|
||||
"dispatch": [MockFunction mockUseDispatch],
|
||||
"language": "lAnG",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
/>
|
||||
</Dropdown>
|
||||
}
|
||||
subtitle="sOmeTiTLE"
|
||||
/>
|
||||
<injectIntl(ShimmedIntlComponent)
|
||||
language="lAnG"
|
||||
title="sOmeTiTLE"
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`TranscriptListItem Snapshots snapshots: renders as expected with default props: show confirm delete 1`] = `
|
||||
<div
|
||||
className="mb-2"
|
||||
>
|
||||
<Card>
|
||||
<Card.Header
|
||||
title={
|
||||
<FormattedMessage
|
||||
defaultMessage="Delete This Transcript?"
|
||||
description="Title for Warning which allows users to select next step in the process of deleting a transcript"
|
||||
id="authoring.videoeditor.transcripts.deleteConfirmationTitle"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Card.Body>
|
||||
<Card.Section>
|
||||
<FormattedMessage
|
||||
defaultMessage="Are you sure you want to delete this transcript?"
|
||||
description="Warning which allows users to select next step in the process of deleting a transcript"
|
||||
id="authoring.videoeditor.transcripts.deleteConfirmationMessage"
|
||||
/>
|
||||
</Card.Section>
|
||||
<Card.Footer>
|
||||
<Button
|
||||
className="mb-2 mb-sm-0"
|
||||
onClick={[MockFunction cancelDelete]}
|
||||
variant="tertiary"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Cancel"
|
||||
description="Label For Button, which allows users to stop the process of deleting a transcript"
|
||||
id="authoring.videoeditor.transcripts.cancelDeleteLabel"
|
||||
/>
|
||||
</Button>
|
||||
<Button
|
||||
className="mb-2 mb-sm-0"
|
||||
onClick={[Function]}
|
||||
variant="danger"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Delete"
|
||||
description="Label For Button, which allows users to confirm the process of deleting a transcript"
|
||||
id="authoring.videoeditor.transcripts.confirmDeleteLabel"
|
||||
/>
|
||||
</Button>
|
||||
</Card.Footer>
|
||||
</Card.Body>
|
||||
</Card>
|
||||
</div>
|
||||
`;
|
||||
@@ -1,13 +1,126 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`TranscriptWidget snapshots snapshots: renders as expected with allowTranscriptDownloads true 1`] = `
|
||||
exports[`TranscriptWidget snapshots snapshot: renders ErrorAlert with delete error message 1`] = `
|
||||
<injectIntl(ShimmedIntlComponent)
|
||||
isError={false}
|
||||
subtitle="english"
|
||||
subtitle="English"
|
||||
title="Transcript"
|
||||
>
|
||||
<ErrorAlert
|
||||
dismissError={[Function]}
|
||||
dismissError={null}
|
||||
hideHeading={true}
|
||||
isError={false}
|
||||
>
|
||||
<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={null}
|
||||
hideHeading={true}
|
||||
isError={true}
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Failed to delete transcript. Please try again."
|
||||
description="Message presented to user when transcript fails to delete"
|
||||
id="authoring.videoeditor.transcript.error.deleteTranscriptError"
|
||||
/>
|
||||
</ErrorAlert>
|
||||
<Stack
|
||||
gap={3}
|
||||
>
|
||||
<Form.Group
|
||||
className="mt-4.5"
|
||||
>
|
||||
<injectIntl(ShimmedIntlComponent)
|
||||
language="en"
|
||||
/>
|
||||
<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>
|
||||
<OverLayTrigger
|
||||
key="right"
|
||||
overlay={
|
||||
<ToolTip>
|
||||
<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"
|
||||
/>
|
||||
</ToolTip>
|
||||
}
|
||||
placement="right"
|
||||
>
|
||||
<Icon
|
||||
className="d-inline-block mx-3"
|
||||
/>
|
||||
</OverLayTrigger>
|
||||
</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 snapshot: renders ErrorAlert with upload error message 1`] = `
|
||||
<injectIntl(ShimmedIntlComponent)
|
||||
isError={false}
|
||||
subtitle="English"
|
||||
title="Transcript"
|
||||
>
|
||||
<ErrorAlert
|
||||
dismissError={null}
|
||||
hideHeading={true}
|
||||
isError={true}
|
||||
>
|
||||
@@ -18,14 +131,14 @@ exports[`TranscriptWidget snapshots snapshots: renders as expected with allowTra
|
||||
/>
|
||||
</ErrorAlert>
|
||||
<ErrorAlert
|
||||
dismissError={[Function]}
|
||||
dismissError={null}
|
||||
hideHeading={true}
|
||||
isError={true}
|
||||
isError={false}
|
||||
>
|
||||
<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"
|
||||
defaultMessage="Failed to delete transcript. Please try again."
|
||||
description="Message presented to user when transcript fails to delete"
|
||||
id="authoring.videoeditor.transcript.error.deleteTranscriptError"
|
||||
/>
|
||||
</ErrorAlert>
|
||||
<Stack
|
||||
@@ -34,9 +147,122 @@ exports[`TranscriptWidget snapshots snapshots: renders as expected with allowTra
|
||||
<Form.Group
|
||||
className="mt-4.5"
|
||||
>
|
||||
<b>
|
||||
Transcript widget:
|
||||
</b>
|
||||
<injectIntl(ShimmedIntlComponent)
|
||||
language="en"
|
||||
/>
|
||||
<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>
|
||||
<OverLayTrigger
|
||||
key="right"
|
||||
overlay={
|
||||
<ToolTip>
|
||||
<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"
|
||||
/>
|
||||
</ToolTip>
|
||||
}
|
||||
placement="right"
|
||||
>
|
||||
<Icon
|
||||
className="d-inline-block mx-3"
|
||||
/>
|
||||
</OverLayTrigger>
|
||||
</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 allowTranscriptDownloads true 1`] = `
|
||||
<injectIntl(ShimmedIntlComponent)
|
||||
isError={false}
|
||||
subtitle="English"
|
||||
title="Transcript"
|
||||
>
|
||||
<ErrorAlert
|
||||
dismissError={null}
|
||||
hideHeading={true}
|
||||
isError={false}
|
||||
>
|
||||
<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={null}
|
||||
hideHeading={true}
|
||||
isError={false}
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Failed to delete transcript. Please try again."
|
||||
description="Message presented to user when transcript fails to delete"
|
||||
id="authoring.videoeditor.transcript.error.deleteTranscriptError"
|
||||
/>
|
||||
</ErrorAlert>
|
||||
<Stack
|
||||
gap={3}
|
||||
>
|
||||
<Form.Group
|
||||
className="mt-4.5"
|
||||
>
|
||||
<injectIntl(ShimmedIntlComponent)
|
||||
language="en"
|
||||
/>
|
||||
<div
|
||||
className="mb-1"
|
||||
>
|
||||
@@ -53,23 +279,23 @@ exports[`TranscriptWidget snapshots snapshots: renders as expected with allowTra
|
||||
/>
|
||||
</Form.Label>
|
||||
</Form.Checkbox>
|
||||
<Component
|
||||
<OverLayTrigger
|
||||
key="right"
|
||||
overlay={
|
||||
<UNDEFINED>
|
||||
<ToolTip>
|
||||
<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>
|
||||
</ToolTip>
|
||||
}
|
||||
placement="right"
|
||||
>
|
||||
<Icon
|
||||
className="d-inline-block mx-3"
|
||||
/>
|
||||
</Component>
|
||||
</OverLayTrigger>
|
||||
</div>
|
||||
<Form.Checkbox
|
||||
checked={false}
|
||||
@@ -120,9 +346,9 @@ exports[`TranscriptWidget snapshots snapshots: renders as expected with default
|
||||
title="Transcript"
|
||||
>
|
||||
<ErrorAlert
|
||||
dismissError={[Function]}
|
||||
dismissError={null}
|
||||
hideHeading={true}
|
||||
isError={true}
|
||||
isError={false}
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Failed to upload transcript. Please try again."
|
||||
@@ -131,14 +357,14 @@ exports[`TranscriptWidget snapshots snapshots: renders as expected with default
|
||||
/>
|
||||
</ErrorAlert>
|
||||
<ErrorAlert
|
||||
dismissError={[Function]}
|
||||
dismissError={null}
|
||||
hideHeading={true}
|
||||
isError={true}
|
||||
isError={false}
|
||||
>
|
||||
<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"
|
||||
defaultMessage="Failed to delete transcript. Please try again."
|
||||
description="Message presented to user when transcript fails to delete"
|
||||
id="authoring.videoeditor.transcript.error.deleteTranscriptError"
|
||||
/>
|
||||
</ErrorAlert>
|
||||
<Stack
|
||||
@@ -147,7 +373,11 @@ exports[`TranscriptWidget snapshots snapshots: renders as expected with default
|
||||
<Alert
|
||||
variant="danger"
|
||||
>
|
||||
Only SRT files can be uploaded. Please select a file ending in .srt to upload.
|
||||
<FormattedMessage
|
||||
defaultMessage="Only SRT files can be uploaded. Please select a file ending in .srt to upload."
|
||||
description="Message warning users to only upload .srt files"
|
||||
id="authoring.videoeditor.transcripts.fileTypeWarning"
|
||||
/>
|
||||
</Alert>
|
||||
<FormattedMessage
|
||||
defaultMessage="Add video transcripts for improved accessibility."
|
||||
@@ -183,13 +413,13 @@ exports[`TranscriptWidget snapshots snapshots: renders as expected with default
|
||||
exports[`TranscriptWidget snapshots snapshots: renders as expected with showTranscriptByDefault true 1`] = `
|
||||
<injectIntl(ShimmedIntlComponent)
|
||||
isError={false}
|
||||
subtitle="english"
|
||||
subtitle="English"
|
||||
title="Transcript"
|
||||
>
|
||||
<ErrorAlert
|
||||
dismissError={[Function]}
|
||||
dismissError={null}
|
||||
hideHeading={true}
|
||||
isError={true}
|
||||
isError={false}
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Failed to upload transcript. Please try again."
|
||||
@@ -198,14 +428,14 @@ exports[`TranscriptWidget snapshots snapshots: renders as expected with showTran
|
||||
/>
|
||||
</ErrorAlert>
|
||||
<ErrorAlert
|
||||
dismissError={[Function]}
|
||||
dismissError={null}
|
||||
hideHeading={true}
|
||||
isError={true}
|
||||
isError={false}
|
||||
>
|
||||
<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"
|
||||
defaultMessage="Failed to delete transcript. Please try again."
|
||||
description="Message presented to user when transcript fails to delete"
|
||||
id="authoring.videoeditor.transcript.error.deleteTranscriptError"
|
||||
/>
|
||||
</ErrorAlert>
|
||||
<Stack
|
||||
@@ -214,9 +444,9 @@ exports[`TranscriptWidget snapshots snapshots: renders as expected with showTran
|
||||
<Form.Group
|
||||
className="mt-4.5"
|
||||
>
|
||||
<b>
|
||||
Transcript widget:
|
||||
</b>
|
||||
<injectIntl(ShimmedIntlComponent)
|
||||
language="en"
|
||||
/>
|
||||
<div
|
||||
className="mb-1"
|
||||
>
|
||||
@@ -233,23 +463,23 @@ exports[`TranscriptWidget snapshots snapshots: renders as expected with showTran
|
||||
/>
|
||||
</Form.Label>
|
||||
</Form.Checkbox>
|
||||
<Component
|
||||
<OverLayTrigger
|
||||
key="right"
|
||||
overlay={
|
||||
<UNDEFINED>
|
||||
<ToolTip>
|
||||
<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>
|
||||
</ToolTip>
|
||||
}
|
||||
placement="right"
|
||||
>
|
||||
<Icon
|
||||
className="d-inline-block mx-3"
|
||||
/>
|
||||
</Component>
|
||||
</OverLayTrigger>
|
||||
</div>
|
||||
<Form.Checkbox
|
||||
checked={true}
|
||||
@@ -296,13 +526,13 @@ exports[`TranscriptWidget snapshots snapshots: renders as expected with showTran
|
||||
exports[`TranscriptWidget snapshots snapshots: renders as expected with transcripts 1`] = `
|
||||
<injectIntl(ShimmedIntlComponent)
|
||||
isError={false}
|
||||
subtitle="english"
|
||||
subtitle="English"
|
||||
title="Transcript"
|
||||
>
|
||||
<ErrorAlert
|
||||
dismissError={[Function]}
|
||||
dismissError={null}
|
||||
hideHeading={true}
|
||||
isError={true}
|
||||
isError={false}
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Failed to upload transcript. Please try again."
|
||||
@@ -311,14 +541,14 @@ exports[`TranscriptWidget snapshots snapshots: renders as expected with transcri
|
||||
/>
|
||||
</ErrorAlert>
|
||||
<ErrorAlert
|
||||
dismissError={[Function]}
|
||||
dismissError={null}
|
||||
hideHeading={true}
|
||||
isError={true}
|
||||
isError={false}
|
||||
>
|
||||
<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"
|
||||
defaultMessage="Failed to delete transcript. Please try again."
|
||||
description="Message presented to user when transcript fails to delete"
|
||||
id="authoring.videoeditor.transcript.error.deleteTranscriptError"
|
||||
/>
|
||||
</ErrorAlert>
|
||||
<Stack
|
||||
@@ -327,9 +557,9 @@ exports[`TranscriptWidget snapshots snapshots: renders as expected with transcri
|
||||
<Form.Group
|
||||
className="mt-4.5"
|
||||
>
|
||||
<b>
|
||||
Transcript widget:
|
||||
</b>
|
||||
<injectIntl(ShimmedIntlComponent)
|
||||
language="en"
|
||||
/>
|
||||
<div
|
||||
className="mb-1"
|
||||
>
|
||||
@@ -346,23 +576,23 @@ exports[`TranscriptWidget snapshots snapshots: renders as expected with transcri
|
||||
/>
|
||||
</Form.Label>
|
||||
</Form.Checkbox>
|
||||
<Component
|
||||
<OverLayTrigger
|
||||
key="right"
|
||||
overlay={
|
||||
<UNDEFINED>
|
||||
<ToolTip>
|
||||
<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>
|
||||
</ToolTip>
|
||||
}
|
||||
placement="right"
|
||||
>
|
||||
<Icon
|
||||
className="d-inline-block mx-3"
|
||||
/>
|
||||
</Component>
|
||||
</OverLayTrigger>
|
||||
</div>
|
||||
<Form.Checkbox
|
||||
checked={false}
|
||||
|
||||
@@ -1,24 +1,63 @@
|
||||
import React from 'react';
|
||||
import { thunkActions, actions } from '../../../../../../data/redux';
|
||||
import * as module from './hooks';
|
||||
import { videoTranscriptLanguages } from '../../../../../../data/constants/video';
|
||||
|
||||
export const state = {
|
||||
inDeleteConfirmation: (args) => React.useState(args),
|
||||
};
|
||||
|
||||
export const transcriptLanguages = (transcripts) => {
|
||||
const languages = [];
|
||||
if (transcripts) {
|
||||
if (Object.keys(transcripts).length > 0) {
|
||||
Object.keys(transcripts).forEach(transcript => {
|
||||
languages.push(transcript);
|
||||
languages.push(videoTranscriptLanguages[transcript]);
|
||||
});
|
||||
return languages.join(', ');
|
||||
}
|
||||
return 'None';
|
||||
};
|
||||
|
||||
export const fileInput = () => {
|
||||
export const hasTranscripts = (transcripts) => {
|
||||
if (transcripts && Object.keys(transcripts).length > 0) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
export const onSelectLanguage = ({
|
||||
filename, dispatch, transcripts, languageBeforeChange,
|
||||
}) => (e) => {
|
||||
const { [languageBeforeChange]: removedProperty, ...trimmedTranscripts } = transcripts;
|
||||
const newTranscripts = { [e.target.value]: { filename }, ...trimmedTranscripts };
|
||||
dispatch(actions.video.updateField({ transcripts: newTranscripts }));
|
||||
};
|
||||
|
||||
export const replaceFileCallback = ({ language, dispatch }) => (file) => {
|
||||
dispatch(thunkActions.video.replaceTranscript({
|
||||
newFile: file,
|
||||
newFilename: file.name,
|
||||
language,
|
||||
}));
|
||||
};
|
||||
|
||||
export const addFileCallback = ({ dispatch }) => (file) => {
|
||||
dispatch(thunkActions.video.uploadTranscript({
|
||||
file,
|
||||
filename: file.name,
|
||||
language: null,
|
||||
}));
|
||||
};
|
||||
|
||||
export const fileInput = ({ onAddFile }) => {
|
||||
const ref = React.useRef();
|
||||
const click = () => ref.current.click();
|
||||
const addFile = (e) => {
|
||||
const selectedFile = e.target.files[0];
|
||||
console.log(selectedFile);
|
||||
const file = e.target.files[0];
|
||||
if (file) {
|
||||
onAddFile(file);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
click,
|
||||
addFile,
|
||||
@@ -26,4 +65,20 @@ export const fileInput = () => {
|
||||
};
|
||||
};
|
||||
|
||||
export default { transcriptLanguages, fileInput };
|
||||
export const setUpDeleteConfirmation = () => {
|
||||
const [inDeleteConfirmation, setInDeleteConfirmation] = module.state.inDeleteConfirmation(false);
|
||||
return {
|
||||
inDeleteConfirmation,
|
||||
launchDeleteConfirmation: () => setInDeleteConfirmation(true),
|
||||
cancelDelete: () => setInDeleteConfirmation(false),
|
||||
};
|
||||
};
|
||||
|
||||
export default {
|
||||
transcriptLanguages,
|
||||
fileInput,
|
||||
onSelectLanguage,
|
||||
replaceFileCallback,
|
||||
addFileCallback,
|
||||
setUpDeleteConfirmation,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
import * as module from './hooks';
|
||||
|
||||
import { actions, thunkActions } from '../../../../../../data/redux';
|
||||
import { MockUseState } from '../../../../../../../testUtils';
|
||||
|
||||
const lang1 = 'Kalaallisut';
|
||||
const lang2 = 'Greek';
|
||||
const lang1Code = 'kl';
|
||||
const lang2Code = 'el';
|
||||
const transcript1 = 'fIlEnAme1.srt';
|
||||
const transcript2 = 'fIlenAME2.srt';
|
||||
|
||||
const transcripts = {
|
||||
[lang1Code]: {
|
||||
filename: transcript1,
|
||||
},
|
||||
[lang2Code]: {
|
||||
filename: transcript2,
|
||||
},
|
||||
};
|
||||
|
||||
jest.mock('../../../../../../data/redux', () => ({
|
||||
thunkActions: {
|
||||
video: {
|
||||
replaceTranscript: jest.fn(args => ({ replaceTranscript: args })).mockName('thunkActions.video.replaceTranscript'),
|
||||
uploadTranscript: jest.fn(args => ({ uploadTranscript: args })).mockName('thunkActions.video.uploadTranscript'),
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
video: {
|
||||
updateField: jest.fn(args => ({ updateField: args })).mockName('actions.video.updateField'),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
describe('VideoEditorTranscript hooks', () => {
|
||||
describe('transcriptLanguages', () => {
|
||||
test('it returns none when given empty object', () => {
|
||||
expect(module.transcriptLanguages({})).toEqual('None');
|
||||
});
|
||||
test('it creates a list based on transcript object', () => {
|
||||
expect(module.transcriptLanguages(transcripts)).toEqual(`${lang1}, ${lang2}`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('onSelectLanguage', () => {
|
||||
const mockLangValue = 'soMeLanGuaGeCoDE';
|
||||
const mockEvent = { target: { value: mockLangValue } };
|
||||
const mockDispatch = jest.fn();
|
||||
|
||||
test('it dispatches the correct thunk', () => {
|
||||
const cb = module.onSelectLanguage({
|
||||
filename: transcript1, dispatch: mockDispatch, transcripts, languageBeforeChange: lang1Code,
|
||||
});
|
||||
const newTranscripts = {
|
||||
transcripts: { [lang2Code]: { filename: transcript2 }, [mockLangValue]: { filename: transcript1 } },
|
||||
};
|
||||
cb(mockEvent);
|
||||
expect(actions.video.updateField).toHaveBeenCalledWith(newTranscripts);
|
||||
expect(mockDispatch).toHaveBeenCalledWith({ updateField: newTranscripts });
|
||||
});
|
||||
});
|
||||
|
||||
describe('replaceFileCallback', () => {
|
||||
const mockFile = 'sOmeEbytes';
|
||||
const mockFileName = 'one.srt';
|
||||
const mockEvent = { mockFile, name: mockFileName };
|
||||
const mockDispatch = jest.fn();
|
||||
|
||||
const result = { newFile: { mockFile, name: mockFileName }, newFilename: mockFileName, language: lang1Code };
|
||||
|
||||
test('it dispatches the correct thunk', () => {
|
||||
const cb = module.replaceFileCallback({
|
||||
dispatch: mockDispatch, language: lang1Code,
|
||||
});
|
||||
cb(mockEvent);
|
||||
expect(thunkActions.video.replaceTranscript).toHaveBeenCalledWith(result);
|
||||
expect(mockDispatch).toHaveBeenCalledWith({ replaceTranscript: result });
|
||||
});
|
||||
});
|
||||
describe('addFileCallback', () => {
|
||||
const mockFile = 'sOmeEbytes';
|
||||
const mockFileName = 'one.srt';
|
||||
const mockEvent = { mockFile, name: mockFileName };
|
||||
const mockDispatch = jest.fn();
|
||||
|
||||
const result = { file: { mockFile, name: mockFileName }, filename: mockFileName, language: null };
|
||||
|
||||
test('it dispatches the correct thunk', () => {
|
||||
const cb = module.addFileCallback({
|
||||
dispatch: mockDispatch,
|
||||
});
|
||||
cb(mockEvent);
|
||||
expect(thunkActions.video.uploadTranscript).toHaveBeenCalledWith(result);
|
||||
expect(mockDispatch).toHaveBeenCalledWith({ uploadTranscript: result });
|
||||
});
|
||||
});
|
||||
|
||||
describe('state hooks', () => {
|
||||
const state = new MockUseState(module);
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
describe('state hooks', () => {
|
||||
state.testGetter(state.keys.inDeleteConfirmation);
|
||||
});
|
||||
|
||||
describe('setUpDeleteConfirmation hook', () => {
|
||||
beforeEach(() => {
|
||||
state.mock();
|
||||
});
|
||||
afterEach(() => {
|
||||
state.restore();
|
||||
});
|
||||
test('inDeleteConfirmation: state values', () => {
|
||||
expect(module.setUpDeleteConfirmation().inDeleteConfirmation).toEqual(false);
|
||||
});
|
||||
test('inDeleteConfirmation setters: launch', () => {
|
||||
module.setUpDeleteConfirmation().launchDeleteConfirmation();
|
||||
expect(state.setState[state.keys.inDeleteConfirmation]).toHaveBeenCalledWith(true);
|
||||
});
|
||||
test('inDeleteConfirmation setters: cancel', () => {
|
||||
module.setUpDeleteConfirmation().cancelDelete();
|
||||
expect(state.setState[state.keys.inDeleteConfirmation]).toHaveBeenCalledWith(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { connect, useDispatch } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import {
|
||||
@@ -21,10 +21,14 @@ import { actions, selectors } from '../../../../../../data/redux';
|
||||
import * as hooks from './hooks';
|
||||
import messages from './messages';
|
||||
|
||||
import { RequestKeys } from '../../../../../../data/constants/requests';
|
||||
|
||||
import FileInput from '../../../../../../sharedComponents/FileInput';
|
||||
import ErrorAlert from '../../../../../../sharedComponents/ErrorAlerts/ErrorAlert';
|
||||
import CollapsibleFormWidget from '../CollapsibleFormWidget';
|
||||
|
||||
import TranscriptListItem from './TranscriptListItem';
|
||||
|
||||
/**
|
||||
* Collapsible Form widget controlling video transcripts
|
||||
*/
|
||||
@@ -35,21 +39,12 @@ export const TranscriptWidget = ({
|
||||
allowTranscriptDownloads,
|
||||
showTranscriptByDefault,
|
||||
updateField,
|
||||
isUploadError,
|
||||
isDeleteError,
|
||||
}) => {
|
||||
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,
|
||||
},
|
||||
};
|
||||
const fileInput = hooks.fileInput({ onAddFile: hooks.addFileCallback({ dispatch: useDispatch() }) });
|
||||
const hasTranscripts = hooks.hasTranscripts(transcripts);
|
||||
|
||||
return (
|
||||
<CollapsibleFormWidget
|
||||
@@ -58,23 +53,27 @@ export const TranscriptWidget = ({
|
||||
title="Transcript"
|
||||
>
|
||||
<ErrorAlert
|
||||
dismissError={upload.error.dismiss}
|
||||
hideHeading
|
||||
isError={upload.error.show}
|
||||
isError={isUploadError}
|
||||
>
|
||||
<FormattedMessage {...messages.uploadTranscriptError} />
|
||||
</ErrorAlert>
|
||||
<ErrorAlert
|
||||
dismissError={input.error.dismiss}
|
||||
hideHeading
|
||||
isError={input.error.show}
|
||||
isError={isDeleteError}
|
||||
>
|
||||
<FormattedMessage {...messages.fileSizeError} />
|
||||
<FormattedMessage {...messages.deleteTranscriptError} />
|
||||
</ErrorAlert>
|
||||
<Stack gap={3}>
|
||||
{transcripts ? (
|
||||
{hasTranscripts ? (
|
||||
|
||||
<Form.Group className="mt-4.5">
|
||||
<b>Transcript widget:</b>
|
||||
{ Object.entries(transcripts).map(([language, value]) => (
|
||||
<TranscriptListItem
|
||||
language={language}
|
||||
title={value.filename}
|
||||
/>
|
||||
))}
|
||||
<div className="mb-1">
|
||||
<Form.Checkbox
|
||||
checked={allowTranscriptDownloads}
|
||||
@@ -109,8 +108,8 @@ export const TranscriptWidget = ({
|
||||
</Form.Group>
|
||||
) : (
|
||||
<>
|
||||
<Alert variant="danger" icon={Info}>
|
||||
Only SRT files can be uploaded. Please select a file ending in .srt to upload.
|
||||
<Alert variant="danger">
|
||||
<FormattedMessage {...messages.fileTypeWarning} />
|
||||
</Alert>
|
||||
<FormattedMessage {...messages.addFirstTranscript} />
|
||||
</>
|
||||
@@ -134,11 +133,15 @@ TranscriptWidget.propTypes = {
|
||||
allowTranscriptDownloads: PropTypes.bool.isRequired,
|
||||
showTranscriptByDefault: PropTypes.bool.isRequired,
|
||||
updateField: PropTypes.func.isRequired,
|
||||
isUploadError: PropTypes.bool.isRequired,
|
||||
isDeleteError: PropTypes.bool.isRequired,
|
||||
};
|
||||
export const mapStateToProps = (state) => ({
|
||||
transcripts: selectors.video.transcripts(state),
|
||||
allowTranscriptDownloads: selectors.video.allowTranscriptDownloads(state),
|
||||
showTranscriptByDefault: selectors.video.showTranscriptByDefault(state),
|
||||
isUploadError: selectors.requests.isFailed(state, { requestKey: RequestKeys.uploadTranscript }),
|
||||
isDeleteError: selectors.requests.isFailed(state, { requestKey: RequestKeys.deleteTranscript }),
|
||||
});
|
||||
|
||||
export const mapDispatchToProps = (dispatch) => ({
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { RequestKeys } from '../../../../../../data/constants/requests';
|
||||
|
||||
import { formatMessage } from '../../../../../../../testUtils';
|
||||
import { actions, selectors } from '../../../../../../data/redux';
|
||||
import { TranscriptWidget, mapStateToProps, mapDispatchToProps } from '.';
|
||||
@@ -11,11 +13,21 @@ jest.mock('../../../../../../data/redux', () => ({
|
||||
updateField: jest.fn().mockName('actions.video.updateField'),
|
||||
},
|
||||
},
|
||||
thunkActions: {
|
||||
video: {
|
||||
deleteTranscript: jest.fn().mockName('actions.video.deleteTranscript'),
|
||||
},
|
||||
},
|
||||
|
||||
selectors: {
|
||||
video: {
|
||||
transcripts: jest.fn(state => ({ transcripts: state })),
|
||||
allowTranscriptDownloads: jest.fn(state => ({ allowTranscriptDownloads: state })),
|
||||
showTranscriptByDefault: jest.fn(state => ({ showTranscriptByDefault: state })),
|
||||
|
||||
},
|
||||
requests: {
|
||||
isFailed: jest.fn(state => ({ isFailed: state })),
|
||||
},
|
||||
},
|
||||
}));
|
||||
@@ -25,13 +37,13 @@ describe('TranscriptWidget', () => {
|
||||
error: {},
|
||||
subtitle: 'SuBTItle',
|
||||
title: 'tiTLE',
|
||||
// inject
|
||||
intl: { formatMessage },
|
||||
// redux
|
||||
transcripts: null,
|
||||
transcripts: {},
|
||||
allowTranscriptDownloads: false,
|
||||
showTranscriptByDefault: false,
|
||||
updateField: jest.fn().mockName('args.updateField'),
|
||||
isUploadError: false,
|
||||
isDeleteError: false,
|
||||
};
|
||||
|
||||
describe('snapshots', () => {
|
||||
@@ -42,17 +54,27 @@ describe('TranscriptWidget', () => {
|
||||
});
|
||||
test('snapshots: renders as expected with transcripts', () => {
|
||||
expect(
|
||||
shallow(<TranscriptWidget {...props} transcripts={{ english: 'sOMeUrl' }} />),
|
||||
shallow(<TranscriptWidget {...props} transcripts={{ en: 'sOMeUrl' }} />),
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
test('snapshots: renders as expected with allowTranscriptDownloads true', () => {
|
||||
expect(
|
||||
shallow(<TranscriptWidget {...props} allowTranscriptDownloads transcripts={{ english: 'sOMeUrl' }} />),
|
||||
shallow(<TranscriptWidget {...props} allowTranscriptDownloads transcripts={{ en: 'sOMeUrl' }} />),
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
test('snapshots: renders as expected with showTranscriptByDefault true', () => {
|
||||
expect(
|
||||
shallow(<TranscriptWidget {...props} showTranscriptByDefault transcripts={{ english: 'sOMeUrl' }} />),
|
||||
shallow(<TranscriptWidget {...props} showTranscriptByDefault transcripts={{ en: 'sOMeUrl' }} />),
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
test('snapshot: renders ErrorAlert with upload error message', () => {
|
||||
expect(
|
||||
shallow(<TranscriptWidget {...props} isUploadError transcripts={{ en: 'sOMeUrl' }} />),
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
test('snapshot: renders ErrorAlert with delete error message', () => {
|
||||
expect(
|
||||
shallow(<TranscriptWidget {...props} isDeleteError transcripts={{ en: 'sOMeUrl' }} />),
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -73,6 +95,16 @@ describe('TranscriptWidget', () => {
|
||||
mapStateToProps(testState).showTranscriptByDefault,
|
||||
).toEqual(selectors.video.showTranscriptByDefault(testState));
|
||||
});
|
||||
test('isUploadError from requests.isFinished', () => {
|
||||
expect(
|
||||
mapStateToProps(testState).isUploadError,
|
||||
).toEqual(selectors.requests.isFailed(testState, { requestKey: RequestKeys.uploadTranscript }));
|
||||
});
|
||||
test('isDeleteError from requests.isFinished', () => {
|
||||
expect(
|
||||
mapStateToProps(testState).isDeleteError,
|
||||
).toEqual(selectors.requests.isFailed(testState, { requestKey: RequestKeys.deleteTranscript }));
|
||||
});
|
||||
});
|
||||
describe('mapDispatchToProps', () => {
|
||||
const dispatch = jest.fn();
|
||||
|
||||
@@ -39,6 +39,56 @@ export const messages = {
|
||||
defaultMessage: 'Transcript file size exeeds the maximum. Please try again.',
|
||||
description: 'Message presented to user when transcript file size is too large',
|
||||
},
|
||||
deleteTranscript: {
|
||||
id: 'authoring.videoeditor.transcript.deleteTranscript',
|
||||
defaultMessage: 'Delete',
|
||||
description: 'Message Presented To user for action to delete transcript',
|
||||
},
|
||||
deleteTranscriptError: {
|
||||
id: 'authoring.videoeditor.transcript.error.deleteTranscriptError',
|
||||
defaultMessage: 'Failed to delete transcript. Please try again.',
|
||||
description: 'Message presented to user when transcript fails to delete',
|
||||
},
|
||||
replaceTranscript: {
|
||||
id: 'authoring.videoeditor.transcript.replaceTranscript',
|
||||
defaultMessage: 'Replace',
|
||||
description: 'Message Presented To user for action to replace transcript',
|
||||
},
|
||||
downloadTranscript: {
|
||||
id: 'authoring.videoeditor.transcript.downloadTranscript',
|
||||
defaultMessage: 'Download',
|
||||
description: 'Message Presented To user for action to download transcript',
|
||||
},
|
||||
languageSelectLabel: {
|
||||
id: 'authoring.videoeditor.transcripts.languageSelectLabel',
|
||||
defaultMessage: 'Languages',
|
||||
description: 'Label For Dropdown, which allows users to set the language associtated with a transcript',
|
||||
},
|
||||
cancelDeleteLabel: {
|
||||
id: 'authoring.videoeditor.transcripts.cancelDeleteLabel',
|
||||
defaultMessage: 'Cancel',
|
||||
description: 'Label For Button, which allows users to stop the process of deleting a transcript',
|
||||
},
|
||||
confirmDeleteLabel: {
|
||||
id: 'authoring.videoeditor.transcripts.confirmDeleteLabel',
|
||||
defaultMessage: 'Delete',
|
||||
description: 'Label For Button, which allows users to confirm the process of deleting a transcript',
|
||||
},
|
||||
deleteConfirmationMessage: {
|
||||
id: 'authoring.videoeditor.transcripts.deleteConfirmationMessage',
|
||||
defaultMessage: 'Are you sure you want to delete this transcript?',
|
||||
description: 'Warning which allows users to select next step in the process of deleting a transcript',
|
||||
},
|
||||
deleteConfirmationHeader: {
|
||||
id: 'authoring.videoeditor.transcripts.deleteConfirmationTitle',
|
||||
defaultMessage: 'Delete This Transcript?',
|
||||
description: 'Title for Warning which allows users to select next step in the process of deleting a transcript',
|
||||
},
|
||||
fileTypeWarning: {
|
||||
id: 'authoring.videoeditor.transcripts.fileTypeWarning',
|
||||
defaultMessage: 'Only SRT files can be uploaded. Please select a file ending in .srt to upload.',
|
||||
description: 'Message warning users to only upload .srt files',
|
||||
},
|
||||
};
|
||||
|
||||
export default messages;
|
||||
|
||||
@@ -14,4 +14,6 @@ export const RequestKeys = StrictDict({
|
||||
fetchUnit: 'fetchUnit',
|
||||
saveBlock: 'saveBlock',
|
||||
uploadImage: 'uploadImage',
|
||||
uploadTranscript: 'uploadTranscript',
|
||||
deleteTranscript: 'deleteTranscript',
|
||||
});
|
||||
|
||||
@@ -1,5 +1,194 @@
|
||||
import { StrictDict } from '../../utils';
|
||||
|
||||
export const videoTranscriptLanguages = StrictDict({
|
||||
aa: 'Afar',
|
||||
ab: 'Abkhazian',
|
||||
af: 'Afrikaans',
|
||||
ak: 'Akan',
|
||||
sq: 'Albanian',
|
||||
am: 'Amharic',
|
||||
ar: 'Arabic',
|
||||
an: 'Aragonese',
|
||||
hy: 'Armenian',
|
||||
as: 'Assamese',
|
||||
av: 'Avaric',
|
||||
ae: 'Avestan',
|
||||
ay: 'Aymara',
|
||||
az: 'Azerbaijani',
|
||||
ba: 'Bashkir',
|
||||
bm: 'Bambara',
|
||||
eu: 'Basque',
|
||||
be: 'Belarusian',
|
||||
bn: 'Bengali',
|
||||
bh: 'Bihari languages',
|
||||
bi: 'Bislama',
|
||||
bs: 'Bosnian',
|
||||
br: 'Breton',
|
||||
bg: 'Bulgarian',
|
||||
my: 'Burmese',
|
||||
ca: 'Catalan',
|
||||
ch: 'Chamorro',
|
||||
ce: 'Chechen',
|
||||
zh: 'Chinese',
|
||||
zh_HANS: 'Simplified Chinese',
|
||||
zh_HANT: 'Traditional Chinese',
|
||||
cu: 'Church Slavic',
|
||||
cv: 'Chuvash',
|
||||
kw: 'Cornish',
|
||||
co: 'Corsican',
|
||||
cr: 'Cree',
|
||||
cs: 'Czech',
|
||||
da: 'Danish',
|
||||
dv: 'Divehi',
|
||||
nl: 'Dutch',
|
||||
dz: 'Dzongkha',
|
||||
en: 'English',
|
||||
eo: 'Esperanto',
|
||||
et: 'Estonian',
|
||||
ee: 'Ewe',
|
||||
fo: 'Faroese',
|
||||
fj: 'Fijian',
|
||||
fi: 'Finnish',
|
||||
fr: 'French',
|
||||
fy: 'Western Frisian',
|
||||
ff: 'Fulah',
|
||||
ka: 'Georgian',
|
||||
de: 'German',
|
||||
gd: 'Gaelic',
|
||||
ga: 'Irish',
|
||||
gl: 'Galician',
|
||||
gv: 'Manx',
|
||||
el: 'Greek',
|
||||
gn: 'Guarani',
|
||||
gu: 'Gujarati',
|
||||
ht: 'Haitian',
|
||||
ha: 'Hausa',
|
||||
he: 'Hebrew',
|
||||
hz: 'Herero',
|
||||
hi: 'Hindi',
|
||||
ho: 'Hiri Motu',
|
||||
hr: 'Croatian',
|
||||
hu: 'Hungarian',
|
||||
ig: 'Igbo',
|
||||
is: 'Icelandic',
|
||||
io: 'Ido',
|
||||
ii: 'Sichuan Yi',
|
||||
iu: 'Inuktitut',
|
||||
ie: 'Interlingue',
|
||||
ia: 'Interlingua',
|
||||
id: 'Indonesian',
|
||||
ik: 'Inupiaq',
|
||||
it: 'Italian',
|
||||
jv: 'Javanese',
|
||||
ja: 'Japanese',
|
||||
kl: 'Kalaallisut',
|
||||
kn: 'Kannada',
|
||||
ks: 'Kashmiri',
|
||||
kr: 'Kanuri',
|
||||
kk: 'Kazakh',
|
||||
km: 'Central Khmer',
|
||||
ki: 'Kikuyu',
|
||||
rw: 'Kinyarwanda',
|
||||
ky: 'Kirghiz',
|
||||
kv: 'Komi',
|
||||
kg: 'Kongo',
|
||||
ko: 'Korean',
|
||||
kj: 'Kuanyama',
|
||||
ku: 'Kurdish',
|
||||
lo: 'Lao',
|
||||
la: 'Latin',
|
||||
lv: 'Latvian',
|
||||
li: 'Limburgan',
|
||||
ln: 'Lingala',
|
||||
lt: 'Lithuanian',
|
||||
lb: 'Luxembourgish',
|
||||
lu: 'Luba-Katanga',
|
||||
lg: 'Ganda',
|
||||
mk: 'Macedonian',
|
||||
mh: 'Marshallese',
|
||||
ml: 'Malayalam',
|
||||
mi: 'Maori',
|
||||
mr: 'Marathi',
|
||||
ms: 'Malay',
|
||||
mg: 'Malagasy',
|
||||
mt: 'Maltese',
|
||||
mn: 'Mongolian',
|
||||
na: 'Nauru',
|
||||
nv: 'Navajo',
|
||||
nr: 'Ndebele: South',
|
||||
nd: 'Ndebele: North',
|
||||
ng: 'Ndonga',
|
||||
ne: 'Nepali',
|
||||
nn: 'Norwegian Nynorsk',
|
||||
nb: 'Bokmål: Norwegian',
|
||||
no: 'Norwegian',
|
||||
ny: 'Chichewa',
|
||||
oc: 'Occitan',
|
||||
oj: 'Ojibwa',
|
||||
or: 'Oriya',
|
||||
om: 'Oromo',
|
||||
os: 'Ossetian',
|
||||
pa: 'Panjabi',
|
||||
fa: 'Persian',
|
||||
pi: 'Pali',
|
||||
pl: 'Polish',
|
||||
pt: 'Portuguese',
|
||||
ps: 'Pushto',
|
||||
qu: 'Quechua',
|
||||
rm: 'Romansh',
|
||||
ro: 'Romanian',
|
||||
rn: 'Rundi',
|
||||
ru: 'Russian',
|
||||
sg: 'Sango',
|
||||
sa: 'Sanskrit',
|
||||
si: 'Sinhala',
|
||||
sk: 'Slovak',
|
||||
sl: 'Slovenian',
|
||||
se: 'Northern Sami',
|
||||
sm: 'Samoan',
|
||||
sn: 'Shona',
|
||||
sd: 'Sindhi',
|
||||
so: 'Somali',
|
||||
st: 'Sotho: Southern',
|
||||
es: 'Spanish',
|
||||
sc: 'Sardinian',
|
||||
sr: 'Serbian',
|
||||
ss: 'Swati',
|
||||
su: 'Sundanese',
|
||||
sw: 'Swahili',
|
||||
sv: 'Swedish',
|
||||
ty: 'Tahitian',
|
||||
ta: 'Tamil',
|
||||
tt: 'Tatar',
|
||||
te: 'Telugu',
|
||||
tg: 'Tajik',
|
||||
tl: 'Tagalog',
|
||||
th: 'Thai',
|
||||
bo: 'Tibetan',
|
||||
ti: 'Tigrinya',
|
||||
to: 'Tonga (Tonga Islands)',
|
||||
tn: 'Tswana',
|
||||
ts: 'Tsonga',
|
||||
tk: 'Turkmen',
|
||||
tr: 'Turkish',
|
||||
tw: 'Twi',
|
||||
ug: 'Uighur',
|
||||
uk: 'Ukrainian',
|
||||
ur: 'Urdu',
|
||||
uz: 'Uzbek',
|
||||
ve: 'Venda',
|
||||
vi: 'Vietnamese',
|
||||
vo: 'Volapük',
|
||||
cy: 'Welsh',
|
||||
wa: 'Walloon',
|
||||
wo: 'Wolof',
|
||||
xh: 'Xhosa',
|
||||
yi: 'Yiddish',
|
||||
yo: 'Yoruba',
|
||||
za: 'Zhuang',
|
||||
zu: 'Zulu',
|
||||
});
|
||||
|
||||
export const timeKeys = StrictDict({
|
||||
startTime: 'startTime',
|
||||
stopTime: 'stopTime',
|
||||
@@ -7,4 +196,5 @@ export const timeKeys = StrictDict({
|
||||
|
||||
export default {
|
||||
timeKeys,
|
||||
videoTranscriptLanguages,
|
||||
};
|
||||
|
||||
@@ -11,6 +11,8 @@ const initialState = {
|
||||
[RequestKeys.saveBlock]: { status: RequestStates.inactive },
|
||||
[RequestKeys.fetchImages]: { status: RequestStates.inactive },
|
||||
[RequestKeys.uploadImage]: { status: RequestStates.inactive },
|
||||
[RequestKeys.uploadTranscript]: { status: RequestStates.inactive },
|
||||
[RequestKeys.deleteTranscript]: { status: RequestStates.inactive },
|
||||
|
||||
};
|
||||
|
||||
|
||||
@@ -135,6 +135,38 @@ export const fetchImages = ({ ...rest }) => (dispatch, getState) => {
|
||||
}));
|
||||
};
|
||||
|
||||
export const deleteTranscript = ({ language, videoId, ...rest }) => (dispatch, getState) => {
|
||||
dispatch(module.networkRequest({
|
||||
requestKey: RequestKeys.deleteTranscript,
|
||||
promise: api.deleteTranscript({
|
||||
blockId: selectors.app.blockId(getState()),
|
||||
language,
|
||||
videoId,
|
||||
studioEndpointUrl: selectors.app.studioEndpointUrl(getState()),
|
||||
}),
|
||||
...rest,
|
||||
}));
|
||||
};
|
||||
|
||||
export const uploadTranscript = ({
|
||||
transcript,
|
||||
videoId,
|
||||
language,
|
||||
...rest
|
||||
}) => (dispatch, getState) => {
|
||||
dispatch(module.networkRequest({
|
||||
requestKey: RequestKeys.uploadTranscript,
|
||||
promise: api.uploadTranscript({
|
||||
blockId: selectors.app.blockId(getState()),
|
||||
transcript,
|
||||
videoId,
|
||||
language,
|
||||
studioEndpointUrl: selectors.app.studioEndpointUrl(getState()),
|
||||
}),
|
||||
...rest,
|
||||
}));
|
||||
};
|
||||
|
||||
export default StrictDict({
|
||||
fetchBlock,
|
||||
fetchImages,
|
||||
@@ -142,4 +174,6 @@ export default StrictDict({
|
||||
fetchUnit,
|
||||
saveBlock,
|
||||
uploadImage,
|
||||
deleteTranscript,
|
||||
uploadTranscript,
|
||||
});
|
||||
|
||||
@@ -9,6 +9,10 @@ const testState = {
|
||||
};
|
||||
|
||||
jest.mock('../app/selectors', () => ({
|
||||
simpleSelectors: {
|
||||
studioEndpointUrl: (state) => ({ studioEndpointUrl: state }),
|
||||
blockId: (state) => ({ blockId: state }),
|
||||
},
|
||||
studioEndpointUrl: (state) => ({ studioEndpointUrl: state }),
|
||||
blockId: (state) => ({ blockId: state }),
|
||||
blockType: (state) => ({ blockType: state }),
|
||||
@@ -24,6 +28,8 @@ jest.mock('../../services/cms/api', () => ({
|
||||
fetchImages: ({ id, url }) => ({ id, url }),
|
||||
uploadImage: (args) => args,
|
||||
loadImages: jest.fn(),
|
||||
uploadTranscript: jest.fn(),
|
||||
deleteTranscript: jest.fn(),
|
||||
}));
|
||||
|
||||
const apiKeys = keyStore(api);
|
||||
@@ -239,7 +245,6 @@ describe('requests thunkActions module', () => {
|
||||
expect(loadImages).toHaveBeenCalledWith({ fetchImages: expectedArgs });
|
||||
});
|
||||
});
|
||||
|
||||
describe('saveBlock', () => {
|
||||
const content = 'SoME HtMl CoNtent As String';
|
||||
testNetworkRequestAction({
|
||||
@@ -260,7 +265,6 @@ describe('requests thunkActions module', () => {
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
describe('uploadImage', () => {
|
||||
const image = 'SoME iMage CoNtent As String';
|
||||
testNetworkRequestAction({
|
||||
@@ -278,5 +282,50 @@ describe('requests thunkActions module', () => {
|
||||
},
|
||||
});
|
||||
});
|
||||
describe('deleteTranscript', () => {
|
||||
const language = 'SoME laNGUage CoNtent As String';
|
||||
const videoId = 'SoME VidEOid CoNtent As String';
|
||||
testNetworkRequestAction({
|
||||
action: requests.deleteTranscript,
|
||||
args: { language, videoId, ...fetchParams },
|
||||
expectedString: 'with deleteTranscript promise',
|
||||
expectedData: {
|
||||
...fetchParams,
|
||||
requestKey: RequestKeys.deleteTranscript,
|
||||
promise: api.deleteTranscript({
|
||||
blockId: selectors.app.blockId(testState),
|
||||
language,
|
||||
videoId,
|
||||
studioEndpointUrl: selectors.app.studioEndpointUrl(testState),
|
||||
}),
|
||||
},
|
||||
});
|
||||
});
|
||||
describe('uploadTranscript', () => {
|
||||
const language = 'SoME laNGUage CoNtent As String';
|
||||
const videoId = 'SoME VidEOid CoNtent As String';
|
||||
const transcript = 'SoME tRANscRIPt CoNtent As String';
|
||||
testNetworkRequestAction({
|
||||
action: requests.uploadTranscript,
|
||||
args: {
|
||||
transcript,
|
||||
language,
|
||||
videoId,
|
||||
...fetchParams,
|
||||
},
|
||||
expectedString: 'with uploadTranscript promise',
|
||||
expectedData: {
|
||||
...fetchParams,
|
||||
requestKey: RequestKeys.uploadTranscript,
|
||||
promise: api.uploadTranscript({
|
||||
blockId: selectors.app.blockId(testState),
|
||||
transcript,
|
||||
videoId,
|
||||
language,
|
||||
studioEndpointUrl: selectors.app.studioEndpointUrl(testState),
|
||||
}),
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { singleVideoData } from '../../services/cms/mockVideoData';
|
||||
import { actions } from '..';
|
||||
import { actions, selectors } from '..';
|
||||
import * as requests from './requests';
|
||||
|
||||
export const loadVideoData = () => (dispatch) => {
|
||||
dispatch(actions.video.load(singleVideoData));
|
||||
@@ -10,7 +11,77 @@ export const saveVideoData = () => () => {
|
||||
// dispatch(requests.saveBlock({ });
|
||||
};
|
||||
|
||||
// Transcript Thunks:
|
||||
|
||||
export const uploadTranscript = ({ language, filename, file }) => (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const { transcripts, videoId } = state.video;
|
||||
let lang = language;
|
||||
if (!language) {
|
||||
[[lang]] = selectors.video.openLanguages(state);
|
||||
}
|
||||
dispatch(requests.uploadTranscript({
|
||||
language: lang,
|
||||
videoId,
|
||||
transcript: file,
|
||||
onSuccess: (response) => {
|
||||
dispatch(actions.video.updateField({
|
||||
transcripts: {
|
||||
...transcripts,
|
||||
[lang]: { filename },
|
||||
},
|
||||
}));
|
||||
if (selectors.video.videoId(state) === '') {
|
||||
dispatch(actions.video.updateField({
|
||||
videoId: response.edx_video_id,
|
||||
}));
|
||||
}
|
||||
},
|
||||
|
||||
}));
|
||||
};
|
||||
|
||||
export const deleteTranscript = ({ language }) => (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const { transcripts, videoId } = state.video;
|
||||
dispatch(requests.deleteTranscript({
|
||||
language,
|
||||
videoId,
|
||||
onSuccess: () => {
|
||||
const updateTranscripts = {};
|
||||
Object.keys(transcripts).forEach((key) => {
|
||||
if (key !== language) {
|
||||
updateTranscripts[key] = transcripts[key];
|
||||
}
|
||||
});
|
||||
dispatch(actions.video.updateField({ transcripts: updateTranscripts }));
|
||||
},
|
||||
}));
|
||||
};
|
||||
|
||||
export const replaceTranscript = ({ newFile, newFilename, language }) => (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const { transcripts, videoId } = state.video;
|
||||
dispatch(requests.deleteTranscript({
|
||||
language,
|
||||
videoId,
|
||||
onSuccess: () => {
|
||||
const updateTranscripts = {};
|
||||
Object.keys(transcripts).forEach((key) => {
|
||||
if (key !== language) {
|
||||
updateTranscripts[key] = transcripts[key];
|
||||
}
|
||||
});
|
||||
dispatch(actions.video.updateField({ transcripts: updateTranscripts }));
|
||||
dispatch(uploadTranscript({ language, file: newFile, filename: newFilename }));
|
||||
},
|
||||
}));
|
||||
};
|
||||
|
||||
export default {
|
||||
loadVideoData,
|
||||
saveVideoData,
|
||||
uploadTranscript,
|
||||
deleteTranscript,
|
||||
replaceTranscript,
|
||||
};
|
||||
|
||||
89
src/editors/data/redux/thunkActions/video.test.js
Normal file
89
src/editors/data/redux/thunkActions/video.test.js
Normal file
@@ -0,0 +1,89 @@
|
||||
import { actions } from '..';
|
||||
import { keyStore } from '../../../utils';
|
||||
import * as thunkActions from './video';
|
||||
|
||||
jest.mock('./requests', () => ({
|
||||
deleteTranscript: (args) => ({ deleteTranscript: args }),
|
||||
uploadTranscript: (args) => ({ uploadTranscript: args }),
|
||||
}));
|
||||
const thunkActionsKeys = keyStore(thunkActions);
|
||||
|
||||
const mockLanguage = 'la';
|
||||
const mockFile = 'soMEtRANscRipT';
|
||||
const mockFilename = 'soMEtRANscRipT.srt';
|
||||
|
||||
const testState = { transcripts: { la: 'test VALUE' }, videoId: 'soMEvIDEo' };
|
||||
const testUpload = { transcripts: { la: { filename: mockFilename } } };
|
||||
const testReplaceUpload = {
|
||||
file: mockFile,
|
||||
language: mockLanguage,
|
||||
filename: mockFilename,
|
||||
};
|
||||
|
||||
describe('video thunkActions', () => {
|
||||
let dispatch;
|
||||
let getState;
|
||||
let dispatchedAction;
|
||||
beforeEach(() => {
|
||||
dispatch = jest.fn((action) => ({ dispatch: action }));
|
||||
getState = jest.fn(() => ({
|
||||
app: { studioEndpointUrl: 'soMEeNDPoiNT', blockId: 'soMEBloCk' },
|
||||
video: testState,
|
||||
}));
|
||||
});
|
||||
describe('deleteTranscript', () => {
|
||||
beforeEach(() => {
|
||||
thunkActions.deleteTranscript({ language: mockLanguage })(dispatch, getState);
|
||||
[[dispatchedAction]] = dispatch.mock.calls;
|
||||
});
|
||||
it('dispatches deleteTranscript action', () => {
|
||||
expect(dispatchedAction.deleteTranscript).not.toEqual(undefined);
|
||||
});
|
||||
it('dispatches actions.video.updateField on success', () => {
|
||||
dispatch.mockClear();
|
||||
dispatchedAction.deleteTranscript.onSuccess();
|
||||
expect(dispatch).toHaveBeenCalledWith(actions.video.updateField({ transcripts: {} }));
|
||||
});
|
||||
});
|
||||
describe('uploadTranscript', () => {
|
||||
beforeEach(() => {
|
||||
thunkActions.uploadTranscript({
|
||||
language: mockLanguage,
|
||||
filename: mockFilename,
|
||||
file: mockFile,
|
||||
})(dispatch, getState);
|
||||
[[dispatchedAction]] = dispatch.mock.calls;
|
||||
});
|
||||
it('dispatches uploadTranscript action', () => {
|
||||
expect(dispatchedAction.uploadTranscript).not.toEqual(undefined);
|
||||
});
|
||||
it('dispatches actions.video.updateField on success', () => {
|
||||
dispatch.mockClear();
|
||||
dispatchedAction.uploadTranscript.onSuccess();
|
||||
expect(dispatch).toHaveBeenCalledWith(actions.video.updateField(testUpload));
|
||||
});
|
||||
});
|
||||
describe('replaceTranscript', () => {
|
||||
const spies = {};
|
||||
beforeEach(() => {
|
||||
spies.uploadTranscript = jest.spyOn(thunkActions, thunkActionsKeys.uploadTranscript)
|
||||
.mockReturnValueOnce(testReplaceUpload);
|
||||
thunkActions.replaceTranscript({
|
||||
newFile: mockFile,
|
||||
newFilename: mockFilename,
|
||||
language: mockLanguage,
|
||||
})(dispatch, getState, spies.uploadTranscript);
|
||||
[[dispatchedAction]] = dispatch.mock.calls;
|
||||
});
|
||||
it('dispatches deleteTranscript action', () => {
|
||||
expect(dispatchedAction.deleteTranscript).not.toEqual(undefined);
|
||||
});
|
||||
it('dispatches actions.video.updateField and replaceTranscript success', () => {
|
||||
dispatch.mockClear();
|
||||
dispatchedAction.deleteTranscript.onSuccess();
|
||||
expect(dispatch).toHaveBeenCalledTimes(2);
|
||||
expect(dispatch).toHaveBeenNthCalledWith(1, actions.video.updateField({ transcripts: {} }));
|
||||
expect(dispatch).toHaveBeenNthCalledWith(2, expect.any(Function));
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -4,6 +4,7 @@ import { StrictDict } from '../../../utils';
|
||||
|
||||
const initialState = {
|
||||
videoSource: '',
|
||||
videoId: '',
|
||||
fallbackVideos: [
|
||||
'',
|
||||
'',
|
||||
@@ -40,6 +41,23 @@ const video = createSlice({
|
||||
load: (state, { payload }) => ({
|
||||
...payload,
|
||||
}),
|
||||
addTranscript: (state, { payload }) => ({
|
||||
transcripts: { [payload.language]: payload.filename, ...state.transcripts },
|
||||
...state,
|
||||
}),
|
||||
replaceTranscript: (state, { payload }) => ({
|
||||
transcripts: { [payload.language]: payload.newFilename, ...state.transcripts },
|
||||
...state,
|
||||
}),
|
||||
deleteTranscript: (state, { payload }) => {
|
||||
const lang = payload.language;
|
||||
const { [lang]: removedProperty, ...trimmedTranscripts } = state.transcripts;
|
||||
return {
|
||||
transcripts: trimmedTranscripts,
|
||||
...state,
|
||||
};
|
||||
},
|
||||
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
// import { createSelector } from 'reselect';
|
||||
import { createSelector } from 'reselect';
|
||||
|
||||
import { keyStore } from '../../../utils';
|
||||
import { videoTranscriptLanguages } from '../../constants/video';
|
||||
|
||||
import { initialState } from './reducer';
|
||||
// import * as module from './selectors';
|
||||
import * as module from './selectors';
|
||||
import * as AppSelectors from '../app/selectors';
|
||||
import { downloadVideoTranscriptURL } from '../../services/cms/urls';
|
||||
|
||||
const stateKeys = keyStore(initialState);
|
||||
|
||||
@@ -11,6 +14,7 @@ export const video = (state) => state.video;
|
||||
|
||||
export const simpleSelectors = [
|
||||
stateKeys.videoSource,
|
||||
stateKeys.videoId,
|
||||
stateKeys.fallbackVideos,
|
||||
stateKeys.allowVideoDownloads,
|
||||
stateKeys.thumbnail,
|
||||
@@ -23,6 +27,27 @@ export const simpleSelectors = [
|
||||
stateKeys.licenseDetails,
|
||||
].reduce((obj, key) => ({ ...obj, [key]: state => state.video[key] }), {});
|
||||
|
||||
export const openLanguages = createSelector(
|
||||
[module.simpleSelectors.transcripts],
|
||||
(transcripts) => {
|
||||
const open = Object.entries(videoTranscriptLanguages).filter(
|
||||
([lang]) => !Object.keys(transcripts).includes(lang),
|
||||
);
|
||||
return open;
|
||||
},
|
||||
);
|
||||
|
||||
export const getTranscriptDownloadUrl = createSelector(
|
||||
[AppSelectors.simpleSelectors.studioEndpointUrl, AppSelectors.simpleSelectors.blockId],
|
||||
(studioEndpointUrl, blockId) => ({ language }) => downloadVideoTranscriptURL({
|
||||
studioEndpointUrl,
|
||||
blockId,
|
||||
language,
|
||||
}),
|
||||
);
|
||||
|
||||
export default {
|
||||
openLanguages,
|
||||
getTranscriptDownloadUrl,
|
||||
...simpleSelectors,
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { camelizeKeys } from '../../../utils';
|
||||
import * as urls from './urls';
|
||||
import { get, post } from './utils';
|
||||
import { get, post, deleteObject } from './utils';
|
||||
import * as module from './api';
|
||||
import * as mockApi from './mockApi';
|
||||
|
||||
@@ -29,6 +29,35 @@ export const apiMethods = {
|
||||
data,
|
||||
);
|
||||
},
|
||||
deleteTranscript: ({
|
||||
studioEndpointUrl,
|
||||
language,
|
||||
blockId,
|
||||
videoId,
|
||||
}) => {
|
||||
const deleteJSON = { data: { lang: language, edx_video_id: videoId } };
|
||||
return deleteObject(
|
||||
urls.videoTranscripts({ studioEndpointUrl, blockId }),
|
||||
deleteJSON,
|
||||
);
|
||||
},
|
||||
uploadTranscript: ({
|
||||
blockId,
|
||||
studioEndpointUrl,
|
||||
transcript,
|
||||
videoId,
|
||||
language,
|
||||
}) => {
|
||||
const data = new FormData();
|
||||
data.append('file', transcript);
|
||||
data.append('edx_video_id', videoId);
|
||||
data.append('language_code', language);
|
||||
data.append('new_language_code', language);
|
||||
return post(
|
||||
urls.videoTranscripts({ studioEndpointUrl, blockId }),
|
||||
data,
|
||||
);
|
||||
},
|
||||
normalizeContent: ({
|
||||
blockId,
|
||||
blockType,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as utils from '../../../utils';
|
||||
import * as api from './api';
|
||||
import * as urls from './urls';
|
||||
import { get, post } from './utils';
|
||||
import { get, post, deleteObject } from './utils';
|
||||
|
||||
jest.mock('../../../utils', () => {
|
||||
const camelizeMap = (obj) => ({ ...obj, camelized: true });
|
||||
@@ -18,11 +18,13 @@ jest.mock('./urls', () => ({
|
||||
blockStudioView: jest.fn().mockName('urls.StudioView'),
|
||||
courseImages: jest.fn().mockName('urls.courseImages'),
|
||||
courseAssets: jest.fn().mockName('urls.courseAssets'),
|
||||
videoTranscripts: jest.fn().mockName('urls.videoTranscripts'),
|
||||
}));
|
||||
|
||||
jest.mock('./utils', () => ({
|
||||
get: jest.fn().mockName('get'),
|
||||
post: jest.fn().mockName('post'),
|
||||
deleteObject: jest.fn().mockName('deleteObject'),
|
||||
}));
|
||||
|
||||
const { camelize } = utils;
|
||||
@@ -122,7 +124,7 @@ describe('cms api', () => {
|
||||
image,
|
||||
});
|
||||
expect(post).toHaveBeenCalledWith(
|
||||
urls.courseAssets({ studioEndpointUrl, learningContextId }),
|
||||
urls.videoTranscripts({ studioEndpointUrl, learningContextId }),
|
||||
mockFormdata,
|
||||
);
|
||||
});
|
||||
@@ -159,4 +161,44 @@ describe('cms api', () => {
|
||||
api.loadImage = oldLoadImage;
|
||||
});
|
||||
});
|
||||
describe('videoTranscripts', () => {
|
||||
const language = 'la';
|
||||
const videoId = 'sOmeVIDeoiD';
|
||||
describe('uploadTranscript', () => {
|
||||
const transcript = { transcript: 'dAta' };
|
||||
it('should call post with urls.videoTranscripts and transcript data', () => {
|
||||
const mockFormdata = new FormData();
|
||||
mockFormdata.append('file', transcript);
|
||||
mockFormdata.append('edx_video_id', videoId);
|
||||
mockFormdata.append('language_code', language);
|
||||
mockFormdata.append('new_language_code', language);
|
||||
apiMethods.uploadTranscript({
|
||||
blockId,
|
||||
studioEndpointUrl,
|
||||
transcript,
|
||||
videoId,
|
||||
language,
|
||||
});
|
||||
expect(post).toHaveBeenCalledWith(
|
||||
urls.videoTranscripts({ studioEndpointUrl, blockId }),
|
||||
mockFormdata,
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('transcript delete', () => {
|
||||
it('should call deleteObject with urls.videoTranscripts and transcript data', () => {
|
||||
const mockDeleteJSON = { data: { lang: language, edx_video_id: videoId } };
|
||||
apiMethods.deleteTranscript({
|
||||
blockId,
|
||||
studioEndpointUrl,
|
||||
videoId,
|
||||
language,
|
||||
});
|
||||
expect(deleteObject).toHaveBeenCalledWith(
|
||||
urls.videoTranscripts({ studioEndpointUrl, blockId }),
|
||||
mockDeleteJSON,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,6 +3,7 @@ import LicenseTypes from '../../constants/licenses';
|
||||
|
||||
export const videoDataProps = {
|
||||
videoSource: PropTypes.string,
|
||||
videoId: PropTypes.string,
|
||||
fallbackVideos: PropTypes.arrayOf(PropTypes.string),
|
||||
allowVideoDownloads: PropTypes.bool,
|
||||
thumbnail: PropTypes.string,
|
||||
@@ -26,6 +27,7 @@ export const videoDataProps = {
|
||||
|
||||
export const singleVideoData = {
|
||||
videoSource: 'https://www.youtube.com/watch?v=dQw4w9WgXcQ',
|
||||
videoId: 'f36f06b5-92e5-47c7-bb26-bcf986799cb7',
|
||||
fallbackVideos: [
|
||||
'https://www.youtube.com/watch?v=dQw4w9WgXcQ',
|
||||
'https://www.youtube.com/watch?v=dQw4w9WgXcQ',
|
||||
@@ -33,7 +35,7 @@ export const singleVideoData = {
|
||||
allowVideoDownloads: true,
|
||||
thumbnail: 'my-thumbnail-file-url', // filename
|
||||
transcripts: {
|
||||
english: 'my-transcript-url',
|
||||
en: { filename: 'my-transcript-url' },
|
||||
},
|
||||
allowTranscriptDownloads: false,
|
||||
duration: {
|
||||
|
||||
@@ -2,6 +2,7 @@ import PropTypes from 'prop-types';
|
||||
|
||||
export const videoDataProps = {
|
||||
videoSource: PropTypes.string,
|
||||
videoId: PropTypes.string,
|
||||
fallbackVideos: PropTypes.arrayOf(PropTypes.string),
|
||||
allowVideoDownloads: PropTypes.bool,
|
||||
thumbnail: PropTypes.string,
|
||||
|
||||
@@ -34,3 +34,11 @@ export const courseAssets = ({ studioEndpointUrl, learningContextId }) => (
|
||||
export const courseImages = ({ studioEndpointUrl, learningContextId }) => (
|
||||
`${courseAssets({ studioEndpointUrl, learningContextId })}?sort=uploadDate&direction=desc&asset_type=Images`
|
||||
);
|
||||
|
||||
export const videoTranscripts = ({ studioEndpointUrl, blockId }) => (
|
||||
`${block({ studioEndpointUrl, blockId })}/handler/studio_transcript/translation`
|
||||
);
|
||||
|
||||
export const downloadVideoTranscriptURL = ({ studioEndpointUrl, blockId, language }) => (
|
||||
`${videoTranscripts({ studioEndpointUrl, blockId })}?language_code=${language}`
|
||||
);
|
||||
|
||||
@@ -7,6 +7,8 @@ import {
|
||||
blockStudioView,
|
||||
courseAssets,
|
||||
courseImages,
|
||||
downloadVideoTranscriptURL,
|
||||
videoTranscripts,
|
||||
} from './urls';
|
||||
|
||||
describe('cms url methods', () => {
|
||||
@@ -15,6 +17,7 @@ describe('cms url methods', () => {
|
||||
const learningContextId = 'lEarnIngCOntextId123';
|
||||
const courseId = 'course-v1:courseId123';
|
||||
const libraryV1Id = 'library-v1:libaryId123';
|
||||
const language = 'la';
|
||||
describe('return to learning context urls', () => {
|
||||
const unitUrl = {
|
||||
data: {
|
||||
@@ -77,4 +80,16 @@ describe('cms url methods', () => {
|
||||
.toEqual(`${courseAssets({ studioEndpointUrl, learningContextId })}?sort=uploadDate&direction=desc&asset_type=Images`);
|
||||
});
|
||||
});
|
||||
describe('videoTranscripts', () => {
|
||||
it('returns url with studioEndpointUrl and blockId', () => {
|
||||
expect(videoTranscripts({ studioEndpointUrl, blockId }))
|
||||
.toEqual(`${block({ studioEndpointUrl, blockId })}/handler/studio_transcript/translation`);
|
||||
});
|
||||
});
|
||||
describe('downloadVideoTranscriptURL', () => {
|
||||
it('returns url with studioEndpointUrl, blockId and language query', () => {
|
||||
expect(downloadVideoTranscriptURL({ studioEndpointUrl, blockId, language }))
|
||||
.toEqual(`${videoTranscripts({ studioEndpointUrl, blockId })}?language_code=${language}`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -13,5 +13,12 @@ export const get = (...args) => getAuthenticatedHttpClient().get(...args);
|
||||
* @param {object|string} data - post payload
|
||||
*/
|
||||
export const post = (...args) => getAuthenticatedHttpClient().post(...args);
|
||||
/**
|
||||
* delete(url, data)
|
||||
* simple wrapper providing an authenticated Http client delete action
|
||||
* @param {string} url - target url
|
||||
* @param {object|string} data - delete payload
|
||||
*/
|
||||
export const deleteObject = (...args) => getAuthenticatedHttpClient().delete(...args);
|
||||
|
||||
export const client = getAuthenticatedHttpClient;
|
||||
|
||||
@@ -22,4 +22,12 @@ describe('cms service utils', () => {
|
||||
expect(utils.post(...args)).toEqual(post(...args));
|
||||
});
|
||||
});
|
||||
// describe('deleteObject', () => {
|
||||
// it('forwards arguments to authenticatedHttpClient().delete', () => {
|
||||
// const deleteObject = jest.fn((...args) => ({ delete: args }));
|
||||
// getAuthenticatedHttpClient.mockReturnValue({ deleteObject });
|
||||
// const args = ['some', 'args', 'for', 'the', 'test'];
|
||||
// expect(utils.deleteObject(...args)).toEqual(deleteObject(...args));
|
||||
// });
|
||||
// });
|
||||
});
|
||||
|
||||
@@ -21,7 +21,9 @@ export const hooks = {
|
||||
isDismissed,
|
||||
dismissAlert: () => {
|
||||
setIsDismissed(true);
|
||||
dismissError();
|
||||
if (dismissError) {
|
||||
dismissError();
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
@@ -73,6 +73,12 @@ jest.mock('@edx/paragon', () => jest.requireActual('testUtils').mockNestedCompon
|
||||
Trigger: 'Trigger',
|
||||
Visible: 'Visible',
|
||||
},
|
||||
Card: {
|
||||
Header: 'Card.Header',
|
||||
Section: 'Card.Section',
|
||||
Footer: 'Card.Footer',
|
||||
Body: 'Card.Body',
|
||||
},
|
||||
Dropdown: {
|
||||
Item: 'Dropdown.Item',
|
||||
Menu: 'Dropdown.Menu',
|
||||
@@ -101,9 +107,12 @@ jest.mock('@edx/paragon', () => jest.requireActual('testUtils').mockNestedCompon
|
||||
SelectableBox: {
|
||||
Set: 'SelectableBox.Set',
|
||||
},
|
||||
|
||||
Spinner: 'Spinner',
|
||||
Stack: 'Stack',
|
||||
Toast: 'Toast',
|
||||
Tooltip: 'ToolTip',
|
||||
OverlayTrigger: 'OverLayTrigger',
|
||||
}));
|
||||
|
||||
jest.mock('@edx/paragon/icons', () => ({
|
||||
|
||||
Reference in New Issue
Block a user