Skip to content

Commit cee8888

Browse files
authored
feat: enable problem bank button functionality on unit page (#1480)
1 parent 033acc4 commit cee8888

17 files changed

Lines changed: 230 additions & 96 deletions

File tree

src/course-unit/CourseUnit.test.jsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -282,7 +282,7 @@ describe('<CourseUnit />', () => {
282282

283283
await waitFor(() => {
284284
const problemButton = getByRole('button', {
285-
name: new RegExp(`${addComponentMessages.buttonText.defaultMessage} Problem`, 'i'),
285+
name: new RegExp(`problem ${addComponentMessages.buttonText.defaultMessage} Problem`, 'i'),
286286
});
287287

288288
userEvent.click(problemButton);

src/course-unit/__mocks__/courseSectionVertical.js

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ module.exports = {
8282
allow_unsupported_xblocks: false,
8383
documentation_label: 'Your Platform Name Here Support Levels:',
8484
},
85+
beta: false,
8586
},
8687
{
8788
type: 'discussion',
@@ -101,6 +102,7 @@ module.exports = {
101102
allow_unsupported_xblocks: false,
102103
documentation_label: 'Your Platform Name Here Support Levels:',
103104
},
105+
beta: false,
104106
},
105107
{
106108
type: 'library',
@@ -114,12 +116,13 @@ module.exports = {
114116
support_level: true,
115117
},
116118
],
117-
display_name: 'Library Content',
119+
display_name: 'Legacy Library Content',
118120
support_legend: {
119121
show_legend: false,
120122
allow_unsupported_xblocks: false,
121123
documentation_label: 'Your Platform Name Here Support Levels:',
122124
},
125+
beta: false,
123126
},
124127
{
125128
type: 'html',
@@ -179,6 +182,7 @@ module.exports = {
179182
allow_unsupported_xblocks: false,
180183
documentation_label: 'Your Platform Name Here Support Levels:',
181184
},
185+
beta: false,
182186
},
183187
{
184188
type: 'openassessment',
@@ -230,6 +234,7 @@ module.exports = {
230234
allow_unsupported_xblocks: false,
231235
documentation_label: 'Your Platform Name Here Support Levels:',
232236
},
237+
beta: false,
233238
},
234239
{
235240
type: 'problem',
@@ -249,6 +254,7 @@ module.exports = {
249254
allow_unsupported_xblocks: false,
250255
documentation_label: 'Your Platform Name Here Support Levels:',
251256
},
257+
beta: false,
252258
},
253259
{
254260
type: 'video',
@@ -268,6 +274,7 @@ module.exports = {
268274
allow_unsupported_xblocks: false,
269275
documentation_label: 'Your Platform Name Here Support Levels:',
270276
},
277+
beta: false,
271278
},
272279
{
273280
type: 'drag-and-drop-v2',
@@ -287,6 +294,47 @@ module.exports = {
287294
allow_unsupported_xblocks: false,
288295
documentation_label: 'Your Platform Name Here Support Levels:',
289296
},
297+
beta: false,
298+
},
299+
{
300+
type: 'library_v2',
301+
templates: [
302+
{
303+
display_name: 'Library Content',
304+
category: 'library_v2',
305+
boilerplate_name: null,
306+
hinted: false,
307+
tab: 'advanced',
308+
support_level: true,
309+
},
310+
],
311+
display_name: 'Library Content',
312+
support_legend: {
313+
show_legend: false,
314+
allow_unsupported_xblocks: false,
315+
documentation_label: 'Your Platform Name Here Support Levels:',
316+
},
317+
beta: true,
318+
},
319+
{
320+
type: 'itembank',
321+
templates: [
322+
{
323+
display_name: 'Problem Bank',
324+
category: 'itembank',
325+
boilerplate_name: null,
326+
hinted: false,
327+
tab: 'advanced',
328+
support_level: true,
329+
},
330+
],
331+
display_name: 'Problem Bank',
332+
support_legend: {
333+
show_legend: false,
334+
allow_unsupported_xblocks: false,
335+
documentation_label: 'Your Platform Name Here Support Levels:',
336+
},
337+
beta: true,
290338
},
291339
],
292340
course_sequence_ids: [

src/course-unit/add-component/AddComponent.jsx

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,14 @@ import PropTypes from 'prop-types';
22
import { useSelector } from 'react-redux';
33
import { useNavigate } from 'react-router-dom';
44
import { useIntl } from '@edx/frontend-platform/i18n';
5-
import { useToggle } from '@openedx/paragon';
5+
import { StandardModal, useToggle } from '@openedx/paragon';
66

77
import { getCourseSectionVertical } from '../data/selectors';
88
import { COMPONENT_TYPES } from '../../generic/block-type-utils/constants';
99
import ComponentModalView from './add-component-modals/ComponentModalView';
1010
import AddComponentButton from './add-component-btn';
1111
import messages from './messages';
12+
import { ComponentPicker } from '../../library-authoring/component-picker';
1213

1314
const AddComponent = ({ blockId, handleCreateNewCourseXBlock }) => {
1415
const navigate = useNavigate();
@@ -17,6 +18,17 @@ const AddComponent = ({ blockId, handleCreateNewCourseXBlock }) => {
1718
const [isOpenHtml, openHtml, closeHtml] = useToggle(false);
1819
const [isOpenOpenAssessment, openOpenAssessment, closeOpenAssessment] = useToggle(false);
1920
const { componentTemplates } = useSelector(getCourseSectionVertical);
21+
const [isAddLibraryContentModalOpen, showAddLibraryContentModal, closeAddLibraryContentModal] = useToggle();
22+
23+
const handleLibraryV2Selection = (selection) => {
24+
handleCreateNewCourseXBlock({
25+
type: COMPONENT_TYPES.libraryV2,
26+
category: selection.blockType,
27+
parentLocator: blockId,
28+
libraryContentKey: selection.usageKey,
29+
});
30+
closeAddLibraryContentModal();
31+
};
2032

2133
const handleCreateNewXBlock = (type, moduleName) => {
2234
switch (type) {
@@ -35,6 +47,12 @@ const AddComponent = ({ blockId, handleCreateNewCourseXBlock }) => {
3547
case COMPONENT_TYPES.library:
3648
handleCreateNewCourseXBlock({ type, category: 'library_content', parentLocator: blockId });
3749
break;
50+
case COMPONENT_TYPES.itembank:
51+
handleCreateNewCourseXBlock({ type, category: 'itembank', parentLocator: blockId });
52+
break;
53+
case COMPONENT_TYPES.libraryV2:
54+
showAddLibraryContentModal();
55+
break;
3856
case COMPONENT_TYPES.advanced:
3957
handleCreateNewCourseXBlock({
4058
type: moduleName, category: moduleName, parentLocator: blockId,
@@ -67,7 +85,7 @@ const AddComponent = ({ blockId, handleCreateNewCourseXBlock }) => {
6785
<h5 className="h3 mb-4 text-center">{intl.formatMessage(messages.title)}</h5>
6886
<ul className="new-component-type list-unstyled m-0 d-flex flex-wrap justify-content-center">
6987
{componentTemplates.map((component) => {
70-
const { type, displayName } = component;
88+
const { type, displayName, beta } = component;
7189
let modalParams;
7290

7391
if (!component.templates.length) {
@@ -103,6 +121,7 @@ const AddComponent = ({ blockId, handleCreateNewCourseXBlock }) => {
103121
onClick={() => handleCreateNewXBlock(type)}
104122
displayName={displayName}
105123
type={type}
124+
beta={beta}
106125
/>
107126
</li>
108127
);
@@ -118,6 +137,18 @@ const AddComponent = ({ blockId, handleCreateNewCourseXBlock }) => {
118137
);
119138
})}
120139
</ul>
140+
<StandardModal
141+
title="Select component"
142+
isOpen={isAddLibraryContentModalOpen}
143+
onClose={closeAddLibraryContentModal}
144+
isOverflowVisible={false}
145+
size="xl"
146+
>
147+
<ComponentPicker
148+
showOnlyPublished
149+
onComponentSelected={handleLibraryV2Selection}
150+
/>
151+
</StandardModal>
121152
</div>
122153
);
123154
};

src/course-unit/add-component/AddComponent.test.jsx

Lines changed: 57 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,11 @@ let axiosMock;
2323
const blockId = '123';
2424
const handleCreateNewCourseXBlockMock = jest.fn();
2525

26+
// Mock ComponentPicker to call onComponentSelected on load
27+
jest.mock('../../library-authoring/component-picker', () => ({
28+
ComponentPicker: (props) => props.onComponentSelected({ usageKey: 'test-usage-key', blockType: 'html' }),
29+
}));
30+
2631
const renderComponent = (props) => render(
2732
<AppProvider store={store}>
2833
<IntlProvider locale="en">
@@ -59,11 +64,19 @@ describe('<AddComponent />', () => {
5964
const componentTemplates = courseSectionVerticalMock.component_templates;
6065

6166
expect(getByRole('heading', { name: messages.title.defaultMessage })).toBeInTheDocument();
62-
Object.keys(componentTemplates).map((component) => (
63-
expect(getByRole('button', {
64-
name: new RegExp(`${messages.buttonText.defaultMessage} ${componentTemplates[component].display_name}`, 'i'),
65-
})).toBeInTheDocument()
66-
));
67+
Object.keys(componentTemplates).forEach((component) => {
68+
const btn = getByRole('button', {
69+
name: new RegExp(
70+
`${componentTemplates[component].type
71+
} ${messages.buttonText.defaultMessage} ${componentTemplates[component].display_name}`,
72+
'i',
73+
),
74+
});
75+
expect(btn).toBeInTheDocument();
76+
if (component.beta) {
77+
expect(within(btn).queryByText('Beta')).toBeInTheDocument();
78+
}
79+
});
6780
});
6881

6982
it('AddComponent component doesn\'t render when there aren\'t componentTemplates', async () => {
@@ -111,7 +124,11 @@ describe('<AddComponent />', () => {
111124
}
112125

113126
return expect(getByRole('button', {
114-
name: new RegExp(`${messages.buttonText.defaultMessage} ${componentTemplates[component].display_name}`, 'i'),
127+
name: new RegExp(
128+
`${componentTemplates[component].type
129+
} ${messages.buttonText.defaultMessage} ${componentTemplates[component].display_name}`,
130+
'i',
131+
),
115132
})).toBeInTheDocument();
116133
});
117134
});
@@ -176,7 +193,7 @@ describe('<AddComponent />', () => {
176193
const { getByRole } = renderComponent();
177194

178195
const discussionButton = getByRole('button', {
179-
name: new RegExp(`${messages.buttonText.defaultMessage} Problem`, 'i'),
196+
name: new RegExp(`problem ${messages.buttonText.defaultMessage} Problem`, 'i'),
180197
});
181198

182199
userEvent.click(discussionButton);
@@ -187,6 +204,22 @@ describe('<AddComponent />', () => {
187204
}, expect.any(Function));
188205
});
189206

207+
it('calls handleCreateNewCourseXBlock with correct parameters when Problem bank xblock create button is clicked', () => {
208+
const { getByRole } = renderComponent();
209+
210+
const problemBankBtn = getByRole('button', {
211+
name: new RegExp(`${messages.buttonText.defaultMessage} Problem Bank`, 'i'),
212+
});
213+
214+
userEvent.click(problemBankBtn);
215+
expect(handleCreateNewCourseXBlockMock).toHaveBeenCalled();
216+
expect(handleCreateNewCourseXBlockMock).toHaveBeenCalledWith({
217+
parentLocator: '123',
218+
type: COMPONENT_TYPES.itembank,
219+
category: 'itembank',
220+
});
221+
});
222+
190223
it('calls handleCreateNewCourseXBlock with correct parameters when Video xblock create button is clicked', () => {
191224
const { getByRole } = renderComponent();
192225

@@ -206,7 +239,7 @@ describe('<AddComponent />', () => {
206239
const { getByRole } = renderComponent();
207240

208241
const libraryButton = getByRole('button', {
209-
name: new RegExp(`${messages.buttonText.defaultMessage} Library Content`, 'i'),
242+
name: new RegExp(`${messages.buttonText.defaultMessage} Legacy Library Content`, 'i'),
210243
});
211244

212245
userEvent.click(libraryButton);
@@ -379,6 +412,22 @@ describe('<AddComponent />', () => {
379412
});
380413
});
381414

415+
it('shows library picker on clicking v2 library content btn', async () => {
416+
const { findByRole } = renderComponent();
417+
const libBtn = await findByRole('button', {
418+
name: new RegExp(`${messages.buttonText.defaultMessage} Library content`, 'i'),
419+
});
420+
421+
userEvent.click(libBtn);
422+
expect(handleCreateNewCourseXBlockMock).toHaveBeenCalled();
423+
expect(handleCreateNewCourseXBlockMock).toHaveBeenCalledWith({
424+
type: COMPONENT_TYPES.libraryV2,
425+
parentLocator: '123',
426+
category: 'html',
427+
libraryContentKey: 'test-usage-key',
428+
});
429+
});
430+
382431
describe('component support label', () => {
383432
it('component support label is hidden if component support legend is disabled', async () => {
384433
const supportLevels = ['fs', 'ps'];

src/course-unit/add-component/add-component-btn/index.jsx

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
import PropTypes from 'prop-types';
2-
import { Button } from '@openedx/paragon';
2+
import { Badge, Button } from '@openedx/paragon';
33
import { useIntl } from '@edx/frontend-platform/i18n';
44

55
import messages from '../messages';
66
import AddComponentIcon from './AddComponentIcon';
77

8-
const AddComponentButton = ({ type, displayName, onClick }) => {
8+
const AddComponentButton = ({
9+
type, displayName, onClick, beta,
10+
}) => {
911
const intl = useIntl();
1012

1113
return (
@@ -17,14 +19,20 @@ const AddComponentButton = ({ type, displayName, onClick }) => {
1719
<AddComponentIcon type={type} />
1820
<span className="sr-only">{intl.formatMessage(messages.buttonText)}</span>
1921
<span className="small mt-2">{displayName}</span>
22+
{beta && <Badge className="pb-1 mt-1" variant="primary">Beta</Badge>}
2023
</Button>
2124
);
2225
};
2326

27+
AddComponentButton.defaultProps = {
28+
beta: false,
29+
};
30+
2431
AddComponentButton.propTypes = {
2532
type: PropTypes.string.isRequired,
2633
displayName: PropTypes.string.isRequired,
2734
onClick: PropTypes.func.isRequired,
35+
beta: PropTypes.bool,
2836
};
2937

3038
export default AddComponentButton;

src/course-unit/add-component/add-component-modals/ComponentModalView.jsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,9 +58,11 @@ const ComponentModalView = ({
5858
const isDisplaySupportLabel = supportLegend.showLegend && supportLabels[componentTemplate.supportLevel];
5959

6060
return (
61-
<div className="d-flex justify-content-between w-100 mb-2.5 align-items-end">
61+
<div
62+
key={componentTemplate.displayName}
63+
className="d-flex justify-content-between w-100 mb-2.5 align-items-end"
64+
>
6265
<Form.Radio
63-
key={componentTemplate.displayName}
6466
className="add-component-modal-radio"
6567
value={value}
6668
>

src/course-unit/data/api.js

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,9 +63,16 @@ export async function getCourseSectionVerticalData(unitId) {
6363
* @param {string} [options.displayName] - The display name.
6464
* @param {string} [options.boilerplate] - The boilerplate.
6565
* @param {string} [options.stagedContent] - The staged content.
66+
* @param {string} [options.libraryContentKey] - component key from library if being imported.
6667
*/
6768
export async function createCourseXblock({
68-
type, category, parentLocator, displayName, boilerplate, stagedContent,
69+
type,
70+
category,
71+
parentLocator,
72+
displayName,
73+
boilerplate,
74+
stagedContent,
75+
libraryContentKey,
6976
}) {
7077
const body = {
7178
type,
@@ -74,6 +81,7 @@ export async function createCourseXblock({
7481
parent_locator: parentLocator,
7582
display_name: displayName,
7683
staged_content: stagedContent,
84+
library_content_key: libraryContentKey,
7785
};
7886

7987
const { data } = await getAuthenticatedHttpClient()

0 commit comments

Comments
 (0)