Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import { useDispatch } from 'react-redux';

import { FormattedMessage } from '@edx/frontend-platform/i18n';
import {
Expand All @@ -15,47 +15,38 @@ import { Close } from '@openedx/paragon/icons';
import messages from './messages';
import { thunkActions } from '../../../../../../data/redux';

const ImportTranscriptCard = ({
setOpen,
// redux
importTranscript,
}) => (
<Stack gap={3} className="border rounded border-primary-200 p-4">
<ActionRow className="h5">
<FormattedMessage {...messages.importHeader} />
<ActionRow.Spacer />
<IconButton
src={Close}
iconAs={Icon}
onClick={() => setOpen(false)}
/>
</ActionRow>
<FormattedMessage {...messages.importMessage} />
<Button
variant="outline-primary"
size="sm"
onClick={importTranscript}
>
<FormattedMessage {...messages.importButtonLabel} />
</Button>
</Stack>
);

ImportTranscriptCard.defaultProps = {
setOpen: true,
const ImportTranscriptCard = ({ setOpen }) => {
const dispatch = useDispatch();

const importTranscript = () => {
dispatch(thunkActions.video.importTranscript());
};

return (
<Stack gap={3} className="border rounded border-primary-200 p-4">
<ActionRow className="h5">
<FormattedMessage {...messages.importHeader} />
<ActionRow.Spacer />
<IconButton
src={Close}
iconAs={Icon}
onClick={() => setOpen(false)}
/>
</ActionRow>
<FormattedMessage {...messages.importMessage} />
<Button
variant="outline-primary"
size="sm"
onClick={importTranscript}
>
<FormattedMessage {...messages.importButtonLabel} />
</Button>
</Stack>
);
};

ImportTranscriptCard.propTypes = {
setOpen: PropTypes.func,
// redux
importTranscript: PropTypes.func.isRequired,
};

export const mapStateToProps = () => ({});

export const mapDispatchToProps = {
importTranscript: thunkActions.video.importTranscript,
setOpen: PropTypes.func.isRequired,
};

export const ImportTranscriptCardInternal = ImportTranscriptCard; // For testing only
export default connect(mapStateToProps, mapDispatchToProps)(ImportTranscriptCard);
export { ImportTranscriptCard };
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,22 @@ import React from 'react';
import {
render, screen, fireEvent, initializeMocks,
} from '@src/testUtils';
import { ImportTranscriptCardInternal as ImportTranscriptCard } from './ImportTranscriptCard';
import * as ReactRedux from 'react-redux';
import { ImportTranscriptCard } from './ImportTranscriptCard';

jest.mock('../../../../../../data/redux', () => ({
thunkActions: {
video: {
importTranscript: jest.fn().mockName('thunkActions.video.importTranscript'),
},
},
}));
const mockDispatch = jest.fn();

describe('ImportTranscriptCard (RTL)', () => {
const mockSetOpen = jest.fn();
const mockImportTranscript = jest.fn();

beforeEach(() => {
jest.spyOn(ReactRedux, 'useDispatch').mockReturnValue(mockDispatch);
initializeMocks();
});

it('renders header, message, and button', () => {
render(
<ImportTranscriptCard setOpen={mockSetOpen} importTranscript={mockImportTranscript} />,
<ImportTranscriptCard setOpen={mockSetOpen} />,
);
expect(screen.getByText('Import transcript from YouTube?')).toBeInTheDocument();
expect(screen.getByText('We found transcript for this video on YouTube. Would you like to import it now?')).toBeInTheDocument();
Expand All @@ -31,7 +26,7 @@ describe('ImportTranscriptCard (RTL)', () => {

it('calls setOpen(false) when close IconButton is clicked', () => {
const { container } = render(
<ImportTranscriptCard setOpen={mockSetOpen} importTranscript={mockImportTranscript} />,
<ImportTranscriptCard setOpen={mockSetOpen} />,
);
const closeButton = container.querySelector('.btn-icon-primary');
expect(closeButton).toBeInTheDocument();
Expand All @@ -41,10 +36,10 @@ describe('ImportTranscriptCard (RTL)', () => {

it('calls importTranscript when import button is clicked', () => {
render(
<ImportTranscriptCard setOpen={mockSetOpen} importTranscript={mockImportTranscript} />,
<ImportTranscriptCard setOpen={mockSetOpen} />,
);
const importBtn = screen.getByRole('button', { name: 'Import Transcript' });
fireEvent.click(importBtn);
expect(mockImportTranscript).toHaveBeenCalled();
expect(mockDispatch).toHaveBeenCalledWith(expect.any(Function)); // dispatched thunk
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -7,54 +7,69 @@ import {
Button,
Icon,
} from '@openedx/paragon';

import { Check } from '@openedx/paragon/icons';
import { connect, useDispatch } from 'react-redux';
import { useDispatch, useSelector } from 'react-redux';
import { useIntl } 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';

/* istanbul ignore next */
export const hooks = {
onSelectLanguage: ({
dispatch, languageBeforeChange, triggerupload, setLocalLang,
}) => ({ newLang }) => {
// IF Language is unset, set language and begin upload prompt.
setLocalLang(newLang);
if (languageBeforeChange === '') {
triggerupload();
return;
}
// Else: update language
dispatch(
thunkActions.video.updateTranscriptLanguage({
newLanguageCode: newLang, languageBeforeChange,
}),
);
},

addFileCallback: ({ dispatch, localLang }) => (file) => {
dispatch(thunkActions.video.uploadTranscript({
file,
filename: file.name,
language: localLang,
}));
},
onSelectLanguage:
({
dispatch, languageBeforeChange, triggerupload, setLocalLang,
}) => ({ newLang }) => {
// IF Language is unset, set language and begin upload prompt.
setLocalLang(newLang);
if (languageBeforeChange === '') {
triggerupload();
return;
}
// Else: update language
dispatch(
thunkActions.video.updateTranscriptLanguage({
newLanguageCode: newLang,
languageBeforeChange,
}),
);
},

addFileCallback:
({ dispatch, localLang }) => (file) => {
dispatch(
thunkActions.video.uploadTranscript({
file,
filename: file.name,
language: localLang,
}),
);
},
};
/* istanbul ignore next */

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
}) => {
const intl = useIntl();
const dispatch = useDispatch();

const openLanguages = useSelector(selectors.video.openLanguages);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be better to only import required selectors and thunk actions from the redux store instead of importing all selectors and thunk actions


const [localLang, setLocalLang] = React.useState(language);
const input = fileInput({ onAddFile: hooks.addFileCallback({ dispatch: useDispatch(), localLang }) });

const input = fileInput({
onAddFile: hooks.addFileCallback({ dispatch, localLang }),
});

const onLanguageChange = hooks.onSelectLanguage({
dispatch: useDispatch(), languageBeforeChange: localLang, setLocalLang, triggerupload: input.click,
dispatch,
languageBeforeChange: localLang,
setLocalLang,
triggerupload: input.click,
});

const getTitle = () => {
Expand All @@ -65,7 +80,6 @@ const LanguageSelector = ({
<ActionRow.Spacer />
<Icon className="text-primary-500" src={Check} />
</ActionRow>

);
}
return (
Expand All @@ -78,10 +92,7 @@ const LanguageSelector = ({

return (
<>

<Dropdown
className="w-100 mb-2"
>
<Dropdown className="w-100 mb-2">
<Dropdown.Toggle
iconAs={Button}
aria-label={intl.formatMessage(messages.languageSelectLabel)}
Expand All @@ -95,12 +106,25 @@ const LanguageSelector = ({
<Dropdown.Menu>
{Object.entries(videoTranscriptLanguages).map(([lang, text]) => {
if (language === lang) {
return (<Dropdown.Item>{text}<Icon className="text-primary-500" src={Check} /></Dropdown.Item>);
return (
<Dropdown.Item key={lang}>
{text}
<Icon className="text-primary-500" src={Check} />
</Dropdown.Item>
);
}
if (openLanguages.some(row => row.includes(lang))) {
return (<Dropdown.Item onClick={() => onLanguageChange({ newLang: lang })}>{text}</Dropdown.Item>);
if (openLanguages.some((row) => row.includes(lang))) {
return (
<Dropdown.Item key={lang} onClick={() => onLanguageChange({ newLang: lang })}>
{text}
</Dropdown.Item>
);
}
return (<Dropdown.Item className="disabled">{text}</Dropdown.Item>);
return (
<Dropdown.Item key={lang} className="disabled">
{text}
</Dropdown.Item>
);
})}
</Dropdown.Menu>
</Dropdown>
Expand All @@ -109,21 +133,9 @@ const LanguageSelector = ({
);
};

LanguageSelector.defaultProps = {
openLanguages: [],
};

LanguageSelector.propTypes = {
openLanguages: PropTypes.arrayOf(PropTypes.string),
index: PropTypes.number.isRequired,
language: PropTypes.string.isRequired,
};

export const mapStateToProps = (state) => ({
openLanguages: selectors.video.openLanguages(state),
});

export const mapDispatchToProps = {};

export const LanguageSelectorInternal = LanguageSelector; // For testing only
export default connect(mapStateToProps, mapDispatchToProps)(LanguageSelector);
export default LanguageSelector;
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import React from 'react';
import {
render, screen, initializeMocks, fireEvent,
screen,
fireEvent,
initializeMocks,
} from '@src/testUtils';
import { editorRender } from '@src/editors/editorTestRender';
import LanguageSelector from './LanguageSelector';
import { selectors } from '../../../../../../data/redux';

const lang1 = 'kLinGon';
const lang1Code = 'kl';
Expand All @@ -21,36 +23,50 @@ jest.mock('../../../../../../data/constants/video', () => ({
}));

describe('LanguageSelector', () => {
const props = {
onSelect: jest.fn().mockName('props.OnSelect'),
const baseProps = {
index: 1,
language: lang1Code,
openLanguages: [[lang2Code, lang2], [lang3Code, lang3]],
};

beforeEach(() => {
initializeMocks();
});

test('renders component with selected language', () => {
const { video } = selectors;
jest.spyOn(video, 'openLanguages').mockReturnValue(props.openLanguages);
const { container } = render(<LanguageSelector {...props} />);
const initialState = {
video: {
transcripts: [],
openLanguages: [[lang2Code, lang2], [lang3Code, lang3]],
},
};

const { container } = editorRender(<LanguageSelector {...baseProps} />, { initialState });
expect(screen.getByRole('button', { name: 'Languages' })).toBeInTheDocument();
expect(screen.getByText(lang1)).toBeInTheDocument();
expect(container.querySelector('input.upload[type="file"]')).toBeInTheDocument();
});

test('renders component with no selection', () => {
const { video } = selectors;
jest.spyOn(video, 'openLanguages').mockReturnValue(props.openLanguages);
render(<LanguageSelector {...props} language="" />);
const initialState = {
video: {
transcripts: [],
openLanguages: [[lang2Code, lang2], [lang3Code, lang3]],
},
};

editorRender(<LanguageSelector {...baseProps} language="" />, { initialState });
expect(screen.getByText('Select Language')).toBeInTheDocument();
});

test('transcripts no Open Languages, all dropdown items should be disabled', () => {
const { video } = selectors;
jest.spyOn(video, 'openLanguages').mockReturnValue([]);
const { container } = render(<LanguageSelector {...props} language="" />);
const initialState = {
video: {
transcripts: ['kl', 'el', 'sl'],
openLanguages: [],
},
};

const { container } = editorRender(<LanguageSelector {...baseProps} language="" />, { initialState });
fireEvent.click(screen.getByRole('button', { name: 'Languages' }));
const disabledItems = container.querySelectorAll('.disabled.dropdown-item');
expect(disabledItems.length).toBe(3);
Expand Down
Loading