Skip to content

Commit 5ce61fa

Browse files
authored
feat: Add ability to create Legacy Libraries (#2551)
This adds a CreateLegacyLibrary component. It functions the same as CreateLibrary, but it calls the V1 (legacy) creation REST API rather the V2 (new/beta) REST API. This reinstates, in the MFE, something that was possible using the legacy frontend until it was prematurely removed by openedx/edx-platform#37454. We plan to re-remove this ability between Ulmo and Verawood as part of: openedx/edx-platform#32457. So, we have intentionally avoided factoring out common logic between CreateLibrary and CreateLegacyLibrary, ensuring that the latter remains easy to remove and clean up.
1 parent 46fa17e commit 5ce61fa

File tree

10 files changed

+489
-9
lines changed

10 files changed

+489
-9
lines changed

src/index.jsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import messages from './i18n';
1919
import {
2020
ComponentPicker,
2121
CreateLibrary,
22+
CreateLegacyLibrary,
2223
LibraryLayout,
2324
PreviewChangesEmbed,
2425
} from './library-authoring';
@@ -67,6 +68,7 @@ const App = () => {
6768
<Route path="/libraries" element={<StudioHome />} />
6869
<Route path="/libraries-v1" element={<StudioHome />} />
6970
<Route path="/libraries-v1/migrate" element={<LegacyLibMigrationPage />} />
71+
<Route path="/libraries-v1/create" element={<CreateLegacyLibrary />} />
7072
<Route path="/library/create" element={<CreateLibrary />} />
7173
<Route path="/library/:libraryId/*" element={<LibraryLayout />} />
7274
<Route
Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
import React from 'react';
2+
import type MockAdapter from 'axios-mock-adapter';
3+
import userEvent from '@testing-library/user-event';
4+
5+
import {
6+
initializeMocks,
7+
render,
8+
screen,
9+
waitFor,
10+
} from '@src/testUtils';
11+
import studioHomeMock from '@src/studio-home/__mocks__/studioHomeMock';
12+
import { getStudioHomeApiUrl } from '@src/studio-home/data/api';
13+
import { getApiWaffleFlagsUrl } from '@src/data/api';
14+
import { CreateLegacyLibrary } from '.';
15+
import { getContentLibraryV1CreateApiUrl } from './data/api';
16+
17+
const mockNavigate = jest.fn();
18+
const realWindowLocationAssign = window.location.assign;
19+
let axiosMock: MockAdapter;
20+
21+
jest.mock('react-router-dom', () => ({
22+
...jest.requireActual('react-router-dom'),
23+
useNavigate: () => mockNavigate,
24+
}));
25+
26+
jest.mock('@src/generic/data/apiHooks', () => ({
27+
...jest.requireActual('@src/generic/data/apiHooks'),
28+
useOrganizationListData: () => ({
29+
data: ['org1', 'org2', 'org3', 'org4', 'org5'],
30+
isLoading: false,
31+
}),
32+
}));
33+
34+
describe('<CreateLegacyLibrary />', () => {
35+
beforeEach(() => {
36+
axiosMock = initializeMocks().axiosMock;
37+
axiosMock
38+
.onGet(getApiWaffleFlagsUrl(undefined))
39+
.reply(200, {});
40+
Object.defineProperty(window, 'location', {
41+
value: { assign: jest.fn() },
42+
});
43+
});
44+
45+
afterEach(() => {
46+
jest.clearAllMocks();
47+
axiosMock.restore();
48+
window.location.assign = realWindowLocationAssign;
49+
});
50+
51+
test('call api data with correct data', async () => {
52+
const user = userEvent.setup();
53+
axiosMock.onGet(getStudioHomeApiUrl()).reply(200, studioHomeMock);
54+
axiosMock.onPost(getContentLibraryV1CreateApiUrl()).reply(200, {
55+
id: 'library-id',
56+
url: '/library/library-id',
57+
});
58+
59+
render(<CreateLegacyLibrary />);
60+
61+
const titleInput = await screen.findByRole('textbox', { name: /library name/i });
62+
await user.click(titleInput);
63+
await user.type(titleInput, 'Test Library Name');
64+
65+
const orgInput = await screen.findByRole('combobox', { name: /organization/i });
66+
await user.click(orgInput);
67+
await user.type(orgInput, 'org1');
68+
await user.tab();
69+
70+
const slugInput = await screen.findByRole('textbox', { name: /library id/i });
71+
await user.click(slugInput);
72+
await user.type(slugInput, 'test_library_slug');
73+
74+
await user.click(await screen.findByRole('button', { name: /create/i }));
75+
await waitFor(() => {
76+
expect(axiosMock.history.post.length).toBe(1);
77+
expect(axiosMock.history.post[0].data).toBe(
78+
'{"display_name":"Test Library Name","org":"org1","number":"test_library_slug"}',
79+
);
80+
expect(window.location.assign).toHaveBeenCalledWith('http://localhost:18010/library/library-id');
81+
});
82+
});
83+
84+
test('cannot create new org unless allowed', async () => {
85+
const user = userEvent.setup();
86+
axiosMock.onGet(getStudioHomeApiUrl()).reply(200, studioHomeMock);
87+
axiosMock.onPost(getContentLibraryV1CreateApiUrl()).reply(200, {
88+
id: 'library-id',
89+
url: '/library/library-id',
90+
});
91+
92+
render(<CreateLegacyLibrary />);
93+
94+
const titleInput = await screen.findByRole('textbox', { name: /library name/i });
95+
await user.click(titleInput);
96+
await user.type(titleInput, 'Test Library Name');
97+
98+
// We cannot create a new org, and so we're restricted to the allowed list
99+
const orgOptions = screen.getByTestId('autosuggest-iconbutton');
100+
await user.click(orgOptions);
101+
expect(screen.getByText('org1')).toBeInTheDocument();
102+
['org2', 'org3', 'org4', 'org5'].forEach((org) => expect(screen.queryByText(org)).not.toBeInTheDocument());
103+
104+
const orgInput = await screen.findByRole('combobox', { name: /organization/i });
105+
await user.click(orgInput);
106+
await user.type(orgInput, 'NewOrg');
107+
await user.tab();
108+
109+
const slugInput = await screen.findByRole('textbox', { name: /library id/i });
110+
await user.click(slugInput);
111+
await user.type(slugInput, 'test_library_slug');
112+
113+
await user.click(await screen.findByRole('button', { name: /create/i }));
114+
await waitFor(() => {
115+
expect(axiosMock.history.post.length).toBe(0);
116+
});
117+
expect(await screen.findByText('Required field.')).toBeInTheDocument();
118+
});
119+
120+
test('can create new org if allowed', async () => {
121+
const user = userEvent.setup();
122+
axiosMock.onGet(getStudioHomeApiUrl()).reply(200, {
123+
...studioHomeMock,
124+
allow_to_create_new_org: true,
125+
});
126+
axiosMock.onPost(getContentLibraryV1CreateApiUrl()).reply(200, {
127+
id: 'library-id',
128+
url: '/library/library-id',
129+
});
130+
131+
render(<CreateLegacyLibrary />);
132+
133+
const titleInput = await screen.findByRole('textbox', { name: /library name/i });
134+
await user.click(titleInput);
135+
await user.type(titleInput, 'Test Library Name');
136+
137+
// We can create a new org, so we're also allowed to use any existing org
138+
const orgOptions = screen.getByTestId('autosuggest-iconbutton');
139+
await user.click(orgOptions);
140+
['org1', 'org2', 'org3', 'org4', 'org5'].forEach((org) => expect(screen.queryByText(org)).toBeInTheDocument());
141+
142+
const orgInput = await screen.findByRole('combobox', { name: /organization/i });
143+
await user.click(orgInput);
144+
await user.type(orgInput, 'NewOrg');
145+
await user.tab();
146+
147+
const slugInput = await screen.findByRole('textbox', { name: /library id/i });
148+
await user.click(slugInput);
149+
await user.type(slugInput, 'test_library_slug');
150+
151+
await user.click(await screen.findByRole('button', { name: /create/i }));
152+
await waitFor(() => {
153+
expect(axiosMock.history.post.length).toBe(1);
154+
expect(axiosMock.history.post[0].data).toBe(
155+
'{"display_name":"Test Library Name","org":"NewOrg","number":"test_library_slug"}',
156+
);
157+
expect(window.location.assign).toHaveBeenCalledWith('http://localhost:18010/library/library-id');
158+
});
159+
});
160+
161+
test('show api error', async () => {
162+
const user = userEvent.setup();
163+
axiosMock.onGet(getStudioHomeApiUrl()).reply(200, studioHomeMock);
164+
axiosMock.onPost(getContentLibraryV1CreateApiUrl()).reply(400, {
165+
field: 'Error message',
166+
});
167+
render(<CreateLegacyLibrary />);
168+
169+
const titleInput = await screen.findByRole('textbox', { name: /library name/i });
170+
await user.click(titleInput);
171+
await user.type(titleInput, 'Test Library Name');
172+
173+
const orgInput = await screen.findByRole('combobox', { name: /organization/i });
174+
await user.click(orgInput);
175+
await user.type(orgInput, 'org1');
176+
await user.tab();
177+
178+
const slugInput = await screen.findByRole('textbox', { name: /library id/i });
179+
await user.click(slugInput);
180+
await user.type(slugInput, 'test_library_slug');
181+
182+
await user.click(await screen.findByRole('button', { name: /create/i }));
183+
await waitFor(async () => {
184+
expect(axiosMock.history.post.length).toBe(1);
185+
expect(axiosMock.history.post[0].data).toBe(
186+
'{"display_name":"Test Library Name","org":"org1","number":"test_library_slug"}',
187+
);
188+
expect(mockNavigate).not.toHaveBeenCalled();
189+
});
190+
await screen.findByText('Request failed with status code 400');
191+
});
192+
193+
test('cancel creating library navigates to libraries page', async () => {
194+
const user = userEvent.setup();
195+
render(<CreateLegacyLibrary />);
196+
197+
await user.click(await screen.findByRole('button', { name: /cancel/i }));
198+
await waitFor(() => {
199+
expect(mockNavigate).toHaveBeenCalledWith('/libraries-v1');
200+
});
201+
});
202+
});

0 commit comments

Comments
 (0)