Skip to content

Commit 35df4a2

Browse files
committed
refactor: Migrate textbooks from Redux store to React Query
1 parent da4e81b commit 35df4a2

29 files changed

Lines changed: 735 additions & 1177 deletions

src/generic/modal-dropzone/useModalDropzone.jsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ const useModalDropzone = ({
9696
const handleUpload = async () => {
9797
if (!selectedFile) { return; }
9898

99-
onSavingStatus(RequestStatus.PENDING);
99+
onSavingStatus({ status: RequestStatus.PENDING });
100100

101101
const onUploadProgress = (progressEvent) => {
102102
const percentCompleted = Math.round((progressEvent.loaded * 100) / progressEvent.total);

src/store.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ import { reducer as genericReducer } from './generic/data/slice';
2020
import { reducer as videosReducer } from './files-and-videos/videos-page/data/slice';
2121
import { reducer as courseOutlineReducer } from './course-outline/data/slice';
2222
import { reducer as courseUnitReducer } from './course-unit/data/slice';
23-
import { reducer as textbooksReducer } from './textbooks/data/slice';
2423
import { reducer as certificatesReducer } from './certificates/data/slice';
2524

2625
type InferState<ReducerType> = ReducerType extends Reducer<infer T> ? T : never;
@@ -52,7 +51,6 @@ export interface DeprecatedReduxState {
5251
componentMode: (typeof MODE_STATES)[keyof typeof MODE_STATES];
5352
certificatesData: any;
5453
};
55-
textbooks: Record<string, any>;
5654
}
5755

5856
export default function initializeStore(preloadedState: Partial<DeprecatedReduxState> | undefined = undefined) {
@@ -73,7 +71,6 @@ export default function initializeStore(preloadedState: Partial<DeprecatedReduxS
7371
courseOutline: courseOutlineReducer,
7472
courseUnit: courseUnitReducer,
7573
certificates: certificatesReducer,
76-
textbooks: textbooksReducer,
7774
},
7875
preloadedState: preloadedState as DeprecatedReduxState | undefined,
7976
});

src/textbooks/Textbook.test.jsx

Lines changed: 0 additions & 94 deletions
This file was deleted.

src/textbooks/Textbook.test.tsx

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import userEvent from '@testing-library/user-event';
2+
3+
import { CourseAuthoringProvider } from '@src/CourseAuthoringContext';
4+
import {
5+
initializeMocks,
6+
render,
7+
screen,
8+
} from '@src/testUtils';
9+
import { getTextbooksApiUrl } from './data/api';
10+
import { textbooksMock } from './__mocks__';
11+
import { Textbooks } from '.';
12+
import messages from './messages';
13+
14+
let axiosMock;
15+
const courseId = 'course-v1:org+101+101';
16+
const emptyTextbooksMock = { textbooks: [] };
17+
18+
const renderComponent = () =>
19+
render(
20+
<CourseAuthoringProvider courseId={courseId}>
21+
<Textbooks />
22+
</CourseAuthoringProvider>,
23+
);
24+
25+
describe('<Textbooks />', () => {
26+
beforeEach(async () => {
27+
const mocks = initializeMocks();
28+
29+
axiosMock = mocks.axiosMock;
30+
axiosMock
31+
.onGet(getTextbooksApiUrl(courseId))
32+
.reply(200, textbooksMock);
33+
});
34+
35+
it('renders Textbooks component correctly', async () => {
36+
renderComponent();
37+
38+
expect(await screen.findByText(messages.headingTitle.defaultMessage)).toBeInTheDocument();
39+
expect(screen.getByText(messages.breadcrumbContent.defaultMessage)).toBeInTheDocument();
40+
expect(screen.getByText(messages.breadcrumbPagesAndResources.defaultMessage)).toBeInTheDocument();
41+
expect(screen.getByRole('button', { name: messages.newTextbookButton.defaultMessage })).toBeInTheDocument();
42+
expect(screen.getAllByTestId('textbook-card')).toHaveLength(2);
43+
expect(screen.queryByTestId('textbooks-empty-placeholder')).not.toBeInTheDocument();
44+
});
45+
46+
it('renders textbooks form when "New textbooks" button is clicked', async () => {
47+
const user = userEvent.setup();
48+
renderComponent();
49+
50+
const newTextbookButton = await screen.findByRole('button', { name: messages.newTextbookButton.defaultMessage });
51+
await user.click(newTextbookButton);
52+
expect(screen.getByTestId('textbook-form')).toBeInTheDocument();
53+
});
54+
55+
it('renders Textbooks component with empty placeholder correctly', async () => {
56+
axiosMock
57+
.onGet(getTextbooksApiUrl(courseId))
58+
.reply(200, emptyTextbooksMock);
59+
60+
renderComponent();
61+
62+
expect(await screen.findByTestId('textbooks-empty-placeholder')).toBeInTheDocument();
63+
expect(screen.queryAllByTestId('textbook-card')).toHaveLength(0);
64+
});
65+
66+
it('displays an alert when API responds with 403', async () => {
67+
axiosMock
68+
.onGet(getTextbooksApiUrl(courseId))
69+
.reply(403);
70+
renderComponent();
71+
72+
expect(await screen.findByTestId('connectionErrorAlert')).toBeInTheDocument();
73+
});
74+
});
Lines changed: 11 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,11 @@ import { Add as AddIcon } from '@openedx/paragon/icons';
1010
import { Helmet } from 'react-helmet';
1111
import { Link } from 'react-router-dom';
1212
import { useCourseAuthoringContext } from '@src/CourseAuthoringContext';
13-
import { RequestStatus } from '@src/data/constants';
13+
import { SavingErrorAlert } from '@src/generic/saving-error-alert';
14+
import { LoadingSpinner } from '@src/generic/Loading';
15+
import SubHeader from '@src/generic/sub-header/SubHeader';
1416

15-
import { useWaffleFlags } from '../data/apiHooks';
16-
import { SavingErrorAlert } from '../generic/saving-error-alert';
17-
import { LoadingSpinner } from '../generic/Loading';
18-
import SubHeader from '../generic/sub-header/SubHeader';
19-
import ConnectionErrorAlert from '../generic/ConnectionErrorAlert';
17+
import ConnectionErrorAlert from '@src/generic/ConnectionErrorAlert';
2018
import EmptyPlaceholder from './empty-placeholder/EmptyPlaceholder';
2119
import TextbookCard from './textbook-card/TextbooksCard';
2220
import TextbookSidebar from './textbook-sidebar/TextbookSidebar';
@@ -27,24 +25,22 @@ import messages from './messages';
2725

2826
const Textbooks = () => {
2927
const intl = useIntl();
30-
const { courseId, courseDetails } = useCourseAuthoringContext();
31-
const waffleFlags = useWaffleFlags(courseId);
28+
const { courseDetails } = useCourseAuthoringContext();
3229

3330
const {
3431
textbooks,
3532
isLoading,
3633
isLoadingFailed,
3734
breadcrumbs,
38-
errorMessage,
39-
savingStatus,
35+
mutationErrorMessage,
36+
anyMutationFailed,
4037
isTextbookFormOpen,
4138
openTextbookForm,
4239
closeTextbookForm,
4340
handleTextbookFormSubmit,
44-
handleSavingStatusDispatch,
4541
handleTextbookEditFormSubmit,
4642
handleTextbookDeleteSubmit,
47-
} = useTextbooks(courseId, waffleFlags);
43+
} = useTextbooks();
4844

4945
if (isLoadingFailed) {
5046
return (
@@ -106,8 +102,6 @@ const Textbooks = () => {
106102
<TextbookCard
107103
key={textbook.id}
108104
textbook={textbook}
109-
courseId={courseId}
110-
handleSavingStatusDispatch={handleSavingStatusDispatch}
111105
onEditSubmit={handleTextbookEditFormSubmit}
112106
onDeleteSubmit={handleTextbookDeleteSubmit}
113107
textbookIndex={index}
@@ -121,23 +115,22 @@ const Textbooks = () => {
121115
closeTextbookForm={closeTextbookForm}
122116
initialFormValues={getTextbookFormInitialValues()}
123117
onSubmit={handleTextbookFormSubmit}
124-
onSavingStatus={handleSavingStatusDispatch}
125118
/>
126119
)}
127120
</div>
128121
</section>
129122
</article>
130123
</Layout.Element>
131124
<Layout.Element>
132-
<TextbookSidebar courseId={courseId} />
125+
<TextbookSidebar />
133126
</Layout.Element>
134127
</Layout>
135128
</section>
136129
</Container>
137130
<div className="alert-toast">
138131
<SavingErrorAlert
139-
isQueryFailed={savingStatus === RequestStatus.FAILED}
140-
errorMessage={errorMessage}
132+
isQueryFailed={anyMutationFailed}
133+
errorMessage={mutationErrorMessage}
141134
/>
142135
</div>
143136
</>
Lines changed: 6 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
1-
import MockAdapter from 'axios-mock-adapter';
2-
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
3-
import { initializeMockApp } from '@edx/frontend-platform';
1+
import { initializeMocks } from '@src/testUtils';
42

53
import { textbooksMock } from 'CourseAuthoring/textbooks/__mocks__';
64
import {
@@ -18,25 +16,14 @@ const courseId = 'course-v1:org+101+101';
1816

1917
describe('getTextbooks', () => {
2018
beforeEach(async () => {
21-
initializeMockApp({
22-
authenticatedUser: {
23-
userId: 3,
24-
username: 'abc123',
25-
administrator: true,
26-
roles: [],
27-
},
28-
});
19+
const mocks = initializeMocks();
2920

30-
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
21+
axiosMock = mocks.axiosMock;
3122
axiosMock
3223
.onGet(getTextbooksApiUrl(courseId))
3324
.reply(200, textbooksMock);
3425
});
3526

36-
afterEach(() => {
37-
axiosMock.reset();
38-
});
39-
4027
it('should fetch textbooks for a course', async () => {
4128
const textbooksData = [{ id: 1, title: 'Textbook 1' }, { id: 2, title: 'Textbook 2' }];
4229
axiosMock.onGet(getTextbooksApiUrl(courseId)).reply(200, textbooksData);
@@ -49,7 +36,7 @@ describe('getTextbooks', () => {
4936

5037
describe('createTextbook', () => {
5138
it('should create a new textbook for a course', async () => {
52-
const textbookData = { title: 'New Textbook', chapters: [] };
39+
const textbookData = { tabTitle: 'New Textbook', chapters: [] };
5340
axiosMock.onPost(getUpdateTextbooksApiUrl(courseId)).reply(200, textbookData);
5441

5542
const result = await createTextbook(courseId, textbookData);
@@ -61,7 +48,7 @@ describe('createTextbook', () => {
6148
describe('editTextbook', () => {
6249
it('should edit an existing textbook for a course', async () => {
6350
const textbookId = '1';
64-
const editedTextbookData = { id: '1', title: 'Edited Textbook', chapters: [] };
51+
const editedTextbookData = { id: '1', tabTitle: 'Edited Textbook', chapters: [] };
6552
axiosMock.onPut(getEditTextbooksApiUrl(courseId, textbookId)).reply(200, editedTextbookData);
6653

6754
const result = await editTextbook(courseId, editedTextbookData);
@@ -75,8 +62,6 @@ describe('deleteTextbook', () => {
7562
const textbookId = '1';
7663
axiosMock.onDelete(getEditTextbooksApiUrl(courseId, textbookId)).reply(200, {});
7764

78-
const result = await deleteTextbook(courseId, textbookId);
79-
80-
expect(result).toEqual({});
65+
await expect(deleteTextbook(courseId, textbookId)).resolves.toBeUndefined();
8166
});
8267
});

0 commit comments

Comments
 (0)