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