Skip to content

Commit 8326257

Browse files
Ahtesham QuraishFaraz32123
authored andcommitted
refactor: Replace connect with useSelector() and useDispatch() 1/5
1 parent 30cfd26 commit 8326257

File tree

12 files changed

+368
-335
lines changed

12 files changed

+368
-335
lines changed
Lines changed: 9 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,23 @@
11
import PropTypes from 'prop-types';
2-
import { connect, useSelector } from 'react-redux';
2+
import { useSelector } from 'react-redux';
33
import { Button } from '@openedx/paragon';
44
import { Link } from 'react-router-dom';
55

66
import UnitIcon from './UnitIcon';
77
import { getCourseId, getSequenceId } from '../../data/selectors';
88

99
const UnitButton = ({
10-
title, contentType, isActive, unitId, className, showTitle,
10+
unitId,
11+
className,
12+
showTitle,
1113
}) => {
1214
const courseId = useSelector(getCourseId);
1315
const sequenceId = useSelector(getSequenceId);
1416

17+
const unit = useSelector((state) => state.models.units[unitId]);
18+
19+
const { title, contentType, isActive } = unit || {};
20+
1521
return (
1622
<Button
1723
className={className}
@@ -29,26 +35,13 @@ const UnitButton = ({
2935

3036
UnitButton.propTypes = {
3137
className: PropTypes.string,
32-
contentType: PropTypes.string.isRequired,
33-
isActive: PropTypes.bool,
3438
showTitle: PropTypes.bool,
35-
title: PropTypes.string.isRequired,
3639
unitId: PropTypes.string.isRequired,
3740
};
3841

3942
UnitButton.defaultProps = {
4043
className: undefined,
41-
isActive: false,
4244
showTitle: false,
4345
};
4446

45-
const mapStateToProps = (state, props) => {
46-
if (props.unitId) {
47-
return {
48-
...state.models.units[props.unitId],
49-
};
50-
}
51-
return {};
52-
};
53-
54-
export default connect(mapStateToProps)(UnitButton);
47+
export default UnitButton;

src/editors/containers/ProblemEditor/components/EditProblemView/AnswerWidget/AnswerOption.jsx

Lines changed: 20 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import React, { memo } from 'react';
2-
import { connect, useDispatch } from 'react-redux';
2+
import { useSelector, useDispatch } from 'react-redux';
33
import PropTypes from 'prop-types';
44
import {
55
Collapsible,
@@ -22,15 +22,16 @@ import ExpandableTextArea from '../../../../../sharedComponents/ExpandableTextAr
2222
const AnswerOption = ({
2323
answer,
2424
hasSingleAnswer,
25-
// redux
26-
problemType,
27-
images,
28-
isLibrary,
29-
blockId,
30-
learningContextId,
3125
}) => {
3226
const intl = useIntl();
3327
const dispatch = useDispatch();
28+
29+
const problemType = useSelector(selectors.problem.problemType);
30+
const images = useSelector(selectors.app.images);
31+
const isLibrary = useSelector(selectors.app.isLibrary);
32+
const learningContextId = useSelector(selectors.app.learningContextId);
33+
const blockId = useSelector(selectors.app.blockId);
34+
3435
const removeAnswer = hooks.removeAnswer({ answer, dispatch });
3536
const setAnswer = hooks.setAnswer({ answer, hasSingleAnswer, dispatch });
3637
const setAnswerTitle = hooks.setAnswerTitle({
@@ -42,10 +43,10 @@ const AnswerOption = ({
4243
const setSelectedFeedback = hooks.setSelectedFeedback({ answer, hasSingleAnswer, dispatch });
4344
const setUnselectedFeedback = hooks.setUnselectedFeedback({ answer, hasSingleAnswer, dispatch });
4445
const { isFeedbackVisible, toggleFeedback } = hooks.useFeedback(answer);
45-
let staticRootUrl;
46-
if (isLibrary) {
47-
staticRootUrl = `${getConfig().STUDIO_BASE_URL }/library_assets/blocks/${ blockId }/`;
48-
}
46+
47+
const staticRootUrl = isLibrary
48+
? `${getConfig().STUDIO_BASE_URL}/library_assets/blocks/${blockId}/`
49+
: undefined;
4950

5051
const getInputArea = () => {
5152
if ([ProblemTypeKeys.SINGLESELECT, ProblemTypeKeys.MULTISELECT].includes(problemType)) {
@@ -55,12 +56,10 @@ const AnswerOption = ({
5556
setContent={setAnswerTitle}
5657
placeholder={intl.formatMessage(messages.answerTextboxPlaceholder)}
5758
id={`answer-${answer.id}`}
58-
{...{
59-
images,
60-
isLibrary,
61-
learningContextId,
62-
staticRootUrl,
63-
}}
59+
images={images}
60+
isLibrary={isLibrary}
61+
learningContextId={learningContextId}
62+
staticRootUrl={staticRootUrl}
6463
/>
6564
);
6665
}
@@ -93,7 +92,6 @@ const AnswerOption = ({
9392
<FormattedMessage {...messages.answerRangeHelperText} />
9493
</div>
9594
</div>
96-
9795
);
9896
};
9997

@@ -120,11 +118,9 @@ const AnswerOption = ({
120118
setSelectedFeedback={setSelectedFeedback}
121119
setUnselectedFeedback={setUnselectedFeedback}
122120
intl={intl}
123-
{...{
124-
images,
125-
isLibrary,
126-
learningContextId,
127-
}}
121+
images={images}
122+
isLibrary={isLibrary}
123+
learningContextId={learningContextId}
128124
/>
129125
</Collapsible.Body>
130126
</div>
@@ -150,22 +146,6 @@ const AnswerOption = ({
150146
AnswerOption.propTypes = {
151147
answer: answerOptionProps.isRequired,
152148
hasSingleAnswer: PropTypes.bool.isRequired,
153-
// redux
154-
problemType: PropTypes.string.isRequired,
155-
images: PropTypes.shape({}).isRequired,
156-
learningContextId: PropTypes.string.isRequired,
157-
isLibrary: PropTypes.bool.isRequired,
158-
blockId: PropTypes.string.isRequired,
159149
};
160150

161-
export const mapStateToProps = (state) => ({
162-
problemType: selectors.problem.problemType(state),
163-
images: selectors.app.images(state),
164-
isLibrary: selectors.app.isLibrary(state),
165-
learningContextId: selectors.app.learningContextId(state),
166-
blockId: selectors.app.blockId(state),
167-
});
168-
169-
export const mapDispatchToProps = {};
170-
export const AnswerOptionInternal = AnswerOption; // For testing only
171-
export default connect(mapStateToProps, mapDispatchToProps)(memo(AnswerOption));
151+
export default memo(AnswerOption);

src/editors/containers/ProblemEditor/components/EditProblemView/AnswerWidget/AnswerOption.test.tsx

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,23 @@ import { selectors } from '../../../../../data/redux';
66

77
const { problem } = selectors;
88

9+
const initialState = {
10+
problem: {
11+
problemType: 'multiplechoiceresponse', // No problem type selected by default
12+
// ... other problem-related state
13+
},
14+
app: {
15+
images: {}, // No images loaded by default; use {} if it's an object keyed by IDs, or [] if it's a list
16+
isLibrary: false, // Default to false; not in library context initially
17+
learningContextId: 'course+org+run', // No context ID by default
18+
blockId: 'block-id', // No block ID initially
19+
// ... other app-related state
20+
},
21+
// ... any other top-level state slices
22+
};
23+
24+
export default initialState;
25+
926
jest.mock('../../../../../data/redux', () => ({
1027
__esModule: true,
1128
default: jest.fn(),
@@ -57,12 +74,6 @@ describe('AnswerOption', () => {
5774
const props = {
5875
hasSingleAnswer: false,
5976
answer: answerWithOnlyFeedback,
60-
// redux
61-
problemType: 'multiplechoiceresponse',
62-
images: {},
63-
isLibrary: false,
64-
learningContextId: 'course+org+run',
65-
blockId: 'block-id',
6677
};
6778

6879
beforeEach(() => {
@@ -75,7 +86,7 @@ describe('AnswerOption', () => {
7586
isFeedbackVisible: false,
7687
toggleFeedback: jest.fn(),
7788
});
78-
initializeMocks();
89+
initializeMocks({ initialState });
7990
});
8091

8192
test('renders correct option with feedback', () => {

src/editors/containers/ProblemEditor/components/EditProblemView/AnswerWidget/AnswersContainer.jsx

Lines changed: 10 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,26 @@
11
import React from 'react';
22
import PropTypes from 'prop-types';
3-
import { connect } from 'react-redux';
3+
import { useSelector, useDispatch } from 'react-redux';
44
import { FormattedMessage } from '@edx/frontend-platform/i18n';
55

66
import { Dropdown, Icon } from '@openedx/paragon';
77
import { Add } from '@openedx/paragon/icons';
88
import messages from './messages';
99
import { useAnswerContainer, isSingleAnswerProblem } from './hooks';
1010
import { actions, selectors } from '../../../../../data/redux';
11-
import { answerOptionProps } from '../../../../../data/services/cms/types';
1211
import AnswerOption from './AnswerOption';
1312
import Button from '../../../../../sharedComponents/Button';
1413
import { ProblemTypeKeys } from '../../../../../data/constants/problem';
1514

16-
const AnswersContainer = ({
17-
problemType,
18-
// Redux
19-
answers,
20-
addAnswer,
21-
addAnswerRange,
22-
updateField,
23-
}) => {
15+
const AnswersContainer = ({ problemType }) => {
16+
const dispatch = useDispatch();
17+
const answers = useSelector(selectors.problem.answers);
2418
const hasSingleAnswer = isSingleAnswerProblem(problemType);
2519

20+
const addAnswer = () => dispatch(actions.problem.addAnswer());
21+
const addAnswerRange = () => dispatch(actions.problem.addAnswerRange());
22+
const updateField = (field, value) => dispatch(actions.problem.updateField(field, value));
23+
2624
useAnswerContainer({ answers, problemType, updateField });
2725

2826
return (
@@ -42,17 +40,14 @@ const AnswersContainer = ({
4240
>
4341
<FormattedMessage {...messages.addAnswerButtonText} />
4442
</Button>
45-
4643
) : (
4744
<Dropdown>
4845
<Dropdown.Toggle
4946
id="Add-Answer-Or-Answer-Range"
5047
variant="tertiary"
5148
className="pl-0"
5249
>
53-
<Icon
54-
src={Add}
55-
/>
50+
<Icon src={Add} />
5651
<FormattedMessage {...messages.addAnswerButtonText} />
5752
</Dropdown.Toggle>
5853
<Dropdown.Menu>
@@ -79,21 +74,6 @@ const AnswersContainer = ({
7974

8075
AnswersContainer.propTypes = {
8176
problemType: PropTypes.string.isRequired,
82-
answers: PropTypes.arrayOf(answerOptionProps).isRequired,
83-
addAnswer: PropTypes.func.isRequired,
84-
addAnswerRange: PropTypes.func.isRequired,
85-
updateField: PropTypes.func.isRequired,
86-
};
87-
88-
export const mapStateToProps = (state) => ({
89-
answers: selectors.problem.answers(state),
90-
});
91-
92-
export const mapDispatchToProps = {
93-
addAnswer: actions.problem.addAnswer,
94-
addAnswerRange: actions.problem.addAnswerRange,
95-
updateField: actions.problem.updateField,
9677
};
9778

98-
export const AnswersContainerInternal = AnswersContainer; // For testing only
99-
export default connect(mapStateToProps, mapDispatchToProps)(AnswersContainer);
79+
export default AnswersContainer;

src/editors/containers/ProblemEditor/components/EditProblemView/AnswerWidget/AnswersContainer.test.tsx

Lines changed: 49 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,50 @@ import React from 'react';
22
import {
33
render, screen, fireEvent, initializeMocks,
44
} from '../../../../../../testUtils';
5-
import { AnswersContainerInternal as AnswersContainer } from './AnswersContainer';
5+
import AnswersContainer from './AnswersContainer';
66
import { ProblemTypeKeys } from '../../../../../data/constants/problem';
7+
// Import actions after mocking to access mocked functions
8+
import { actions } from '../../../../../data/redux';
79

810
const { useAnswerContainer } = require('./hooks');
911

12+
// Mock answers for state
13+
const answers = [
14+
{ id: 'a1', isAnswerRange: false },
15+
{ id: 'a2', isAnswerRange: false },
16+
];
17+
18+
const initialState = {
19+
problem: {
20+
answers,
21+
},
22+
};
23+
24+
// Mock actions module
25+
jest.mock('../../../../../data/redux', () => ({
26+
__esModule: true,
27+
actions: {
28+
problem: {
29+
addAnswer: jest.fn(() => ({ type: 'ADD_ANSWER' })),
30+
addAnswerRange: jest.fn(() => ({ type: 'ADD_ANSWER_RANGE' })),
31+
updateField: jest.fn((field, value) => ({ type: 'UPDATE_FIELD', payload: { field, value } })),
32+
},
33+
},
34+
selectors: {
35+
problem: {
36+
answers: jest.fn(() => answers),
37+
},
38+
},
39+
}));
40+
41+
// Mock AnswerOption and Button components
1042
jest.mock('./AnswerOption', () => jest.fn(({ answer }) => <div>AnswerOption-{answer.id}</div>));
1143
jest.mock(
1244
'../../../../../sharedComponents/Button',
1345
() => jest.fn(({ children, ...props }) => <button type="button" {...props}>{children}</button>),
1446
);
1547

48+
// Mock hooks
1649
jest.mock('./hooks', () => ({
1750
useAnswerContainer: jest.fn(),
1851
isSingleAnswerProblem: jest.fn(() => false),
@@ -21,17 +54,11 @@ jest.mock('./hooks', () => ({
2154
describe('AnswersContainer', () => {
2255
const defaultProps = {
2356
problemType: 'multiple_choice',
24-
answers: [
25-
{ id: 'a1', isAnswerRange: false },
26-
{ id: 'a2', isAnswerRange: false },
27-
],
28-
addAnswer: jest.fn(),
29-
addAnswerRange: jest.fn(),
30-
updateField: jest.fn(),
3157
};
3258

3359
beforeEach(() => {
34-
initializeMocks();
60+
initializeMocks({ initialState });
61+
jest.clearAllMocks();
3562
});
3663

3764
it('renders AnswerOption for each answer', () => {
@@ -45,7 +72,7 @@ describe('AnswersContainer', () => {
4572
const button = screen.getByRole('button', { name: 'Add answer' });
4673
expect(button).toBeInTheDocument();
4774
fireEvent.click(button);
48-
expect(defaultProps.addAnswer).toHaveBeenCalled();
75+
expect(actions.problem.addAnswer).toHaveBeenCalled();
4976
});
5077

5178
it('renders Dropdown for NUMERIC problemType', () => {
@@ -57,9 +84,19 @@ describe('AnswersContainer', () => {
5784
it('calls useAnswerContainer with correct args', () => {
5885
render(<AnswersContainer {...defaultProps} />);
5986
expect(useAnswerContainer).toHaveBeenCalledWith({
60-
answers: defaultProps.answers,
87+
answers,
6188
problemType: defaultProps.problemType,
62-
updateField: defaultProps.updateField,
89+
updateField: expect.any(Function),
6390
});
6491
});
92+
93+
it('dispatches updateField with correct params', () => {
94+
render(<AnswersContainer {...defaultProps} />);
95+
// Directly call updateField to test
96+
const field = 'exampleField';
97+
const value = 'exampleValue';
98+
const { updateField } = actions.problem;
99+
updateField({ field, value });
100+
expect(updateField).toHaveBeenCalledWith({ field, value });
101+
});
65102
});

0 commit comments

Comments
 (0)