Compare commits
4 Commits
master
...
renovate/n
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d24bc96a00 | ||
|
|
1efd559786 | ||
|
|
df79861685 | ||
|
|
24e1c73f6b |
28
package-lock.json
generated
28
package-lock.json
generated
@@ -74,7 +74,7 @@
|
||||
"redux-logger": "^3.0.6",
|
||||
"redux-thunk": "^2.4.1",
|
||||
"reselect": "^4.1.5",
|
||||
"tinymce": "^5.10.4",
|
||||
"tinymce": "^7.0.0",
|
||||
"universal-cookie": "^8.0.0",
|
||||
"uuid": "^11.1.0",
|
||||
"xmlchecker": "^0.1.0",
|
||||
@@ -8978,9 +8978,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/baseline-browser-mapping": {
|
||||
"version": "2.10.0",
|
||||
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.0.tgz",
|
||||
"integrity": "sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA==",
|
||||
"version": "2.10.8",
|
||||
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.8.tgz",
|
||||
"integrity": "sha512-PCLz/LXGBsNTErbtB6i5u4eLpHeMfi93aUv5duMmj6caNu6IphS4q6UevDnL36sZQv9lrP11dbPKGMaXPwMKfQ==",
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"baseline-browser-mapping": "dist/cli.cjs"
|
||||
@@ -9361,9 +9361,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/caniuse-lite": {
|
||||
"version": "1.0.30001777",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001777.tgz",
|
||||
"integrity": "sha512-tmN+fJxroPndC74efCdp12j+0rk0RHwV5Jwa1zWaFVyw2ZxAuPeG8ZgWC3Wz7uSjT3qMRQ5XHZ4COgQmsCMJAQ==",
|
||||
"version": "1.0.30001779",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001779.tgz",
|
||||
"integrity": "sha512-U5og2PN7V4DMgF50YPNtnZJGWVLFjjsN3zb6uMT5VGYIewieDj1upwfuVNXf4Kor+89c3iCRJnSzMD5LmTvsfA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
@@ -13178,6 +13178,12 @@
|
||||
"tinymce": "^5.10.4"
|
||||
}
|
||||
},
|
||||
"node_modules/frontend-components-tinymce-advanced-plugins/node_modules/tinymce": {
|
||||
"version": "5.10.9",
|
||||
"resolved": "https://registry.npmjs.org/tinymce/-/tinymce-5.10.9.tgz",
|
||||
"integrity": "sha512-5bkrors87X9LhYX2xq8GgPHrIgJYHl87YNs+kBcjQ5I3CiUgzo/vFcGvT3MZQ9QHsEeYMhYO6a5CLGGffR8hMg==",
|
||||
"license": "LGPL-2.1"
|
||||
},
|
||||
"node_modules/fs-extra": {
|
||||
"version": "9.1.0",
|
||||
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz",
|
||||
@@ -22961,10 +22967,10 @@
|
||||
}
|
||||
},
|
||||
"node_modules/tinymce": {
|
||||
"version": "5.10.9",
|
||||
"resolved": "https://registry.npmjs.org/tinymce/-/tinymce-5.10.9.tgz",
|
||||
"integrity": "sha512-5bkrors87X9LhYX2xq8GgPHrIgJYHl87YNs+kBcjQ5I3CiUgzo/vFcGvT3MZQ9QHsEeYMhYO6a5CLGGffR8hMg==",
|
||||
"license": "LGPL-2.1"
|
||||
"version": "7.9.2",
|
||||
"resolved": "https://registry.npmjs.org/tinymce/-/tinymce-7.9.2.tgz",
|
||||
"integrity": "sha512-zS2gn2CPQmZhUqLzkhwYH+WGsx/DIRY/mS18RVzsIcuQg2lN2uzaqoHSJU6DdMUpvXBtBFnLNpumC3QrDwLBzA==",
|
||||
"license": "GPL-2.0-or-later"
|
||||
},
|
||||
"node_modules/tmp": {
|
||||
"version": "0.2.5",
|
||||
|
||||
@@ -98,7 +98,7 @@
|
||||
"redux-logger": "^3.0.6",
|
||||
"redux-thunk": "^2.4.1",
|
||||
"reselect": "^4.1.5",
|
||||
"tinymce": "^5.10.4",
|
||||
"tinymce": "^7.0.0",
|
||||
"universal-cookie": "^8.0.0",
|
||||
"uuid": "^11.1.0",
|
||||
"xmlchecker": "^0.1.0",
|
||||
|
||||
@@ -19,7 +19,7 @@ export type OutlineSidebarPages = {
|
||||
align?: SidebarPage;
|
||||
};
|
||||
|
||||
export const getOutlineSidebarPages = () => ({
|
||||
const getOutlineSidebarPages = () => ({
|
||||
info: {
|
||||
component: InfoSidebar,
|
||||
icon: Info,
|
||||
@@ -55,24 +55,24 @@ export const getOutlineSidebarPages = () => ({
|
||||
* export function CourseOutlineSidebarWrapper(
|
||||
* { component, pluginProps }: { component: React.ReactNode, pluginProps: CourseOutlineAspectsPageProps },
|
||||
* ) {
|
||||
* const AnalyticsPage = React.useCallback(() => <CourseOutlineAspectsPage {...pluginProps} />, [pluginProps]);
|
||||
* const sidebarPages = useOutlineSidebarPagesContext();
|
||||
*
|
||||
* const AnalyticsPage = React.useCallback(() => <CourseOutlineAspectsPage {...pluginProps} />, [pluginProps]);
|
||||
* const sidebarPages = useOutlineSidebarPagesContext();
|
||||
* const overridedPages = useMemo(() => ({
|
||||
* ...sidebarPages,
|
||||
* analytics: {
|
||||
* component: AnalyticsPage,
|
||||
* icon: AutoGraph,
|
||||
* title: messages.analyticsLabel,
|
||||
* },
|
||||
* }), [sidebarPages, AnalyticsPage]);
|
||||
*
|
||||
* const overridedPages = useMemo(() => ({
|
||||
* ...sidebarPages,
|
||||
* analytics: {
|
||||
* component: AnalyticsPage,
|
||||
* icon: AutoGraph,
|
||||
* title: messages.analyticsLabel,
|
||||
* },
|
||||
* }), [sidebarPages, AnalyticsPage]);
|
||||
*
|
||||
* return (
|
||||
* <OutlineSidebarPagesContext.Provider value={overridedPages}>
|
||||
* {component}
|
||||
* </OutlineSidebarPagesContext.Provider>
|
||||
*}
|
||||
* return (
|
||||
* <OutlineSidebarPagesContext.Provider value={overridedPages}>
|
||||
* {component}
|
||||
* </OutlineSidebarPagesContext.Provider>
|
||||
* );
|
||||
* }
|
||||
*/
|
||||
export const OutlineSidebarPagesContext = createContext<OutlineSidebarPages | undefined>(undefined);
|
||||
|
||||
@@ -94,6 +94,7 @@ export const OutlineSidebarPagesProvider = ({ children }: OutlineSidebarPagesPro
|
||||
|
||||
export const useOutlineSidebarPagesContext = (): OutlineSidebarPages => {
|
||||
const ctx = useContext(OutlineSidebarPagesContext);
|
||||
// istanbul ignore if: this should never happen
|
||||
if (ctx === undefined) { throw new Error('useOutlineSidebarPages must be used within an OutlineSidebarPagesProvider'); }
|
||||
return ctx;
|
||||
};
|
||||
|
||||
@@ -122,10 +122,10 @@ jest.mock('@src/studio-home/hooks', () => ({
|
||||
* This can be used to mimic events like deletion or other actions
|
||||
* sent from Backbone or other sources via postMessage.
|
||||
*
|
||||
* @param {string} type - The type of the message event (e.g., 'deleteXBlock').
|
||||
* @param {Object} payload - The payload data for the message event.
|
||||
* @param type - The type of the message event (e.g., 'deleteXBlock').
|
||||
* @param payload - The payload data for the message event.
|
||||
*/
|
||||
function simulatePostMessageEvent(type, payload) {
|
||||
function simulatePostMessageEvent(type: string, payload?: object) {
|
||||
const messageEvent = new MessageEvent('message', {
|
||||
data: { type, payload },
|
||||
});
|
||||
@@ -331,7 +331,7 @@ describe('<CourseUnit />', () => {
|
||||
expect(iframe).toHaveAttribute(
|
||||
'aria-label',
|
||||
xblockContainerIframeMessages.xblockIframeLabel.defaultMessage
|
||||
.replace('{xblockCount}', courseVerticalChildrenMock.children.length),
|
||||
.replace('{xblockCount}', courseVerticalChildrenMock.children.length.toString()),
|
||||
);
|
||||
|
||||
simulatePostMessageEvent(messageTypes.deleteXBlock, {
|
||||
@@ -422,7 +422,7 @@ describe('<CourseUnit />', () => {
|
||||
)).toHaveAttribute(
|
||||
'aria-label',
|
||||
xblockContainerIframeMessages.xblockIframeLabel.defaultMessage
|
||||
.replace('{xblockCount}', updatedCourseVerticalChildren.length),
|
||||
.replace('{xblockCount}', updatedCourseVerticalChildren.length.toString()),
|
||||
);
|
||||
// after removing the xblock, the sidebar status changes to Draft (unpublished changes)
|
||||
expect(await screen.findByText(
|
||||
@@ -485,10 +485,7 @@ describe('<CourseUnit />', () => {
|
||||
});
|
||||
|
||||
axiosMock
|
||||
.onPost(postXBlockBaseApiUrl({
|
||||
parent_locator: blockId,
|
||||
duplicate_source_locator: courseVerticalChildrenMock.children[0].block_id,
|
||||
}))
|
||||
.onPost(postXBlockBaseApiUrl())
|
||||
.replyOnce(200, { locator: '1234567890' });
|
||||
|
||||
const updatedCourseVerticalChildren = [
|
||||
@@ -520,7 +517,7 @@ describe('<CourseUnit />', () => {
|
||||
expect(iframe).toHaveAttribute(
|
||||
'aria-label',
|
||||
xblockContainerIframeMessages.xblockIframeLabel.defaultMessage
|
||||
.replace('{xblockCount}', courseVerticalChildrenMock.children.length),
|
||||
.replace('{xblockCount}', courseVerticalChildrenMock.children.length.toString()),
|
||||
);
|
||||
|
||||
simulatePostMessageEvent(messageTypes.duplicateXBlock, {
|
||||
@@ -566,7 +563,7 @@ describe('<CourseUnit />', () => {
|
||||
expect(xblockIframe).toHaveAttribute(
|
||||
'aria-label',
|
||||
xblockContainerIframeMessages.xblockIframeLabel.defaultMessage
|
||||
.replace('{xblockCount}', updatedCourseVerticalChildren.length),
|
||||
.replace('{xblockCount}', updatedCourseVerticalChildren.length.toString()),
|
||||
);
|
||||
|
||||
// after duplicate the xblock, the sidebar status changes to Draft (unpublished changes)
|
||||
@@ -616,16 +613,14 @@ describe('<CourseUnit />', () => {
|
||||
it('checks courseUnit title changing when edit query is successfully', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<RootWrapper />);
|
||||
let editTitleButton = null;
|
||||
let titleEditField = null;
|
||||
const newDisplayName = `${unitDisplayName} new`;
|
||||
|
||||
axiosMock
|
||||
.onPost(getXBlockBaseApiUrl(blockId, {
|
||||
.onPost(getXBlockBaseApiUrl(blockId), {
|
||||
metadata: {
|
||||
display_name: newDisplayName,
|
||||
},
|
||||
}))
|
||||
})
|
||||
.reply(200, { dummy: 'value' });
|
||||
axiosMock
|
||||
.onGet(getCourseSectionVerticalApiUrl(blockId))
|
||||
@@ -634,7 +629,6 @@ describe('<CourseUnit />', () => {
|
||||
xblock_info: {
|
||||
...courseSectionVerticalMock.xblock_info,
|
||||
metadata: {
|
||||
...courseSectionVerticalMock.xblock_info.metadata,
|
||||
display_name: newDisplayName,
|
||||
},
|
||||
},
|
||||
@@ -653,15 +647,14 @@ describe('<CourseUnit />', () => {
|
||||
},
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
const unitHeaderTitle = screen.getByTestId('unit-header-title');
|
||||
editTitleButton = within(unitHeaderTitle)
|
||||
.getByRole('button', { name: headerTitleMessages.altButtonEdit.defaultMessage });
|
||||
titleEditField = within(unitHeaderTitle)
|
||||
.queryByRole('textbox', { name: headerTitleMessages.ariaLabelButtonEdit.defaultMessage });
|
||||
});
|
||||
const unitHeaderTitle = await screen.findByTestId('unit-header-title');
|
||||
const editTitleButton = within(unitHeaderTitle)
|
||||
.getByRole('button', { name: headerTitleMessages.altButtonEdit.defaultMessage });
|
||||
let titleEditField = within(unitHeaderTitle)
|
||||
.queryByRole('textbox', { name: headerTitleMessages.ariaLabelButtonEdit.defaultMessage });
|
||||
expect(titleEditField).not.toBeInTheDocument();
|
||||
await user.click(editTitleButton);
|
||||
|
||||
titleEditField = screen.getByRole('textbox', { name: headerTitleMessages.ariaLabelButtonEdit.defaultMessage });
|
||||
|
||||
await user.clear(titleEditField);
|
||||
@@ -680,7 +673,7 @@ describe('<CourseUnit />', () => {
|
||||
const user = userEvent.setup();
|
||||
const { courseKey, locator } = courseCreateXblockMock;
|
||||
axiosMock
|
||||
.onPost(postXBlockBaseApiUrl({ type: 'video', category: 'video', parentLocator: blockId }))
|
||||
.onPost(postXBlockBaseApiUrl(), { type: 'video', category: 'video', parent_locator: blockId })
|
||||
.reply(500, {});
|
||||
render(<RootWrapper />);
|
||||
|
||||
@@ -695,7 +688,7 @@ describe('<CourseUnit />', () => {
|
||||
it('handle creating Problem xblock and showing editor modal', async () => {
|
||||
const user = userEvent.setup();
|
||||
axiosMock
|
||||
.onPost(postXBlockBaseApiUrl({ type: 'problem', category: 'problem', parentLocator: blockId }))
|
||||
.onPost(postXBlockBaseApiUrl(), { type: 'problem', category: 'problem', parent_locator: blockId })
|
||||
.reply(200, courseCreateXblockMock);
|
||||
render(<RootWrapper />);
|
||||
|
||||
@@ -759,17 +752,17 @@ describe('<CourseUnit />', () => {
|
||||
it('correct addition of a new course unit after click on the "Add new unit" button', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<RootWrapper />);
|
||||
let units = null;
|
||||
let units: HTMLElement[] | null = null;
|
||||
const updatedCourseSectionVerticalData = cloneDeep(courseSectionVerticalMock);
|
||||
const updatedAncestorsChild = updatedCourseSectionVerticalData.xblock_info.ancestor_info.ancestors[0];
|
||||
set(updatedCourseSectionVerticalData, 'xblock_info.ancestor_info.ancestors[0].child_info.children', [
|
||||
...updatedAncestorsChild.child_info.children,
|
||||
...updatedAncestorsChild.child_info!.children,
|
||||
courseUnitMock,
|
||||
]);
|
||||
|
||||
await waitFor(async () => {
|
||||
units = screen.getAllByTestId('course-unit-btn');
|
||||
const courseUnits = courseSectionVerticalMock.xblock_info.ancestor_info.ancestors[0].child_info.children;
|
||||
const courseUnits = courseSectionVerticalMock.xblock_info.ancestor_info.ancestors[0].child_info!.children;
|
||||
expect(units).toHaveLength(courseUnits.length);
|
||||
});
|
||||
|
||||
@@ -788,7 +781,7 @@ describe('<CourseUnit />', () => {
|
||||
const addNewUnitBtn = screen.getByRole('button', { name: courseSequenceMessages.newUnitBtnText.defaultMessage });
|
||||
units = screen.getAllByTestId('course-unit-btn');
|
||||
const updatedCourseUnits = updatedCourseSectionVerticalData
|
||||
.xblock_info.ancestor_info.ancestors[0].child_info.children;
|
||||
.xblock_info.ancestor_info.ancestors[0].child_info!.children;
|
||||
|
||||
await user.click(addNewUnitBtn);
|
||||
expect(units.length).toEqual(updatedCourseUnits.length);
|
||||
@@ -826,18 +819,18 @@ describe('<CourseUnit />', () => {
|
||||
const updatedCourseSectionVerticalData = cloneDeep(courseSectionVerticalMock);
|
||||
const updatedAncestorsChild = updatedCourseSectionVerticalData.xblock_info.ancestor_info.ancestors[0];
|
||||
set(updatedCourseSectionVerticalData, 'xblock_info.ancestor_info.ancestors[0].child_info.children', [
|
||||
...updatedAncestorsChild.child_info.children,
|
||||
...updatedAncestorsChild.child_info!.children,
|
||||
courseUnitMock,
|
||||
]);
|
||||
|
||||
const newDisplayName = `${unitDisplayName} new`;
|
||||
|
||||
axiosMock
|
||||
.onPost(getXBlockBaseApiUrl(blockId, {
|
||||
.onPost(getXBlockBaseApiUrl(blockId), {
|
||||
metadata: {
|
||||
display_name: newDisplayName,
|
||||
},
|
||||
}))
|
||||
})
|
||||
.reply(200, { dummy: 'value' })
|
||||
.onGet(getCourseSectionVerticalApiUrl(blockId))
|
||||
.reply(200, {
|
||||
@@ -845,7 +838,6 @@ describe('<CourseUnit />', () => {
|
||||
xblock_info: {
|
||||
...courseSectionVerticalMock.xblock_info,
|
||||
metadata: {
|
||||
...courseSectionVerticalMock.xblock_info.metadata,
|
||||
display_name: newDisplayName,
|
||||
},
|
||||
},
|
||||
@@ -879,7 +871,7 @@ describe('<CourseUnit />', () => {
|
||||
const waffleSpy = mockWaffleFlags({ useVideoGalleryFlow: true });
|
||||
|
||||
axiosMock
|
||||
.onPost(postXBlockBaseApiUrl({ type: 'video', category: 'video', parentLocator: blockId }))
|
||||
.onPost(postXBlockBaseApiUrl(), { type: 'video', category: 'video', parent_locator: blockId })
|
||||
.reply(200, courseCreateXblockMock);
|
||||
render(<RootWrapper />);
|
||||
|
||||
@@ -950,12 +942,13 @@ describe('<CourseUnit />', () => {
|
||||
.replace('{sectionName}', courseSectionVerticalMock.xblock_info.release_date_from),
|
||||
)).toBeInTheDocument();
|
||||
expect(screen.getByRole('heading', { name: /add video to your course/i, hidden: true })).toBeInTheDocument();
|
||||
|
||||
waffleSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('handles creating Video xblock and showing editor modal', async () => {
|
||||
axiosMock
|
||||
.onPost(postXBlockBaseApiUrl({ type: 'video', category: 'video', parentLocator: blockId }))
|
||||
.onPost(postXBlockBaseApiUrl(), { type: 'video', category: 'video', parent_locator: blockId })
|
||||
.reply(200, courseCreateXblockMock);
|
||||
const user = userEvent.setup();
|
||||
render(<RootWrapper />);
|
||||
@@ -1160,11 +1153,12 @@ describe('<CourseUnit />', () => {
|
||||
const modalNotification = screen.getByRole('dialog');
|
||||
const makeVisibilityBtn = within(modalNotification).getByRole('button', { name: unitInfoMessages.modalMakeVisibilityActionButtonText.defaultMessage });
|
||||
const cancelBtn = within(modalNotification).getByRole('button', { name: unitInfoMessages.modalMakeVisibilityCancelButtonText.defaultMessage });
|
||||
const headingElement = within(modalNotification).getByRole('heading', { name: unitInfoMessages.modalMakeVisibilityTitle.defaultMessage, class: 'pgn__modal-title' });
|
||||
const headingElement = within(modalNotification).getByRole('heading', { name: unitInfoMessages.modalMakeVisibilityTitle.defaultMessage });
|
||||
|
||||
expect(makeVisibilityBtn).toBeInTheDocument();
|
||||
expect(cancelBtn).toBeInTheDocument();
|
||||
expect(headingElement).toBeInTheDocument();
|
||||
expect(headingElement).toHaveClass('pgn__modal-title');
|
||||
expect(within(modalNotification)
|
||||
.getByText(unitInfoMessages.modalMakeVisibilityDescription.defaultMessage)).toBeInTheDocument();
|
||||
|
||||
@@ -1252,8 +1246,9 @@ describe('<CourseUnit />', () => {
|
||||
.getByText(unitInfoMessages.modalDiscardUnitChangesDescription.defaultMessage)).toBeInTheDocument();
|
||||
expect(within(modalNotification)
|
||||
.getByText(unitInfoMessages.modalDiscardUnitChangesCancelButtonText.defaultMessage)).toBeInTheDocument();
|
||||
const headingElement = within(modalNotification).getByRole('heading', { name: unitInfoMessages.modalDiscardUnitChangesTitle.defaultMessage, class: 'pgn__modal-title' });
|
||||
const headingElement = within(modalNotification).getByRole('heading', { name: unitInfoMessages.modalDiscardUnitChangesTitle.defaultMessage });
|
||||
expect(headingElement).toBeInTheDocument();
|
||||
expect(headingElement).toHaveClass('pgn__modal-title');
|
||||
const actionBtn = within(modalNotification).getByRole('button', { name: unitInfoMessages.modalDiscardUnitChangesActionButtonText.defaultMessage });
|
||||
expect(actionBtn).toBeInTheDocument();
|
||||
|
||||
@@ -1398,17 +1393,17 @@ describe('<CourseUnit />', () => {
|
||||
await user.click(screen.getByRole('button', { name: legacySidebarMessages.actionButtonCopyUnitTitle.defaultMessage }));
|
||||
await user.click(screen.getByRole('button', { name: courseSequenceMessages.pasteAsNewUnitLink.defaultMessage }));
|
||||
|
||||
let units = null;
|
||||
let units: HTMLElement[] | null = null;
|
||||
const updatedCourseSectionVerticalData = cloneDeep(courseSectionVerticalMock);
|
||||
const updatedAncestorsChild = updatedCourseSectionVerticalData.xblock_info.ancestor_info.ancestors[0];
|
||||
set(updatedCourseSectionVerticalData, 'xblock_info.ancestor_info.ancestors[0].child_info.children', [
|
||||
...updatedAncestorsChild.child_info.children,
|
||||
...updatedAncestorsChild.child_info!.children,
|
||||
courseUnitMock,
|
||||
]);
|
||||
|
||||
await waitFor(() => {
|
||||
units = screen.getAllByTestId('course-unit-btn');
|
||||
const courseUnits = courseSectionVerticalMock.xblock_info.ancestor_info.ancestors[0].child_info.children;
|
||||
const courseUnits = courseSectionVerticalMock.xblock_info.ancestor_info.ancestors[0].child_info!.children;
|
||||
expect(units).toHaveLength(courseUnits.length);
|
||||
});
|
||||
|
||||
@@ -1425,7 +1420,7 @@ describe('<CourseUnit />', () => {
|
||||
|
||||
units = screen.getAllByTestId('course-unit-btn');
|
||||
const updatedCourseUnits = updatedCourseSectionVerticalData
|
||||
.xblock_info.ancestor_info.ancestors[0].child_info.children;
|
||||
.xblock_info.ancestor_info.ancestors[0].child_info!.children;
|
||||
|
||||
expect(units.length).toEqual(updatedCourseUnits.length);
|
||||
expect(mockedUsedNavigate).toHaveBeenCalled();
|
||||
@@ -1459,7 +1454,7 @@ describe('<CourseUnit />', () => {
|
||||
expect(iframe).toHaveAttribute(
|
||||
'aria-label',
|
||||
xblockContainerIframeMessages.xblockIframeLabel.defaultMessage
|
||||
.replace('{xblockCount}', courseVerticalChildrenMock.children.length),
|
||||
.replace('{xblockCount}', courseVerticalChildrenMock.children.length.toString()),
|
||||
);
|
||||
|
||||
simulatePostMessageEvent(messageTypes.copyXBlock, {
|
||||
@@ -1495,7 +1490,7 @@ describe('<CourseUnit />', () => {
|
||||
expect(iframe).toHaveAttribute(
|
||||
'aria-label',
|
||||
xblockContainerIframeMessages.xblockIframeLabel.defaultMessage
|
||||
.replace('{xblockCount}', updatedCourseVerticalChildren.length),
|
||||
.replace('{xblockCount}', updatedCourseVerticalChildren.length.toString()),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1522,12 +1517,12 @@ describe('<CourseUnit />', () => {
|
||||
const updatedCourseSectionVerticalData = cloneDeep(courseSectionVerticalMock);
|
||||
const updatedAncestorsChild = updatedCourseSectionVerticalData.xblock_info.ancestor_info.ancestors[0];
|
||||
set(updatedCourseSectionVerticalData, 'xblock_info.ancestor_info.ancestors[0].child_info.children', [
|
||||
...updatedAncestorsChild.child_info.children,
|
||||
...updatedAncestorsChild.child_info!.children,
|
||||
courseUnitMock,
|
||||
]);
|
||||
|
||||
axiosMock
|
||||
.onPost(postXBlockBaseApiUrl(postXBlockBody))
|
||||
.onPost(postXBlockBaseApiUrl(), postXBlockBody)
|
||||
.reply(200, clipboardMockResponse);
|
||||
axiosMock
|
||||
.onGet(getCourseSectionVerticalApiUrl(blockId))
|
||||
@@ -1575,12 +1570,12 @@ describe('<CourseUnit />', () => {
|
||||
const updatedCourseSectionVerticalData = cloneDeep(courseSectionVerticalMock);
|
||||
const updatedAncestorsChild = updatedCourseSectionVerticalData.xblock_info.ancestor_info.ancestors[0];
|
||||
set(updatedCourseSectionVerticalData, 'xblock_info.ancestor_info.ancestors[0].child_info.children', [
|
||||
...updatedAncestorsChild.child_info.children,
|
||||
...updatedAncestorsChild.child_info!.children,
|
||||
courseUnitMock,
|
||||
]);
|
||||
|
||||
axiosMock
|
||||
.onPost(postXBlockBaseApiUrl(postXBlockBody))
|
||||
.onPost(postXBlockBaseApiUrl(), postXBlockBody)
|
||||
.reply(200, clipboardMockResponse);
|
||||
axiosMock
|
||||
.onGet(getCourseSectionVerticalApiUrl(blockId))
|
||||
@@ -1630,12 +1625,12 @@ describe('<CourseUnit />', () => {
|
||||
const updatedCourseSectionVerticalData = cloneDeep(courseSectionVerticalMock);
|
||||
const updatedAncestorsChild = updatedCourseSectionVerticalData.xblock_info.ancestor_info.ancestors[0];
|
||||
set(updatedCourseSectionVerticalData, 'xblock_info.ancestor_info.ancestors[0].child_info.children', [
|
||||
...updatedAncestorsChild.child_info.children,
|
||||
...updatedAncestorsChild.child_info!.children,
|
||||
courseUnitMock,
|
||||
]);
|
||||
|
||||
axiosMock
|
||||
.onPost(postXBlockBaseApiUrl(postXBlockBody))
|
||||
.onPost(postXBlockBaseApiUrl(), postXBlockBody)
|
||||
.reply(200, clipboardMockResponse);
|
||||
axiosMock
|
||||
.onGet(getCourseSectionVerticalApiUrl(blockId))
|
||||
@@ -1808,7 +1803,7 @@ describe('<CourseUnit />', () => {
|
||||
});
|
||||
|
||||
await waitFor(async () => {
|
||||
const currentUnit = currentSubsection.child_info.children[0];
|
||||
const currentUnit = currentSubsection.child_info!.children[0];
|
||||
const currentUnitItemBtn = screen.getByRole('button', {
|
||||
name: `${currentUnit.display_name} ${moveModalMessages.moveModalOutlineItemViewText.defaultMessage}`,
|
||||
});
|
||||
@@ -1848,13 +1843,13 @@ describe('<CourseUnit />', () => {
|
||||
|
||||
simulatePostMessageEvent(messageTypes.rollbackMovedXBlock, { locator: requestData.sourceLocator });
|
||||
|
||||
const dismissButton = screen.queryByRole('button', {
|
||||
const dismissButton = screen.getByRole('button', {
|
||||
name: /dismiss/i, hidden: true,
|
||||
});
|
||||
const undoButton = screen.queryByRole('button', {
|
||||
const undoButton = screen.getByRole('button', {
|
||||
name: messages.undoMoveButton.defaultMessage, hidden: true,
|
||||
});
|
||||
const newLocationButton = screen.queryByRole('button', {
|
||||
const newLocationButton = screen.getByRole('button', {
|
||||
name: messages.newLocationButton.defaultMessage, hidden: true,
|
||||
});
|
||||
|
||||
@@ -1894,7 +1889,7 @@ describe('<CourseUnit />', () => {
|
||||
callbackFn: requestData.callbackFn,
|
||||
}), store.dispatch);
|
||||
|
||||
const newLocationButton = screen.queryByRole('button', {
|
||||
const newLocationButton = screen.getByRole('button', {
|
||||
name: messages.newLocationButton.defaultMessage, hidden: true,
|
||||
});
|
||||
await user.click(newLocationButton);
|
||||
@@ -2248,6 +2243,7 @@ describe('<CourseUnit />', () => {
|
||||
];
|
||||
|
||||
sidebarContent.forEach(({ query, type, name }) => {
|
||||
// @ts-ignore
|
||||
expect(type ? query(type, { name }) : query(name)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
@@ -2273,10 +2269,10 @@ describe('<CourseUnit />', () => {
|
||||
targetChild.block_id = 'block-v1:OpenedX+L153+3T2023+type@html+block@test123original';
|
||||
|
||||
axiosMock
|
||||
.onPost(postXBlockBaseApiUrl({
|
||||
.onPost(postXBlockBaseApiUrl(), {
|
||||
parent_locator: blockId,
|
||||
duplicate_source_locator: targetChild.block_id,
|
||||
}))
|
||||
})
|
||||
.replyOnce(200, { locator: '1234567890' });
|
||||
|
||||
axiosMock
|
||||
@@ -2973,7 +2969,7 @@ describe('<CourseUnit />', () => {
|
||||
// The Meilisearch client-side API uses fetch, not Axios.
|
||||
fetchMock.mockReset();
|
||||
fetchMock.post(searchEndpoint, (_url, req) => {
|
||||
const requestData = JSON.parse((req.body ?? ''));
|
||||
const requestData = JSON.parse((req.body ?? '') as string);
|
||||
const query = requestData?.queries[0]?.q ?? '';
|
||||
// We have to replace the query (search keywords) in the mock results with the actual query,
|
||||
// because otherwise Instantsearch will update the UI and change the query,
|
||||
@@ -48,6 +48,7 @@ import MoveModal from './move-modal';
|
||||
import IframePreviewLibraryXBlockChanges from './preview-changes';
|
||||
import CourseUnitHeaderActionsSlot from '../plugin-slots/CourseUnitHeaderActionsSlot';
|
||||
import { UnitSidebarProvider } from './unit-sidebar/UnitSidebarContext';
|
||||
import { UnitSidebarPagesProvider } from './unit-sidebar/UnitSidebarPagesContext';
|
||||
import { UNIT_VISIBILITY_STATES } from './constants';
|
||||
import { isUnitPageNewDesignEnabled } from './utils';
|
||||
|
||||
@@ -242,178 +243,180 @@ const CourseUnit = () => {
|
||||
|
||||
return (
|
||||
<UnitSidebarProvider readOnly={readOnly}>
|
||||
<Container fluid className="course-unit px-4">
|
||||
<section className="course-unit-container mb-4 mt-5">
|
||||
<TransitionReplace>
|
||||
{movedXBlockParams.isSuccess ? (
|
||||
<AlertMessage
|
||||
key="xblock-moved-alert"
|
||||
data-testid="xblock-moved-alert"
|
||||
show={movedXBlockParams.isSuccess}
|
||||
variant="success"
|
||||
icon={CheckCircleIcon}
|
||||
title={movedXBlockParams.isUndo
|
||||
? intl.formatMessage(messages.alertMoveCancelTitle)
|
||||
: intl.formatMessage(messages.alertMoveSuccessTitle)}
|
||||
description={movedXBlockParams.isUndo
|
||||
? intl.formatMessage(messages.alertMoveCancelDescription, { title: movedXBlockParams.title })
|
||||
: intl.formatMessage(messages.alertMoveSuccessDescription, { title: movedXBlockParams.title })}
|
||||
aria-hidden={movedXBlockParams.isSuccess}
|
||||
dismissible
|
||||
actions={movedXBlockParams.isUndo ? undefined : [
|
||||
<Button
|
||||
onClick={handleRollbackMovedXBlock}
|
||||
key="xblock-moved-alert-undo-move-button"
|
||||
>
|
||||
{intl.formatMessage(messages.undoMoveButton)}
|
||||
</Button>,
|
||||
<Button
|
||||
onClick={handleNavigateToTargetUnit}
|
||||
key="xblock-moved-alert-new-location-button"
|
||||
>
|
||||
{intl.formatMessage(messages.newLocationButton)}
|
||||
</Button>,
|
||||
]}
|
||||
onClose={handleCloseXBlockMovedAlert}
|
||||
/>
|
||||
) : null}
|
||||
</TransitionReplace>
|
||||
{courseUnit.upstreamInfo?.upstreamLink && (
|
||||
<AlertMessage
|
||||
title={intl.formatMessage(
|
||||
messages.alertLibraryUnitReadOnlyText,
|
||||
{
|
||||
link: (
|
||||
<Alert.Link
|
||||
href={courseUnit.upstreamInfo.upstreamLink}
|
||||
>
|
||||
{intl.formatMessage(messages.alertLibraryUnitReadOnlyLinkText)}
|
||||
</Alert.Link>
|
||||
),
|
||||
},
|
||||
)}
|
||||
variant="info"
|
||||
/>
|
||||
)}
|
||||
<SubHeader
|
||||
hideBorder
|
||||
title={(
|
||||
<HeaderTitle
|
||||
unitTitle={unitTitle}
|
||||
isTitleEditFormOpen={isTitleEditFormOpen}
|
||||
handleTitleEdit={handleTitleEdit}
|
||||
handleTitleEditSubmit={handleTitleEditSubmit}
|
||||
handleConfigureSubmit={handleConfigureSubmit}
|
||||
/>
|
||||
)}
|
||||
breadcrumbs={(
|
||||
<Breadcrumbs
|
||||
courseId={courseId}
|
||||
parentUnitId={sequenceId}
|
||||
/>
|
||||
)}
|
||||
headerActions={(
|
||||
<CourseUnitHeaderActionsSlot
|
||||
category={unitCategory}
|
||||
headerNavigationsActions={headerNavigationsActions}
|
||||
unitTitle={unitTitle}
|
||||
verticalBlocks={courseVerticalChildren.children}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<div className="unit-header-status-bar h5 mt-2 mb-4 font-weight-normal">
|
||||
{isUnitPageNewDesignEnabled() && isUnitVerticalType && (
|
||||
<StatusBar courseUnit={courseUnit} />
|
||||
)}
|
||||
</div>
|
||||
{isUnitVerticalType && (
|
||||
<Sequence
|
||||
courseId={courseId}
|
||||
sequenceId={sequenceId}
|
||||
unitId={blockId}
|
||||
handleCreateNewCourseXBlock={handleCreateNewCourseXBlock}
|
||||
showPasteUnit={showPasteUnit}
|
||||
/>
|
||||
)}
|
||||
<div className="d-flex align-items-baseline">
|
||||
<div className="flex-fill">
|
||||
{currentlyVisibleToStudents && (
|
||||
<UnitSidebarPagesProvider>
|
||||
<Container fluid className="course-unit px-4">
|
||||
<section className="course-unit-container mb-4 mt-5">
|
||||
<TransitionReplace>
|
||||
{movedXBlockParams.isSuccess ? (
|
||||
<AlertMessage
|
||||
className="course-unit__alert"
|
||||
title={intl.formatMessage(messages.alertUnpublishedVersion)}
|
||||
variant="warning"
|
||||
icon={WarningIcon}
|
||||
key="xblock-moved-alert"
|
||||
data-testid="xblock-moved-alert"
|
||||
show={movedXBlockParams.isSuccess}
|
||||
variant="success"
|
||||
icon={CheckCircleIcon}
|
||||
title={movedXBlockParams.isUndo
|
||||
? intl.formatMessage(messages.alertMoveCancelTitle)
|
||||
: intl.formatMessage(messages.alertMoveSuccessTitle)}
|
||||
description={movedXBlockParams.isUndo
|
||||
? intl.formatMessage(messages.alertMoveCancelDescription, { title: movedXBlockParams.title })
|
||||
: intl.formatMessage(messages.alertMoveSuccessDescription, { title: movedXBlockParams.title })}
|
||||
aria-hidden={movedXBlockParams.isSuccess}
|
||||
dismissible
|
||||
actions={movedXBlockParams.isUndo ? undefined : [
|
||||
<Button
|
||||
onClick={handleRollbackMovedXBlock}
|
||||
key="xblock-moved-alert-undo-move-button"
|
||||
>
|
||||
{intl.formatMessage(messages.undoMoveButton)}
|
||||
</Button>,
|
||||
<Button
|
||||
onClick={handleNavigateToTargetUnit}
|
||||
key="xblock-moved-alert-new-location-button"
|
||||
>
|
||||
{intl.formatMessage(messages.newLocationButton)}
|
||||
</Button>,
|
||||
]}
|
||||
onClose={handleCloseXBlockMovedAlert}
|
||||
/>
|
||||
)}
|
||||
{staticFileNotices && (
|
||||
<PasteNotificationAlert
|
||||
staticFileNotices={staticFileNotices}
|
||||
courseId={courseId}
|
||||
/>
|
||||
)}
|
||||
{blockId && (
|
||||
<XBlockContainerIframe
|
||||
courseId={courseId}
|
||||
blockId={blockId}
|
||||
isUnitVerticalType={isUnitVerticalType}
|
||||
unitXBlockActions={unitXBlockActions}
|
||||
courseVerticalChildren={courseVerticalChildren.children}
|
||||
) : null}
|
||||
</TransitionReplace>
|
||||
{courseUnit.upstreamInfo?.upstreamLink && (
|
||||
<AlertMessage
|
||||
title={intl.formatMessage(
|
||||
messages.alertLibraryUnitReadOnlyText,
|
||||
{
|
||||
link: (
|
||||
<Alert.Link
|
||||
href={courseUnit.upstreamInfo.upstreamLink}
|
||||
>
|
||||
{intl.formatMessage(messages.alertLibraryUnitReadOnlyLinkText)}
|
||||
</Alert.Link>
|
||||
),
|
||||
},
|
||||
)}
|
||||
variant="info"
|
||||
/>
|
||||
)}
|
||||
<SubHeader
|
||||
hideBorder
|
||||
title={(
|
||||
<HeaderTitle
|
||||
unitTitle={unitTitle}
|
||||
isTitleEditFormOpen={isTitleEditFormOpen}
|
||||
handleTitleEdit={handleTitleEdit}
|
||||
handleTitleEditSubmit={handleTitleEditSubmit}
|
||||
handleConfigureSubmit={handleConfigureSubmit}
|
||||
/>
|
||||
)}
|
||||
{!readOnly && showPasteXBlock && canPasteComponent && isUnitVerticalType && sharedClipboardData
|
||||
&& /* istanbul ignore next */ (
|
||||
<PasteComponent
|
||||
clipboardData={sharedClipboardData}
|
||||
onClick={
|
||||
/* istanbul ignore next */
|
||||
() => handleCreateNewCourseXBlock({ stagedContent: 'clipboard', parentLocator: blockId })
|
||||
}
|
||||
text={intl.formatMessage(messages.pasteButtonText)}
|
||||
/>
|
||||
)}
|
||||
{!readOnly && blockId && (
|
||||
<AddComponent
|
||||
parentLocator={blockId}
|
||||
isSplitTestType={isSplitTestType}
|
||||
isUnitVerticalType={isUnitVerticalType}
|
||||
isProblemBankType={isProblemBankType}
|
||||
handleCreateNewCourseXBlock={handleCreateNewCourseXBlock}
|
||||
addComponentTemplateData={addComponentTemplateData}
|
||||
breadcrumbs={(
|
||||
<Breadcrumbs
|
||||
courseId={courseId}
|
||||
parentUnitId={sequenceId}
|
||||
/>
|
||||
)}
|
||||
<MoveModal
|
||||
isOpenModal={isMoveModalOpen}
|
||||
openModal={openMoveModal}
|
||||
closeModal={closeMoveModal}
|
||||
courseId={courseId}
|
||||
/>
|
||||
<IframePreviewLibraryXBlockChanges />
|
||||
headerActions={(
|
||||
<CourseUnitHeaderActionsSlot
|
||||
category={unitCategory}
|
||||
headerNavigationsActions={headerNavigationsActions}
|
||||
unitTitle={unitTitle}
|
||||
verticalBlocks={courseVerticalChildren.children}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<div className="unit-header-status-bar h5 mt-2 mb-4 font-weight-normal">
|
||||
{isUnitPageNewDesignEnabled() && isUnitVerticalType && (
|
||||
<StatusBar courseUnit={courseUnit} />
|
||||
)}
|
||||
</div>
|
||||
{!isUnitLegacyLibraryType && (
|
||||
<CourseAuthoringUnitSidebarSlot
|
||||
{isUnitVerticalType && (
|
||||
<Sequence
|
||||
courseId={courseId}
|
||||
blockId={blockId}
|
||||
unitTitle={unitTitle}
|
||||
xBlocks={courseVerticalChildren.children}
|
||||
readOnly={readOnly}
|
||||
isUnitVerticalType={isUnitVerticalType}
|
||||
isSplitTestType={isSplitTestType}
|
||||
sequenceId={sequenceId}
|
||||
unitId={blockId}
|
||||
handleCreateNewCourseXBlock={handleCreateNewCourseXBlock}
|
||||
showPasteUnit={showPasteUnit}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
</Container>
|
||||
<div className="alert-toast">
|
||||
<ProcessingNotification
|
||||
isShow={isShowProcessingNotification}
|
||||
title={processingNotificationTitle}
|
||||
/>
|
||||
<SavingErrorAlert
|
||||
savingStatus={savingStatus}
|
||||
errorMessage={errorMessage}
|
||||
/>
|
||||
</div>
|
||||
<div className="d-flex align-items-baseline">
|
||||
<div className="flex-fill">
|
||||
{currentlyVisibleToStudents && (
|
||||
<AlertMessage
|
||||
className="course-unit__alert"
|
||||
title={intl.formatMessage(messages.alertUnpublishedVersion)}
|
||||
variant="warning"
|
||||
icon={WarningIcon}
|
||||
/>
|
||||
)}
|
||||
{staticFileNotices && (
|
||||
<PasteNotificationAlert
|
||||
staticFileNotices={staticFileNotices}
|
||||
courseId={courseId}
|
||||
/>
|
||||
)}
|
||||
{blockId && (
|
||||
<XBlockContainerIframe
|
||||
courseId={courseId}
|
||||
blockId={blockId}
|
||||
isUnitVerticalType={isUnitVerticalType}
|
||||
unitXBlockActions={unitXBlockActions}
|
||||
courseVerticalChildren={courseVerticalChildren.children}
|
||||
handleConfigureSubmit={handleConfigureSubmit}
|
||||
/>
|
||||
)}
|
||||
{!readOnly && showPasteXBlock && canPasteComponent && isUnitVerticalType && sharedClipboardData
|
||||
&& /* istanbul ignore next */ (
|
||||
<PasteComponent
|
||||
clipboardData={sharedClipboardData}
|
||||
onClick={
|
||||
/* istanbul ignore next */
|
||||
() => handleCreateNewCourseXBlock({ stagedContent: 'clipboard', parentLocator: blockId })
|
||||
}
|
||||
text={intl.formatMessage(messages.pasteButtonText)}
|
||||
/>
|
||||
)}
|
||||
{!readOnly && blockId && (
|
||||
<AddComponent
|
||||
parentLocator={blockId}
|
||||
isSplitTestType={isSplitTestType}
|
||||
isUnitVerticalType={isUnitVerticalType}
|
||||
isProblemBankType={isProblemBankType}
|
||||
handleCreateNewCourseXBlock={handleCreateNewCourseXBlock}
|
||||
addComponentTemplateData={addComponentTemplateData}
|
||||
/>
|
||||
)}
|
||||
<MoveModal
|
||||
isOpenModal={isMoveModalOpen}
|
||||
openModal={openMoveModal}
|
||||
closeModal={closeMoveModal}
|
||||
courseId={courseId}
|
||||
/>
|
||||
<IframePreviewLibraryXBlockChanges />
|
||||
</div>
|
||||
{!isUnitLegacyLibraryType && (
|
||||
<CourseAuthoringUnitSidebarSlot
|
||||
courseId={courseId}
|
||||
blockId={blockId}
|
||||
unitTitle={unitTitle}
|
||||
xBlocks={courseVerticalChildren.children}
|
||||
readOnly={readOnly}
|
||||
isUnitVerticalType={isUnitVerticalType}
|
||||
isSplitTestType={isSplitTestType}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
</Container>
|
||||
<div className="alert-toast">
|
||||
<ProcessingNotification
|
||||
isShow={isShowProcessingNotification}
|
||||
title={processingNotificationTitle}
|
||||
/>
|
||||
<SavingErrorAlert
|
||||
savingStatus={savingStatus}
|
||||
errorMessage={errorMessage}
|
||||
/>
|
||||
</div>
|
||||
</UnitSidebarPagesProvider>
|
||||
</UnitSidebarProvider>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -4,7 +4,8 @@ import classNames from 'classnames';
|
||||
import { breakpoints, useWindowSize } from '@openedx/paragon';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import Loading from '../../generic/Loading';
|
||||
import Loading from '@src/generic/Loading';
|
||||
|
||||
import { RequestStatus } from '../../data/constants';
|
||||
import SequenceNavigation from './sequence-navigation/SequenceNavigation';
|
||||
import messages from './messages';
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { Sidebar } from '@src/generic/sidebar';
|
||||
|
||||
import LegacySidebar, { LegacySidebarProps } from '../legacy-sidebar';
|
||||
import { UnitSidebarPageKeys, useUnitSidebarContext } from './UnitSidebarContext';
|
||||
import { isUnitPageNewDesignEnabled } from '../utils';
|
||||
import { useUnitSidebarPages } from './sidebarPages';
|
||||
import { UnitSidebarPageKeys, useUnitSidebarContext } from './UnitSidebarContext';
|
||||
import { useUnitSidebarPagesContext } from './UnitSidebarPagesContext';
|
||||
|
||||
export type UnitSidebarProps = {
|
||||
legacySidebarProps: LegacySidebarProps,
|
||||
@@ -22,7 +23,7 @@ export const UnitSidebar = ({
|
||||
toggle,
|
||||
} = useUnitSidebarContext();
|
||||
|
||||
const sidebarPages = useUnitSidebarPages();
|
||||
const sidebarPages = useUnitSidebarPagesContext();
|
||||
|
||||
if (!isUnitPageNewDesignEnabled()) {
|
||||
return (
|
||||
|
||||
@@ -94,9 +94,11 @@ export const UnitSidebarProvider = ({
|
||||
);
|
||||
};
|
||||
|
||||
export function useUnitSidebarContext(): UnitSidebarContextData {
|
||||
export function useUnitSidebarContext(raiseError?: true): UnitSidebarContextData;
|
||||
export function useUnitSidebarContext(raiseError?: boolean): UnitSidebarContextData | undefined;
|
||||
export function useUnitSidebarContext(raiseError: boolean = true): UnitSidebarContextData | undefined {
|
||||
const ctx = useContext(UnitSidebarContext);
|
||||
if (ctx === undefined) {
|
||||
if (ctx === undefined && raiseError) {
|
||||
/* istanbul ignore next */
|
||||
throw new Error('useUnitSidebarContext() was used in a component without a <UnitSidebarProvider> ancestor.');
|
||||
}
|
||||
|
||||
104
src/course-unit/unit-sidebar/UnitSidebarPagesContext.tsx
Normal file
104
src/course-unit/unit-sidebar/UnitSidebarPagesContext.tsx
Normal file
@@ -0,0 +1,104 @@
|
||||
import { createContext, useContext, useMemo } from 'react';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { Info, Plus, Tag } from '@openedx/paragon/icons';
|
||||
|
||||
import type { SidebarPage } from '@src/generic/sidebar';
|
||||
|
||||
import { InfoSidebar } from './unit-info/InfoSidebar';
|
||||
import { AddSidebar } from './AddSidebar';
|
||||
import { UnitAlignSidebar } from './UnitAlignSidebar';
|
||||
import { useUnitSidebarContext } from './UnitSidebarContext';
|
||||
import messages from './messages';
|
||||
|
||||
export type UnitSidebarPages = {
|
||||
info: SidebarPage;
|
||||
add?: SidebarPage;
|
||||
align?: SidebarPage;
|
||||
};
|
||||
|
||||
const getUnitSidebarPages = (readOnly: boolean, hasComponentSelected: boolean) => {
|
||||
const showAlignSidebar = getConfig().ENABLE_TAGGING_TAXONOMY_PAGES === 'true';
|
||||
|
||||
return {
|
||||
info: {
|
||||
component: InfoSidebar,
|
||||
icon: Info,
|
||||
title: messages.sidebarButtonInfo,
|
||||
},
|
||||
...(!readOnly && {
|
||||
add: {
|
||||
component: AddSidebar,
|
||||
icon: Plus,
|
||||
title: messages.sidebarButtonAdd,
|
||||
disabled: hasComponentSelected,
|
||||
tooltip: hasComponentSelected ? messages.sidebarDisabledAddTooltip : undefined,
|
||||
},
|
||||
}),
|
||||
...(showAlignSidebar && {
|
||||
align: {
|
||||
component: UnitAlignSidebar,
|
||||
icon: Tag,
|
||||
title: messages.sidebarButtonAlign,
|
||||
},
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Context for the Unit Sidebar Pages.
|
||||
*
|
||||
* This could be used in plugins to add new pages to the sidebar.
|
||||
*
|
||||
* @example
|
||||
*
|
||||
* ```tsx
|
||||
* export function UnitOutlineSidebarWrapper(
|
||||
* { component, pluginProps }: { component: React.ReactNode, pluginProps: UnitOutlineAspectsPageProps},
|
||||
* ) {
|
||||
* const sidebarPages = useUnitSidebarPagesContext();
|
||||
* const AnalyticsPage = useCallback(() => <UnitOutlineAspectsPage {...pluginProps} />, [pluginProps]);
|
||||
*
|
||||
* const overridedPages = useMemo(() => ({
|
||||
* ...sidebarPages,
|
||||
* analytics: {
|
||||
* component: AnalyticsPage,
|
||||
* icon: AutoGraph,
|
||||
* title: messages.analyticsLabel,
|
||||
* },
|
||||
* }), [sidebarPages, AnalyticsPage]);
|
||||
*
|
||||
* return (
|
||||
* <UnitSidebarPagesContext.Provider value={overridedPages}>
|
||||
* {component}
|
||||
* </UnitSidebarPagesContext.Provider>
|
||||
* );
|
||||
* }
|
||||
*/
|
||||
export const UnitSidebarPagesContext = createContext<UnitSidebarPages | undefined>(undefined);
|
||||
|
||||
type UnitSidebarPagesProviderProps = {
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
export const UnitSidebarPagesProvider = ({ children }: UnitSidebarPagesProviderProps) => {
|
||||
const { readOnly, selectedComponentId } = useUnitSidebarContext();
|
||||
|
||||
const hasComponentSelected = selectedComponentId !== undefined;
|
||||
|
||||
const sidebarPages = useMemo(
|
||||
() => getUnitSidebarPages(readOnly, hasComponentSelected),
|
||||
[readOnly, hasComponentSelected],
|
||||
);
|
||||
|
||||
return (
|
||||
<UnitSidebarPagesContext.Provider value={sidebarPages}>
|
||||
{children}
|
||||
</UnitSidebarPagesContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useUnitSidebarPagesContext = (): UnitSidebarPages => {
|
||||
const ctx = useContext(UnitSidebarPagesContext);
|
||||
if (ctx === undefined) { throw new Error('useUnitSidebarPages must be used within an UnitSidebarPagesProvider'); }
|
||||
return ctx;
|
||||
};
|
||||
@@ -1,49 +0,0 @@
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { Info, Tag, Plus } from '@openedx/paragon/icons';
|
||||
import { SidebarPage } from '@src/generic/sidebar';
|
||||
import messages from './messages';
|
||||
import { UnitAlignSidebar } from './UnitAlignSidebar';
|
||||
import { AddSidebar } from './AddSidebar';
|
||||
import { useUnitSidebarContext } from './UnitSidebarContext';
|
||||
import { InfoSidebar } from './unit-info/InfoSidebar';
|
||||
|
||||
export type UnitSidebarPages = {
|
||||
info: SidebarPage;
|
||||
align?: SidebarPage;
|
||||
add?: SidebarPage;
|
||||
};
|
||||
|
||||
/**
|
||||
* Sidebar pages for the unit sidebar
|
||||
*
|
||||
* This has been separated from the context to avoid a cyclical import
|
||||
* if you want to use the context in the sidebar pages.
|
||||
*/
|
||||
export const useUnitSidebarPages = (): UnitSidebarPages => {
|
||||
const showAlignSidebar = getConfig().ENABLE_TAGGING_TAXONOMY_PAGES === 'true';
|
||||
const { readOnly, selectedComponentId } = useUnitSidebarContext();
|
||||
const hasComponentSelected = selectedComponentId !== undefined;
|
||||
return {
|
||||
info: {
|
||||
component: InfoSidebar,
|
||||
icon: Info,
|
||||
title: messages.sidebarButtonInfo,
|
||||
},
|
||||
...(!readOnly && {
|
||||
add: {
|
||||
component: AddSidebar,
|
||||
icon: Plus,
|
||||
title: messages.sidebarButtonAdd,
|
||||
disabled: hasComponentSelected,
|
||||
tooltip: hasComponentSelected ? messages.sidebarDisabledAddTooltip : undefined,
|
||||
},
|
||||
}),
|
||||
...(showAlignSidebar && {
|
||||
align: {
|
||||
component: UnitAlignSidebar,
|
||||
icon: Tag,
|
||||
title: messages.sidebarButtonAlign,
|
||||
},
|
||||
}),
|
||||
};
|
||||
};
|
||||
@@ -205,7 +205,7 @@ export const UnitInfoSidebar = () => {
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<>
|
||||
<SidebarTitle
|
||||
title={currentItemData.displayName}
|
||||
icon={getItemIcon('unit')}
|
||||
@@ -233,6 +233,6 @@ export const UnitInfoSidebar = () => {
|
||||
</div>
|
||||
</Tab>
|
||||
</Tabs>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -58,7 +58,7 @@ const XBlockContainerIframe: FC<XBlockContainerIframeProps> = ({
|
||||
const {
|
||||
setCurrentPageKey,
|
||||
setSelectedComponentId,
|
||||
} = useUnitSidebarContext();
|
||||
} = useUnitSidebarContext(!readonly) || {};
|
||||
|
||||
// Useful to reload iframe
|
||||
const [iframeKey, setIframeKey] = useState(0);
|
||||
@@ -138,7 +138,7 @@ const XBlockContainerIframe: FC<XBlockContainerIframeProps> = ({
|
||||
const onDeleteSubmit = async () => {
|
||||
if (deleteXBlockId) {
|
||||
await unitXBlockActions.handleDelete(deleteXBlockId);
|
||||
setSelectedComponentId(undefined);
|
||||
setSelectedComponentId?.(undefined);
|
||||
closeDeleteModal();
|
||||
}
|
||||
};
|
||||
@@ -192,7 +192,7 @@ const XBlockContainerIframe: FC<XBlockContainerIframeProps> = ({
|
||||
|
||||
const handleOpenManageTagsModal = (id: string) => {
|
||||
if (isUnitPageNewDesignEnabled()) {
|
||||
setCurrentPageKey('align', id);
|
||||
setCurrentPageKey?.('align', id);
|
||||
sendMessageToIframe(messageTypes.selectXblock, { locator: id });
|
||||
} else {
|
||||
// Legacy manage tags modal
|
||||
@@ -219,7 +219,7 @@ const XBlockContainerIframe: FC<XBlockContainerIframeProps> = ({
|
||||
};
|
||||
|
||||
const handleXBlockSelected = (id) => {
|
||||
setCurrentPageKey('info', id);
|
||||
setCurrentPageKey?.('info', id);
|
||||
};
|
||||
|
||||
const messageHandlers = useMessageHandlers({
|
||||
|
||||
@@ -29,20 +29,20 @@ describe('useWaffleFlags', () => {
|
||||
axiosMock.onGet(getApiWaffleFlagsUrl()).reply(() => promise);
|
||||
|
||||
render(<FlagComponent />);
|
||||
expect(screen.getByLabelText('isLoading')).toHaveTextContent('loading');
|
||||
expect(screen.getByLabelText('isError')).toHaveTextContent('false');
|
||||
expect(await screen.findByLabelText('isLoading')).toHaveTextContent('loading');
|
||||
expect(await screen.findByLabelText('isError')).toHaveTextContent('false');
|
||||
// The default should be enabled, even before we hear back from the server:
|
||||
expect(screen.getByLabelText('useNewCourseOutlinePage')).toHaveTextContent('enabled');
|
||||
expect(await screen.findByLabelText('useNewCourseOutlinePage')).toHaveTextContent('enabled');
|
||||
|
||||
// Then, the server responds with a new value:
|
||||
resolveResponse([200, { useNewCourseOutlinePage: false }]);
|
||||
|
||||
// Now, we're no longer loading and we have the new value:
|
||||
await waitFor(() => {
|
||||
expect(screen.getByLabelText('isLoading')).toHaveTextContent('false');
|
||||
await waitFor(async () => {
|
||||
expect(await screen.findByLabelText('isLoading')).toHaveTextContent('false');
|
||||
});
|
||||
expect(screen.getByLabelText('isError')).toHaveTextContent('false');
|
||||
expect(screen.getByLabelText('useNewCourseOutlinePage')).toHaveTextContent('disabled');
|
||||
expect(await screen.findByLabelText('isError')).toHaveTextContent('false');
|
||||
expect(await screen.findByLabelText('useNewCourseOutlinePage')).toHaveTextContent('disabled');
|
||||
});
|
||||
|
||||
it('uses the default values if there\'s an error', async () => {
|
||||
@@ -53,20 +53,20 @@ describe('useWaffleFlags', () => {
|
||||
axiosMock.onGet(getApiWaffleFlagsUrl()).reply(() => promise);
|
||||
|
||||
render(<FlagComponent />);
|
||||
expect(screen.getByLabelText('isLoading')).toHaveTextContent('loading');
|
||||
expect(screen.getByLabelText('isError')).toHaveTextContent('false');
|
||||
expect(await screen.findByLabelText('isLoading')).toHaveTextContent('loading');
|
||||
expect(await screen.findByLabelText('isError')).toHaveTextContent('false');
|
||||
// The default should be enabled, even before we hear back from the server:
|
||||
expect(screen.getByLabelText('useNewCourseOutlinePage')).toHaveTextContent('enabled');
|
||||
expect(await screen.findByLabelText('useNewCourseOutlinePage')).toHaveTextContent('enabled');
|
||||
|
||||
// Then, the server responds with an error
|
||||
resolveResponse([500, {}]);
|
||||
|
||||
// Now, we're no longer loading, we have an error state, and we still have the default value:
|
||||
await waitFor(() => {
|
||||
expect(screen.getByLabelText('isLoading')).toHaveTextContent('false');
|
||||
await waitFor(async () => {
|
||||
expect(await screen.findByLabelText('isLoading')).toHaveTextContent('false');
|
||||
});
|
||||
expect(screen.getByLabelText('isError')).toHaveTextContent('error');
|
||||
expect(screen.getByLabelText('useNewCourseOutlinePage')).toHaveTextContent('enabled');
|
||||
expect(await screen.findByLabelText('isError')).toHaveTextContent('error');
|
||||
expect(await screen.findByLabelText('useNewCourseOutlinePage')).toHaveTextContent('enabled');
|
||||
});
|
||||
|
||||
it('uses the global flag values while loading the course-specific flags', async () => {
|
||||
@@ -81,9 +81,9 @@ describe('useWaffleFlags', () => {
|
||||
|
||||
// Check the global flag:
|
||||
render(<FlagComponent />);
|
||||
await waitFor(() => {
|
||||
await waitFor(async () => {
|
||||
// Once it loads the flags from the server, the global 'false' value will override the default 'true':
|
||||
expect(screen.getByLabelText('useNewCourseOutlinePage')).toHaveTextContent('disabled');
|
||||
expect(await screen.findByLabelText('useNewCourseOutlinePage')).toHaveTextContent('disabled');
|
||||
});
|
||||
|
||||
// Now check the course-specific flag:
|
||||
@@ -91,16 +91,16 @@ describe('useWaffleFlags', () => {
|
||||
render(<FlagComponent courseId={courseId} />);
|
||||
|
||||
// Now, the course-specific value is loading but in the meantime we use the global default:
|
||||
expect(screen.getByLabelText('isLoading')).toHaveTextContent('loading');
|
||||
expect(screen.getByLabelText('isError')).toHaveTextContent('false');
|
||||
expect(screen.getByLabelText('useNewCourseOutlinePage')).toHaveTextContent('disabled');
|
||||
expect(await screen.findByLabelText('isLoading')).toHaveTextContent('loading');
|
||||
expect(await screen.findByLabelText('isError')).toHaveTextContent('false');
|
||||
expect(await screen.findByLabelText('useNewCourseOutlinePage')).toHaveTextContent('disabled');
|
||||
|
||||
// Now the server responds: the course-specific flag is ON:
|
||||
resolveResponse([200, { useNewCourseOutlinePage: true }]);
|
||||
await waitFor(() => {
|
||||
expect(screen.getByLabelText('isLoading')).toHaveTextContent('false');
|
||||
await waitFor(async () => {
|
||||
expect(await screen.findByLabelText('isLoading')).toHaveTextContent('false');
|
||||
});
|
||||
expect(screen.getByLabelText('isError')).toHaveTextContent('false');
|
||||
expect(screen.getByLabelText('useNewCourseOutlinePage')).toHaveTextContent('enabled');
|
||||
expect(await screen.findByLabelText('isError')).toHaveTextContent('false');
|
||||
expect(await screen.findByLabelText('useNewCourseOutlinePage')).toHaveTextContent('enabled');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -144,7 +144,6 @@ export function Sidebar<T extends SidebarPages>({
|
||||
>
|
||||
{Object.entries(pages).map(([key, page]) => {
|
||||
const buttonData = {
|
||||
key,
|
||||
value: key,
|
||||
src: page.icon,
|
||||
alt: intl.formatMessage(page.title),
|
||||
@@ -155,6 +154,7 @@ export function Sidebar<T extends SidebarPages>({
|
||||
if (page.tooltip) {
|
||||
return (
|
||||
<IconButtonWithTooltip
|
||||
key={key}
|
||||
{...buttonData}
|
||||
style={{ pointerEvents: 'all' }}
|
||||
tooltipContent={<div>{intl.formatMessage(page.tooltip)}</div>}
|
||||
|
||||
@@ -27,14 +27,22 @@ interface SidebarContentProps {
|
||||
* </SidebarContent>
|
||||
* ```
|
||||
*/
|
||||
export const SidebarContent = ({ children } : SidebarContentProps) => (
|
||||
<Stack gap={1} className="px-3 py-1">
|
||||
{Array.isArray(children) ? children.map((child, index) => (
|
||||
// eslint-disable-next-line react/no-array-index-key
|
||||
<React.Fragment key={index}>
|
||||
{child}
|
||||
{index !== children.length - 1 && <hr className="w-100" />}
|
||||
</React.Fragment>
|
||||
)) : children}
|
||||
</Stack>
|
||||
);
|
||||
export const SidebarContent = ({ children } : SidebarContentProps) => {
|
||||
// Flatten the array and filter out empty children to correctly render
|
||||
// the hr element between each child.
|
||||
const nonEmptyChildren = Array.isArray(children)
|
||||
? children.flat(Infinity).filter(child => !!child)
|
||||
: [children];
|
||||
|
||||
return (
|
||||
<Stack gap={1} className="px-3 py-1">
|
||||
{nonEmptyChildren.map((child, index) => (
|
||||
// eslint-disable-next-line react/no-array-index-key
|
||||
<React.Fragment key={index}>
|
||||
{child}
|
||||
{index !== nonEmptyChildren.length - 1 && <hr className="w-100" />}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user