Skip to content

Commit da4e81b

Browse files
authored
feat: Migrate groupsConfiguration from Redux store to ReactQuery (openedx#2980)
1 parent 31b6ea7 commit da4e81b

23 files changed

Lines changed: 524 additions & 846 deletions

src/certificates/layout/MainLayout.jsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import PropTypes from 'prop-types';
22
import { Container, Layout } from '@openedx/paragon';
33
import { useIntl } from '@edx/frontend-platform/i18n';
4+
import { RequestStatus } from '@src/data/constants';
45

56
import { SavingErrorAlert } from '../../generic/saving-error-alert';
67
import SubHeader from '../../generic/sub-header/SubHeader';
@@ -48,7 +49,7 @@ const MainLayout = ({ courseId, showHeaderButtons, children }) => {
4849
</Container>
4950
<div className="certificates alert-toast">
5051
<SavingErrorAlert
51-
savingStatus={savingStatus}
52+
isQueryFailed={savingStatus === RequestStatus.FAILED}
5253
errorMessage={errorMessage}
5354
/>
5455
</div>

src/course-unit/CourseUnit.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -403,7 +403,7 @@ const CourseUnit = () => {
403403
</Container>
404404
<div className="alert-toast">
405405
<SavingErrorAlert
406-
savingStatus={savingStatus}
406+
isQueryFailed={savingStatus === RequestStatus.FAILED}
407407
errorMessage={errorMessage}
408408
/>
409409
</div>

src/generic/saving-error-alert/SavingErrorAlert.jsx renamed to src/generic/saving-error-alert/SavingErrorAlert.tsx

Lines changed: 8 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,23 @@
11
import { useEffect, useState } from 'react';
2-
import PropTypes from 'prop-types';
32
import { Warning as WarningIcon } from '@openedx/paragon/icons';
43
import { useIntl } from '@edx/frontend-platform/i18n';
54

6-
import { RequestStatus } from '../../data/constants';
75
import AlertMessage from '../alert-message';
86
import messages from './messages';
97

8+
export interface SavingErrorAlertProps {
9+
/** Whether a mutation query has failed, triggering the error alert to appear. */
10+
isQueryFailed: boolean;
11+
errorMessage?: string;
12+
}
13+
1014
const SavingErrorAlert = ({
11-
savingStatus,
15+
isQueryFailed,
1216
errorMessage,
13-
}) => {
17+
}: SavingErrorAlertProps) => {
1418
const intl = useIntl();
1519
const [showAlert, setShowAlert] = useState(false);
1620
const [isOnline, setIsOnline] = useState(window.navigator.onLine);
17-
const isQueryFailed = savingStatus === RequestStatus.FAILED;
1821

1922
useEffect(() => {
2023
const handleOnlineStatus = () => setIsOnline(window.navigator.onLine);
@@ -54,13 +57,4 @@ const SavingErrorAlert = ({
5457
);
5558
};
5659

57-
SavingErrorAlert.defaultProps = {
58-
errorMessage: undefined,
59-
};
60-
61-
SavingErrorAlert.propTypes = {
62-
savingStatus: PropTypes.string.isRequired,
63-
errorMessage: PropTypes.string,
64-
};
65-
6660
export default SavingErrorAlert;

src/generic/saving-error-alert/utils.js

Lines changed: 0 additions & 25 deletions
This file was deleted.
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/* eslint-disable import/no-extraneous-dependencies */
2+
import { AxiosError } from 'axios';
3+
import { RequestStatus } from '@src/data/constants';
4+
5+
export const handleResponseErrors = (error: any, dispatch?: Function, savingStatusFunction?: Function) => {
6+
let errorMessage = '';
7+
8+
try {
9+
const {
10+
customAttributes: { httpErrorResponseData },
11+
} = error;
12+
const parsedData = JSON.parse(httpErrorResponseData);
13+
errorMessage = parsedData?.error || errorMessage;
14+
} catch {
15+
errorMessage = '';
16+
}
17+
18+
if (dispatch && savingStatusFunction) {
19+
dispatch(
20+
savingStatusFunction({
21+
status: RequestStatus.FAILED,
22+
errorMessage,
23+
}),
24+
);
25+
}
26+
27+
return false;
28+
};
29+
30+
export const getMessageFromAxiosError = (error: AxiosError) => {
31+
let errorMessage: string | undefined;
32+
try {
33+
// @ts-ignore
34+
errorMessage = error.response?.data?.error;
35+
} catch {
36+
errorMessage = undefined;
37+
}
38+
39+
return errorMessage;
40+
};

src/group-configurations/GroupConfigurations.test.jsx

Lines changed: 0 additions & 117 deletions
This file was deleted.
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import { screen } from '@testing-library/react';
2+
import { CourseAuthoringProvider } from '@src/CourseAuthoringContext';
3+
import {
4+
initializeMocks,
5+
render,
6+
within,
7+
} from '../testUtils';
8+
import { getContentStoreApiUrl } from './data/api';
9+
import { groupConfigurationResponseMock } from './__mocks__';
10+
import messages from './messages';
11+
import experimentMessages from './experiment-configurations-section/messages';
12+
import contentGroupsMessages from './content-groups-section/messages';
13+
import GroupConfigurations from '.';
14+
15+
let axiosMock;
16+
const courseId = 'course-v1:org+101+101';
17+
const enrollmentTrackGroups = groupConfigurationResponseMock.allGroupConfigurations[0];
18+
const contentGroups = groupConfigurationResponseMock.allGroupConfigurations[1];
19+
const teamGroups = groupConfigurationResponseMock.allGroupConfigurations[2];
20+
21+
const renderComponent = () =>
22+
render(
23+
<CourseAuthoringProvider courseId={courseId}>
24+
<GroupConfigurations />
25+
</CourseAuthoringProvider>,
26+
);
27+
28+
describe('<GroupConfigurations />', () => {
29+
beforeEach(async () => {
30+
const mocks = initializeMocks();
31+
axiosMock = mocks.axiosMock;
32+
axiosMock
33+
.onGet(getContentStoreApiUrl(courseId))
34+
.reply(200, groupConfigurationResponseMock);
35+
});
36+
37+
it('renders component correctly', async () => {
38+
renderComponent();
39+
40+
const mainContent = await screen.findByTestId('group-configurations-main-content-wrapper');
41+
const groupConfigurationsTitle = screen.getAllByText(messages.headingTitle.defaultMessage)[0];
42+
43+
expect(groupConfigurationsTitle).toBeInTheDocument();
44+
expect(
45+
screen.getByText(messages.headingSubtitle.defaultMessage),
46+
).toBeInTheDocument();
47+
expect(
48+
within(mainContent).getByText(contentGroupsMessages.addNewGroup.defaultMessage),
49+
).toBeInTheDocument();
50+
expect(
51+
within(mainContent).getByText(experimentMessages.addNewGroup.defaultMessage),
52+
).toBeInTheDocument();
53+
expect(
54+
within(mainContent).getByText(experimentMessages.title.defaultMessage),
55+
).toBeInTheDocument();
56+
expect(screen.getByText(contentGroups.name)).toBeInTheDocument();
57+
expect(screen.getByText(enrollmentTrackGroups.name)).toBeInTheDocument();
58+
expect(screen.getByText(teamGroups.name)).toBeInTheDocument();
59+
});
60+
61+
it('does not render an empty section for enrollment track groups if it is empty', async () => {
62+
const shouldNotShowEnrollmentTrackResponse = {
63+
...groupConfigurationResponseMock,
64+
shouldShowEnrollmentTrack: false,
65+
};
66+
axiosMock
67+
.onGet(getContentStoreApiUrl(courseId))
68+
.reply(200, shouldNotShowEnrollmentTrackResponse);
69+
70+
renderComponent();
71+
72+
await screen.findByTestId('group-configurations-main-content-wrapper');
73+
expect(
74+
screen.queryByTestId('group-configurations-empty-placeholder'),
75+
).not.toBeInTheDocument();
76+
});
77+
78+
it('does not show a connection error when the request fails with a non-403 status', async () => {
79+
axiosMock
80+
.onGet(getContentStoreApiUrl(courseId))
81+
.reply(404);
82+
83+
renderComponent();
84+
85+
await screen.findByTestId('group-configurations-main-content-wrapper');
86+
expect(screen.queryByTestId('connectionErrorAlert')).not.toBeInTheDocument();
87+
});
88+
89+
it('displays a connection error alert when API responds with 403', async () => {
90+
axiosMock
91+
.onGet(getContentStoreApiUrl(courseId))
92+
.reply(403);
93+
94+
renderComponent();
95+
96+
expect(await screen.findByTestId('connectionErrorAlert')).toBeInTheDocument();
97+
});
98+
});

src/group-configurations/data/api.test.js

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -101,26 +101,22 @@ describe('group configurations API calls', () => {
101101
it('should delete content group', async () => {
102102
const parentGroupId = contentGroups.id;
103103
const groupId = contentGroups.groups[0].id;
104-
const response = { ...groupConfigurationResponseMock };
105104
const updatedContentGroups = {
106105
...contentGroups,
107106
groups: contentGroups.groups.filter((group) => group.id !== groupId),
108107
};
109108

110-
response.allGroupConfigurations[1] = updatedContentGroups;
111109
axiosMock
112110
.onDelete(
113111
getLegacyApiUrl(courseId, parentGroupId, groupId),
114112
)
115-
.reply(200, response);
113+
.reply(200, {});
116114

117-
const result = await deleteContentGroup(courseId, parentGroupId, groupId);
118-
const expected = camelCaseObject(response);
115+
await deleteContentGroup(courseId, parentGroupId, groupId);
119116

120117
expect(axiosMock.history.delete[0].url).toEqual(
121118
getLegacyApiUrl(courseId, updatedContentGroups.id, groupId),
122119
);
123-
expect(result).toEqual(expected);
124120
});
125121

126122
it('should create experiment configurations', async () => {

0 commit comments

Comments
 (0)