Skip to content

Commit d6b51ec

Browse files
ChrisChVpomegranitedbradenmacdonald
authored
feat: Add unit from library into course (openedx#1829)
* feat: Initial worflow to add unit to course * test: Add initial tests * feat: Show only published units * test: Update Subsection card test and ComponentPicker tests * feat: Connect add unit from library API * test: Test for Add unit from library in CourseOutline * fix: create a new Vertical from a Library Unit * docs: add a little note about avoiding 'vertical' where possible * refactor: Use visibleTabs instead of showOnlyHomeTab --------- Co-authored-by: Jillian Vogel <[email protected]> Co-authored-by: Braden MacDonald <[email protected]>
1 parent 1fe1f93 commit d6b51ec

12 files changed

Lines changed: 372 additions & 94 deletions

File tree

src/course-outline/CourseOutline.jsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ const CourseOutline = ({ courseId }) => {
103103
handleNewSectionSubmit,
104104
handleNewSubsectionSubmit,
105105
handleNewUnitSubmit,
106+
handleAddUnitFromLibrary,
106107
getUnitUrl,
107108
handleVideoSharingOptionChange,
108109
handlePasteClipboardClick,
@@ -383,6 +384,7 @@ const CourseOutline = ({ courseId }) => {
383384
onDuplicateSubmit={handleDuplicateSubsectionSubmit}
384385
onOpenConfigureModal={openConfigureModal}
385386
onNewUnitSubmit={handleNewUnitSubmit}
387+
onAddUnitFromLibrary={handleAddUnitFromLibrary}
386388
onOrderChange={updateSubsectionOrderByIndex}
387389
onPasteClick={handlePasteClipboardClick}
388390
>

src/course-outline/CourseOutline.test.jsx

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,11 +60,14 @@ import {
6060
moveSubsection,
6161
moveUnit,
6262
} from './drag-helper/utils';
63+
import { postXBlockBaseApiUrl } from '../course-unit/data/api';
64+
import { COMPONENT_TYPES } from '../generic/block-type-utils/constants';
6365

6466
let axiosMock;
6567
let store;
6668
const mockPathname = '/foo-bar';
6769
const courseId = '123';
70+
const containerKey = 'lct:org:lib:unit:1';
6871

6972
window.HTMLElement.prototype.scrollIntoView = jest.fn();
7073

@@ -94,6 +97,30 @@ jest.mock('./data/api', () => ({
9497
getTagsCount: () => jest.fn().mockResolvedValue({}),
9598
}));
9699

100+
jest.mock('../studio-home/hooks', () => ({
101+
useStudioHome: () => ({
102+
librariesV2Enabled: true,
103+
}),
104+
}));
105+
106+
// Mock ComponentPicker to call onComponentSelected on click
107+
jest.mock('../library-authoring/component-picker', () => ({
108+
ComponentPicker: (props) => {
109+
const onClick = () => {
110+
// eslint-disable-next-line react/prop-types
111+
props.onComponentSelected({
112+
usageKey: containerKey,
113+
blockType: 'unti',
114+
});
115+
};
116+
return (
117+
<button type="submit" onClick={onClick}>
118+
Dummy button
119+
</button>
120+
);
121+
},
122+
}));
123+
97124
const queryClient = new QueryClient();
98125

99126
jest.mock('@dnd-kit/core', () => ({
@@ -390,6 +417,42 @@ describe('<CourseOutline />', () => {
390417
}));
391418
});
392419

420+
it('adds a unit from library correctly', async () => {
421+
render(<RootWrapper />);
422+
const [sectionElement] = await screen.findAllByTestId('section-card');
423+
const [subsectionElement] = await within(sectionElement).findAllByTestId('subsection-card');
424+
const expandBtn = await within(subsectionElement).findByTestId('subsection-card-header__expanded-btn');
425+
fireEvent.click(expandBtn);
426+
const units = await within(subsectionElement).findAllByTestId('unit-card');
427+
expect(units.length).toBe(1);
428+
429+
axiosMock
430+
.onPost(postXBlockBaseApiUrl())
431+
.reply(200, {
432+
locator: 'some',
433+
});
434+
435+
const addUnitFromLibraryButton = within(subsectionElement).getByRole('button', {
436+
name: /use unit from library/i,
437+
});
438+
fireEvent.click(addUnitFromLibraryButton);
439+
440+
// click dummy button to execute onComponentSelected prop.
441+
const dummyBtn = await screen.findByRole('button', { name: 'Dummy button' });
442+
fireEvent.click(dummyBtn);
443+
444+
waitFor(() => expect(axiosMock.history.post.length).toBe(1));
445+
446+
const [section] = courseOutlineIndexMock.courseStructure.childInfo.children;
447+
const [subsection] = section.childInfo.children;
448+
expect(axiosMock.history.post[0].data).toBe(JSON.stringify({
449+
type: COMPONENT_TYPES.libraryV2,
450+
category: 'vertical',
451+
parent_locator: subsection.id,
452+
library_content_key: containerKey,
453+
}));
454+
});
455+
393456
it('render checklist value correctly', async () => {
394457
const { getByText } = render(<RootWrapper />);
395458

src/course-outline/data/thunk.js

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ import {
5353
setPasteFileNotices,
5454
updateCourseLaunchQueryStatus,
5555
} from './slice';
56+
import { createCourseXblock } from '../../course-unit/data/api';
5657

5758
export function fetchCourseOutlineIndexQuery(courseId) {
5859
return async (dispatch) => {
@@ -540,6 +541,26 @@ export function addNewUnitQuery(parentLocator, callback) {
540541
};
541542
}
542543

544+
export function addUnitFromLibrary(body, callback) {
545+
return async (dispatch) => {
546+
dispatch(updateSavingStatus({ status: RequestStatus.PENDING }));
547+
dispatch(showProcessingNotification(NOTIFICATION_MESSAGES.saving));
548+
549+
try {
550+
await createCourseXblock(body).then(async (result) => {
551+
if (result) {
552+
dispatch(updateSavingStatus({ status: RequestStatus.SUCCESSFUL }));
553+
dispatch(hideProcessingNotification());
554+
callback(result.locator);
555+
}
556+
});
557+
} catch (error) /* istanbul ignore next */ {
558+
dispatch(hideProcessingNotification());
559+
dispatch(updateSavingStatus({ status: RequestStatus.FAILED }));
560+
}
561+
};
562+
}
563+
543564
function setBlockOrderListQuery(
544565
parentId,
545566
blockIds,

src/course-outline/hooks.jsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ import {
5353
setUnitOrderListQuery,
5454
pasteClipboardContent,
5555
dismissNotificationQuery,
56+
addUnitFromLibrary,
5657
} from './data/thunk';
5758

5859
const useCourseOutline = ({ courseId }) => {
@@ -128,6 +129,10 @@ const useCourseOutline = ({ courseId }) => {
128129
dispatch(addNewUnitQuery(subsectionId, openUnitPage));
129130
};
130131

132+
const handleAddUnitFromLibrary = (body) => {
133+
dispatch(addUnitFromLibrary(body, openUnitPage));
134+
};
135+
131136
const headerNavigationsActions = {
132137
handleNewSection: handleNewSectionSubmit,
133138
handleReIndex: () => {
@@ -336,6 +341,7 @@ const useCourseOutline = ({ courseId }) => {
336341
getUnitUrl,
337342
openUnitPage,
338343
handleNewUnitSubmit,
344+
handleAddUnitFromLibrary,
339345
handleVideoSharingOptionChange,
340346
handlePasteClipboardClick,
341347
notificationDismissUrl,

0 commit comments

Comments
 (0)