Skip to content

Commit 61fd599

Browse files
Ahtesham QuraishAhtesham Quraish
authored andcommitted
refactor: Replace connect with useSelector() and useDispatch() 5/5 #2318
1 parent 2fb04d6 commit 61fd599

File tree

14 files changed

+549
-594
lines changed

14 files changed

+549
-594
lines changed
Lines changed: 31 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import React from 'react';
2-
import { connect } from 'react-redux';
32
import PropTypes from 'prop-types';
3+
import { useDispatch } from 'react-redux';
44

55
import { FormattedMessage } from '@edx/frontend-platform/i18n';
66
import {
@@ -15,47 +15,39 @@ import { Close } from '@openedx/paragon/icons';
1515
import messages from './messages';
1616
import { thunkActions } from '../../../../../../data/redux';
1717

18-
const ImportTranscriptCard = ({
19-
setOpen,
20-
// redux
21-
importTranscript,
22-
}) => (
23-
<Stack gap={3} className="border rounded border-primary-200 p-4">
24-
<ActionRow className="h5">
25-
<FormattedMessage {...messages.importHeader} />
26-
<ActionRow.Spacer />
27-
<IconButton
28-
src={Close}
29-
iconAs={Icon}
30-
onClick={() => setOpen(false)}
31-
/>
32-
</ActionRow>
33-
<FormattedMessage {...messages.importMessage} />
34-
<Button
35-
variant="outline-primary"
36-
size="sm"
37-
onClick={importTranscript}
38-
>
39-
<FormattedMessage {...messages.importButtonLabel} />
40-
</Button>
41-
</Stack>
42-
);
43-
44-
ImportTranscriptCard.defaultProps = {
45-
setOpen: true,
18+
const ImportTranscriptCard = ({ setOpen }) => {
19+
const dispatch = useDispatch();
20+
21+
const importTranscript = () => {
22+
dispatch(thunkActions.video.importTranscript());
23+
};
24+
25+
return (
26+
<Stack gap={3} className="border rounded border-primary-200 p-4">
27+
<ActionRow className="h5">
28+
<FormattedMessage {...messages.importHeader} />
29+
<ActionRow.Spacer />
30+
<IconButton
31+
src={Close}
32+
iconAs={Icon}
33+
onClick={() => setOpen(false)}
34+
/>
35+
</ActionRow>
36+
<FormattedMessage {...messages.importMessage} />
37+
<Button
38+
variant="outline-primary"
39+
size="sm"
40+
onClick={importTranscript}
41+
>
42+
<FormattedMessage {...messages.importButtonLabel} />
43+
</Button>
44+
</Stack>
45+
);
4646
};
4747

4848
ImportTranscriptCard.propTypes = {
49-
setOpen: PropTypes.func,
50-
// redux
51-
importTranscript: PropTypes.func.isRequired,
52-
};
53-
54-
export const mapStateToProps = () => ({});
55-
56-
export const mapDispatchToProps = {
57-
importTranscript: thunkActions.video.importTranscript,
49+
setOpen: PropTypes.func.isRequired,
5850
};
5951

6052
export const ImportTranscriptCardInternal = ImportTranscriptCard; // For testing only
61-
export default connect(mapStateToProps, mapDispatchToProps)(ImportTranscriptCard);
53+
export default ImportTranscriptCard;

src/editors/containers/VideoEditor/components/VideoSettingsModal/components/TranscriptWidget/ImportTranscriptCard.test.tsx

Lines changed: 7 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,27 +2,22 @@ import React from 'react';
22
import {
33
render, screen, fireEvent, initializeMocks,
44
} from '@src/testUtils';
5+
import * as ReactRedux from 'react-redux';
56
import { ImportTranscriptCardInternal as ImportTranscriptCard } from './ImportTranscriptCard';
67

7-
jest.mock('../../../../../../data/redux', () => ({
8-
thunkActions: {
9-
video: {
10-
importTranscript: jest.fn().mockName('thunkActions.video.importTranscript'),
11-
},
12-
},
13-
}));
8+
const mockDispatch = jest.fn();
149

1510
describe('ImportTranscriptCard (RTL)', () => {
1611
const mockSetOpen = jest.fn();
17-
const mockImportTranscript = jest.fn();
1812

1913
beforeEach(() => {
14+
jest.spyOn(ReactRedux, 'useDispatch').mockReturnValue(mockDispatch);
2015
initializeMocks();
2116
});
2217

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

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

4237
it('calls importTranscript when import button is clicked', () => {
4338
render(
44-
<ImportTranscriptCard setOpen={mockSetOpen} importTranscript={mockImportTranscript} />,
39+
<ImportTranscriptCard setOpen={mockSetOpen} />,
4540
);
4641
const importBtn = screen.getByRole('button', { name: 'Import Transcript' });
4742
fireEvent.click(importBtn);
48-
expect(mockImportTranscript).toHaveBeenCalled();
43+
expect(mockDispatch).toHaveBeenCalledWith(expect.any(Function)); // dispatched thunk
4944
});
5045
});

src/editors/containers/VideoEditor/components/VideoSettingsModal/components/TranscriptWidget/LanguageSelector.jsx

Lines changed: 65 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -7,54 +7,66 @@ import {
77
Button,
88
Icon,
99
} from '@openedx/paragon';
10-
1110
import { Check } from '@openedx/paragon/icons';
12-
import { connect, useDispatch } from 'react-redux';
11+
import { useDispatch, useSelector } from 'react-redux';
1312
import { useIntl } from '@edx/frontend-platform/i18n';
13+
1414
import { thunkActions, selectors } from '../../../../../../data/redux';
1515
import { videoTranscriptLanguages } from '../../../../../../data/constants/video';
1616
import { FileInput, fileInput } from '../../../../../../sharedComponents/FileInput';
1717
import messages from './messages';
1818

19+
/* istanbul ignore next */
1920
export const hooks = {
20-
onSelectLanguage: ({
21-
dispatch, languageBeforeChange, triggerupload, setLocalLang,
22-
}) => ({ newLang }) => {
23-
// IF Language is unset, set language and begin upload prompt.
24-
setLocalLang(newLang);
25-
if (languageBeforeChange === '') {
26-
triggerupload();
27-
return;
28-
}
29-
// Else: update language
30-
dispatch(
31-
thunkActions.video.updateTranscriptLanguage({
32-
newLanguageCode: newLang, languageBeforeChange,
33-
}),
34-
);
35-
},
36-
37-
addFileCallback: ({ dispatch, localLang }) => (file) => {
38-
dispatch(thunkActions.video.uploadTranscript({
39-
file,
40-
filename: file.name,
41-
language: localLang,
42-
}));
43-
},
21+
onSelectLanguage:
22+
({
23+
dispatch, languageBeforeChange, triggerupload, setLocalLang,
24+
}) => ({ newLang }) => {
25+
// IF Language is unset, set language and begin upload prompt.
26+
setLocalLang(newLang);
27+
if (languageBeforeChange === '') {
28+
triggerupload();
29+
return;
30+
}
31+
// Else: update language
32+
dispatch(
33+
thunkActions.video.updateTranscriptLanguage({
34+
newLanguageCode: newLang,
35+
languageBeforeChange,
36+
}),
37+
);
38+
},
4439

40+
addFileCallback:
41+
({ dispatch, localLang }) => (file) => {
42+
dispatch(
43+
thunkActions.video.uploadTranscript({
44+
file,
45+
filename: file.name,
46+
language: localLang,
47+
}),
48+
);
49+
},
4550
};
51+
/* istanbul ignore next */
4652

47-
const LanguageSelector = ({
48-
index, // For a unique id for the form control
49-
language,
50-
// Redux
51-
openLanguages, // Only allow those languages not already associated with a transcript to be selected
52-
}) => {
53+
const LanguageSelector = ({ index, language }) => {
5354
const intl = useIntl();
55+
const dispatch = useDispatch();
56+
57+
const openLanguages = useSelector(selectors.video.openLanguages);
58+
5459
const [localLang, setLocalLang] = React.useState(language);
55-
const input = fileInput({ onAddFile: hooks.addFileCallback({ dispatch: useDispatch(), localLang }) });
60+
61+
const input = fileInput({
62+
onAddFile: hooks.addFileCallback({ dispatch, localLang }),
63+
});
64+
5665
const onLanguageChange = hooks.onSelectLanguage({
57-
dispatch: useDispatch(), languageBeforeChange: localLang, setLocalLang, triggerupload: input.click,
66+
dispatch,
67+
languageBeforeChange: localLang,
68+
setLocalLang,
69+
triggerupload: input.click,
5870
});
5971

6072
const getTitle = () => {
@@ -65,7 +77,6 @@ const LanguageSelector = ({
6577
<ActionRow.Spacer />
6678
<Icon className="text-primary-500" src={Check} />
6779
</ActionRow>
68-
6980
);
7081
}
7182
return (
@@ -78,10 +89,7 @@ const LanguageSelector = ({
7889

7990
return (
8091
<>
81-
82-
<Dropdown
83-
className="w-100 mb-2"
84-
>
92+
<Dropdown className="w-100 mb-2">
8593
<Dropdown.Toggle
8694
iconAs={Button}
8795
aria-label={intl.formatMessage(messages.languageSelectLabel)}
@@ -95,12 +103,25 @@ const LanguageSelector = ({
95103
<Dropdown.Menu>
96104
{Object.entries(videoTranscriptLanguages).map(([lang, text]) => {
97105
if (language === lang) {
98-
return (<Dropdown.Item>{text}<Icon className="text-primary-500" src={Check} /></Dropdown.Item>);
106+
return (
107+
<Dropdown.Item key={lang}>
108+
{text}
109+
<Icon className="text-primary-500" src={Check} />
110+
</Dropdown.Item>
111+
);
99112
}
100-
if (openLanguages.some(row => row.includes(lang))) {
101-
return (<Dropdown.Item onClick={() => onLanguageChange({ newLang: lang })}>{text}</Dropdown.Item>);
113+
if (openLanguages.some((row) => row.includes(lang))) {
114+
return (
115+
<Dropdown.Item key={lang} onClick={() => onLanguageChange({ newLang: lang })}>
116+
{text}
117+
</Dropdown.Item>
118+
);
102119
}
103-
return (<Dropdown.Item className="disabled">{text}</Dropdown.Item>);
120+
return (
121+
<Dropdown.Item key={lang} className="disabled">
122+
{text}
123+
</Dropdown.Item>
124+
);
104125
})}
105126
</Dropdown.Menu>
106127
</Dropdown>
@@ -109,21 +130,10 @@ const LanguageSelector = ({
109130
);
110131
};
111132

112-
LanguageSelector.defaultProps = {
113-
openLanguages: [],
114-
};
115-
116133
LanguageSelector.propTypes = {
117-
openLanguages: PropTypes.arrayOf(PropTypes.string),
118134
index: PropTypes.number.isRequired,
119135
language: PropTypes.string.isRequired,
120136
};
121137

122-
export const mapStateToProps = (state) => ({
123-
openLanguages: selectors.video.openLanguages(state),
124-
});
125-
126-
export const mapDispatchToProps = {};
127-
128138
export const LanguageSelectorInternal = LanguageSelector; // For testing only
129-
export default connect(mapStateToProps, mapDispatchToProps)(LanguageSelector);
139+
export default LanguageSelector;

src/editors/containers/VideoEditor/components/VideoSettingsModal/components/TranscriptWidget/LanguageSelector.test.jsx

Lines changed: 30 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import React from 'react';
22
import {
3-
render, screen, initializeMocks, fireEvent,
3+
screen,
4+
fireEvent,
5+
initializeMocks,
46
} from '@src/testUtils';
7+
import { editorRender } from '@src/editors/editorTestRender';
58
import LanguageSelector from './LanguageSelector';
6-
import { selectors } from '../../../../../../data/redux';
79

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

2325
describe('LanguageSelector', () => {
24-
const props = {
25-
onSelect: jest.fn().mockName('props.OnSelect'),
26+
const baseProps = {
2627
index: 1,
2728
language: lang1Code,
28-
openLanguages: [[lang2Code, lang2], [lang3Code, lang3]],
2929
};
30+
3031
beforeEach(() => {
3132
initializeMocks();
3233
});
3334

3435
test('renders component with selected language', () => {
35-
const { video } = selectors;
36-
jest.spyOn(video, 'openLanguages').mockReturnValue(props.openLanguages);
37-
const { container } = render(<LanguageSelector {...props} />);
36+
const initialState = {
37+
video: {
38+
transcripts: [],
39+
openLanguages: [[lang2Code, lang2], [lang3Code, lang3]],
40+
},
41+
};
42+
43+
const { container } = editorRender(<LanguageSelector {...baseProps} />, { initialState });
3844
expect(screen.getByRole('button', { name: 'Languages' })).toBeInTheDocument();
3945
expect(screen.getByText(lang1)).toBeInTheDocument();
4046
expect(container.querySelector('input.upload[type="file"]')).toBeInTheDocument();
4147
});
4248

4349
test('renders component with no selection', () => {
44-
const { video } = selectors;
45-
jest.spyOn(video, 'openLanguages').mockReturnValue(props.openLanguages);
46-
render(<LanguageSelector {...props} language="" />);
50+
const initialState = {
51+
video: {
52+
transcripts: [],
53+
openLanguages: [[lang2Code, lang2], [lang3Code, lang3]],
54+
},
55+
};
56+
57+
editorRender(<LanguageSelector {...baseProps} language="" />, { initialState });
4758
expect(screen.getByText('Select Language')).toBeInTheDocument();
4859
});
4960

5061
test('transcripts no Open Languages, all dropdown items should be disabled', () => {
51-
const { video } = selectors;
52-
jest.spyOn(video, 'openLanguages').mockReturnValue([]);
53-
const { container } = render(<LanguageSelector {...props} language="" />);
62+
const initialState = {
63+
video: {
64+
transcripts: ['kl', 'el', 'sl'],
65+
openLanguages: [],
66+
},
67+
};
68+
69+
const { container } = editorRender(<LanguageSelector {...baseProps} language="" />, { initialState });
5470
fireEvent.click(screen.getByRole('button', { name: 'Languages' }));
5571
const disabledItems = container.querySelectorAll('.disabled.dropdown-item');
5672
expect(disabledItems.length).toBe(3);

0 commit comments

Comments
 (0)