Compare commits

...

4 Commits

Author SHA1 Message Date
Asad Ali
14e122a672 fix: do not reload multiple tabs on block save (backport #2600) (#2704) 2025-12-01 18:26:06 -05:00
Asad Ali
f459f53343 fix: load sequences in unit page (#1867) (#2424)
This handles loading errors when opening the course unit page via direct link as an unauthorized user.

Co-authored-by: Ihor Romaniuk <ihor.romaniuk@raccoongang.com>
2025-09-08 22:23:06 -07:00
Asad Ali
a5a7d03d12 fix: allow thumbnail upload on Videos page if no thumbnail (#2388) (#2434)
* fix: allow thumbnail upload if no thumbnail

* fix: improve thumbnail upload impl

* test: fix tests

* test: fix tests

* fix: do not show thumbnail upload if not allowed

* test: fix coverage

* test: add thumbnail test

* fix: display thumbnail overlay when video status is success
2025-09-08 20:45:27 -07:00
Chris Chávez
41fc478efe [Teak] style: Fixing nits about sync units [FC-0097] (#2320)
* Add a warning banner about units in the libraries sync page.
* Update the message in the sync unit modal.
* Stay visible the sync icon in the course outline.
* Add a tooltip to the edit (in normal and disabled mode) and sync button.
2025-08-28 11:15:24 -05:00
23 changed files with 398 additions and 1470 deletions

View File

@@ -82,7 +82,7 @@ describe('<CourseLibraries />', () => {
expect(reviewTab).toHaveAttribute('aria-selected', 'true');
userEvent.click(allTab);
const alert = await screen.findByRole('alert');
const alert = (await screen.findAllByRole('alert'))[0];
expect(await within(alert).findByText(
'5 library components are out of sync. Review updates to accept or ignore changes',
)).toBeInTheDocument();
@@ -105,7 +105,7 @@ describe('<CourseLibraries />', () => {
userEvent.click(allTab);
expect(allTab).toHaveAttribute('aria-selected', 'true');
const alert = await screen.findByRole('alert');
const alert = (await screen.findAllByRole('alert'))[0];
expect(await within(alert).findByText(
'5 library components are out of sync. Review updates to accept or ignore changes',
)).toBeInTheDocument();
@@ -133,7 +133,7 @@ describe('<CourseLibraries />', () => {
expect(reviewTab).toHaveAttribute('aria-selected', 'true');
userEvent.click(allTab);
const alert = await screen.findByRole('alert');
const alert = (await screen.findAllByRole('alert'))[0];
expect(await within(alert).findByText(
'5 library components are out of sync. Review updates to accept or ignore changes',
)).toBeInTheDocument();
@@ -156,7 +156,7 @@ describe('<CourseLibraries />', () => {
screen.logTestingPlaygroundURL();
expect(screen.queryByRole('alert')).not.toBeInTheDocument();
expect(screen.queryAllByRole('alert').length).toEqual(1);
});
});

View File

@@ -17,7 +17,7 @@ import {
Tabs,
} from '@openedx/paragon';
import {
Cached, CheckCircle, Launch, Loop,
Cached, CheckCircle, Launch, Loop, Info,
} from '@openedx/paragon/icons';
import sumBy from 'lodash/sumBy';
@@ -33,6 +33,7 @@ import { useStudioHome } from '../studio-home/hooks';
import NewsstandIcon from '../generic/NewsstandIcon';
import ReviewTabContent from './ReviewTabContent';
import { OutOfSyncAlert } from './OutOfSyncAlert';
import AlertMessage from '../generic/alert-message';
interface Props {
courseId: string;
@@ -199,6 +200,12 @@ export const CourseLibraries: React.FC<Props> = ({ courseId }) => {
showAlert={showReviewAlert && tabKey === CourseLibraryTabs.all}
setShowAlert={setShowReviewAlert}
/>
{ /* TODO: Remove this alert after implement container in this page */}
<AlertMessage
title={intl.formatMessage(messages.unitsUpdatesWarning)}
icon={Info}
variant="info"
/>
<SubHeader
title={intl.formatMessage(messages.headingTitle)}
subtitle={intl.formatMessage(messages.headingSubtitle)}

View File

@@ -116,6 +116,11 @@ const messages = defineMessages({
defaultMessage: 'Something went wrong! Could not fetch results.',
description: 'Generic error message displayed when fetching link data fails.',
},
unitsUpdatesWarning: {
id: 'course-authoring.course-libraries.home-tab.warning.units',
defaultMessage: 'Currently this page only tracks component updates. To check for unit updates, go to your Course Outline.',
description: 'Warning message shown in library sync page about units updates.',
},
});
export default messages;

View File

@@ -1,6 +1,7 @@
// @ts-check
import React, { useEffect, useRef, useState } from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { getConfig } from '@edx/frontend-platform';
import { useIntl } from '@edx/frontend-platform/i18n';
import { useSearchParams } from 'react-router-dom';
@@ -10,6 +11,7 @@ import {
Hyperlink,
Icon,
IconButton,
IconButtonWithTooltip,
useToggle,
} from '@openedx/paragon';
import {
@@ -133,19 +135,24 @@ const CardHeader = ({
) : (
<>
{titleComponent}
{readyToSync && (
<IconButton
className="item-card-button-icon"
data-testid={`${namePrefix}-sync-button`}
alt={intl.formatMessage(messages.readyToSyncButtonAlt)}
iconAs={SyncIcon}
onClick={onClickSync}
/>
)}
<IconButton
className="item-card-button-icon"
<IconButtonWithTooltip
className={classNames(
'item-card-button-icon',
{
'item-card-button-icon-disabled': isDisabledEditField,
},
)}
data-testid={`${namePrefix}-edit-button`}
alt={intl.formatMessage(messages.altButtonEdit)}
alt={intl.formatMessage(
isDisabledEditField ? messages.cannotEditTooltip : messages.altButtonRename,
)}
tooltipContent={(
<div>
{intl.formatMessage(
isDisabledEditField ? messages.cannotEditTooltip : messages.altButtonRename,
)}
</div>
)}
iconAs={EditIcon}
onClick={onClickEdit}
// @ts-ignore
@@ -161,6 +168,15 @@ const CardHeader = ({
<TagCount count={contentTagCount} onClick={openManageTagsDrawer} />
)}
{extraActionsComponent}
{readyToSync && (
<IconButtonWithTooltip
data-testid={`${namePrefix}-sync-button`}
alt={intl.formatMessage(messages.readyToSyncButtonAlt)}
iconAs={SyncIcon}
tooltipContent={<div>{intl.formatMessage(messages.readyToSyncButtonAlt)}</div>}
onClick={onClickSync}
/>
)}
<Dropdown data-testid={`${namePrefix}-card-header__menu`} onClick={onClickMenuButton}>
<Dropdown.Toggle
className="item-card-header__menu"

View File

@@ -25,6 +25,12 @@
&:hover {
.item-card-button-icon {
opacity: 1;
&.item-card-button-icon-disabled {
pointer-events: all;
opacity: .5;
cursor: default;
}
}
}
}

View File

@@ -29,9 +29,9 @@ const messages = defineMessages({
id: 'course-authoring.course-outline.card.status-badge.draft-unpublished-changes',
defaultMessage: 'Draft (Unpublished changes)',
},
altButtonEdit: {
altButtonRename: {
id: 'course-authoring.course-outline.card.button.edit.alt',
defaultMessage: 'Edit',
defaultMessage: 'Rename',
},
menuPublish: {
id: 'course-authoring.course-outline.card.menu.publish',
@@ -82,6 +82,11 @@ const messages = defineMessages({
defaultMessage: 'Update available - click to sync',
description: 'Alt text for the sync icon button.',
},
cannotEditTooltip: {
id: 'course-authoring.course-outline.card.button.edit.disable.tooltip',
defaultMessage: 'This object was added from a library, so it cannot be edited.',
description: 'Tooltip text of button when the object was added from a library.',
},
});
export default messages;

View File

@@ -176,7 +176,7 @@ describe('<UnitCard />', () => {
// Should open compare preview modal
expect(screen.getByRole('heading', { name: /preview changes: unit name/i })).toBeInTheDocument();
expect(screen.getByText('Preview not available')).toBeInTheDocument();
expect(screen.getByText('Preview not available for unit changes at this time')).toBeInTheDocument();
// Click on accept changes
const acceptChangesButton = screen.getByText(/accept changes/i);
@@ -196,7 +196,7 @@ describe('<UnitCard />', () => {
// Should open compare preview modal
expect(screen.getByRole('heading', { name: /preview changes: unit name/i })).toBeInTheDocument();
expect(screen.getByText('Preview not available')).toBeInTheDocument();
expect(screen.getByText('Preview not available for unit changes at this time')).toBeInTheDocument();
// Click on ignore changes
const ignoreChangesButton = screen.getByRole('button', { name: /ignore changes/i });

View File

@@ -17,7 +17,6 @@ import { cloneDeep, set } from 'lodash';
import {
getCourseSectionVerticalApiUrl,
getCourseUnitApiUrl,
getCourseVerticalChildrenApiUrl,
getCourseOutlineInfoUrl,
getXBlockBaseApiUrl,
@@ -28,7 +27,6 @@ import {
deleteUnitItemQuery,
editCourseUnitVisibilityAndData,
fetchCourseSectionVerticalData,
fetchCourseUnitQuery,
fetchCourseVerticalChildrenData,
getCourseOutlineInfoQuery,
patchUnitItemQuery,
@@ -37,13 +35,12 @@ import initializeStore from '../store';
import {
courseCreateXblockMock,
courseSectionVerticalMock,
courseUnitIndexMock,
courseUnitMock,
courseVerticalChildrenMock,
clipboardMockResponse,
courseOutlineInfoMock,
} from './__mocks__';
import { clipboardUnit, clipboardXBlock } from '../__mocks__';
import { clipboardUnit } from '../__mocks__';
import { executeThunk } from '../utils';
import { IFRAME_FEATURE_POLICY } from '../constants';
import pasteComponentMessages from '../generic/clipboard/paste-component/messages';
@@ -72,7 +69,8 @@ let store;
let queryClient;
const courseId = '123';
const blockId = '567890';
const unitDisplayName = courseUnitIndexMock.metadata.display_name;
const sequenceId = 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@19a30717eff543078a5d94ae9d6c18a5';
const unitDisplayName = courseSectionVerticalMock.xblock_info.display_name;
const mockedUsedNavigate = jest.fn();
const userName = 'openedx';
const handleConfigureSubmitMock = jest.fn();
@@ -90,7 +88,7 @@ const postXBlockBody = {
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useParams: () => ({ blockId }),
useParams: () => ({ blockId, sequenceId }),
useNavigate: () => mockedUsedNavigate,
}));
@@ -146,14 +144,10 @@ describe('<CourseUnit />', () => {
axiosMock
.onGet(getClipboardUrl())
.reply(200, clipboardUnit);
axiosMock
.onGet(getCourseUnitApiUrl(courseId))
.reply(200, courseUnitIndexMock);
await executeThunk(fetchCourseUnitQuery(courseId), store.dispatch);
axiosMock
.onGet(getCourseSectionVerticalApiUrl(blockId))
.reply(200, courseSectionVerticalMock);
await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch);
await executeThunk(fetchCourseSectionVerticalData(blockId, courseId), store.dispatch);
axiosMock
.onGet(getCourseVerticalChildrenApiUrl(blockId))
.reply(200, courseVerticalChildrenMock);
@@ -168,8 +162,8 @@ describe('<CourseUnit />', () => {
it('render CourseUnit component correctly', async () => {
render(<RootWrapper />);
const currentSectionName = courseUnitIndexMock.ancestor_info.ancestors[1].display_name;
const currentSubSectionName = courseUnitIndexMock.ancestor_info.ancestors[1].display_name;
const currentSectionName = courseSectionVerticalMock.xblock_info.ancestor_info.ancestors[1].display_name;
const currentSubSectionName = courseSectionVerticalMock.xblock_info.ancestor_info.ancestors[1].display_name;
await waitFor(() => {
const unitHeaderTitle = screen.getByTestId('unit-header-title');
@@ -278,11 +272,14 @@ describe('<CourseUnit />', () => {
});
axiosMock
.onGet(getCourseUnitApiUrl(blockId))
.onGet(getCourseSectionVerticalApiUrl(blockId))
.reply(200, {
...courseUnitIndexMock,
has_changes: true,
published_by: userName,
...courseSectionVerticalMock,
xblock_info: {
...courseSectionVerticalMock.xblock_info,
has_changes: true,
published_by: userName,
},
});
await waitFor(() => {
@@ -314,11 +311,14 @@ describe('<CourseUnit />', () => {
});
axiosMock
.onGet(getCourseUnitApiUrl(blockId))
.onGet(getCourseSectionVerticalApiUrl(blockId))
.reply(200, {
...courseUnitIndexMock,
has_changes: true,
published_by: userName,
...courseSectionVerticalMock,
xblock_info: {
...courseSectionVerticalMock.xblock_info,
has_changes: true,
published_by: userName,
},
});
await waitFor(() => {
@@ -381,12 +381,15 @@ describe('<CourseUnit />', () => {
})
.reply(200, { dummy: 'value' });
axiosMock
.onGet(getCourseUnitApiUrl(blockId))
.onGet(getCourseSectionVerticalApiUrl(blockId))
.reply(200, {
...courseUnitIndexMock,
visibility_state: UNIT_VISIBILITY_STATES.live,
has_changes: false,
published_by: userName,
...courseSectionVerticalMock,
xblock_info: {
...courseSectionVerticalMock.xblock_info,
visibility_state: UNIT_VISIBILITY_STATES.live,
has_changes: false,
published_by: userName,
},
});
await executeThunk(editCourseUnitVisibilityAndData(blockId, PUBLISH_TYPES.makePublic, true), store.dispatch);
@@ -395,7 +398,7 @@ describe('<CourseUnit />', () => {
expect(screen.getByText(sidebarMessages.sidebarTitlePublishedAndLive.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(
sidebarMessages.publishLastPublished.defaultMessage
.replace('{publishedOn}', courseUnitIndexMock.published_on)
.replace('{publishedOn}', courseSectionVerticalMock.xblock_info.published_on)
.replace('{publishedBy}', userName),
)).toBeInTheDocument();
expect(screen.queryByRole('button', { name: sidebarMessages.actionButtonPublishTitle.defaultMessage })).not.toBeInTheDocument();
@@ -405,6 +408,9 @@ describe('<CourseUnit />', () => {
axiosMock
.onDelete(getXBlockBaseApiUrl(courseVerticalChildrenMock.children[0].block_id))
.reply(200, { dummy: 'value' });
axiosMock
.onGet(getCourseSectionVerticalApiUrl(courseId))
.reply(200, courseSectionVerticalMock);
await executeThunk(deleteUnitItemQuery(
courseId,
courseVerticalChildrenMock.children[0].block_id,
@@ -425,8 +431,8 @@ describe('<CourseUnit />', () => {
await executeThunk(fetchCourseVerticalChildrenData(blockId), store.dispatch);
axiosMock
.onGet(getCourseUnitApiUrl(blockId))
.reply(200, courseUnitIndexMock);
.onGet(getCourseSectionVerticalApiUrl(blockId))
.reply(200, courseSectionVerticalMock);
await executeThunk(editCourseUnitVisibilityAndData(blockId, PUBLISH_TYPES.makePublic, true), store.dispatch);
await waitFor(() => {
@@ -445,15 +451,15 @@ describe('<CourseUnit />', () => {
expect(screen.getByText(sidebarMessages.visibilityCheckboxTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.actionButtonPublishTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.actionButtonDiscardChangesTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(courseUnitIndexMock.release_date)).toBeInTheDocument();
expect(screen.getByText(courseSectionVerticalMock.xblock_info.release_date)).toBeInTheDocument();
expect(screen.getByText(
sidebarMessages.publishInfoDraftSaved.defaultMessage
.replace('{editedOn}', courseUnitIndexMock.edited_on)
.replace('{editedBy}', courseUnitIndexMock.edited_by),
.replace('{editedOn}', courseSectionVerticalMock.xblock_info.edited_on)
.replace('{editedBy}', courseSectionVerticalMock.xblock_info.edited_by),
)).toBeInTheDocument();
expect(screen.getByText(
sidebarMessages.releaseInfoWithSection.defaultMessage
.replace('{sectionName}', courseUnitIndexMock.release_date_from),
.replace('{sectionName}', courseSectionVerticalMock.xblock_info.release_date_from),
)).toBeInTheDocument();
});
});
@@ -465,10 +471,6 @@ describe('<CourseUnit />', () => {
id: courseVerticalChildrenMock.children[0].block_id,
});
axiosMock
.onGet(getCourseUnitApiUrl(blockId))
.reply(200, courseUnitIndexMock);
axiosMock
.onPost(postXBlockBaseApiUrl({
parent_locator: blockId,
@@ -518,12 +520,15 @@ describe('<CourseUnit />', () => {
})
.reply(200, { dummy: 'value' });
axiosMock
.onGet(getCourseUnitApiUrl(blockId))
.onGet(getCourseSectionVerticalApiUrl(blockId))
.reply(200, {
...courseUnitIndexMock,
visibility_state: UNIT_VISIBILITY_STATES.live,
has_changes: false,
published_by: userName,
...courseSectionVerticalMock,
xblock_info: {
...courseSectionVerticalMock.xblock_info,
visibility_state: UNIT_VISIBILITY_STATES.live,
has_changes: false,
published_by: userName,
},
});
await executeThunk(editCourseUnitVisibilityAndData(blockId, PUBLISH_TYPES.makePublic, true), store.dispatch);
@@ -532,7 +537,7 @@ describe('<CourseUnit />', () => {
expect(screen.getByText(sidebarMessages.sidebarTitlePublishedAndLive.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(
sidebarMessages.publishLastPublished.defaultMessage
.replace('{publishedOn}', courseUnitIndexMock.published_on)
.replace('{publishedOn}', courseSectionVerticalMock.xblock_info.published_on)
.replace('{publishedBy}', userName),
)).toBeInTheDocument();
expect(screen.queryByRole('button', { name: sidebarMessages.actionButtonPublishTitle.defaultMessage })).not.toBeInTheDocument();
@@ -540,8 +545,8 @@ describe('<CourseUnit />', () => {
});
axiosMock
.onGet(getCourseUnitApiUrl(blockId))
.reply(200, courseUnitIndexMock);
.onGet(getCourseSectionVerticalApiUrl(blockId))
.reply(200, courseSectionVerticalMock);
await executeThunk(editCourseUnitVisibilityAndData(blockId, PUBLISH_TYPES.makePublic, true), store.dispatch);
await waitFor(() => {
@@ -561,15 +566,15 @@ describe('<CourseUnit />', () => {
expect(screen.getByText(sidebarMessages.visibilityCheckboxTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.actionButtonPublishTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.actionButtonDiscardChangesTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(courseUnitIndexMock.release_date)).toBeInTheDocument();
expect(screen.getByText(courseSectionVerticalMock.xblock_info.release_date)).toBeInTheDocument();
expect(screen.getByText(
sidebarMessages.publishInfoDraftSaved.defaultMessage
.replace('{editedOn}', courseUnitIndexMock.edited_on)
.replace('{editedBy}', courseUnitIndexMock.edited_by),
.replace('{editedOn}', courseSectionVerticalMock.xblock_info.edited_on)
.replace('{editedBy}', courseSectionVerticalMock.xblock_info.edited_by),
)).toBeInTheDocument();
expect(screen.getByText(
sidebarMessages.releaseInfoWithSection.defaultMessage
.replace('{sectionName}', courseUnitIndexMock.release_date_from),
.replace('{sectionName}', courseSectionVerticalMock.xblock_info.release_date_from),
)).toBeInTheDocument();
});
});
@@ -612,12 +617,15 @@ describe('<CourseUnit />', () => {
}))
.reply(200, { dummy: 'value' });
axiosMock
.onGet(getCourseUnitApiUrl(blockId))
.onGet(getCourseSectionVerticalApiUrl(blockId))
.reply(200, {
...courseUnitIndexMock,
metadata: {
...courseUnitIndexMock.metadata,
display_name: newDisplayName,
...courseSectionVerticalMock,
xblock_info: {
...courseSectionVerticalMock.xblock_info,
metadata: {
...courseSectionVerticalMock.xblock_info.metadata,
display_name: newDisplayName,
},
},
});
axiosMock
@@ -690,12 +698,15 @@ describe('<CourseUnit />', () => {
})
.reply(200, { dummy: 'value' });
axiosMock
.onGet(getCourseUnitApiUrl(blockId))
.onGet(getCourseSectionVerticalApiUrl(blockId))
.reply(200, {
...courseUnitIndexMock,
visibility_state: UNIT_VISIBILITY_STATES.live,
has_changes: false,
published_by: userName,
...courseSectionVerticalMock,
xblock_info: {
...courseSectionVerticalMock.xblock_info,
visibility_state: UNIT_VISIBILITY_STATES.live,
has_changes: false,
published_by: userName,
},
});
await executeThunk(editCourseUnitVisibilityAndData(blockId, PUBLISH_TYPES.makePublic, true), store.dispatch);
@@ -710,8 +721,8 @@ describe('<CourseUnit />', () => {
});
axiosMock
.onGet(getCourseUnitApiUrl(blockId))
.reply(200, courseUnitIndexMock);
.onGet(getCourseSectionVerticalApiUrl(blockId))
.reply(200, courseSectionVerticalMock);
await executeThunk(editCourseUnitVisibilityAndData(blockId, PUBLISH_TYPES.makePublic, true), store.dispatch);
@@ -724,15 +735,15 @@ describe('<CourseUnit />', () => {
expect(screen.getByText(sidebarMessages.visibilityCheckboxTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.actionButtonPublishTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.actionButtonDiscardChangesTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(courseUnitIndexMock.release_date)).toBeInTheDocument();
expect(screen.getByText(courseSectionVerticalMock.xblock_info.release_date)).toBeInTheDocument();
expect(screen.getByText(
sidebarMessages.publishInfoDraftSaved.defaultMessage
.replace('{editedOn}', courseUnitIndexMock.edited_on)
.replace('{editedBy}', courseUnitIndexMock.edited_by),
.replace('{editedOn}', courseSectionVerticalMock.xblock_info.edited_on)
.replace('{editedBy}', courseSectionVerticalMock.xblock_info.edited_by),
)).toBeInTheDocument();
expect(screen.getByText(
sidebarMessages.releaseInfoWithSection.defaultMessage
.replace('{sectionName}', courseUnitIndexMock.release_date_from),
.replace('{sectionName}', courseSectionVerticalMock.xblock_info.release_date_from),
)).toBeInTheDocument();
});
@@ -794,12 +805,15 @@ describe('<CourseUnit />', () => {
},
}))
.reply(200, { dummy: 'value' })
.onGet(getCourseUnitApiUrl(blockId))
.onGet(getCourseSectionVerticalApiUrl(blockId))
.reply(200, {
...courseUnitIndexMock,
metadata: {
...courseUnitIndexMock.metadata,
display_name: newDisplayName,
...courseSectionVerticalMock,
xblock_info: {
...courseSectionVerticalMock.xblock_info,
metadata: {
...courseSectionVerticalMock.xblock_info.metadata,
display_name: newDisplayName,
},
},
})
.onGet(getCourseSectionVerticalApiUrl(blockId))
@@ -844,12 +858,15 @@ describe('<CourseUnit />', () => {
})
.reply(200, { dummy: 'value' });
axiosMock
.onGet(getCourseUnitApiUrl(blockId))
.onGet(getCourseSectionVerticalApiUrl(blockId))
.reply(200, {
...courseUnitIndexMock,
visibility_state: UNIT_VISIBILITY_STATES.live,
has_changes: false,
published_by: userName,
...courseSectionVerticalMock,
xblock_info: {
...courseSectionVerticalMock.xblock_info,
visibility_state: UNIT_VISIBILITY_STATES.live,
has_changes: false,
published_by: userName,
},
});
await executeThunk(editCourseUnitVisibilityAndData(blockId, PUBLISH_TYPES.makePublic, true), store.dispatch);
@@ -861,7 +878,7 @@ describe('<CourseUnit />', () => {
expect(screen.getByText(
sidebarMessages.publishLastPublished.defaultMessage
.replace('{publishedOn}', courseUnitIndexMock.published_on)
.replace('{publishedOn}', courseSectionVerticalMock.xblock_info.published_on)
.replace('{publishedBy}', userName),
)).toBeInTheDocument();
expect(screen.queryByRole('button', { name: sidebarMessages.actionButtonPublishTitle.defaultMessage })).not.toBeInTheDocument();
@@ -874,8 +891,8 @@ describe('<CourseUnit />', () => {
userEvent.click(videoButton);
axiosMock
.onGet(getCourseUnitApiUrl(blockId))
.reply(200, courseUnitIndexMock);
.onGet(getCourseSectionVerticalApiUrl(blockId))
.reply(200, courseSectionVerticalMock);
await executeThunk(editCourseUnitVisibilityAndData(blockId, PUBLISH_TYPES.makePublic, true), store.dispatch);
@@ -888,15 +905,15 @@ describe('<CourseUnit />', () => {
expect(screen.getByText(sidebarMessages.visibilityCheckboxTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.actionButtonPublishTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.actionButtonDiscardChangesTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(courseUnitIndexMock.release_date)).toBeInTheDocument();
expect(screen.getByText(courseSectionVerticalMock.xblock_info.release_date)).toBeInTheDocument();
expect(screen.getByText(
sidebarMessages.publishInfoDraftSaved.defaultMessage
.replace('{editedOn}', courseUnitIndexMock.edited_on)
.replace('{editedBy}', courseUnitIndexMock.edited_by),
.replace('{editedOn}', courseSectionVerticalMock.xblock_info.edited_on)
.replace('{editedBy}', courseSectionVerticalMock.xblock_info.edited_by),
)).toBeInTheDocument();
expect(screen.getByText(
sidebarMessages.releaseInfoWithSection.defaultMessage
.replace('{sectionName}', courseUnitIndexMock.release_date_from),
.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();
@@ -918,12 +935,15 @@ describe('<CourseUnit />', () => {
})
.reply(200, { dummy: 'value' });
axiosMock
.onGet(getCourseUnitApiUrl(blockId))
.onGet(getCourseSectionVerticalApiUrl(blockId))
.reply(200, {
...courseUnitIndexMock,
visibility_state: UNIT_VISIBILITY_STATES.live,
has_changes: false,
published_by: userName,
...courseSectionVerticalMock,
xblock_info: {
...courseSectionVerticalMock.xblock_info,
visibility_state: UNIT_VISIBILITY_STATES.live,
has_changes: false,
published_by: userName,
},
});
await executeThunk(editCourseUnitVisibilityAndData(blockId, PUBLISH_TYPES.makePublic, true), store.dispatch);
@@ -933,7 +953,7 @@ describe('<CourseUnit />', () => {
expect(screen.getByText(sidebarMessages.sidebarTitlePublishedAndLive.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(
sidebarMessages.publishLastPublished.defaultMessage
.replace('{publishedOn}', courseUnitIndexMock.published_on)
.replace('{publishedOn}', courseSectionVerticalMock.xblock_info.published_on)
.replace('{publishedBy}', userName),
)).toBeInTheDocument();
expect(screen.queryByRole('button', { name: sidebarMessages.actionButtonPublishTitle.defaultMessage })).not.toBeInTheDocument();
@@ -953,8 +973,8 @@ describe('<CourseUnit />', () => {
*/
axiosMock
.onGet(getCourseUnitApiUrl(blockId))
.reply(200, courseUnitIndexMock);
.onGet(getCourseSectionVerticalApiUrl(blockId))
.reply(200, courseSectionVerticalMock);
await executeThunk(editCourseUnitVisibilityAndData(blockId, PUBLISH_TYPES.makePublic, true), store.dispatch);
@@ -967,15 +987,15 @@ describe('<CourseUnit />', () => {
expect(screen.getByText(sidebarMessages.visibilityCheckboxTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.actionButtonPublishTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.actionButtonDiscardChangesTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(courseUnitIndexMock.release_date)).toBeInTheDocument();
expect(screen.getByText(courseSectionVerticalMock.xblock_info.release_date)).toBeInTheDocument();
expect(screen.getByText(
sidebarMessages.publishInfoDraftSaved.defaultMessage
.replace('{editedOn}', courseUnitIndexMock.edited_on)
.replace('{editedBy}', courseUnitIndexMock.edited_by),
.replace('{editedOn}', courseSectionVerticalMock.xblock_info.edited_on)
.replace('{editedBy}', courseSectionVerticalMock.xblock_info.edited_by),
)).toBeInTheDocument();
expect(screen.getByText(
sidebarMessages.releaseInfoWithSection.defaultMessage
.replace('{sectionName}', courseUnitIndexMock.release_date_from),
.replace('{sectionName}', courseSectionVerticalMock.xblock_info.release_date_from),
)).toBeInTheDocument();
});
@@ -991,22 +1011,22 @@ describe('<CourseUnit />', () => {
expect(screen.getByText(sidebarMessages.visibilityCheckboxTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.actionButtonPublishTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.actionButtonDiscardChangesTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(courseUnitIndexMock.release_date)).toBeInTheDocument();
expect(screen.getByText(courseSectionVerticalMock.xblock_info.release_date)).toBeInTheDocument();
expect(screen.getByText(
sidebarMessages.publishInfoDraftSaved.defaultMessage
.replace('{editedOn}', courseUnitIndexMock.edited_on)
.replace('{editedBy}', courseUnitIndexMock.edited_by),
.replace('{editedOn}', courseSectionVerticalMock.xblock_info.edited_on)
.replace('{editedBy}', courseSectionVerticalMock.xblock_info.edited_by),
)).toBeInTheDocument();
expect(screen.getByText(
sidebarMessages.releaseInfoWithSection.defaultMessage
.replace('{sectionName}', courseUnitIndexMock.release_date_from),
.replace('{sectionName}', courseSectionVerticalMock.xblock_info.release_date_from),
)).toBeInTheDocument();
});
});
it('renders course unit details in the sidebar', async () => {
render(<RootWrapper />);
const courseUnitLocationId = extractCourseUnitId(courseUnitIndexMock.id);
const courseUnitLocationId = extractCourseUnitId(courseSectionVerticalMock.xblock_info.id);
await waitFor(() => {
expect(screen.getByText(sidebarMessages.sidebarHeaderUnitLocationTitle.defaultMessage)).toBeInTheDocument();
@@ -1033,13 +1053,16 @@ describe('<CourseUnit />', () => {
render(<RootWrapper />);
axiosMock
.onGet(getCourseUnitApiUrl(courseId))
.onGet(getCourseSectionVerticalApiUrl(courseId))
.reply(200, {
...courseUnitIndexMock,
currently_visible_to_students: false,
...courseSectionVerticalMock,
xblock_info: {
...courseSectionVerticalMock.xblock_info,
currently_visible_to_students: false,
},
});
await executeThunk(fetchCourseUnitQuery(courseId), store.dispatch);
await executeThunk(fetchCourseSectionVerticalData(courseId), store.dispatch);
await waitFor(() => {
const alert = screen.queryAllByRole('alert').find(
@@ -1076,11 +1099,14 @@ describe('<CourseUnit />', () => {
})
.reply(200, { dummy: 'value' });
axiosMock
.onGet(getCourseUnitApiUrl(blockId))
.onGet(getCourseSectionVerticalApiUrl(blockId))
.reply(200, {
...courseUnitIndexMock,
visibility_state: UNIT_VISIBILITY_STATES.staffOnly,
has_explicit_staff_lock: true,
...courseSectionVerticalMock,
xblock_info: {
...courseSectionVerticalMock.xblock_info,
visibility_state: UNIT_VISIBILITY_STATES.staffOnly,
has_explicit_staff_lock: true,
},
});
await executeThunk(editCourseUnitVisibilityAndData(blockId, PUBLISH_TYPES.republish, true), store.dispatch);
@@ -1113,8 +1139,8 @@ describe('<CourseUnit />', () => {
})
.reply(200, { dummy: 'value' });
axiosMock
.onGet(getCourseUnitApiUrl(blockId))
.reply(200, courseUnitIndexMock);
.onGet(getCourseSectionVerticalApiUrl(blockId))
.reply(200, courseSectionVerticalMock);
await executeThunk(editCourseUnitVisibilityAndData(blockId, PUBLISH_TYPES.republish, null), store.dispatch);
@@ -1143,12 +1169,15 @@ describe('<CourseUnit />', () => {
})
.reply(200, { dummy: 'value' });
axiosMock
.onGet(getCourseUnitApiUrl(blockId))
.onGet(getCourseSectionVerticalApiUrl(blockId))
.reply(200, {
...courseUnitIndexMock,
visibility_state: UNIT_VISIBILITY_STATES.live,
has_changes: false,
published_by: userName,
...courseSectionVerticalMock,
xblock_info: {
...courseSectionVerticalMock.xblock_info,
visibility_state: UNIT_VISIBILITY_STATES.live,
has_changes: false,
published_by: userName,
},
});
await executeThunk(editCourseUnitVisibilityAndData(blockId, PUBLISH_TYPES.makePublic, true), store.dispatch);
@@ -1157,7 +1186,7 @@ describe('<CourseUnit />', () => {
.getByText(sidebarMessages.sidebarTitlePublishedAndLive.defaultMessage)).toBeInTheDocument();
expect(within(courseUnitSidebar).getByText(
sidebarMessages.publishLastPublished.defaultMessage
.replace('{publishedOn}', courseUnitIndexMock.published_on)
.replace('{publishedOn}', courseSectionVerticalMock.xblock_info.published_on)
.replace('{publishedBy}', userName),
)).toBeInTheDocument();
expect(publishBtn).not.toBeInTheDocument();
@@ -1199,9 +1228,14 @@ describe('<CourseUnit />', () => {
})
.reply(200, { dummy: 'value' });
axiosMock
.onGet(getCourseUnitApiUrl(blockId))
.onGet(getCourseSectionVerticalApiUrl(blockId))
.reply(200, {
...courseUnitIndexMock, published: true, has_changes: false,
...courseSectionVerticalMock,
xblock_info: {
...courseSectionVerticalMock.xblock_info,
published: true,
has_changes: false,
},
});
await executeThunk(editCourseUnitVisibilityAndData(
@@ -1258,17 +1292,20 @@ describe('<CourseUnit />', () => {
});
axiosMock
.onPost(getXBlockBaseApiUrl(courseUnitIndexMock.id), {
.onPost(getXBlockBaseApiUrl(courseSectionVerticalMock.xblock_info.id), {
publish: null,
metadata: { visible_to_staff_only: true, group_access: { 50: [2] }, discussion_enabled: true },
})
.reply(200, { dummy: 'value' });
axiosMock
.onGet(getCourseUnitApiUrl(blockId))
.replyOnce(200, {
...courseUnitIndexMock,
visibility_state: UNIT_VISIBILITY_STATES.staffOnly,
has_explicit_staff_lock: true,
.onGet(getCourseSectionVerticalApiUrl(blockId))
.reply(200, {
...courseSectionVerticalMock,
xblock_info: {
...courseSectionVerticalMock.xblock_info,
visibility_state: UNIT_VISIBILITY_STATES.staffOnly,
has_explicit_staff_lock: true,
},
});
const modalSaveBtn = within(configureModal)
@@ -1307,13 +1344,15 @@ describe('<CourseUnit />', () => {
render(<RootWrapper />);
axiosMock
.onGet(getCourseUnitApiUrl(courseId))
.onGet(getCourseSectionVerticalApiUrl(blockId))
.reply(200, {
...courseUnitIndexMock,
enable_copy_paste_units: true,
...courseSectionVerticalMock,
xblock_info: {
...courseSectionVerticalMock.xblock_info,
enable_copy_paste_units: true,
},
});
await executeThunk(fetchCourseUnitQuery(courseId), store.dispatch);
await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch);
userEvent.click(screen.getByRole('button', { name: sidebarMessages.actionButtonCopyUnitTitle.defaultMessage }));
@@ -1362,20 +1401,17 @@ describe('<CourseUnit />', () => {
});
axiosMock
.onGet(getClipboardUrl())
.reply(200, clipboardXBlock);
axiosMock
.onGet(getCourseUnitApiUrl(courseId))
.onGet(getCourseSectionVerticalApiUrl(blockId))
.reply(200, {
...courseUnitIndexMock,
enable_copy_paste_units: true,
...courseSectionVerticalMock,
xblock_info: {
...courseSectionVerticalMock.xblock_info,
enable_copy_paste_units: true,
},
});
await executeThunk(fetchCourseUnitQuery(courseId), store.dispatch);
await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch);
userEvent.click(screen.getByRole('button', { name: sidebarMessages.actionButtonCopyUnitTitle.defaultMessage }));
userEvent.click(screen.getByRole('button', { name: messages.pasteButtonText.defaultMessage }));
await waitFor(() => {
const iframe = screen.getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
@@ -1427,13 +1463,15 @@ describe('<CourseUnit />', () => {
render(<RootWrapper />);
axiosMock
.onGet(getCourseUnitApiUrl(courseId))
.onGet(getCourseSectionVerticalApiUrl(blockId))
.reply(200, {
...courseUnitIndexMock,
enable_copy_paste_units: true,
...courseSectionVerticalMock,
xblock_info: {
...courseSectionVerticalMock.xblock_info,
enable_copy_paste_units: true,
},
});
await executeThunk(fetchCourseUnitQuery(courseId), store.dispatch);
await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch);
userEvent.click(screen.getByRole('button', { name: sidebarMessages.actionButtonCopyUnitTitle.defaultMessage }));
@@ -1477,13 +1515,15 @@ describe('<CourseUnit />', () => {
render(<RootWrapper />);
axiosMock
.onGet(getCourseUnitApiUrl(courseId))
.onGet(getCourseSectionVerticalApiUrl(blockId))
.reply(200, {
...courseUnitIndexMock,
enable_copy_paste_units: true,
...courseSectionVerticalMock,
xblock_info: {
...courseSectionVerticalMock.xblock_info,
enable_copy_paste_units: true,
},
});
await executeThunk(fetchCourseUnitQuery(courseId), store.dispatch);
await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch);
userEvent.click(screen.getByRole('button', { name: sidebarMessages.actionButtonCopyUnitTitle.defaultMessage }));
@@ -1529,13 +1569,15 @@ describe('<CourseUnit />', () => {
render(<RootWrapper />);
axiosMock
.onGet(getCourseUnitApiUrl(courseId))
.onGet(getCourseSectionVerticalApiUrl(blockId))
.reply(200, {
...courseUnitIndexMock,
enable_copy_paste_units: true,
...courseSectionVerticalMock,
xblock_info: {
...courseSectionVerticalMock.xblock_info,
enable_copy_paste_units: true,
},
});
await executeThunk(fetchCourseUnitQuery(courseId), store.dispatch);
await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch);
userEvent.click(screen.getByRole('button', { name: sidebarMessages.actionButtonCopyUnitTitle.defaultMessage }));
@@ -1687,8 +1729,8 @@ describe('<CourseUnit />', () => {
.reply(200, {});
axiosMock
.onGet(getCourseUnitApiUrl(blockId))
.reply(200, courseUnitIndexMock);
.onGet(getCourseSectionVerticalApiUrl(blockId))
.reply(200, courseSectionVerticalMock);
await screen.findByText(unitDisplayName);
@@ -1970,7 +2012,6 @@ describe('<CourseUnit />', () => {
describe('Library Content page', () => {
const newUnitId = '12345';
const sequenceId = courseSectionVerticalMock.subsection_location;
beforeEach(async () => {
axiosMock
@@ -1987,20 +2028,6 @@ describe('<CourseUnit />', () => {
},
});
await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch);
axiosMock
.onGet(getCourseUnitApiUrl(courseId))
.reply(200, {
...courseUnitIndexMock,
category: 'library_content',
ancestor_info: {
...courseUnitIndexMock.ancestor_info,
child_info: {
...courseUnitIndexMock.ancestor_info.child_info,
category: 'library_content',
},
},
});
await executeThunk(fetchCourseUnitQuery(courseId), store.dispatch);
});
it('navigates to library content page on receive window event', async () => {
@@ -2020,8 +2047,8 @@ describe('<CourseUnit />', () => {
findByTestId,
} = render(<RootWrapper />);
const currentSectionName = courseUnitIndexMock.ancestor_info.ancestors[1].display_name;
const currentSubSectionName = courseUnitIndexMock.ancestor_info.ancestors[1].display_name;
const currentSectionName = courseSectionVerticalMock.xblock_info.ancestor_info.ancestors[1].display_name;
const currentSubSectionName = courseSectionVerticalMock.xblock_info.ancestor_info.ancestors[1].display_name;
const unitHeaderTitle = await findByTestId('unit-header-title');
await findByText(unitDisplayName);
@@ -2049,7 +2076,6 @@ describe('<CourseUnit />', () => {
describe('Split Test Content page', () => {
const newUnitId = '12345';
const sequenceId = courseSectionVerticalMock.subsection_location;
beforeEach(async () => {
axiosMock
@@ -2066,20 +2092,6 @@ describe('<CourseUnit />', () => {
},
});
await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch);
axiosMock
.onGet(getCourseUnitApiUrl(courseId))
.reply(200, {
...courseUnitIndexMock,
category: 'split_test',
ancestor_info: {
...courseUnitIndexMock.ancestor_info,
child_info: {
...courseUnitIndexMock.ancestor_info.child_info,
category: 'split_test',
},
},
});
await executeThunk(fetchCourseUnitQuery(courseId), store.dispatch);
});
it('navigates to split test content page on receive window event', async () => {
@@ -2124,8 +2136,8 @@ describe('<CourseUnit />', () => {
it('should render split test content page correctly', async () => {
render(<RootWrapper />);
const currentSectionName = courseUnitIndexMock.ancestor_info.ancestors[1].display_name;
const currentSubSectionName = courseUnitIndexMock.ancestor_info.ancestors[1].display_name;
const currentSectionName = courseSectionVerticalMock.xblock_info.ancestor_info.ancestors[1].display_name;
const currentSubSectionName = courseSectionVerticalMock.xblock_info.ancestor_info.ancestors[1].display_name;
const helpLinkUrl = 'https://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/latest/developing_course/course_components.html#components-that-contain-other-components';
waitFor(() => {
@@ -2210,10 +2222,6 @@ describe('<CourseUnit />', () => {
? { ...child, block_type: 'html' }
: child));
axiosMock
.onGet(getCourseUnitApiUrl(blockId))
.reply(200, courseUnitIndexMock);
axiosMock
.onPost(postXBlockBaseApiUrl({
parent_locator: blockId,
@@ -2251,15 +2259,19 @@ describe('<CourseUnit />', () => {
render(<RootWrapper />);
axiosMock
.onGet(getCourseUnitApiUrl(courseId))
.onGet(getCourseSectionVerticalApiUrl(courseId))
.reply(200, {
...courseUnitIndexMock,
upstreamInfo: {
upstreamRef: 'lct:org:lib:unit:unit-1',
upstreamLink: 'some-link',
...courseSectionVerticalMock,
xblock_info: {
...courseSectionVerticalMock.xblock_info,
upstreamInfo: {
...courseSectionVerticalMock.xblock_info,
upstreamRef: 'lct:org:lib:unit:unit-1',
upstreamLink: 'some-link',
},
},
});
await executeThunk(fetchCourseUnitQuery(courseId), store.dispatch);
await executeThunk(fetchCourseSectionVerticalData(courseId), store.dispatch);
expect(screen.getByText(/this unit can only be edited from the \./i)).toBeInTheDocument();

View File

@@ -1,1126 +0,0 @@
module.exports = {
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@867dddb6f55d410caaa9c1eb9c6743ec',
display_name: 'Getting Started',
category: 'vertical',
has_children: true,
edited_on: 'Jan 03, 2024 at 12:06 UTC',
published: true,
published_on: 'Dec 28, 2023 at 10:00 UTC',
studio_url: '/container/block-v1:edX+DemoX+Demo_Course+type@vertical+block@867dddb6f55d410caaa9c1eb9c6743ec',
released_to_students: true,
release_date: 'Feb 05, 2013 at 05:00 UTC',
visibility_state: 'needs_attention',
has_explicit_staff_lock: false,
start: '2013-02-05T05:00:00Z',
graded: false,
due_date: '',
due: null,
relative_weeks_due: null,
format: null,
course_graders: [
'Homework',
'Exam',
],
has_changes: true,
actions: {
deletable: true,
draggable: true,
childAddable: true,
duplicable: true,
},
explanatory_message: null,
group_access: {},
user_partitions: [
{
id: 50,
name: 'Enrollment Track Groups',
scheme: 'enrollment_track',
groups: [
{
id: 2,
name: 'Verified Certificate',
selected: false,
deleted: false,
},
{
id: 1,
name: 'Audit',
selected: false,
deleted: false,
},
],
},
],
show_correctness: 'always',
discussion_enabled: true,
data: '',
metadata: {
display_name: 'Getting Started',
xml_attributes: {
filename: [
'vertical/867dddb6f55d410caaa9c1eb9c6743ec.xml',
'vertical/867dddb6f55d410caaa9c1eb9c6743ec.xml',
],
},
},
ancestor_info: {
ancestors: [
{
id: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@19a30717eff543078a5d94ae9d6c18a5',
display_name: 'Lesson 1 - Getting Started',
category: 'sequential',
has_children: true,
edited_on: 'Jan 03, 2024 at 12:06 UTC',
published: true,
published_on: 'Dec 28, 2023 at 10:00 UTC',
studio_url: '/course/course-v1:edX+DemoX+Demo_Course?show=block-v1%3AedX%2BDemoX%2BDemo_Course%2Btype%40sequential%2Bblock%4019a30717eff543078a5d94ae9d6c18a5',
released_to_students: true,
release_date: 'Feb 05, 2013 at 05:00 UTC',
visibility_state: 'needs_attention',
has_explicit_staff_lock: false,
start: '2013-02-05T05:00:00Z',
graded: false,
due_date: '',
due: null,
relative_weeks_due: null,
format: null,
course_graders: [
'Homework',
'Exam',
],
has_changes: null,
actions: {
deletable: true,
draggable: true,
childAddable: true,
duplicable: true,
},
explanatory_message: null,
group_access: {},
user_partitions: [
{
id: 50,
name: 'Enrollment Track Groups',
scheme: 'enrollment_track',
groups: [
{
id: 2,
name: 'Verified Certificate',
selected: false,
deleted: false,
},
{
id: 1,
name: 'Audit',
selected: false,
deleted: false,
},
],
},
],
show_correctness: 'always',
hide_after_due: false,
is_proctored_exam: false,
was_exam_ever_linked_with_external: false,
online_proctoring_rules: '',
is_practice_exam: false,
is_onboarding_exam: false,
is_time_limited: false,
exam_review_rules: '',
default_time_limit_minutes: null,
proctoring_exam_configuration_link: null,
supports_onboarding: false,
show_review_rules: true,
child_info: {
category: 'vertical',
display_name: 'Unit',
children: [
{
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@867dddb6f55d410caaa9c1eb9c6743ec',
display_name: 'Getting Started',
category: 'vertical',
has_children: true,
edited_on: 'Jan 03, 2024 at 12:06 UTC',
published: true,
published_on: 'Dec 28, 2023 at 10:00 UTC',
studio_url: '/container/block-v1:edX+DemoX+Demo_Course+type@vertical+block@867dddb6f55d410caaa9c1eb9c6743ec',
released_to_students: true,
release_date: 'Feb 05, 2013 at 05:00 UTC',
visibility_state: 'needs_attention',
has_explicit_staff_lock: false,
start: '2013-02-05T05:00:00Z',
graded: false,
due_date: '',
due: null,
relative_weeks_due: null,
format: null,
course_graders: [
'Homework',
'Exam',
],
has_changes: true,
actions: {
deletable: true,
draggable: true,
childAddable: true,
duplicable: true,
},
explanatory_message: null,
group_access: {},
user_partitions: [
{
id: 50,
name: 'Enrollment Track Groups',
scheme: 'enrollment_track',
groups: [
{
id: 2,
name: 'Verified Certificate',
selected: false,
deleted: false,
},
{
id: 1,
name: 'Audit',
selected: false,
deleted: false,
},
],
},
],
show_correctness: 'always',
discussion_enabled: true,
ancestor_has_staff_lock: false,
user_partition_info: {
selectable_partitions: [
{
id: 50,
name: 'Enrollment Track Groups',
scheme: 'enrollment_track',
groups: [
{
id: 2,
name: 'Verified Certificate',
selected: false,
deleted: false,
},
{
id: 1,
name: 'Audit',
selected: false,
deleted: false,
},
],
},
],
selected_partition_index: -1,
selected_groups_label: '',
},
enable_copy_paste_units: false,
},
{
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@4f6c1b4e316a419ab5b6bf30e6c708e9',
display_name: 'Working with Videos',
category: 'vertical',
has_children: true,
edited_on: 'Dec 28, 2023 at 10:00 UTC',
published: true,
published_on: 'Dec 28, 2023 at 10:00 UTC',
studio_url: '/container/block-v1:edX+DemoX+Demo_Course+type@vertical+block@4f6c1b4e316a419ab5b6bf30e6c708e9',
released_to_students: true,
release_date: 'Feb 05, 2013 at 05:00 UTC',
visibility_state: 'live',
has_explicit_staff_lock: false,
start: '2013-02-05T05:00:00Z',
graded: false,
due_date: '',
due: null,
relative_weeks_due: null,
format: null,
course_graders: [
'Homework',
'Exam',
],
has_changes: false,
actions: {
deletable: true,
draggable: true,
childAddable: true,
duplicable: true,
},
explanatory_message: null,
group_access: {},
user_partitions: [
{
id: 50,
name: 'Enrollment Track Groups',
scheme: 'enrollment_track',
groups: [
{
id: 2,
name: 'Verified Certificate',
selected: false,
deleted: false,
},
{
id: 1,
name: 'Audit',
selected: false,
deleted: false,
},
],
},
],
show_correctness: 'always',
discussion_enabled: true,
ancestor_has_staff_lock: false,
user_partition_info: {
selectable_partitions: [
{
id: 50,
name: 'Enrollment Track Groups',
scheme: 'enrollment_track',
groups: [
{
id: 2,
name: 'Verified Certificate',
selected: false,
deleted: false,
},
{
id: 1,
name: 'Audit',
selected: false,
deleted: false,
},
],
},
],
selected_partition_index: -1,
selected_groups_label: '',
},
enable_copy_paste_units: false,
},
{
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@3dc16db8d14842e38324e95d4030b8a0',
display_name: 'Videos on edX',
category: 'vertical',
has_children: true,
edited_on: 'Dec 28, 2023 at 10:00 UTC',
published: true,
published_on: 'Dec 28, 2023 at 10:00 UTC',
studio_url: '/container/block-v1:edX+DemoX+Demo_Course+type@vertical+block@3dc16db8d14842e38324e95d4030b8a0',
released_to_students: true,
release_date: 'Feb 05, 2013 at 05:00 UTC',
visibility_state: 'live',
has_explicit_staff_lock: false,
start: '2013-02-05T05:00:00Z',
graded: false,
due_date: '',
due: null,
relative_weeks_due: null,
format: null,
course_graders: [
'Homework',
'Exam',
],
has_changes: false,
actions: {
deletable: true,
draggable: true,
childAddable: true,
duplicable: true,
},
explanatory_message: null,
group_access: {},
user_partitions: [
{
id: 50,
name: 'Enrollment Track Groups',
scheme: 'enrollment_track',
groups: [
{
id: 2,
name: 'Verified Certificate',
selected: false,
deleted: false,
},
{
id: 1,
name: 'Audit',
selected: false,
deleted: false,
},
],
},
],
show_correctness: 'always',
discussion_enabled: true,
ancestor_has_staff_lock: false,
user_partition_info: {
selectable_partitions: [
{
id: 50,
name: 'Enrollment Track Groups',
scheme: 'enrollment_track',
groups: [
{
id: 2,
name: 'Verified Certificate',
selected: false,
deleted: false,
},
{
id: 1,
name: 'Audit',
selected: false,
deleted: false,
},
],
},
],
selected_partition_index: -1,
selected_groups_label: '',
},
enable_copy_paste_units: false,
},
{
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@4a1bba2a403f40bca5ec245e945b0d76',
display_name: 'Video Demonstrations',
category: 'vertical',
has_children: true,
edited_on: 'Dec 28, 2023 at 10:00 UTC',
published: true,
published_on: 'Dec 28, 2023 at 10:00 UTC',
studio_url: '/container/block-v1:edX+DemoX+Demo_Course+type@vertical+block@4a1bba2a403f40bca5ec245e945b0d76',
released_to_students: true,
release_date: 'Feb 05, 2013 at 05:00 UTC',
visibility_state: 'live',
has_explicit_staff_lock: false,
start: '2013-02-05T05:00:00Z',
graded: false,
due_date: '',
due: null,
relative_weeks_due: null,
format: null,
course_graders: [
'Homework',
'Exam',
],
has_changes: false,
actions: {
deletable: true,
draggable: true,
childAddable: true,
duplicable: true,
},
explanatory_message: null,
group_access: {},
user_partitions: [
{
id: 50,
name: 'Enrollment Track Groups',
scheme: 'enrollment_track',
groups: [
{
id: 2,
name: 'Verified Certificate',
selected: false,
deleted: false,
},
{
id: 1,
name: 'Audit',
selected: false,
deleted: false,
},
],
},
],
show_correctness: 'always',
discussion_enabled: true,
ancestor_has_staff_lock: false,
user_partition_info: {
selectable_partitions: [
{
id: 50,
name: 'Enrollment Track Groups',
scheme: 'enrollment_track',
groups: [
{
id: 2,
name: 'Verified Certificate',
selected: false,
deleted: false,
},
{
id: 1,
name: 'Audit',
selected: false,
deleted: false,
},
],
},
],
selected_partition_index: -1,
selected_groups_label: '',
},
enable_copy_paste_units: false,
},
{
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@256f17a44983429fb1a60802203ee4e0',
display_name: 'Video Presentation Styles',
category: 'vertical',
has_children: true,
edited_on: 'Dec 28, 2023 at 10:00 UTC',
published: true,
published_on: 'Dec 28, 2023 at 10:00 UTC',
studio_url: '/container/block-v1:edX+DemoX+Demo_Course+type@vertical+block@256f17a44983429fb1a60802203ee4e0',
released_to_students: true,
release_date: 'Feb 05, 2013 at 05:00 UTC',
visibility_state: 'live',
has_explicit_staff_lock: false,
start: '2013-02-05T05:00:00Z',
graded: false,
due_date: '',
due: null,
relative_weeks_due: null,
format: null,
course_graders: [
'Homework',
'Exam',
],
has_changes: false,
actions: {
deletable: true,
draggable: true,
childAddable: true,
duplicable: true,
},
explanatory_message: null,
group_access: {},
user_partitions: [
{
id: 50,
name: 'Enrollment Track Groups',
scheme: 'enrollment_track',
groups: [
{
id: 2,
name: 'Verified Certificate',
selected: false,
deleted: false,
},
{
id: 1,
name: 'Audit',
selected: false,
deleted: false,
},
],
},
],
show_correctness: 'always',
discussion_enabled: true,
ancestor_has_staff_lock: false,
user_partition_info: {
selectable_partitions: [
{
id: 50,
name: 'Enrollment Track Groups',
scheme: 'enrollment_track',
groups: [
{
id: 2,
name: 'Verified Certificate',
selected: false,
deleted: false,
},
{
id: 1,
name: 'Audit',
selected: false,
deleted: false,
},
],
},
],
selected_partition_index: -1,
selected_groups_label: '',
},
enable_copy_paste_units: false,
},
{
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@e3601c0abee6427d8c17e6d6f8fdddd1',
display_name: 'Interactive Questions',
category: 'vertical',
has_children: true,
edited_on: 'Dec 28, 2023 at 10:00 UTC',
published: true,
published_on: 'Dec 28, 2023 at 10:00 UTC',
studio_url: '/container/block-v1:edX+DemoX+Demo_Course+type@vertical+block@e3601c0abee6427d8c17e6d6f8fdddd1',
released_to_students: true,
release_date: 'Feb 05, 2013 at 05:00 UTC',
visibility_state: 'live',
has_explicit_staff_lock: false,
start: '2013-02-05T05:00:00Z',
graded: false,
due_date: '',
due: null,
relative_weeks_due: null,
format: null,
course_graders: [
'Homework',
'Exam',
],
has_changes: false,
actions: {
deletable: true,
draggable: true,
childAddable: true,
duplicable: true,
},
explanatory_message: null,
group_access: {},
user_partitions: [
{
id: 50,
name: 'Enrollment Track Groups',
scheme: 'enrollment_track',
groups: [
{
id: 2,
name: 'Verified Certificate',
selected: false,
deleted: false,
},
{
id: 1,
name: 'Audit',
selected: false,
deleted: false,
},
],
},
],
show_correctness: 'always',
discussion_enabled: true,
ancestor_has_staff_lock: false,
user_partition_info: {
selectable_partitions: [
{
id: 50,
name: 'Enrollment Track Groups',
scheme: 'enrollment_track',
groups: [
{
id: 2,
name: 'Verified Certificate',
selected: false,
deleted: false,
},
{
id: 1,
name: 'Audit',
selected: false,
deleted: false,
},
],
},
],
selected_partition_index: -1,
selected_groups_label: '',
},
enable_copy_paste_units: false,
},
{
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@a79d59cd72034188a71d388f4954a606',
display_name: 'Exciting Labs and Tools',
category: 'vertical',
has_children: true,
edited_on: 'Dec 28, 2023 at 10:00 UTC',
published: true,
published_on: 'Dec 28, 2023 at 10:00 UTC',
studio_url: '/container/block-v1:edX+DemoX+Demo_Course+type@vertical+block@a79d59cd72034188a71d388f4954a606',
released_to_students: true,
release_date: 'Feb 05, 2013 at 05:00 UTC',
visibility_state: 'live',
has_explicit_staff_lock: false,
start: '2013-02-05T05:00:00Z',
graded: false,
due_date: '',
due: null,
relative_weeks_due: null,
format: null,
course_graders: [
'Homework',
'Exam',
],
has_changes: false,
actions: {
deletable: true,
draggable: true,
childAddable: true,
duplicable: true,
},
explanatory_message: null,
group_access: {},
user_partitions: [
{
id: 50,
name: 'Enrollment Track Groups',
scheme: 'enrollment_track',
groups: [
{
id: 2,
name: 'Verified Certificate',
selected: false,
deleted: false,
},
{
id: 1,
name: 'Audit',
selected: false,
deleted: false,
},
],
},
],
show_correctness: 'always',
discussion_enabled: true,
ancestor_has_staff_lock: false,
user_partition_info: {
selectable_partitions: [
{
id: 50,
name: 'Enrollment Track Groups',
scheme: 'enrollment_track',
groups: [
{
id: 2,
name: 'Verified Certificate',
selected: false,
deleted: false,
},
{
id: 1,
name: 'Audit',
selected: false,
deleted: false,
},
],
},
],
selected_partition_index: -1,
selected_groups_label: '',
},
enable_copy_paste_units: false,
},
{
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@134df56c516a4a0dbb24dd5facef746e',
display_name: 'Reading Assignments',
category: 'vertical',
has_children: true,
edited_on: 'Dec 28, 2023 at 10:00 UTC',
published: true,
published_on: 'Dec 28, 2023 at 10:00 UTC',
studio_url: '/container/block-v1:edX+DemoX+Demo_Course+type@vertical+block@134df56c516a4a0dbb24dd5facef746e',
released_to_students: true,
release_date: 'Feb 05, 2013 at 05:00 UTC',
visibility_state: 'live',
has_explicit_staff_lock: false,
start: '2013-02-05T05:00:00Z',
graded: false,
due_date: '',
due: null,
relative_weeks_due: null,
format: null,
course_graders: [
'Homework',
'Exam',
],
has_changes: false,
actions: {
deletable: true,
draggable: true,
childAddable: true,
duplicable: true,
},
explanatory_message: null,
group_access: {},
user_partitions: [
{
id: 50,
name: 'Enrollment Track Groups',
scheme: 'enrollment_track',
groups: [
{
id: 2,
name: 'Verified Certificate',
selected: false,
deleted: false,
},
{
id: 1,
name: 'Audit',
selected: false,
deleted: false,
},
],
},
],
show_correctness: 'always',
discussion_enabled: true,
ancestor_has_staff_lock: false,
user_partition_info: {
selectable_partitions: [
{
id: 50,
name: 'Enrollment Track Groups',
scheme: 'enrollment_track',
groups: [
{
id: 2,
name: 'Verified Certificate',
selected: false,
deleted: false,
},
{
id: 1,
name: 'Audit',
selected: false,
deleted: false,
},
],
},
],
selected_partition_index: -1,
selected_groups_label: '',
},
enable_copy_paste_units: false,
},
{
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@d91b9e5d8bc64d57a1332d06bf2f2193',
display_name: 'When Are Your Exams? ',
category: 'vertical',
has_children: true,
edited_on: 'Dec 28, 2023 at 10:00 UTC',
published: true,
published_on: 'Dec 28, 2023 at 10:00 UTC',
studio_url: '/container/block-v1:edX+DemoX+Demo_Course+type@vertical+block@d91b9e5d8bc64d57a1332d06bf2f2193',
released_to_students: true,
release_date: 'Feb 05, 2013 at 05:00 UTC',
visibility_state: 'live',
has_explicit_staff_lock: false,
start: '2013-02-05T05:00:00Z',
graded: false,
due_date: '',
due: null,
relative_weeks_due: null,
format: null,
course_graders: [
'Homework',
'Exam',
],
has_changes: false,
actions: {
deletable: true,
draggable: true,
childAddable: true,
duplicable: true,
},
explanatory_message: null,
group_access: {},
user_partitions: [
{
id: 50,
name: 'Enrollment Track Groups',
scheme: 'enrollment_track',
groups: [
{
id: 2,
name: 'Verified Certificate',
selected: false,
deleted: false,
},
{
id: 1,
name: 'Audit',
selected: false,
deleted: false,
},
],
},
],
show_correctness: 'always',
discussion_enabled: true,
ancestor_has_staff_lock: false,
user_partition_info: {
selectable_partitions: [
{
id: 50,
name: 'Enrollment Track Groups',
scheme: 'enrollment_track',
groups: [
{
id: 2,
name: 'Verified Certificate',
selected: false,
deleted: false,
},
{
id: 1,
name: 'Audit',
selected: false,
deleted: false,
},
],
},
],
selected_partition_index: -1,
selected_groups_label: '',
},
enable_copy_paste_units: false,
},
],
},
ancestor_has_staff_lock: false,
user_partition_info: {
selectable_partitions: [
{
id: 50,
name: 'Enrollment Track Groups',
scheme: 'enrollment_track',
groups: [
{
id: 2,
name: 'Verified Certificate',
selected: false,
deleted: false,
},
{
id: 1,
name: 'Audit',
selected: false,
deleted: false,
},
],
},
],
selected_partition_index: -1,
selected_groups_label: '',
},
},
{
id: 'block-v1:edX+DemoX+Demo_Course+type@chapter+block@graded_interactions',
display_name: 'Example Week 1: Getting Started',
category: 'chapter',
has_children: true,
edited_on: 'Jan 03, 2024 at 12:06 UTC',
published: true,
published_on: 'Dec 28, 2023 at 10:00 UTC',
studio_url: '/course/course-v1:edX+DemoX+Demo_Course?show=block-v1%3AedX%2BDemoX%2BDemo_Course%2Btype%40chapter%2Bblock%40interactive_demonstrations',
released_to_students: true,
release_date: 'Feb 05, 2013 at 05:00 UTC',
visibility_state: 'live',
has_explicit_staff_lock: false,
start: '2013-02-05T05:00:00Z',
graded: false,
due_date: '',
due: null,
relative_weeks_due: null,
format: null,
course_graders: [
'Homework',
'Exam',
],
has_changes: null,
actions: {
deletable: true,
draggable: true,
childAddable: true,
duplicable: true,
},
explanatory_message: null,
group_access: {},
user_partitions: [
{
id: 50,
name: 'Enrollment Track Groups',
scheme: 'enrollment_track',
groups: [
{
id: 2,
name: 'Verified Certificate',
selected: false,
deleted: false,
},
{
id: 1,
name: 'Audit',
selected: false,
deleted: false,
},
],
},
],
show_correctness: 'always',
highlights: [],
highlights_enabled: true,
highlights_preview_only: false,
highlights_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/manage_course_highlight_emails.html',
ancestor_has_staff_lock: false,
user_partition_info: {
selectable_partitions: [
{
id: 50,
name: 'Enrollment Track Groups',
scheme: 'enrollment_track',
groups: [
{
id: 2,
name: 'Verified Certificate',
selected: false,
deleted: false,
},
{
id: 1,
name: 'Audit',
selected: false,
deleted: false,
},
],
},
],
selected_partition_index: -1,
selected_groups_label: '',
},
},
{
id: 'block-v1:edX+DemoX+Demo_Course+type@course+block@course',
display_name: 'Demonstration Course',
category: 'course',
has_children: true,
unit_level_discussions: false,
edited_on: 'Jan 03, 2024 at 12:06 UTC',
published: true,
published_on: 'Jan 03, 2024 at 08:57 UTC',
studio_url: '/course/course-v1:edX+DemoX+Demo_Course',
released_to_students: true,
release_date: 'Feb 05, 2013 at 05:00 UTC',
visibility_state: null,
has_explicit_staff_lock: false,
start: '2013-02-05T05:00:00Z',
graded: false,
due_date: '',
due: null,
relative_weeks_due: null,
format: null,
course_graders: [
'Homework',
'Exam',
],
has_changes: null,
actions: {
deletable: true,
draggable: true,
childAddable: true,
duplicable: true,
},
explanatory_message: null,
group_access: {},
user_partitions: [
{
id: 50,
name: 'Enrollment Track Groups',
scheme: 'enrollment_track',
groups: [
{
id: 2,
name: 'Verified Certificate',
selected: false,
deleted: false,
},
{
id: 1,
name: 'Audit',
selected: false,
deleted: false,
},
],
},
],
show_correctness: 'always',
highlights_enabled_for_messaging: false,
highlights_enabled: true,
highlights_preview_only: false,
highlights_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/manage_course_highlight_emails.html',
enable_proctored_exams: false,
create_zendesk_tickets: true,
enable_timed_exams: true,
ancestor_has_staff_lock: false,
user_partition_info: {
selectable_partitions: [
{
id: 50,
name: 'Enrollment Track Groups',
scheme: 'enrollment_track',
groups: [
{
id: 2,
name: 'Verified Certificate',
selected: false,
deleted: false,
},
{
id: 1,
name: 'Audit',
selected: false,
deleted: false,
},
],
},
],
selected_partition_index: -1,
selected_groups_label: '',
},
},
],
},
ancestor_has_staff_lock: false,
user_partition_info: {
selectable_partitions: [
{
id: 50,
name: 'Enrollment Track Groups',
scheme: 'enrollment_track',
groups: [
{
id: 2,
name: 'Verified Certificate',
selected: false,
deleted: false,
},
{
id: 1,
name: 'Audit',
selected: false,
deleted: false,
},
],
},
],
selected_partition_index: -1,
selected_groups_label: '',
},
enable_copy_paste_units: false,
edited_by: 'edx',
published_by: null,
currently_visible_to_students: true,
has_partition_group_components: false,
release_date_from: 'Section "Example Week 1: Getting Started"',
staff_lock_from: null,
upstreamInfo: {
upstreamLink: undefined,
},
};

View File

@@ -1,4 +1,3 @@
export { default as courseUnitIndexMock } from './courseUnitIndex';
export { default as courseSectionVerticalMock } from './courseSectionVertical';
export { default as courseUnitMock } from './courseUnit';
export { default as courseCreateXblockMock } from './courseCreateXblock';

View File

@@ -5,11 +5,11 @@ import {
} from '../../testUtils';
import { executeThunk } from '../../utils';
import { getCourseSectionVerticalApiUrl, getCourseUnitApiUrl } from '../data/api';
import { getCourseSectionVerticalApiUrl } from '../data/api';
import { getApiWaffleFlagsUrl } from '../../data/api';
import { fetchWaffleFlags } from '../../data/thunks';
import { fetchCourseSectionVerticalData, fetchCourseUnitQuery } from '../data/thunk';
import { courseSectionVerticalMock, courseUnitIndexMock } from '../__mocks__';
import { fetchCourseSectionVerticalData } from '../data/thunk';
import { courseSectionVerticalMock } from '../__mocks__';
import Breadcrumbs from './Breadcrumbs';
let axiosMock;
@@ -43,9 +43,9 @@ describe('<Breadcrumbs />', () => {
reduxStore = mocks.reduxStore;
axiosMock
.onGet(getCourseUnitApiUrl(courseId))
.reply(200, courseUnitIndexMock);
await executeThunk(fetchCourseUnitQuery(courseId), reduxStore.dispatch);
.onGet(getCourseSectionVerticalApiUrl(courseId))
.reply(200, courseSectionVerticalMock);
await executeThunk(fetchCourseSectionVerticalData(courseId), reduxStore.dispatch);
axiosMock
.onGet(getCourseSectionVerticalApiUrl(courseId))
.reply(200, courseSectionVerticalMock);

View File

@@ -35,7 +35,7 @@ const SequenceNavigation = ({
const shouldDisplayNotificationTriggerInSequence = useWindowSize().width < breakpoints.small.minWidth;
const renderUnitButtons = () => {
if (sequence?.unitIds?.length === 0 || unitId === null) {
if (sequence.unitIds.length === 0 || unitId === null) {
return (
<div style={{ flexBasis: '100%', minWidth: 0, borderBottom: 'solid 1px #EAEAEA' }} />
);

View File

@@ -7,7 +7,6 @@ import { isUnitReadOnly, normalizeCourseSectionVerticalData, updateXBlockBlockId
const getStudioBaseUrl = () => getConfig().STUDIO_BASE_URL;
export const getCourseUnitApiUrl = (itemId) => `${getStudioBaseUrl()}/xblock/container/${itemId}`;
export const getXBlockBaseApiUrl = (itemId) => `${getStudioBaseUrl()}/xblock/${itemId}`;
export const getCourseSectionVerticalApiUrl = (itemId) => `${getStudioBaseUrl()}/api/contentstore/v1/container_handler/${itemId}`;
export const getCourseVerticalChildrenApiUrl = (itemId) => `${getStudioBaseUrl()}/api/contentstore/v1/container/vertical/${itemId}/children`;
@@ -15,20 +14,6 @@ export const getCourseOutlineInfoUrl = (courseId) => `${getStudioBaseUrl()}/cour
export const postXBlockBaseApiUrl = () => `${getStudioBaseUrl()}/xblock/`;
export const libraryBlockChangesUrl = (blockId) => `${getStudioBaseUrl()}/api/contentstore/v2/downstreams/${blockId}/sync`;
/**
* Get course unit.
* @param {string} unitId
* @returns {Promise<Object>}
*/
export async function getCourseUnitData(unitId) {
const { data } = await getAuthenticatedHttpClient()
.get(getCourseUnitApiUrl(unitId));
const result = camelCaseObject(data);
result.readOnly = isUnitReadOnly(result);
return result;
}
/**
* Edit course unit display name.
* @param {string} unitId
@@ -47,15 +32,18 @@ export async function editUnitDisplayName(unitId, displayName) {
}
/**
* Get an object containing course section vertical data.
* Fetch vertical block data from the container_handler endpoint.
* @param {string} unitId
* @returns {Promise<Object>}
*/
export async function getCourseSectionVerticalData(unitId) {
export async function getVerticalData(unitId) {
const { data } = await getAuthenticatedHttpClient()
.get(getCourseSectionVerticalApiUrl(unitId));
return normalizeCourseSectionVerticalData(data);
const courseSectionVerticalData = normalizeCourseSectionVerticalData(data);
courseSectionVerticalData.xblockInfo.readOnly = isUnitReadOnly(courseSectionVerticalData.xblockInfo);
return courseSectionVerticalData;
}
/**

View File

@@ -1,7 +1,7 @@
import { createSelector } from '@reduxjs/toolkit';
import { RequestStatus } from 'CourseAuthoring/data/constants';
export const getCourseUnitData = (state) => state.courseUnit.unit;
export const getCourseUnitData = (state) => state.courseUnit.courseSectionVertical.xblockInfo ?? {};
export const getCanEdit = (state) => state.courseUnit.canEdit;
export const getStaticFileNotices = (state) => state.courseUnit.staticFileNotices;
export const getCourseUnit = (state) => state.courseUnit;

View File

@@ -12,11 +12,9 @@ const slice = createSlice({
isTitleEditFormOpen: false,
canEdit: true,
loadingStatus: {
fetchUnitLoadingStatus: RequestStatus.IN_PROGRESS,
courseSectionVerticalLoadingStatus: RequestStatus.IN_PROGRESS,
courseVerticalChildrenLoadingStatus: RequestStatus.IN_PROGRESS,
},
unit: {},
courseSectionVertical: {},
courseVerticalChildren: { children: [], isPublished: true },
staticFileNotices: {},
@@ -31,15 +29,6 @@ const slice = createSlice({
},
},
reducers: {
fetchCourseItemSuccess: (state, { payload }) => {
state.unit = payload;
},
updateLoadingCourseUnitStatus: (state, { payload }) => {
state.loadingStatus = {
...state.loadingStatus,
fetchUnitLoadingStatus: payload.status,
};
},
updateQueryPendingStatus: (state, { payload }) => {
state.isQueryPending = payload;
},
@@ -81,12 +70,6 @@ const slice = createSlice({
createUnitXblockLoadingStatus: payload.status,
};
},
addNewUnitStatus: (state, { payload }) => {
state.loadingStatus = {
...state.loadingStatus,
fetchUnitLoadingStatus: payload.status,
};
},
updateCourseVerticalChildren: (state, { payload }) => {
state.courseVerticalChildren = payload;
},
@@ -109,8 +92,6 @@ const slice = createSlice({
});
export const {
fetchCourseItemSuccess,
updateLoadingCourseUnitStatus,
updateSavingStatus,
updateModel,
fetchSequenceRequest,

View File

@@ -10,9 +10,8 @@ import { NOTIFICATION_MESSAGES } from '../../constants';
import { updateModel, updateModels } from '../../generic/model-store';
import { messageTypes } from '../constants';
import {
getCourseUnitData,
editUnitDisplayName,
getCourseSectionVerticalData,
getVerticalData,
createCourseXblock,
getCourseVerticalChildren,
handleCourseUnitVisibilityAndData,
@@ -22,8 +21,6 @@ import {
patchUnitItem,
} from './api';
import {
updateLoadingCourseUnitStatus,
fetchCourseItemSuccess,
updateSavingStatus,
fetchSequenceRequest,
fetchSequenceFailure,
@@ -40,30 +37,13 @@ import {
} from './slice';
import { getNotificationMessage } from './utils';
export function fetchCourseUnitQuery(courseId) {
return async (dispatch) => {
dispatch(updateLoadingCourseUnitStatus({ status: RequestStatus.IN_PROGRESS }));
try {
const courseUnit = await getCourseUnitData(courseId);
dispatch(fetchCourseItemSuccess(courseUnit));
dispatch(updateLoadingCourseUnitStatus({ status: RequestStatus.SUCCESSFUL }));
return true;
} catch (error) {
dispatch(updateLoadingCourseUnitStatus({ status: RequestStatus.FAILED }));
return false;
}
};
}
export function fetchCourseSectionVerticalData(courseId, sequenceId) {
return async (dispatch) => {
dispatch(updateLoadingCourseSectionVerticalDataStatus({ status: RequestStatus.IN_PROGRESS }));
dispatch(fetchSequenceRequest({ sequenceId }));
try {
const courseSectionVerticalData = await getCourseSectionVerticalData(courseId);
const courseSectionVerticalData = await getVerticalData(courseId);
dispatch(fetchCourseSectionVerticalDataSuccess(courseSectionVerticalData));
dispatch(updateLoadingCourseSectionVerticalDataStatus({ status: RequestStatus.SUCCESSFUL }));
dispatch(updateModel({
@@ -94,8 +74,7 @@ export function editCourseItemQuery(itemId, displayName, sequenceId) {
try {
await editUnitDisplayName(itemId, displayName).then(async (result) => {
if (result) {
const courseUnit = await getCourseUnitData(itemId);
const courseSectionVerticalData = await getCourseSectionVerticalData(itemId);
const courseSectionVerticalData = await getVerticalData(itemId);
dispatch(fetchCourseSectionVerticalDataSuccess(courseSectionVerticalData));
dispatch(updateLoadingCourseSectionVerticalDataStatus({ status: RequestStatus.SUCCESSFUL }));
dispatch(updateModel({
@@ -107,7 +86,6 @@ export function editCourseItemQuery(itemId, displayName, sequenceId) {
models: courseSectionVerticalData.units || [],
}));
dispatch(fetchSequenceSuccess({ sequenceId }));
dispatch(fetchCourseItemSuccess(courseUnit));
dispatch(hideProcessingNotification());
dispatch(updateSavingStatus({ status: RequestStatus.SUCCESSFUL }));
}
@@ -146,8 +124,8 @@ export function editCourseUnitVisibilityAndData(
if (callback) {
callback();
}
const courseUnit = await getCourseUnitData(blockId);
dispatch(fetchCourseItemSuccess(courseUnit));
const courseSectionVerticalData = await getVerticalData(blockId);
dispatch(fetchCourseSectionVerticalDataSuccess(courseSectionVerticalData));
const courseVerticalChildrenData = await getCourseVerticalChildren(blockId);
dispatch(updateCourseVerticalChildren(courseVerticalChildrenData));
dispatch(hideProcessingNotification());
@@ -174,7 +152,7 @@ export function createNewCourseXBlock(body, callback, blockId, sendMessageToIfra
if (result) {
const formattedResult = camelCaseObject(result);
if (body.category === 'vertical') {
const courseSectionVerticalData = await getCourseSectionVerticalData(formattedResult.locator);
const courseSectionVerticalData = await getVerticalData(formattedResult.locator);
dispatch(fetchCourseSectionVerticalDataSuccess(courseSectionVerticalData));
}
if (body.stagedContent) {
@@ -194,8 +172,8 @@ export function createNewCourseXBlock(body, callback, blockId, sendMessageToIfra
sendMessageToIframe(messageTypes.addXBlock, { data: result });
}
const currentBlockId = body.category === 'vertical' ? formattedResult.locator : blockId;
const courseUnit = await getCourseUnitData(currentBlockId);
dispatch(fetchCourseItemSuccess(courseUnit));
const courseSectionVerticalData = await getVerticalData(currentBlockId);
dispatch(fetchCourseSectionVerticalDataSuccess(courseSectionVerticalData));
}
});
} catch (error) {
@@ -240,8 +218,8 @@ export function deleteUnitItemQuery(itemId, xblockId, sendMessageToIframe) {
try {
await deleteUnitItem(xblockId);
sendMessageToIframe(messageTypes.completeXBlockDeleting, { locator: xblockId });
const courseUnit = await getCourseUnitData(itemId);
dispatch(fetchCourseItemSuccess(courseUnit));
const courseSectionVerticalData = await getVerticalData(itemId);
dispatch(fetchCourseSectionVerticalDataSuccess(courseSectionVerticalData));
dispatch(hideProcessingNotification());
dispatch(updateSavingStatus({ status: RequestStatus.SUCCESSFUL }));
} catch (error) {
@@ -259,8 +237,8 @@ export function duplicateUnitItemQuery(itemId, xblockId, callback) {
try {
const { courseKey, locator } = await duplicateUnitItem(itemId, xblockId);
callback(courseKey, locator);
const courseUnit = await getCourseUnitData(itemId);
dispatch(fetchCourseItemSuccess(courseUnit));
const courseSectionVerticalData = await getVerticalData(itemId);
dispatch(fetchCourseSectionVerticalDataSuccess(courseSectionVerticalData));
const courseVerticalChildrenData = await getCourseVerticalChildren(itemId);
dispatch(updateCourseVerticalChildren(courseVerticalChildrenData));
dispatch(hideProcessingNotification());
@@ -316,8 +294,8 @@ export function patchUnitItemQuery({
dispatch(updateCourseOutlineInfoLoadingStatus({ status: RequestStatus.IN_PROGRESS }));
callbackFn(sourceLocator);
try {
const courseUnit = await getCourseUnitData(currentParentLocator);
dispatch(fetchCourseItemSuccess(courseUnit));
const courseSectionVerticalData = await getVerticalData(currentParentLocator);
dispatch(fetchCourseSectionVerticalDataSuccess(courseSectionVerticalData));
} catch (error) {
handleResponseErrors(error, dispatch, updateSavingStatus);
}
@@ -335,8 +313,8 @@ export function updateCourseUnitSidebar(itemId) {
dispatch(showProcessingNotification(NOTIFICATION_MESSAGES.saving));
try {
const courseUnit = await getCourseUnitData(itemId);
dispatch(fetchCourseItemSuccess(courseUnit));
const courseSectionVerticalData = await getVerticalData(itemId);
dispatch(fetchCourseSectionVerticalDataSuccess(courseSectionVerticalData));
dispatch(hideProcessingNotification());
dispatch(updateSavingStatus({ status: RequestStatus.SUCCESSFUL }));
} catch (error) {

View File

@@ -8,9 +8,9 @@ import { initializeMockApp } from '@edx/frontend-platform';
import initializeStore from '../../store';
import { executeThunk } from '../../utils';
import { getCourseUnitApiUrl } from '../data/api';
import { fetchCourseUnitQuery } from '../data/thunk';
import { courseUnitIndexMock } from '../__mocks__';
import { getCourseSectionVerticalApiUrl } from '../data/api';
import { fetchCourseSectionVerticalData } from '../data/thunk';
import { courseSectionVerticalMock } from '../__mocks__';
import HeaderTitle from './HeaderTitle';
import messages from './messages';
@@ -52,9 +52,9 @@ describe('<HeaderTitle />', () => {
store = initializeStore();
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
axiosMock
.onGet(getCourseUnitApiUrl(blockId))
.reply(200, courseUnitIndexMock);
await executeThunk(fetchCourseUnitQuery(blockId), store.dispatch);
.onGet(getCourseSectionVerticalApiUrl(blockId))
.reply(200, courseSectionVerticalMock);
await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch);
});
it('render HeaderTitle component correctly', () => {
@@ -80,14 +80,18 @@ describe('<HeaderTitle />', () => {
// Override mock unit with one sourced from an upstream library
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
axiosMock
.onGet(getCourseUnitApiUrl(blockId))
.onGet(getCourseSectionVerticalApiUrl(blockId))
.reply(200, {
...courseUnitIndexMock,
upstreamInfo: {
upstreamRef: 'lct:org:lib:unit:unit-1',
...courseSectionVerticalMock,
xblock_info: {
...courseSectionVerticalMock.xblock_info,
upstreamInfo: {
...courseSectionVerticalMock.xblock_info.upstreamInfo,
upstreamRef: 'lct:org:lib:unit:unit-1',
},
},
});
await executeThunk(fetchCourseUnitQuery(blockId), store.dispatch);
await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch);
const { getByRole } = renderComponent();
@@ -122,16 +126,19 @@ describe('<HeaderTitle />', () => {
it('displays a visibility message with the selected groups for the unit', async () => {
axiosMock
.onGet(getCourseUnitApiUrl(blockId))
.onGet(getCourseSectionVerticalApiUrl(blockId))
.reply(200, {
...courseUnitIndexMock,
user_partition_info: {
...courseUnitIndexMock.user_partition_info,
selected_partition_index: 1,
selected_groups_label: 'Visibility group 1',
...courseSectionVerticalMock,
xblock_info: {
...courseSectionVerticalMock.xblock_info,
user_partition_info: {
...courseSectionVerticalMock.xblock_info.user_partition_info,
selected_partition_index: 1,
selected_groups_label: 'Visibility group 1',
},
},
});
await executeThunk(fetchCourseUnitQuery(blockId), store.dispatch);
await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch);
const { getByText } = renderComponent();
const visibilityMessage = messages.definedVisibilityMessage.defaultMessage
.replace('{selectedGroupsLabel}', 'Visibility group 1');
@@ -143,12 +150,15 @@ describe('<HeaderTitle />', () => {
it('displays a visibility message with the selected groups for some of xblock', async () => {
axiosMock
.onGet(getCourseUnitApiUrl(blockId))
.onGet(getCourseSectionVerticalApiUrl(blockId))
.reply(200, {
...courseUnitIndexMock,
has_partition_group_components: true,
...courseSectionVerticalMock,
xblock_info: {
...courseSectionVerticalMock.xblock_info,
has_partition_group_components: true,
},
});
await executeThunk(fetchCourseUnitQuery(blockId), store.dispatch);
await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch);
const { getByText } = renderComponent();
await waitFor(() => {

View File

@@ -18,10 +18,10 @@ import {
editCourseItemQuery,
editCourseUnitVisibilityAndData,
fetchCourseSectionVerticalData,
fetchCourseUnitQuery,
fetchCourseVerticalChildrenData,
getCourseOutlineInfoQuery,
patchUnitItemQuery,
updateCourseUnitSidebar,
} from './data/thunk';
import {
getCanEdit,
@@ -198,7 +198,6 @@ export const useCourseUnit = ({ courseId, blockId }) => {
}, [savingStatus]);
useEffect(() => {
dispatch(fetchCourseUnitQuery(blockId));
dispatch(fetchCourseSectionVerticalData(blockId, sequenceId));
dispatch(fetchCourseVerticalChildrenData(blockId, isSplitTestType));
handleNavigate(sequenceId);
@@ -223,8 +222,7 @@ export const useCourseUnit = ({ courseId, blockId }) => {
// edits the component using editor which has a separate store
/* istanbul ignore next */
if (event.key === 'courseRefreshTriggerOnComponentEditSave') {
dispatch(fetchCourseSectionVerticalData(blockId, sequenceId));
dispatch(fetchCourseVerticalChildrenData(blockId, isSplitTestType));
dispatch(updateCourseUnitSidebar(blockId));
localStorage.removeItem(event.key);
}
};

View File

@@ -10,10 +10,10 @@ import userEvent from '@testing-library/user-event';
import initializeStore from '../../../../store';
import { executeThunk } from '../../../../utils';
import { clipboardUnit } from '../../../../__mocks__';
import { getCourseUnitApiUrl } from '../../../data/api';
import { getCourseSectionVerticalApiUrl } from '../../../data/api';
import { getClipboardUrl } from '../../../../generic/data/api';
import { fetchCourseUnitQuery } from '../../../data/thunk';
import { courseUnitIndexMock } from '../../../__mocks__';
import { fetchCourseSectionVerticalData } from '../../../data/thunk';
import { courseSectionVerticalMock } from '../../../__mocks__';
import messages from '../../messages';
import ActionButtons from './ActionButtons';
@@ -46,8 +46,14 @@ describe('<ActionButtons />', () => {
store = initializeStore();
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
axiosMock
.onGet(getCourseUnitApiUrl(courseId))
.reply(200, { ...courseUnitIndexMock, enable_copy_paste_units: true });
.onGet(getCourseSectionVerticalApiUrl(courseId))
.reply(200, {
...courseSectionVerticalMock,
xblock_info: {
...courseSectionVerticalMock.xblock_info,
enable_copy_paste_units: true,
},
});
axiosMock
.onPost(getClipboardUrl())
.reply(200, clipboardUnit);
@@ -57,7 +63,7 @@ describe('<ActionButtons />', () => {
queryClient = new QueryClient();
await executeThunk(fetchCourseUnitQuery(courseId), store.dispatch);
await executeThunk(fetchCourseSectionVerticalData(courseId), store.dispatch);
});
it('render ActionButtons component with Copy to clipboard', () => {
@@ -74,7 +80,9 @@ describe('<ActionButtons />', () => {
userEvent.click(copyXBlockBtn);
expect(axiosMock.history.post.length).toBe(1);
expect(axiosMock.history.post[0].data).toBe(JSON.stringify({ usage_key: courseUnitIndexMock.id }));
expect(axiosMock.history.post[0].data).toBe(
JSON.stringify({ usage_key: courseSectionVerticalMock.xblock_info.id }),
);
jest.resetAllMocks();
});
});

View File

@@ -126,9 +126,9 @@ export const saveBlock = (content, returnToUnit) => (dispatch) => {
onSuccess: (response) => {
dispatch(actions.app.setSaveResponse(response));
const parsedData = JSON.parse(response.config.data);
if (parsedData?.has_changes) {
if (parsedData?.has_changes || !('has_changes' in parsedData)) {
const storageKey = 'courseRefreshTriggerOnComponentEditSave';
localStorage.setItem(storageKey, Date.now());
sessionStorage.setItem(storageKey, Date.now());
window.dispatchEvent(new StorageEvent('storage', {
key: storageKey,

View File

@@ -48,21 +48,30 @@ const VideoThumbnail = ({
const isFailed = VIDEO_FAILURE_STATUSES.includes(status);
const failedMessage = intl.formatMessage(messages.failedCheckboxLabel);
const showThumbnail = allowThumbnailUpload && thumbnail && isUploaded;
const showThumbnail = allowThumbnailUpload && isUploaded;
return (
<div className="video-thumbnail row justify-content-center align-itmes-center">
{allowThumbnailUpload && showThumbnail && <div className="thumbnail-overlay" />}
{allowThumbnailUpload && isUploaded && <div className="thumbnail-overlay" />}
{showThumbnail && !thumbnailError && pageLoadStatus === RequestStatus.SUCCESSFUL ? (
<>
<div className="border rounded">
<Image
style={imageSize}
className="m-1 bg-light-300"
src={thumbnail}
alt={intl.formatMessage(messages.thumbnailAltMessage, { displayName })}
onError={() => setThumbnailError(true)}
/>
{ thumbnail ? (
<Image
style={imageSize}
className="m-1 bg-light-300"
src={thumbnail}
alt={intl.formatMessage(messages.thumbnailAltMessage, { displayName })}
onError={() => setThumbnailError(true)}
/>
) : (
<div
className="row justify-content-center align-items-center m-0"
style={imageSize}
>
<Icon src={VideoFile} style={{ height: '48px', width: '48px' }} />
</div>
)}
</div>
<div className="add-thumbnail" data-testid={`video-thumbnail-${id}`}>
<Button

View File

@@ -0,0 +1,32 @@
import React from 'react';
import { render, fireEvent, screen } from '@testing-library/react';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import VideoThumbnail from './VideoThumbnail';
import { VIDEO_SUCCESS_STATUSES } from './data/constants';
import { RequestStatus } from '../../data/constants';
it('shows fallback icon if thumbnail fails to load', () => {
const { container } = render(
<IntlProvider locale="en">
<VideoThumbnail
thumbnail="http://bad-url/image.png"
displayName="Test Video"
id="vid1"
imageSize={{ width: '100px', height: '100px' }}
handleAddThumbnail={jest.fn()}
videoImageSettings={{ videoImageUploadEnabled: true, supportedFileFormats: { jpg: 'image/jpg' } }}
status={VIDEO_SUCCESS_STATUSES[0]}
pageLoadStatus={RequestStatus.SUCCESSFUL}
/>
</IntlProvider>,
);
const image = screen.getByRole('img', { name: /video thumbnail/i });
expect(image).toBeInTheDocument();
fireEvent.error(image);
expect(screen.queryByRole('img', { name: /video thumbnail/i })).toBeNull();
const fallbackSvg = container.querySelector('svg[role="img"]');
expect(fallbackSvg).toBeInTheDocument();
});

View File

@@ -19,7 +19,7 @@ const messages = defineMessages({
},
previewNotAvailable: {
id: 'course-authoring.library-authoring.component-comparison.preview-not-available',
defaultMessage: 'Preview not available',
defaultMessage: 'Preview not available for unit changes at this time',
description: 'Message shown when preview is not available.',
},
});