Feat improve transcript flow (#141)
https://2u-internal.atlassian.net/browse/TNL-10199 Rewrite of Transcripts widget to improve flow.
This commit is contained in:
@@ -50,8 +50,8 @@ export const TitleHeader = ({
|
||||
</Truncate>
|
||||
<IconButton
|
||||
alt={intl.formatMessage(messages.editTitleLabel)}
|
||||
className="mr-2"
|
||||
iconAs={Icon}
|
||||
className="mr-2"
|
||||
onClick={startEditing}
|
||||
size="sm"
|
||||
src={Edit}
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
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,93 @@
|
||||
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 { thunkActions, selectors } from '../../../../../../data/redux';
|
||||
import { videoTranscriptLanguages } from '../../../../../../data/constants/video';
|
||||
import { FileInput, fileInput } from '../../../../../../sharedComponents/FileInput';
|
||||
import messages from './messages';
|
||||
import * as module from './LanguageSelector';
|
||||
|
||||
export const hooks = {
|
||||
onSelectLanguage: ({
|
||||
dispatch, languageBeforeChange, triggerupload, setLocalLang,
|
||||
}) => (event) => {
|
||||
// IF Language is unset, set language and begin upload prompt.
|
||||
setLocalLang(event.target.value);
|
||||
if (languageBeforeChange === '') {
|
||||
triggerupload();
|
||||
return;
|
||||
}
|
||||
// Else: update language
|
||||
dispatch(
|
||||
thunkActions.video.updateTranscriptLanguage({
|
||||
newLanguageCode: event.target.value, languageBeforeChange,
|
||||
}),
|
||||
);
|
||||
},
|
||||
|
||||
addFileCallback: ({ dispatch, localLang }) => (file) => {
|
||||
dispatch(thunkActions.video.uploadTranscript({
|
||||
file,
|
||||
filename: file.name,
|
||||
language: localLang,
|
||||
}));
|
||||
},
|
||||
|
||||
};
|
||||
|
||||
export const LanguageSelector = ({
|
||||
index, // For a unique id for the form control
|
||||
language,
|
||||
// Redux
|
||||
openLanguages, // Only allow those languages not already associated with a transcript to be selected
|
||||
// intl
|
||||
intl,
|
||||
|
||||
}) => {
|
||||
const [localLang, setLocalLang] = React.useState(language);
|
||||
const input = fileInput({ onAddFile: hooks.addFileCallback({ dispatch: useDispatch(), localLang }) });
|
||||
const onLanguageChange = module.hooks.onSelectLanguage({
|
||||
dispatch: useDispatch(), languageBeforeChange: localLang, setLocalLang, triggerupload: input.click,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="col col-11">
|
||||
<Form.Group controlId={`selectLanguage-form-${index}`} className="mw-100">
|
||||
<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>
|
||||
<FileInput fileInput={input} acceptedFiles=".srt" />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
LanguageSelector.defaultProps = {
|
||||
openLanguages: [],
|
||||
};
|
||||
|
||||
LanguageSelector.propTypes = {
|
||||
openLanguages: PropTypes.arrayOf(PropTypes.string),
|
||||
index: PropTypes.number.isRequired,
|
||||
language: PropTypes.string.isRequired,
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
export const mapStateToProps = (state) => ({
|
||||
openLanguages: selectors.video.openLanguages(state),
|
||||
});
|
||||
|
||||
export const mapDispatchToProps = {};
|
||||
|
||||
export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(LanguageSelector));
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { LanguageSelect } from './LanguageSelect';
|
||||
import { LanguageSelector } from './LanguageSelector';
|
||||
import { formatMessage } from '../../../../../../../testUtils';
|
||||
|
||||
const lang1 = 'kLinGon';
|
||||
@@ -18,7 +18,7 @@ jest.mock('../../../../../../data/constants/video', () => ({
|
||||
},
|
||||
}));
|
||||
|
||||
describe('LanguageSelect', () => {
|
||||
describe('LanguageSelector', () => {
|
||||
const props = {
|
||||
intl: { formatMessage },
|
||||
onSelect: jest.fn().mockName('props.OnSelect'),
|
||||
@@ -30,14 +30,14 @@ describe('LanguageSelect', () => {
|
||||
describe('snapshot', () => {
|
||||
test('transcript option', () => {
|
||||
expect(
|
||||
shallow(<LanguageSelect {...props} />),
|
||||
shallow(<LanguageSelector {...props} />),
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
describe('snapshots -- no', () => {
|
||||
test('transcripts no Open Languages, all should be disabled', () => {
|
||||
expect(
|
||||
shallow(<LanguageSelect {...props} openLanguages={[]} />),
|
||||
shallow(<LanguageSelector {...props} openLanguages={[]} />),
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,112 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import {
|
||||
Card, Button, IconButton, Row,
|
||||
Icon,
|
||||
} from '@edx/paragon';
|
||||
import { Delete } from '@edx/paragon/icons';
|
||||
|
||||
import {
|
||||
FormattedMessage,
|
||||
injectIntl,
|
||||
} from '@edx/frontend-platform/i18n';
|
||||
import { thunkActions } from '../../../../../../data/redux';
|
||||
|
||||
import TranscriptActionMenu from './TranscriptActionMenu';
|
||||
import LanguageSelector from './LanguageSelector';
|
||||
import * as module from './Transcript';
|
||||
import messages from './messages';
|
||||
|
||||
export const hooks = {
|
||||
state: {
|
||||
inDeleteConfirmation: (args) => React.useState(args),
|
||||
},
|
||||
setUpDeleteConfirmation: () => {
|
||||
const [inDeleteConfirmation, setInDeleteConfirmation] = module.hooks.state.inDeleteConfirmation(false);
|
||||
return {
|
||||
inDeleteConfirmation,
|
||||
launchDeleteConfirmation: () => setInDeleteConfirmation(true),
|
||||
cancelDelete: () => setInDeleteConfirmation(false),
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
export const Transcript = ({
|
||||
index,
|
||||
language,
|
||||
// redux
|
||||
deleteTranscript,
|
||||
}) => {
|
||||
const { inDeleteConfirmation, launchDeleteConfirmation, cancelDelete } = module.hooks.setUpDeleteConfirmation();
|
||||
return (
|
||||
<>
|
||||
{inDeleteConfirmation
|
||||
? (
|
||||
<Card className="mb-2">
|
||||
<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 });
|
||||
// stop showing the card
|
||||
cancelDelete();
|
||||
}}
|
||||
>
|
||||
<FormattedMessage {...messages.confirmDeleteLabel} />
|
||||
</Button>
|
||||
</Card.Footer>
|
||||
</Card.Body>
|
||||
</Card>
|
||||
)
|
||||
: (
|
||||
<Row>
|
||||
<LanguageSelector
|
||||
title={index}
|
||||
language={language}
|
||||
/>
|
||||
{ language === '' ? (
|
||||
<IconButton
|
||||
className="d-inline-block"
|
||||
iconAs={Icon}
|
||||
src={Delete}
|
||||
onClick={() => launchDeleteConfirmation()}
|
||||
/>
|
||||
) : (
|
||||
<TranscriptActionMenu
|
||||
index={index}
|
||||
language={language}
|
||||
launchDeleteConfirmation={launchDeleteConfirmation}
|
||||
/>
|
||||
)}
|
||||
</Row>
|
||||
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
Transcript.propTypes = {
|
||||
index: PropTypes.number.isRequired,
|
||||
language: PropTypes.string.isRequired,
|
||||
deleteTranscript: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export const mapStateToProps = () => ({
|
||||
});
|
||||
export const mapDispatchToProps = {
|
||||
deleteTranscript: thunkActions.video.deleteTranscript,
|
||||
};
|
||||
|
||||
export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(Transcript));
|
||||
@@ -0,0 +1,85 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import * as module from './Transcript';
|
||||
|
||||
import { MockUseState } from '../../../../../../../testUtils';
|
||||
|
||||
jest.mock('./LanguageSelector', () => 'LanguageSelector');
|
||||
jest.mock('./TranscriptActionMenu', () => 'TranscriptActionMenu');
|
||||
|
||||
describe('Transcript Component', () => {
|
||||
describe('state hooks', () => {
|
||||
const state = new MockUseState(module.hooks);
|
||||
|
||||
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.hooks.setUpDeleteConfirmation().inDeleteConfirmation).toEqual(false);
|
||||
});
|
||||
test('inDeleteConfirmation setters: launch', () => {
|
||||
module.hooks.setUpDeleteConfirmation().launchDeleteConfirmation();
|
||||
expect(state.setState[state.keys.inDeleteConfirmation]).toHaveBeenCalledWith(true);
|
||||
});
|
||||
test('inDeleteConfirmation setters: cancel', () => {
|
||||
module.hooks.setUpDeleteConfirmation().cancelDelete();
|
||||
expect(state.setState[state.keys.inDeleteConfirmation]).toHaveBeenCalledWith(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('component', () => {
|
||||
describe('component', () => {
|
||||
const props = {
|
||||
index: 'sOmenUmBer',
|
||||
language: 'lAnG',
|
||||
deleteTranscript: jest.fn().mockName('thunkActions.video.deleteTranscript'),
|
||||
};
|
||||
afterAll(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
test('snapshots: renders as expected with default props: dont show confirm delete', () => {
|
||||
jest.spyOn(module.hooks, 'setUpDeleteConfirmation').mockImplementationOnce(() => ({
|
||||
inDeleteConfirmation: false,
|
||||
launchDeleteConfirmation: jest.fn().mockName('launchDeleteConfirmation'),
|
||||
cancelDelete: jest.fn().mockName('cancelDelete'),
|
||||
}));
|
||||
expect(
|
||||
shallow(<module.Transcript {...props} />),
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
test('snapshots: renders as expected with default props: dont show confirm delete, language is blank so delete is shown instead of action menu', () => {
|
||||
jest.spyOn(module.hooks, 'setUpDeleteConfirmation').mockImplementationOnce(() => ({
|
||||
inDeleteConfirmation: false,
|
||||
launchDeleteConfirmation: jest.fn().mockName('launchDeleteConfirmation'),
|
||||
cancelDelete: jest.fn().mockName('cancelDelete'),
|
||||
}));
|
||||
expect(
|
||||
shallow(<module.Transcript {...props} language="" />),
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
test('snapshots: renders as expected with default props: show confirm delete', () => {
|
||||
jest.spyOn(module.hooks, 'setUpDeleteConfirmation').mockImplementationOnce(() => ({
|
||||
inDeleteConfirmation: true,
|
||||
launchDeleteConfirmation: jest.fn().mockName('launchDeleteConfirmation'),
|
||||
cancelDelete: jest.fn().mockName('cancelDelete'),
|
||||
}));
|
||||
expect(
|
||||
shallow(<module.Transcript {...props} />),
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,80 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect, useDispatch } from 'react-redux';
|
||||
|
||||
import { FormattedMessage, injectIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Dropdown, Icon, IconButton } from '@edx/paragon';
|
||||
import { MoreHoriz } from '@edx/paragon/icons';
|
||||
|
||||
import { thunkActions, selectors } from '../../../../../../data/redux';
|
||||
|
||||
import { FileInput, fileInput } from '../../../../../../sharedComponents/FileInput';
|
||||
import * as module from './TranscriptActionMenu';
|
||||
import messages from './messages';
|
||||
|
||||
export const hooks = {
|
||||
replaceFileCallback: ({ language, dispatch }) => (file) => {
|
||||
dispatch(thunkActions.video.replaceTranscript({
|
||||
newFile: file,
|
||||
newFilename: file.name,
|
||||
language,
|
||||
}));
|
||||
},
|
||||
};
|
||||
|
||||
export const TranscriptActionMenu = ({
|
||||
index,
|
||||
language,
|
||||
launchDeleteConfirmation,
|
||||
// redux
|
||||
getTranscriptDownloadUrl,
|
||||
|
||||
}) => {
|
||||
const input = fileInput({ onAddFile: module.hooks.replaceFileCallback({ language, dispatch: useDispatch() }) });
|
||||
const downloadLink = getTranscriptDownloadUrl({ language });
|
||||
return (
|
||||
<Dropdown>
|
||||
<Dropdown.Toggle
|
||||
id="dropdown-toggle-with-iconbutton-video-transcript-widget"
|
||||
as={IconButton}
|
||||
src={MoreHoriz}
|
||||
iconAs={Icon}
|
||||
variant="primary"
|
||||
alt="Actions dropdown"
|
||||
/>
|
||||
<Dropdown.Menu className="video_transcript Action Menu">
|
||||
<Dropdown.Item
|
||||
key={`transcript-actions-${index}-replace`}
|
||||
onClick={input.click}
|
||||
>
|
||||
<FormattedMessage {...messages.replaceTranscript} />
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item key={`transcript-actions-${index}-download`} href={downloadLink}>
|
||||
<FormattedMessage {...messages.downloadTranscript} />
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item key={`transcript-actions-${index}-delete`} onClick={launchDeleteConfirmation}>
|
||||
<FormattedMessage {...messages.deleteTranscript} />
|
||||
</Dropdown.Item>
|
||||
</Dropdown.Menu>
|
||||
<FileInput fileInput={input} acceptedFiles=".srt" />
|
||||
</Dropdown>
|
||||
);
|
||||
};
|
||||
|
||||
TranscriptActionMenu.propTypes = {
|
||||
index: PropTypes.number.isRequired,
|
||||
language: PropTypes.string.isRequired,
|
||||
launchDeleteConfirmation: PropTypes.func.isRequired,
|
||||
// redux
|
||||
getTranscriptDownloadUrl: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export const mapStateToProps = (state) => ({
|
||||
getTranscriptDownloadUrl: selectors.video.getTranscriptDownloadUrl(state),
|
||||
});
|
||||
|
||||
export const mapDispatchToProps = {
|
||||
downloadTranscript: thunkActions.video.downloadTranscript,
|
||||
};
|
||||
|
||||
export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(TranscriptActionMenu));
|
||||
@@ -0,0 +1,89 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { thunkActions, selectors } from '../../../../../../data/redux';
|
||||
|
||||
import * as module from './TranscriptActionMenu';
|
||||
|
||||
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('thunkActions.video.deleteTranscript'),
|
||||
replaceTranscript: jest.fn((args) => ({ replaceTranscript: args })).mockName('thunkActions.video.replaceTranscript'),
|
||||
downloadTranscript: jest.fn().mockName('thunkActions.video.downloadTranscript'),
|
||||
},
|
||||
},
|
||||
selectors: {
|
||||
video: {
|
||||
getTranscriptDownloadUrl: jest.fn(args => ({ getTranscriptDownloadUrl: args })).mockName('selectors.video.getTranscriptDownloadUrl'),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('../../../../../../sharedComponents/FileInput', () => ({
|
||||
FileInput: 'FileInput',
|
||||
fileInput: jest.fn((args) => ({ click: jest.fn().mockName('click input'), onAddFile: args.onAddFile })),
|
||||
}));
|
||||
|
||||
describe('TranscriptActionMenu', () => {
|
||||
describe('hooks', () => {
|
||||
describe('replaceFileCallback', () => {
|
||||
const lang1Code = 'coDe';
|
||||
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.hooks.replaceFileCallback({
|
||||
dispatch: mockDispatch, language: lang1Code,
|
||||
});
|
||||
cb(mockEvent);
|
||||
expect(thunkActions.video.replaceTranscript).toHaveBeenCalledWith(result);
|
||||
expect(mockDispatch).toHaveBeenCalledWith({ replaceTranscript: result });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Snapshots', () => {
|
||||
const props = {
|
||||
index: 'sOmenUmBer',
|
||||
language: 'lAnG',
|
||||
launchDeleteConfirmation: jest.fn().mockName('launchDeleteConfirmation'),
|
||||
// redux
|
||||
getTranscriptDownloadUrl: jest.fn().mockName('selectors.video.getTranscriptDownloadUrl'),
|
||||
};
|
||||
afterAll(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
test('snapshots: renders as expected with default props: dont show confirm delete', () => {
|
||||
jest.spyOn(module.hooks, 'replaceFileCallback').mockImplementationOnce(() => jest.fn().mockName('module.hooks.replaceFileCallback'));
|
||||
expect(
|
||||
shallow(<module.TranscriptActionMenu {...props} />),
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
describe('mapStateToProps', () => {
|
||||
const testState = { A: 'pple', B: 'anana', C: 'ucumber' };
|
||||
test('getTranscriptDownloadUrl from video.getTranscriptDownloadUrl', () => {
|
||||
expect(
|
||||
module.mapStateToProps(testState).getTranscriptDownloadUrl,
|
||||
).toEqual(selectors.video.getTranscriptDownloadUrl(testState));
|
||||
});
|
||||
});
|
||||
describe('mapDispatchToProps', () => {
|
||||
test('deleteTranscript from thunkActions.video.deleteTranscript', () => {
|
||||
expect(module.mapDispatchToProps.downloadTranscript).toEqual(thunkActions.video.downloadTranscript);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,108 +0,0 @@
|
||||
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));
|
||||
@@ -1,82 +0,0 @@
|
||||
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('thunkActions.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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,65 +0,0 @@
|
||||
// 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,97 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`LanguageSelector snapshot transcript option 1`] = `
|
||||
<div
|
||||
className="col col-11"
|
||||
>
|
||||
<Form.Group
|
||||
className="mw-100"
|
||||
controlId="selectLanguage-form-undefined"
|
||||
>
|
||||
<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>
|
||||
<FileInput
|
||||
acceptedFiles=".srt"
|
||||
fileInput={
|
||||
Object {
|
||||
"addFile": [Function],
|
||||
"click": [Function],
|
||||
"ref": Object {
|
||||
"current": undefined,
|
||||
},
|
||||
}
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`LanguageSelector snapshots -- no transcripts no Open Languages, all should be disabled 1`] = `
|
||||
<div
|
||||
className="col col-11"
|
||||
>
|
||||
<Form.Group
|
||||
className="mw-100"
|
||||
controlId="selectLanguage-form-undefined"
|
||||
>
|
||||
<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>
|
||||
<FileInput
|
||||
acceptedFiles=".srt"
|
||||
fileInput={
|
||||
Object {
|
||||
"addFile": [Function],
|
||||
"click": [Function],
|
||||
"ref": Object {
|
||||
"current": undefined,
|
||||
},
|
||||
}
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
@@ -0,0 +1,84 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Transcript Component component component snapshots: renders as expected with default props: dont show confirm delete 1`] = `
|
||||
<Fragment>
|
||||
<Component>
|
||||
<LanguageSelector
|
||||
language="lAnG"
|
||||
title="sOmenUmBer"
|
||||
/>
|
||||
<TranscriptActionMenu
|
||||
index="sOmenUmBer"
|
||||
language="lAnG"
|
||||
launchDeleteConfirmation={[MockFunction launchDeleteConfirmation]}
|
||||
/>
|
||||
</Component>
|
||||
</Fragment>
|
||||
`;
|
||||
|
||||
exports[`Transcript Component component component snapshots: renders as expected with default props: dont show confirm delete, language is blank so delete is shown instead of action menu 1`] = `
|
||||
<Fragment>
|
||||
<Component>
|
||||
<LanguageSelector
|
||||
language=""
|
||||
title="sOmenUmBer"
|
||||
/>
|
||||
<IconButton
|
||||
className="d-inline-block"
|
||||
iconAs="Icon"
|
||||
onClick={[Function]}
|
||||
/>
|
||||
</Component>
|
||||
</Fragment>
|
||||
`;
|
||||
|
||||
exports[`Transcript Component component component snapshots: renders as expected with default props: show confirm delete 1`] = `
|
||||
<Fragment>
|
||||
<Card
|
||||
className="mb-2"
|
||||
>
|
||||
<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>
|
||||
</Fragment>
|
||||
`;
|
||||
@@ -0,0 +1,55 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`TranscriptActionMenu Snapshots snapshots: renders as expected with default props: dont show confirm delete 1`] = `
|
||||
<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
|
||||
key="transcript-actions-sOmenUmBer-replace"
|
||||
onClick={[MockFunction click input]}
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Replace"
|
||||
description="Message Presented To user for action to replace transcript"
|
||||
id="authoring.videoeditor.transcript.replaceTranscript"
|
||||
/>
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item
|
||||
key="transcript-actions-sOmenUmBer-download"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Download"
|
||||
description="Message Presented To user for action to download transcript"
|
||||
id="authoring.videoeditor.transcript.downloadTranscript"
|
||||
/>
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item
|
||||
key="transcript-actions-sOmenUmBer-delete"
|
||||
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 click input],
|
||||
"onAddFile": [MockFunction module.hooks.replaceFileCallback],
|
||||
}
|
||||
}
|
||||
/>
|
||||
</Dropdown>
|
||||
`;
|
||||
@@ -1,124 +0,0 @@
|
||||
// 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,7 +1,7 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`TranscriptWidget snapshots snapshot: renders ErrorAlert with delete error message 1`] = `
|
||||
<injectIntl(ShimmedIntlComponent)
|
||||
exports[`TranscriptWidget component snapshots snapshot: renders ErrorAlert with delete error message 1`] = `
|
||||
<CollapsibleFormWidget
|
||||
isError={true}
|
||||
subtitle="English"
|
||||
title="Transcript"
|
||||
@@ -34,7 +34,8 @@ exports[`TranscriptWidget snapshots snapshot: renders ErrorAlert with delete err
|
||||
<Form.Group
|
||||
className="mt-4.5"
|
||||
>
|
||||
<injectIntl(ShimmedIntlComponent)
|
||||
<Transcript
|
||||
index={0}
|
||||
language="en"
|
||||
/>
|
||||
<div
|
||||
@@ -87,36 +88,29 @@ exports[`TranscriptWidget snapshots snapshot: renders ErrorAlert with delete err
|
||||
</Form.Label>
|
||||
</Form.Checkbox>
|
||||
</Form.Group>
|
||||
<FileInput
|
||||
acceptedFiles=".srt"
|
||||
fileInput={
|
||||
Object {
|
||||
"addFile": [Function],
|
||||
"click": [Function],
|
||||
"ref": Object {
|
||||
"current": undefined,
|
||||
},
|
||||
}
|
||||
}
|
||||
/>
|
||||
<Button
|
||||
onClick={[Function]}
|
||||
variant="link"
|
||||
<Stack
|
||||
className="border-primary-100 border-top"
|
||||
gap={3}
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Upload Transcript"
|
||||
description="Label for upload button"
|
||||
id="authoring.videoeditor.transcripts.upload.label"
|
||||
/>
|
||||
</Button>
|
||||
<Button
|
||||
onClick={[Function]}
|
||||
variant="link"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Add a transcript"
|
||||
description="Label for upload button"
|
||||
id="authoring.videoeditor.transcripts.upload.label"
|
||||
/>
|
||||
</Button>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</injectIntl(ShimmedIntlComponent)>
|
||||
</CollapsibleFormWidget>
|
||||
`;
|
||||
|
||||
exports[`TranscriptWidget snapshots snapshot: renders ErrorAlert with upload error message 1`] = `
|
||||
<injectIntl(ShimmedIntlComponent)
|
||||
exports[`TranscriptWidget component snapshots snapshot: renders ErrorAlert with upload error message 1`] = `
|
||||
<CollapsibleFormWidget
|
||||
isError={true}
|
||||
subtitle="English"
|
||||
subtitle="English, French"
|
||||
title="Transcript"
|
||||
>
|
||||
<ErrorAlert
|
||||
@@ -147,9 +141,14 @@ exports[`TranscriptWidget snapshots snapshot: renders ErrorAlert with upload err
|
||||
<Form.Group
|
||||
className="mt-4.5"
|
||||
>
|
||||
<injectIntl(ShimmedIntlComponent)
|
||||
<Transcript
|
||||
index={0}
|
||||
language="en"
|
||||
/>
|
||||
<Transcript
|
||||
index={1}
|
||||
language="fr"
|
||||
/>
|
||||
<div
|
||||
className="mb-1"
|
||||
>
|
||||
@@ -200,34 +199,27 @@ exports[`TranscriptWidget snapshots snapshot: renders ErrorAlert with upload err
|
||||
</Form.Label>
|
||||
</Form.Checkbox>
|
||||
</Form.Group>
|
||||
<FileInput
|
||||
acceptedFiles=".srt"
|
||||
fileInput={
|
||||
Object {
|
||||
"addFile": [Function],
|
||||
"click": [Function],
|
||||
"ref": Object {
|
||||
"current": undefined,
|
||||
},
|
||||
}
|
||||
}
|
||||
/>
|
||||
<Button
|
||||
onClick={[Function]}
|
||||
variant="link"
|
||||
<Stack
|
||||
className="border-primary-100 border-top"
|
||||
gap={3}
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Upload Transcript"
|
||||
description="Label for upload button"
|
||||
id="authoring.videoeditor.transcripts.upload.label"
|
||||
/>
|
||||
</Button>
|
||||
<Button
|
||||
onClick={[Function]}
|
||||
variant="link"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Add a transcript"
|
||||
description="Label for upload button"
|
||||
id="authoring.videoeditor.transcripts.upload.label"
|
||||
/>
|
||||
</Button>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</injectIntl(ShimmedIntlComponent)>
|
||||
</CollapsibleFormWidget>
|
||||
`;
|
||||
|
||||
exports[`TranscriptWidget snapshots snapshots: renders as expected with allowTranscriptDownloads true 1`] = `
|
||||
<injectIntl(ShimmedIntlComponent)
|
||||
exports[`TranscriptWidget component snapshots snapshots: renders as expected with allowTranscriptDownloads true 1`] = `
|
||||
<CollapsibleFormWidget
|
||||
isError={true}
|
||||
subtitle="English"
|
||||
title="Transcript"
|
||||
@@ -260,7 +252,8 @@ exports[`TranscriptWidget snapshots snapshots: renders as expected with allowTra
|
||||
<Form.Group
|
||||
className="mt-4.5"
|
||||
>
|
||||
<injectIntl(ShimmedIntlComponent)
|
||||
<Transcript
|
||||
index={0}
|
||||
language="en"
|
||||
/>
|
||||
<div
|
||||
@@ -313,34 +306,27 @@ exports[`TranscriptWidget snapshots snapshots: renders as expected with allowTra
|
||||
</Form.Label>
|
||||
</Form.Checkbox>
|
||||
</Form.Group>
|
||||
<FileInput
|
||||
acceptedFiles=".srt"
|
||||
fileInput={
|
||||
Object {
|
||||
"addFile": [Function],
|
||||
"click": [Function],
|
||||
"ref": Object {
|
||||
"current": undefined,
|
||||
},
|
||||
}
|
||||
}
|
||||
/>
|
||||
<Button
|
||||
onClick={[Function]}
|
||||
variant="link"
|
||||
<Stack
|
||||
className="border-primary-100 border-top"
|
||||
gap={3}
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Upload Transcript"
|
||||
description="Label for upload button"
|
||||
id="authoring.videoeditor.transcripts.upload.label"
|
||||
/>
|
||||
</Button>
|
||||
<Button
|
||||
onClick={[Function]}
|
||||
variant="link"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Add a transcript"
|
||||
description="Label for upload button"
|
||||
id="authoring.videoeditor.transcripts.upload.label"
|
||||
/>
|
||||
</Button>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</injectIntl(ShimmedIntlComponent)>
|
||||
</CollapsibleFormWidget>
|
||||
`;
|
||||
|
||||
exports[`TranscriptWidget snapshots snapshots: renders as expected with default props 1`] = `
|
||||
<injectIntl(ShimmedIntlComponent)
|
||||
exports[`TranscriptWidget component snapshots snapshots: renders as expected with default props 1`] = `
|
||||
<CollapsibleFormWidget
|
||||
isError={true}
|
||||
subtitle="None"
|
||||
title="Transcript"
|
||||
@@ -384,34 +370,27 @@ exports[`TranscriptWidget snapshots snapshots: renders as expected with default
|
||||
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"
|
||||
<Stack
|
||||
className="border-primary-100 border-top"
|
||||
gap={3}
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Upload Transcript"
|
||||
description="Label for upload button"
|
||||
id="authoring.videoeditor.transcripts.upload.label"
|
||||
/>
|
||||
</Button>
|
||||
<Button
|
||||
onClick={[Function]}
|
||||
variant="link"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Add a transcript"
|
||||
description="Label for upload button"
|
||||
id="authoring.videoeditor.transcripts.upload.label"
|
||||
/>
|
||||
</Button>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</injectIntl(ShimmedIntlComponent)>
|
||||
</CollapsibleFormWidget>
|
||||
`;
|
||||
|
||||
exports[`TranscriptWidget snapshots snapshots: renders as expected with showTranscriptByDefault true 1`] = `
|
||||
<injectIntl(ShimmedIntlComponent)
|
||||
exports[`TranscriptWidget component snapshots snapshots: renders as expected with showTranscriptByDefault true 1`] = `
|
||||
<CollapsibleFormWidget
|
||||
isError={true}
|
||||
subtitle="English"
|
||||
title="Transcript"
|
||||
@@ -444,7 +423,8 @@ exports[`TranscriptWidget snapshots snapshots: renders as expected with showTran
|
||||
<Form.Group
|
||||
className="mt-4.5"
|
||||
>
|
||||
<injectIntl(ShimmedIntlComponent)
|
||||
<Transcript
|
||||
index={0}
|
||||
language="en"
|
||||
/>
|
||||
<div
|
||||
@@ -497,34 +477,27 @@ exports[`TranscriptWidget snapshots snapshots: renders as expected with showTran
|
||||
</Form.Label>
|
||||
</Form.Checkbox>
|
||||
</Form.Group>
|
||||
<FileInput
|
||||
acceptedFiles=".srt"
|
||||
fileInput={
|
||||
Object {
|
||||
"addFile": [Function],
|
||||
"click": [Function],
|
||||
"ref": Object {
|
||||
"current": undefined,
|
||||
},
|
||||
}
|
||||
}
|
||||
/>
|
||||
<Button
|
||||
onClick={[Function]}
|
||||
variant="link"
|
||||
<Stack
|
||||
className="border-primary-100 border-top"
|
||||
gap={3}
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Upload Transcript"
|
||||
description="Label for upload button"
|
||||
id="authoring.videoeditor.transcripts.upload.label"
|
||||
/>
|
||||
</Button>
|
||||
<Button
|
||||
onClick={[Function]}
|
||||
variant="link"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Add a transcript"
|
||||
description="Label for upload button"
|
||||
id="authoring.videoeditor.transcripts.upload.label"
|
||||
/>
|
||||
</Button>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</injectIntl(ShimmedIntlComponent)>
|
||||
</CollapsibleFormWidget>
|
||||
`;
|
||||
|
||||
exports[`TranscriptWidget snapshots snapshots: renders as expected with transcripts 1`] = `
|
||||
<injectIntl(ShimmedIntlComponent)
|
||||
exports[`TranscriptWidget component snapshots snapshots: renders as expected with transcripts 1`] = `
|
||||
<CollapsibleFormWidget
|
||||
isError={true}
|
||||
subtitle="English"
|
||||
title="Transcript"
|
||||
@@ -557,7 +530,8 @@ exports[`TranscriptWidget snapshots snapshots: renders as expected with transcri
|
||||
<Form.Group
|
||||
className="mt-4.5"
|
||||
>
|
||||
<injectIntl(ShimmedIntlComponent)
|
||||
<Transcript
|
||||
index={0}
|
||||
language="en"
|
||||
/>
|
||||
<div
|
||||
@@ -610,28 +584,21 @@ exports[`TranscriptWidget snapshots snapshots: renders as expected with transcri
|
||||
</Form.Label>
|
||||
</Form.Checkbox>
|
||||
</Form.Group>
|
||||
<FileInput
|
||||
acceptedFiles=".srt"
|
||||
fileInput={
|
||||
Object {
|
||||
"addFile": [Function],
|
||||
"click": [Function],
|
||||
"ref": Object {
|
||||
"current": undefined,
|
||||
},
|
||||
}
|
||||
}
|
||||
/>
|
||||
<Button
|
||||
onClick={[Function]}
|
||||
variant="link"
|
||||
<Stack
|
||||
className="border-primary-100 border-top"
|
||||
gap={3}
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Upload Transcript"
|
||||
description="Label for upload button"
|
||||
id="authoring.videoeditor.transcripts.upload.label"
|
||||
/>
|
||||
</Button>
|
||||
<Button
|
||||
onClick={[Function]}
|
||||
variant="link"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Add a transcript"
|
||||
description="Label for upload button"
|
||||
id="authoring.videoeditor.transcripts.upload.label"
|
||||
/>
|
||||
</Button>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</injectIntl(ShimmedIntlComponent)>
|
||||
</CollapsibleFormWidget>
|
||||
`;
|
||||
|
||||
@@ -1,96 +0,0 @@
|
||||
import React from 'react';
|
||||
import { thunkActions, actions } from '../../../../../../data/redux';
|
||||
import * as module from './hooks';
|
||||
import { videoTranscriptLanguages } from '../../../../../../data/constants/video';
|
||||
import { ErrorContext } from '../../../../hooks';
|
||||
import messages from './messages';
|
||||
|
||||
export const state = {
|
||||
inDeleteConfirmation: (args) => React.useState(args),
|
||||
};
|
||||
|
||||
export const updateErrors = ({ isUploadError, isDeleteError }) => {
|
||||
const [error, setError] = React.useContext(ErrorContext).transcripts;
|
||||
if (isUploadError) {
|
||||
setError({ ...error, uploadError: messages.uploadTranscriptError.defaultMessage });
|
||||
}
|
||||
if (isDeleteError) {
|
||||
setError({ ...error, deleteError: messages.deleteTranscriptError.defaultMessage });
|
||||
}
|
||||
};
|
||||
|
||||
export const transcriptLanguages = (transcripts) => {
|
||||
const languages = [];
|
||||
if (transcripts && Object.keys(transcripts).length > 0) {
|
||||
Object.keys(transcripts).forEach(transcript => {
|
||||
languages.push(videoTranscriptLanguages[transcript]);
|
||||
});
|
||||
return languages.join(', ');
|
||||
}
|
||||
return 'None';
|
||||
};
|
||||
|
||||
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 file = e.target.files[0];
|
||||
if (file) {
|
||||
onAddFile(file);
|
||||
}
|
||||
};
|
||||
return {
|
||||
click,
|
||||
addFile,
|
||||
ref,
|
||||
};
|
||||
};
|
||||
|
||||
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,
|
||||
};
|
||||
@@ -1,129 +0,0 @@
|
||||
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,9 +1,10 @@
|
||||
import React from 'react';
|
||||
import { connect, useDispatch } from 'react-redux';
|
||||
import { connect } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
FormattedMessage,
|
||||
injectIntl,
|
||||
intlShape,
|
||||
} from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
Form,
|
||||
@@ -14,20 +15,61 @@ import {
|
||||
Tooltip,
|
||||
Alert,
|
||||
} from '@edx/paragon';
|
||||
import { FileUpload, Info } from '@edx/paragon/icons';
|
||||
import { Add, Info } from '@edx/paragon/icons';
|
||||
|
||||
import { actions, selectors } from '../../../../../../data/redux';
|
||||
import * as hooks from './hooks';
|
||||
import messages from './messages';
|
||||
|
||||
import { RequestKeys } from '../../../../../../data/constants/requests';
|
||||
import { in8lTranscriptLanguages } from '../../../../../../data/constants/video';
|
||||
|
||||
import FileInput from '../../../../../../sharedComponents/FileInput';
|
||||
import ErrorAlert from '../../../../../../sharedComponents/ErrorAlerts/ErrorAlert';
|
||||
import CollapsibleFormWidget from '../CollapsibleFormWidget';
|
||||
|
||||
import TranscriptListItem from './TranscriptListItem';
|
||||
import Transcript from './Transcript';
|
||||
import { ErrorContext } from '../../../../hooks';
|
||||
import * as module from './index';
|
||||
|
||||
export const hooks = {
|
||||
updateErrors: ({ isUploadError, isDeleteError }) => {
|
||||
const [error, setError] = React.useContext(ErrorContext).transcripts;
|
||||
if (isUploadError) {
|
||||
setError({ ...error, uploadError: messages.uploadTranscriptError.defaultMessage });
|
||||
}
|
||||
if (isDeleteError) {
|
||||
setError({ ...error, deleteError: messages.deleteTranscriptError.defaultMessage });
|
||||
}
|
||||
},
|
||||
transcriptLanguages: (transcripts, intl) => {
|
||||
const languages = [];
|
||||
if (transcripts && transcripts.length > 0) {
|
||||
const fullTextTranslatedStrings = in8lTranscriptLanguages(intl);
|
||||
transcripts.forEach(transcript => {
|
||||
if (!(transcript === '')) {
|
||||
languages.push(fullTextTranslatedStrings[transcript]);
|
||||
}
|
||||
});
|
||||
|
||||
return languages.join(', ');
|
||||
}
|
||||
return 'None';
|
||||
},
|
||||
hasTranscripts: (transcripts) => {
|
||||
if (transcripts && transcripts.length > 0) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
onAddNewTranscript: ({ transcripts, updateField }) => {
|
||||
// keep blank lang code for now, will be updated once lang is selected.
|
||||
if (!transcripts) {
|
||||
updateField({ transcripts: [''] });
|
||||
return;
|
||||
}
|
||||
const newTranscripts = [...transcripts, ''];
|
||||
updateField({ transcripts: newTranscripts });
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Collapsible Form widget controlling video transcripts
|
||||
@@ -40,15 +82,16 @@ export const TranscriptWidget = ({
|
||||
updateField,
|
||||
isUploadError,
|
||||
isDeleteError,
|
||||
// intl
|
||||
intl,
|
||||
}) => {
|
||||
const [error] = React.useContext(ErrorContext).transcripts;
|
||||
const languagesArr = hooks.transcriptLanguages(transcripts);
|
||||
const fileInput = hooks.fileInput({ onAddFile: hooks.addFileCallback({ dispatch: useDispatch() }) });
|
||||
const hasTranscripts = hooks.hasTranscripts(transcripts);
|
||||
const fullTextLanguages = module.hooks.transcriptLanguages(transcripts, intl);
|
||||
const hasTranscripts = module.hooks.hasTranscripts(transcripts);
|
||||
return (
|
||||
<CollapsibleFormWidget
|
||||
isError={Object.keys(error).length !== 0}
|
||||
subtitle={languagesArr}
|
||||
subtitle={fullTextLanguages}
|
||||
title="Transcript"
|
||||
>
|
||||
<ErrorAlert
|
||||
@@ -67,10 +110,10 @@ export const TranscriptWidget = ({
|
||||
{hasTranscripts ? (
|
||||
|
||||
<Form.Group className="mt-4.5">
|
||||
{ Object.entries(transcripts).map(([language, value]) => (
|
||||
<TranscriptListItem
|
||||
{ transcripts.map((language, index) => (
|
||||
<Transcript
|
||||
language={language}
|
||||
title={value.filename}
|
||||
index={index}
|
||||
/>
|
||||
))}
|
||||
<div className="mb-1">
|
||||
@@ -113,10 +156,16 @@ export const TranscriptWidget = ({
|
||||
<FormattedMessage {...messages.addFirstTranscript} />
|
||||
</>
|
||||
)}
|
||||
<FileInput fileInput={fileInput} acceptedFiles=".srt" />
|
||||
<Button iconBefore={FileUpload} onClick={fileInput.click} variant="link">
|
||||
<FormattedMessage {...messages.uploadButtonLabel} />
|
||||
</Button>
|
||||
|
||||
<Stack gap={3} className="border-primary-100 border-top">
|
||||
<Button
|
||||
iconBefore={Add}
|
||||
variant="link"
|
||||
onClick={() => module.hooks.onAddNewTranscript({ transcripts, updateField })}
|
||||
>
|
||||
<FormattedMessage {...messages.uploadButtonLabel} />
|
||||
</Button>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</CollapsibleFormWidget>
|
||||
);
|
||||
@@ -126,12 +175,13 @@ TranscriptWidget.defaultProps = {
|
||||
};
|
||||
TranscriptWidget.propTypes = {
|
||||
// redux
|
||||
transcripts: PropTypes.shape({}).isRequired,
|
||||
transcripts: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
allowTranscriptDownloads: PropTypes.bool.isRequired,
|
||||
showTranscriptByDefault: PropTypes.bool.isRequired,
|
||||
updateField: PropTypes.func.isRequired,
|
||||
isUploadError: PropTypes.bool.isRequired,
|
||||
isDeleteError: PropTypes.bool.isRequired,
|
||||
intl: PropTypes.shape(intlShape).isRequired,
|
||||
};
|
||||
export const mapStateToProps = (state) => ({
|
||||
transcripts: selectors.video.transcripts(state),
|
||||
|
||||
@@ -5,7 +5,7 @@ import { RequestKeys } from '../../../../../../data/constants/requests';
|
||||
|
||||
import { formatMessage } from '../../../../../../../testUtils';
|
||||
import { actions, selectors } from '../../../../../../data/redux';
|
||||
import { TranscriptWidget, mapStateToProps, mapDispatchToProps } from '.';
|
||||
import * as module from './index';
|
||||
|
||||
jest.mock('react', () => ({
|
||||
...jest.requireActual('react'),
|
||||
@@ -36,85 +36,130 @@ jest.mock('../../../../../../data/redux', () => ({
|
||||
},
|
||||
},
|
||||
}));
|
||||
jest.mock('../CollapsibleFormWidget', () => 'CollapsibleFormWidget');
|
||||
jest.mock('./Transcript', () => 'Transcript');
|
||||
|
||||
describe('TranscriptWidget', () => {
|
||||
const props = {
|
||||
error: {},
|
||||
subtitle: 'SuBTItle',
|
||||
title: 'tiTLE',
|
||||
intl: { formatMessage },
|
||||
transcripts: {},
|
||||
allowTranscriptDownloads: false,
|
||||
showTranscriptByDefault: false,
|
||||
updateField: jest.fn().mockName('args.updateField'),
|
||||
isUploadError: false,
|
||||
isDeleteError: false,
|
||||
};
|
||||
describe('hooks', () => {
|
||||
describe('transcriptLanguages', () => {
|
||||
test('empty list of transcripts returns ', () => {
|
||||
expect(module.hooks.transcriptLanguages([])).toEqual('None');
|
||||
});
|
||||
test('unset gives none', () => {
|
||||
expect(module.hooks.transcriptLanguages(['', ''])).toEqual('');
|
||||
});
|
||||
test('en gives English', () => {
|
||||
expect(module.hooks.transcriptLanguages(['en'])).toEqual('English');
|
||||
});
|
||||
test('en, FR gives English, French', () => {
|
||||
expect(module.hooks.transcriptLanguages(['en', 'fr'])).toEqual('English, French');
|
||||
});
|
||||
});
|
||||
describe('hasTranscripts', () => {
|
||||
test('null returns false ', () => {
|
||||
expect(module.hooks.hasTranscripts(null)).toEqual(false);
|
||||
});
|
||||
test('empty list returns false', () => {
|
||||
expect(module.hooks.hasTranscripts([])).toEqual(false);
|
||||
});
|
||||
test('content returns true', () => {
|
||||
expect(module.hooks.hasTranscripts(['en'])).toEqual(true);
|
||||
});
|
||||
});
|
||||
describe('onAddNewTranscript', () => {
|
||||
const mockUpdateField = jest.fn();
|
||||
test('null returns [empty string] ', () => {
|
||||
module.hooks.onAddNewTranscript({ transcripts: null, updateField: mockUpdateField });
|
||||
expect(mockUpdateField).toHaveBeenCalledWith({ transcripts: [''] });
|
||||
});
|
||||
test(' transcripts return list with blank added', () => {
|
||||
const mocklist = ['en', 'fr', 3];
|
||||
module.hooks.onAddNewTranscript({ transcripts: mocklist, updateField: mockUpdateField });
|
||||
|
||||
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={{ en: 'sOMeUrl' }} />),
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
test('snapshots: renders as expected with allowTranscriptDownloads true', () => {
|
||||
expect(
|
||||
shallow(<TranscriptWidget {...props} allowTranscriptDownloads transcripts={{ en: 'sOMeUrl' }} />),
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
test('snapshots: renders as expected with showTranscriptByDefault true', () => {
|
||||
expect(
|
||||
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();
|
||||
expect(mockUpdateField).toHaveBeenCalledWith({ transcripts: ['en', 'fr', 3, ''] });
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('mapStateToProps', () => {
|
||||
const testState = { A: 'pple', B: 'anana', C: 'ucumber' };
|
||||
test('transcripts from video.transcript', () => {
|
||||
expect(
|
||||
mapStateToProps(testState).transcripts,
|
||||
).toEqual(selectors.video.transcripts(testState));
|
||||
|
||||
describe('component', () => {
|
||||
const props = {
|
||||
error: {},
|
||||
subtitle: 'SuBTItle',
|
||||
title: 'tiTLE',
|
||||
intl: { formatMessage },
|
||||
transcripts: [],
|
||||
allowTranscriptDownloads: false,
|
||||
showTranscriptByDefault: false,
|
||||
updateField: jest.fn().mockName('args.updateField'),
|
||||
isUploadError: false,
|
||||
isDeleteError: false,
|
||||
};
|
||||
|
||||
describe('snapshots', () => {
|
||||
test('snapshots: renders as expected with default props', () => {
|
||||
expect(
|
||||
shallow(<module.TranscriptWidget {...props} />),
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
test('snapshots: renders as expected with transcripts', () => {
|
||||
expect(
|
||||
shallow(<module.TranscriptWidget {...props} transcripts={['en']} />),
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
test('snapshots: renders as expected with allowTranscriptDownloads true', () => {
|
||||
expect(
|
||||
shallow(<module.TranscriptWidget {...props} allowTranscriptDownloads transcripts={['en']} />),
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
test('snapshots: renders as expected with showTranscriptByDefault true', () => {
|
||||
expect(
|
||||
shallow(<module.TranscriptWidget {...props} showTranscriptByDefault transcripts={['en']} />),
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
test('snapshot: renders ErrorAlert with upload error message', () => {
|
||||
expect(
|
||||
shallow(<module.TranscriptWidget {...props} isUploadError transcripts={['en', 'fr']} />),
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
test('snapshot: renders ErrorAlert with delete error message', () => {
|
||||
expect(
|
||||
shallow(<module.TranscriptWidget {...props} isDeleteError transcripts={['en']} />),
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
test('allowTranscriptDownloads from video.allowTranscriptDownloads', () => {
|
||||
expect(
|
||||
mapStateToProps(testState).allowTranscriptDownloads,
|
||||
).toEqual(selectors.video.allowTranscriptDownloads(testState));
|
||||
describe('mapStateToProps', () => {
|
||||
const testState = { A: 'pple', B: 'anana', C: 'ucumber' };
|
||||
test('transcripts from video.transcript', () => {
|
||||
expect(
|
||||
module.mapStateToProps(testState).transcripts,
|
||||
).toEqual(selectors.video.transcripts(testState));
|
||||
});
|
||||
test('allowTranscriptDownloads from video.allowTranscriptDownloads', () => {
|
||||
expect(
|
||||
module.mapStateToProps(testState).allowTranscriptDownloads,
|
||||
).toEqual(selectors.video.allowTranscriptDownloads(testState));
|
||||
});
|
||||
test('showTranscriptByDefault from video.showTranscriptByDefault', () => {
|
||||
expect(
|
||||
module.mapStateToProps(testState).showTranscriptByDefault,
|
||||
).toEqual(selectors.video.showTranscriptByDefault(testState));
|
||||
});
|
||||
test('isUploadError from requests.isFinished', () => {
|
||||
expect(
|
||||
module.mapStateToProps(testState).isUploadError,
|
||||
).toEqual(selectors.requests.isFailed(testState, { requestKey: RequestKeys.uploadTranscript }));
|
||||
});
|
||||
test('isDeleteError from requests.isFinished', () => {
|
||||
expect(
|
||||
module.mapStateToProps(testState).isDeleteError,
|
||||
).toEqual(selectors.requests.isFailed(testState, { requestKey: RequestKeys.deleteTranscript }));
|
||||
});
|
||||
});
|
||||
test('showTranscriptByDefault from video.showTranscriptByDefault', () => {
|
||||
expect(
|
||||
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();
|
||||
test('updateField from actions.video.updateField', () => {
|
||||
expect(mapDispatchToProps.updateField).toEqual(dispatch(actions.video.updateField));
|
||||
describe('mapDispatchToProps', () => {
|
||||
const dispatch = jest.fn();
|
||||
test('updateField from actions.video.updateField', () => {
|
||||
expect(module.mapDispatchToProps.updateField).toEqual(dispatch(actions.video.updateField));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
export const messages = {
|
||||
uploadButtonLabel: {
|
||||
id: 'authoring.videoeditor.transcripts.upload.label',
|
||||
defaultMessage: 'Upload Transcript',
|
||||
defaultMessage: 'Add a transcript',
|
||||
description: 'Label for upload button',
|
||||
},
|
||||
addFirstTranscript: {
|
||||
|
||||
@@ -20,4 +20,6 @@ export const RequestKeys = StrictDict({
|
||||
uploadTranscript: 'uploadTranscript',
|
||||
deleteTranscript: 'deleteTranscript',
|
||||
fetchCourseDetails: 'fetchCourseDetails',
|
||||
updateTranscriptLanguage: 'updateTranscriptLanguage',
|
||||
getTranscriptFile: 'getTranscriptFile',
|
||||
});
|
||||
|
||||
@@ -189,6 +189,22 @@ export const videoTranscriptLanguages = StrictDict({
|
||||
zu: 'Zulu',
|
||||
});
|
||||
|
||||
export const in8lTranscriptLanguages = (intl) => {
|
||||
const messageLookup = {};
|
||||
// for tests and non-internationlized setups, return en
|
||||
if (!intl?.formatMessage) {
|
||||
return videoTranscriptLanguages;
|
||||
}
|
||||
Object.keys(videoTranscriptLanguages).forEach((code) => {
|
||||
messageLookup[code] = intl.formatMessage({
|
||||
id: `authoring.videoeditor.transcripts.language.${code}`,
|
||||
defaultMessage: videoTranscriptLanguages[code],
|
||||
description: `Name of Language called in English ${videoTranscriptLanguages[code]}`,
|
||||
});
|
||||
});
|
||||
return messageLookup;
|
||||
};
|
||||
|
||||
export const timeKeys = StrictDict({
|
||||
startTime: 'startTime',
|
||||
stopTime: 'stopTime',
|
||||
|
||||
@@ -190,14 +190,47 @@ export const uploadTranscript = ({
|
||||
}));
|
||||
};
|
||||
|
||||
export const updateTranscriptLanguage = ({
|
||||
file,
|
||||
languageBeforeChange,
|
||||
newLanguageCode,
|
||||
videoId,
|
||||
...rest
|
||||
}) => (dispatch, getState) => {
|
||||
dispatch(module.networkRequest({
|
||||
requestKey: RequestKeys.updateTranscriptLanguage,
|
||||
promise: api.uploadTranscript({
|
||||
blockId: selectors.app.blockId(getState()),
|
||||
transcript: file,
|
||||
videoId,
|
||||
language: languageBeforeChange,
|
||||
newLanguage: newLanguageCode,
|
||||
studioEndpointUrl: selectors.app.studioEndpointUrl(getState()),
|
||||
}),
|
||||
...rest,
|
||||
}));
|
||||
};
|
||||
|
||||
export const getTranscriptFile = ({ language, videoId, ...rest }) => (dispatch, getState) => {
|
||||
dispatch(module.networkRequest({
|
||||
requestKey: RequestKeys.getTranscriptFile,
|
||||
promise: api.getTranscript({
|
||||
studioEndpointUrl: selectors.app.studioEndpointUrl(getState()),
|
||||
blockId: selectors.app.blockId(getState()),
|
||||
videoId,
|
||||
language,
|
||||
}),
|
||||
...rest,
|
||||
}));
|
||||
};
|
||||
|
||||
export const fetchCourseDetails = ({ ...rest }) => (dispatch, getState) => {
|
||||
dispatch(module.networkRequest({
|
||||
requestKey: RequestKeys.fetchCourseDetails,
|
||||
promise: api
|
||||
.fetchCourseDetails({
|
||||
studioEndpointUrl: selectors.app.studioEndpointUrl(getState()),
|
||||
learningContextId: selectors.app.learningContextId(getState()),
|
||||
}),
|
||||
promise: api.fetchCourseDetails({
|
||||
studioEndpointUrl: selectors.app.studioEndpointUrl(getState()),
|
||||
learningContextId: selectors.app.learningContextId(getState()),
|
||||
}),
|
||||
...rest,
|
||||
}));
|
||||
};
|
||||
@@ -213,5 +246,7 @@ export default StrictDict({
|
||||
uploadThumbnail,
|
||||
deleteTranscript,
|
||||
uploadTranscript,
|
||||
updateTranscriptLanguage,
|
||||
fetchCourseDetails,
|
||||
getTranscriptFile,
|
||||
});
|
||||
|
||||
@@ -33,6 +33,7 @@ jest.mock('../../services/cms/api', () => ({
|
||||
uploadThumbnail: jest.fn(),
|
||||
uploadTranscript: jest.fn(),
|
||||
deleteTranscript: jest.fn(),
|
||||
getTranscript: jest.fn(),
|
||||
}));
|
||||
|
||||
const apiKeys = keyStore(api);
|
||||
@@ -351,6 +352,52 @@ describe('requests thunkActions module', () => {
|
||||
},
|
||||
});
|
||||
});
|
||||
describe('getTranscriptFile', () => {
|
||||
const language = 'SoME laNGUage CoNtent As String';
|
||||
const videoId = 'SoME VidEOid CoNtent As String';
|
||||
testNetworkRequestAction({
|
||||
action: requests.getTranscriptFile,
|
||||
args: { language, videoId, ...fetchParams },
|
||||
expectedString: 'with getTranscriptFile promise',
|
||||
expectedData: {
|
||||
...fetchParams,
|
||||
requestKey: RequestKeys.getTranscriptFile,
|
||||
promise: api.getTranscript({
|
||||
blockId: selectors.app.blockId(testState),
|
||||
language,
|
||||
videoId,
|
||||
studioEndpointUrl: selectors.app.studioEndpointUrl(testState),
|
||||
}),
|
||||
},
|
||||
});
|
||||
});
|
||||
describe('updateTranscriptLanguage', () => {
|
||||
const languageBeforeChange = 'SoME laNGUage CoNtent As String';
|
||||
const newLanguageCode = 'SoME NEW laNGUage CoNtent As String';
|
||||
const videoId = 'SoME VidEOid CoNtent As String';
|
||||
testNetworkRequestAction({
|
||||
action: requests.updateTranscriptLanguage,
|
||||
args: {
|
||||
languageBeforeChange,
|
||||
newLanguageCode,
|
||||
videoId,
|
||||
...fetchParams,
|
||||
},
|
||||
expectedString: 'with uploadTranscript promise',
|
||||
expectedData: {
|
||||
...fetchParams,
|
||||
requestKey: RequestKeys.updateTranscriptLanguage,
|
||||
promise: api.uploadTranscript({
|
||||
blockId: selectors.app.blockId(testState),
|
||||
videoId,
|
||||
language: languageBeforeChange,
|
||||
newLanguage: newLanguageCode,
|
||||
studioEndpointUrl: selectors.app.studioEndpointUrl(testState),
|
||||
}),
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
describe('uploadTranscript', () => {
|
||||
const language = 'SoME laNGUage CoNtent As String';
|
||||
const videoId = 'SoME VidEOid CoNtent As String';
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { actions, selectors } from '..';
|
||||
import { removeItemOnce } from '../../../utils';
|
||||
import * as requests from './requests';
|
||||
import * as module from './video';
|
||||
import { valueFromDuration } from '../../../containers/VideoEditor/components/VideoSettingsModal/components/duration';
|
||||
@@ -7,7 +8,7 @@ export const loadVideoData = () => (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const rawVideoData = state.app.blockValue.data.metadata ? state.app.blockValue.data.metadata : {};
|
||||
const courseLicenseData = state.app.courseDetails.data ? state.app.courseDetails.data : {};
|
||||
const licenseData = state.app.studioView?.data?.html;
|
||||
const studioView = state.app.studioView?.data?.html;
|
||||
const {
|
||||
videoSource,
|
||||
videoType,
|
||||
@@ -18,18 +19,20 @@ export const loadVideoData = () => (dispatch, getState) => {
|
||||
youtubeId: rawVideoData.youtube_id_1_0,
|
||||
html5Sources: rawVideoData.html5_sources,
|
||||
});
|
||||
const [licenseType, licenseOptions] = module.parseLicense({ licenseData, level: 'block' });
|
||||
const [licenseType, licenseOptions] = module.parseLicense({ licenseData: studioView, level: 'block' });
|
||||
const transcripts = module.parseTranscripts({ transcriptsData: studioView });
|
||||
const [courseLicenseType, courseLicenseDetails] = module.parseLicense({
|
||||
licenseData: courseLicenseData.license,
|
||||
level: 'course',
|
||||
});
|
||||
|
||||
dispatch(actions.video.load({
|
||||
videoSource,
|
||||
videoType,
|
||||
videoId,
|
||||
fallbackVideos,
|
||||
allowVideoDownloads: rawVideoData.download_video,
|
||||
transcripts: rawVideoData.transcripts || {},
|
||||
transcripts,
|
||||
allowTranscriptDownloads: rawVideoData.download_track,
|
||||
showTranscriptByDefault: rawVideoData.show_captions,
|
||||
duration: { // TODO duration is not always sent so they should be calculated.
|
||||
@@ -100,6 +103,16 @@ export const determineVideoSource = ({
|
||||
};
|
||||
};
|
||||
|
||||
export const parseTranscripts = ({ transcriptsData }) => {
|
||||
if (!transcriptsData) {
|
||||
return [];
|
||||
}
|
||||
const startString = 'language.", "value": ';
|
||||
const cleanedStr = transcriptsData.replace(/"/g, '"');
|
||||
const metadataStr = cleanedStr.substring(cleanedStr.indexOf(startString) + startString.length, cleanedStr.indexOf(', "type": "VideoTranslations"'));
|
||||
return Object.keys(JSON.parse(metadataStr));
|
||||
};
|
||||
|
||||
// partially copied from frontend-app-learning/src/courseware/course/course-license/CourseLicense.jsx
|
||||
export const parseLicense = ({ licenseData, level }) => {
|
||||
if (!licenseData) {
|
||||
@@ -205,31 +218,32 @@ export const uploadHandout = ({ file }) => (dispatch) => {
|
||||
|
||||
// Transcript Thunks:
|
||||
|
||||
export const uploadTranscript = ({ language, filename, file }) => (dispatch, getState) => {
|
||||
export const uploadTranscript = ({ language, file }) => (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const { transcripts, videoId } = state.video;
|
||||
let lang = language;
|
||||
if (!language) {
|
||||
[[lang]] = selectors.video.openLanguages(state);
|
||||
}
|
||||
// Remove the placeholder '' from the unset language from the list of transcripts.
|
||||
const transcriptsPlaceholderRemoved = (transcripts === []) ? transcripts : removeItemOnce(transcripts, '');
|
||||
|
||||
dispatch(requests.uploadTranscript({
|
||||
language: lang,
|
||||
language,
|
||||
videoId,
|
||||
transcript: file,
|
||||
onSuccess: (response) => {
|
||||
dispatch(actions.video.updateField({
|
||||
transcripts: {
|
||||
...transcripts,
|
||||
[lang]: { filename },
|
||||
},
|
||||
}));
|
||||
// if we aren't replacing, add the language to the redux store.
|
||||
if (!transcriptsPlaceholderRemoved.includes(language)) {
|
||||
dispatch(actions.video.updateField({
|
||||
transcripts: [
|
||||
...transcriptsPlaceholderRemoved,
|
||||
language],
|
||||
}));
|
||||
}
|
||||
|
||||
if (selectors.video.videoId(state) === '') {
|
||||
dispatch(actions.video.updateField({
|
||||
videoId: response.edx_video_id,
|
||||
videoId: response.data.edx_video_id,
|
||||
}));
|
||||
}
|
||||
},
|
||||
|
||||
}));
|
||||
};
|
||||
|
||||
@@ -240,31 +254,43 @@ export const deleteTranscript = ({ language }) => (dispatch, getState) => {
|
||||
language,
|
||||
videoId,
|
||||
onSuccess: () => {
|
||||
const updateTranscripts = {};
|
||||
Object.keys(transcripts).forEach((key) => {
|
||||
if (key !== language) {
|
||||
updateTranscripts[key] = transcripts[key];
|
||||
}
|
||||
});
|
||||
dispatch(actions.video.updateField({ transcripts: updateTranscripts }));
|
||||
const updatedTranscripts = transcripts.filter((langCode) => langCode !== language);
|
||||
dispatch(actions.video.updateField({ transcripts: updatedTranscripts }));
|
||||
},
|
||||
}));
|
||||
};
|
||||
|
||||
export const updateTranscriptLanguage = ({ newLanguageCode, languageBeforeChange }) => (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const { video: { transcripts, videoId } } = state;
|
||||
selectors.video.getTranscriptDownloadUrl(state);
|
||||
dispatch(requests.getTranscriptFile({
|
||||
videoId,
|
||||
language: languageBeforeChange,
|
||||
onSuccess: (response) => {
|
||||
dispatch(requests.updateTranscriptLanguage({
|
||||
languageBeforeChange,
|
||||
file: new File([new Blob([response.data], { type: 'text/plain' })], `${videoId}_${newLanguageCode}.srt`, { type: 'text/plain' }),
|
||||
newLanguageCode,
|
||||
videoId,
|
||||
onSuccess: () => {
|
||||
const newTranscripts = transcripts
|
||||
.filter(transcript => transcript !== languageBeforeChange);
|
||||
newTranscripts.push(newLanguageCode);
|
||||
dispatch(actions.video.updateField({ transcripts: newTranscripts }));
|
||||
},
|
||||
}));
|
||||
},
|
||||
}));
|
||||
};
|
||||
|
||||
export const replaceTranscript = ({ newFile, newFilename, language }) => (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const { transcripts, videoId } = state.video;
|
||||
const { 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 }));
|
||||
},
|
||||
}));
|
||||
@@ -278,6 +304,7 @@ export default {
|
||||
uploadThumbnail,
|
||||
uploadTranscript,
|
||||
deleteTranscript,
|
||||
updateTranscriptLanguage,
|
||||
replaceTranscript,
|
||||
uploadHandout,
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { actions } from '..';
|
||||
import { keyStore } from '../../../utils';
|
||||
import keyStore from '../../../utils/keyStore';
|
||||
import * as thunkActions from './video';
|
||||
|
||||
jest.mock('..', () => ({
|
||||
@@ -16,6 +16,7 @@ jest.mock('..', () => ({
|
||||
video: {
|
||||
videoId: (state) => ({ videoId: state }),
|
||||
videoSettings: (state) => ({ videoSettings: state }),
|
||||
getTranscriptDownloadUrl: (state) => ({ getTranscriptDownloadUrl: state }),
|
||||
},
|
||||
},
|
||||
}));
|
||||
@@ -25,10 +26,17 @@ jest.mock('./requests', () => ({
|
||||
uploadThumbnail: (args) => ({ uploadThumbnail: args }),
|
||||
deleteTranscript: (args) => ({ deleteTranscript: args }),
|
||||
uploadTranscript: (args) => ({ uploadTranscript: args }),
|
||||
getTranscriptFile: (args) => ({ getTranscriptFile: args }),
|
||||
updateTranscriptLanguage: (args) => ({ updateTranscriptLanguage: args }),
|
||||
}));
|
||||
|
||||
jest.mock('../../../utils', () => ({
|
||||
removeItemOnce: (args) => (args),
|
||||
}));
|
||||
|
||||
const thunkActionsKeys = keyStore(thunkActions);
|
||||
|
||||
const mockLanguage = 'la';
|
||||
const mockLanguage = 'na';
|
||||
const mockFile = 'soMEtRANscRipT';
|
||||
const mockFilename = 'soMEtRANscRipT.srt';
|
||||
const mockThumbnail = 'sOMefILE';
|
||||
@@ -46,16 +54,16 @@ const testMetadata = {
|
||||
license: 'liCENse',
|
||||
show_captions: 'shOWcapTIONS',
|
||||
start_time: 0,
|
||||
transcripts: { la: 'test VALUE' },
|
||||
transcripts: ['do', 're', 'mi'],
|
||||
thumbnail: 'thuMBNaIl',
|
||||
};
|
||||
const testState = {
|
||||
transcripts: { la: 'test VALUE' },
|
||||
transcripts: ['la'],
|
||||
thumbnail: 'sOMefILE',
|
||||
originalThumbnail: null,
|
||||
videoId: 'soMEvIDEo',
|
||||
};
|
||||
const testUpload = { transcripts: { la: { filename: mockFilename } } };
|
||||
const testUpload = { transcripts: ['la', 'na'] };
|
||||
const testReplaceUpload = {
|
||||
file: mockFile,
|
||||
language: mockLanguage,
|
||||
@@ -97,6 +105,9 @@ describe('video thunkActions', () => {
|
||||
sa: false,
|
||||
},
|
||||
]);
|
||||
jest.spyOn(thunkActions, thunkActionsKeys.parseTranscripts).mockReturnValue(
|
||||
testMetadata.transcripts,
|
||||
);
|
||||
thunkActions.loadVideoData()(dispatch, getState);
|
||||
[[dispatchedLoad], [dispatchedAction]] = dispatch.mock.calls;
|
||||
});
|
||||
@@ -320,7 +331,7 @@ describe('video thunkActions', () => {
|
||||
});
|
||||
describe('deleteTranscript', () => {
|
||||
beforeEach(() => {
|
||||
thunkActions.deleteTranscript({ language: mockLanguage })(dispatch, getState);
|
||||
thunkActions.deleteTranscript({ language: 'la' })(dispatch, getState);
|
||||
[[dispatchedAction]] = dispatch.mock.calls;
|
||||
});
|
||||
it('dispatches deleteTranscript action', () => {
|
||||
@@ -329,7 +340,7 @@ describe('video thunkActions', () => {
|
||||
it('dispatches actions.video.updateField on success', () => {
|
||||
dispatch.mockClear();
|
||||
dispatchedAction.deleteTranscript.onSuccess();
|
||||
expect(dispatch).toHaveBeenCalledWith(actions.video.updateField({ transcripts: {} }));
|
||||
expect(dispatch).toHaveBeenCalledWith(actions.video.updateField({ transcripts: [] }));
|
||||
});
|
||||
});
|
||||
describe('uploadTranscript', () => {
|
||||
@@ -350,11 +361,28 @@ describe('video thunkActions', () => {
|
||||
expect(dispatch).toHaveBeenCalledWith(actions.video.updateField(testUpload));
|
||||
});
|
||||
});
|
||||
describe('updateTranscriptLanguage', () => {
|
||||
beforeEach(() => {
|
||||
thunkActions.updateTranscriptLanguage({
|
||||
newLanguageCode: mockLanguage,
|
||||
languageBeforeChange: `${mockLanguage}i`,
|
||||
})(dispatch, getState);
|
||||
[[dispatchedAction]] = dispatch.mock.calls;
|
||||
});
|
||||
it('dispatches uploadTranscript action', () => {
|
||||
expect(dispatchedAction.getTranscriptFile).not.toEqual(undefined);
|
||||
});
|
||||
it('dispatches actions.video.updateField on success', () => {
|
||||
dispatch.mockClear();
|
||||
dispatchedAction.getTranscriptFile.onSuccess({ data: 'sOme StRinG Data' });
|
||||
expect(dispatch).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
describe('replaceTranscript', () => {
|
||||
const spies = {};
|
||||
beforeEach(() => {
|
||||
spies.uploadTranscript = jest.spyOn(thunkActions, thunkActionsKeys.uploadTranscript)
|
||||
.mockReturnValueOnce(testReplaceUpload);
|
||||
spies.uploadTranscript = jest.spyOn(thunkActions, 'uploadTranscript')
|
||||
.mockReturnValue(testReplaceUpload).mockName('uploadTranscript');
|
||||
thunkActions.replaceTranscript({
|
||||
newFile: mockFile,
|
||||
newFilename: mockFilename,
|
||||
@@ -368,9 +396,7 @@ describe('video thunkActions', () => {
|
||||
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));
|
||||
expect(dispatch).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -12,7 +12,7 @@ const initialState = {
|
||||
],
|
||||
allowVideoDownloads: false,
|
||||
thumbnail: null,
|
||||
transcripts: {},
|
||||
transcripts: [],
|
||||
allowTranscriptDownloads: false,
|
||||
duration: {
|
||||
startTime: '00:00:00',
|
||||
|
||||
@@ -37,8 +37,8 @@ export const openLanguages = createSelector(
|
||||
if (!transcripts) {
|
||||
return videoTranscriptLanguages;
|
||||
}
|
||||
const open = Object.entries(videoTranscriptLanguages).filter(
|
||||
([lang]) => !Object.keys(transcripts).includes(lang),
|
||||
const open = Object.keys(videoTranscriptLanguages).filter(
|
||||
(lang) => !transcripts.includes(lang),
|
||||
);
|
||||
return open;
|
||||
},
|
||||
|
||||
@@ -51,6 +51,19 @@ export const apiMethods = {
|
||||
data,
|
||||
);
|
||||
},
|
||||
getTranscript: ({
|
||||
studioEndpointUrl,
|
||||
language,
|
||||
blockId,
|
||||
videoId,
|
||||
}) => {
|
||||
const getJSON = { data: { lang: language, edx_video_id: videoId } };
|
||||
return get(
|
||||
`${urls.videoTranscripts({ studioEndpointUrl, blockId })}?language_code=${language}`,
|
||||
getJSON,
|
||||
);
|
||||
},
|
||||
|
||||
deleteTranscript: ({
|
||||
studioEndpointUrl,
|
||||
language,
|
||||
@@ -69,12 +82,13 @@ export const apiMethods = {
|
||||
transcript,
|
||||
videoId,
|
||||
language,
|
||||
newLanguage = null,
|
||||
}) => {
|
||||
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);
|
||||
data.append('new_language_code', newLanguage || language);
|
||||
return post(
|
||||
urls.videoTranscripts({ studioEndpointUrl, blockId }),
|
||||
data,
|
||||
@@ -159,13 +173,14 @@ export const loadImages = (rawImages) => camelizeKeys(rawImages).reduce(
|
||||
{},
|
||||
);
|
||||
|
||||
export const processVideoIds = ({ videoSource, fallbackVideos }) => {
|
||||
let edxVideoId = '';
|
||||
export const processVideoIds = ({ videoSource, fallbackVideos, edxVideoId }) => {
|
||||
let newEdxVideoId = edxVideoId;
|
||||
let youtubeId = '';
|
||||
const html5Sources = [];
|
||||
|
||||
// overwrite videoId if source is changed.
|
||||
if (module.isEdxVideo(videoSource)) {
|
||||
edxVideoId = videoSource;
|
||||
newEdxVideoId = videoSource;
|
||||
} else if (module.parseYoutubeId(videoSource)) {
|
||||
youtubeId = module.parseYoutubeId(videoSource);
|
||||
} else if (videoSource) {
|
||||
@@ -177,7 +192,7 @@ export const processVideoIds = ({ videoSource, fallbackVideos }) => {
|
||||
}
|
||||
|
||||
return {
|
||||
edxVideoId,
|
||||
edxVideoId: newEdxVideoId,
|
||||
html5Sources,
|
||||
youtubeId,
|
||||
};
|
||||
|
||||
@@ -287,6 +287,21 @@ describe('cms api', () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('transcript get', () => {
|
||||
it('should call get with urls.videoTranscripts and transcript data', () => {
|
||||
const mockJSON = { data: { lang: language, edx_video_id: videoId } };
|
||||
apiMethods.getTranscript({
|
||||
blockId,
|
||||
studioEndpointUrl,
|
||||
videoId,
|
||||
language,
|
||||
});
|
||||
expect(get).toHaveBeenCalledWith(
|
||||
`${urls.videoTranscripts({ studioEndpointUrl, blockId })}?language_code=${language}`,
|
||||
mockJSON,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('processVideoIds', () => {
|
||||
const edxVideoId = 'eDXviDEoid';
|
||||
@@ -305,6 +320,7 @@ describe('cms api', () => {
|
||||
});
|
||||
it('returns edxVideoId when there are no fallbackVideos', () => {
|
||||
expect(api.processVideoIds({
|
||||
edxVideoId,
|
||||
videoSource: edxVideoId,
|
||||
fallbackVideos: [],
|
||||
})).toEqual({
|
||||
@@ -315,6 +331,7 @@ describe('cms api', () => {
|
||||
});
|
||||
it('returns edxVideoId and html5Sources when there are fallbackVideos', () => {
|
||||
expect(api.processVideoIds({
|
||||
edxVideoId,
|
||||
videoSource: edxVideoId,
|
||||
fallbackVideos: html5Sources,
|
||||
})).toEqual({
|
||||
@@ -331,20 +348,22 @@ describe('cms api', () => {
|
||||
});
|
||||
it('returns youtubeId when there are no fallbackVideos', () => {
|
||||
expect(api.processVideoIds({
|
||||
edxVideoId,
|
||||
videoSource: edxVideoId,
|
||||
fallbackVideos: [],
|
||||
})).toEqual({
|
||||
edxVideoId: '',
|
||||
edxVideoId,
|
||||
html5Sources: [],
|
||||
youtubeId,
|
||||
});
|
||||
});
|
||||
it('returns youtubeId and html5Sources when there are fallbackVideos', () => {
|
||||
expect(api.processVideoIds({
|
||||
edxVideoId,
|
||||
videoSource: edxVideoId,
|
||||
fallbackVideos: html5Sources,
|
||||
})).toEqual({
|
||||
edxVideoId: '',
|
||||
edxVideoId,
|
||||
html5Sources,
|
||||
youtubeId,
|
||||
});
|
||||
@@ -357,20 +376,22 @@ describe('cms api', () => {
|
||||
});
|
||||
it('returns html5Sources when there are no fallbackVideos', () => {
|
||||
expect(api.processVideoIds({
|
||||
edxVideoId,
|
||||
videoSource: html5Sources[0],
|
||||
fallbackVideos: [],
|
||||
})).toEqual({
|
||||
edxVideoId: '',
|
||||
edxVideoId,
|
||||
html5Sources: [html5Sources[0]],
|
||||
youtubeId: '',
|
||||
});
|
||||
});
|
||||
it('returns html5Sources when there are fallbackVideos', () => {
|
||||
expect(api.processVideoIds({
|
||||
edxVideoId,
|
||||
videoSource: html5Sources[0],
|
||||
fallbackVideos: [html5Sources[1]],
|
||||
})).toEqual({
|
||||
edxVideoId: '',
|
||||
edxVideoId,
|
||||
html5Sources,
|
||||
youtubeId: '',
|
||||
});
|
||||
|
||||
@@ -1,12 +1,28 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
export const FileInput = ({ fileInput, acceptedFiles }) => (
|
||||
export const fileInput = ({ onAddFile }) => {
|
||||
const ref = React.useRef();
|
||||
const click = () => ref.current.click();
|
||||
const addFile = (e) => {
|
||||
const file = e.target.files[0];
|
||||
if (file) {
|
||||
onAddFile(file);
|
||||
}
|
||||
};
|
||||
return {
|
||||
click,
|
||||
addFile,
|
||||
ref,
|
||||
};
|
||||
};
|
||||
|
||||
export const FileInput = ({ fileInput: hook, acceptedFiles }) => (
|
||||
<input
|
||||
accept={acceptedFiles}
|
||||
className="upload d-none"
|
||||
onChange={fileInput.addFile}
|
||||
ref={fileInput.ref}
|
||||
onChange={hook.addFile}
|
||||
ref={hook.ref}
|
||||
type="file"
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -2,3 +2,4 @@
|
||||
export { default as StrictDict } from './StrictDict';
|
||||
export { default as keyStore } from './keyStore';
|
||||
export { default as camelizeKeys } from './camelizeKeys';
|
||||
export { default as removeItemOnce } from './removeOnce';
|
||||
|
||||
13
src/editors/utils/removeOnce.js
Normal file
13
src/editors/utils/removeOnce.js
Normal file
@@ -0,0 +1,13 @@
|
||||
const removeItemOnce = (arr, value) => {
|
||||
// create a deep copy as array.splice doesn't work if the array has been dereferenced.
|
||||
// structuredClone works in node >11, and we are on node 16.
|
||||
// eslint-disable-next-line
|
||||
const deepCopy = structuredClone(arr);
|
||||
const index = deepCopy.indexOf(value);
|
||||
if (index > -1) {
|
||||
deepCopy.splice(index, 1);
|
||||
}
|
||||
return deepCopy;
|
||||
};
|
||||
|
||||
export default removeItemOnce;
|
||||
Reference in New Issue
Block a user