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

View File

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

View File

@@ -116,6 +116,11 @@ const messages = defineMessages({
defaultMessage: 'Something went wrong! Could not fetch results.', defaultMessage: 'Something went wrong! Could not fetch results.',
description: 'Generic error message displayed when fetching link data fails.', 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; export default messages;

View File

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

View File

@@ -25,6 +25,12 @@
&:hover { &:hover {
.item-card-button-icon { .item-card-button-icon {
opacity: 1; 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', id: 'course-authoring.course-outline.card.status-badge.draft-unpublished-changes',
defaultMessage: 'Draft (Unpublished changes)', defaultMessage: 'Draft (Unpublished changes)',
}, },
altButtonEdit: { altButtonRename: {
id: 'course-authoring.course-outline.card.button.edit.alt', id: 'course-authoring.course-outline.card.button.edit.alt',
defaultMessage: 'Edit', defaultMessage: 'Rename',
}, },
menuPublish: { menuPublish: {
id: 'course-authoring.course-outline.card.menu.publish', id: 'course-authoring.course-outline.card.menu.publish',
@@ -82,6 +82,11 @@ const messages = defineMessages({
defaultMessage: 'Update available - click to sync', defaultMessage: 'Update available - click to sync',
description: 'Alt text for the sync icon button.', 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; export default messages;

View File

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

View File

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

View File

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

View File

@@ -35,7 +35,7 @@ const SequenceNavigation = ({
const shouldDisplayNotificationTriggerInSequence = useWindowSize().width < breakpoints.small.minWidth; const shouldDisplayNotificationTriggerInSequence = useWindowSize().width < breakpoints.small.minWidth;
const renderUnitButtons = () => { const renderUnitButtons = () => {
if (sequence?.unitIds?.length === 0 || unitId === null) { if (sequence.unitIds.length === 0 || unitId === null) {
return ( return (
<div style={{ flexBasis: '100%', minWidth: 0, borderBottom: 'solid 1px #EAEAEA' }} /> <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; const getStudioBaseUrl = () => getConfig().STUDIO_BASE_URL;
export const getCourseUnitApiUrl = (itemId) => `${getStudioBaseUrl()}/xblock/container/${itemId}`;
export const getXBlockBaseApiUrl = (itemId) => `${getStudioBaseUrl()}/xblock/${itemId}`; export const getXBlockBaseApiUrl = (itemId) => `${getStudioBaseUrl()}/xblock/${itemId}`;
export const getCourseSectionVerticalApiUrl = (itemId) => `${getStudioBaseUrl()}/api/contentstore/v1/container_handler/${itemId}`; export const getCourseSectionVerticalApiUrl = (itemId) => `${getStudioBaseUrl()}/api/contentstore/v1/container_handler/${itemId}`;
export const getCourseVerticalChildrenApiUrl = (itemId) => `${getStudioBaseUrl()}/api/contentstore/v1/container/vertical/${itemId}/children`; 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 postXBlockBaseApiUrl = () => `${getStudioBaseUrl()}/xblock/`;
export const libraryBlockChangesUrl = (blockId) => `${getStudioBaseUrl()}/api/contentstore/v2/downstreams/${blockId}/sync`; 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. * Edit course unit display name.
* @param {string} unitId * @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 * @param {string} unitId
* @returns {Promise<Object>} * @returns {Promise<Object>}
*/ */
export async function getCourseSectionVerticalData(unitId) { export async function getVerticalData(unitId) {
const { data } = await getAuthenticatedHttpClient() const { data } = await getAuthenticatedHttpClient()
.get(getCourseSectionVerticalApiUrl(unitId)); .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 { createSelector } from '@reduxjs/toolkit';
import { RequestStatus } from 'CourseAuthoring/data/constants'; 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 getCanEdit = (state) => state.courseUnit.canEdit;
export const getStaticFileNotices = (state) => state.courseUnit.staticFileNotices; export const getStaticFileNotices = (state) => state.courseUnit.staticFileNotices;
export const getCourseUnit = (state) => state.courseUnit; export const getCourseUnit = (state) => state.courseUnit;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -48,21 +48,30 @@ const VideoThumbnail = ({
const isFailed = VIDEO_FAILURE_STATUSES.includes(status); const isFailed = VIDEO_FAILURE_STATUSES.includes(status);
const failedMessage = intl.formatMessage(messages.failedCheckboxLabel); const failedMessage = intl.formatMessage(messages.failedCheckboxLabel);
const showThumbnail = allowThumbnailUpload && thumbnail && isUploaded; const showThumbnail = allowThumbnailUpload && isUploaded;
return ( return (
<div className="video-thumbnail row justify-content-center align-itmes-center"> <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 ? ( {showThumbnail && !thumbnailError && pageLoadStatus === RequestStatus.SUCCESSFUL ? (
<> <>
<div className="border rounded"> <div className="border rounded">
<Image { thumbnail ? (
style={imageSize} <Image
className="m-1 bg-light-300" style={imageSize}
src={thumbnail} className="m-1 bg-light-300"
alt={intl.formatMessage(messages.thumbnailAltMessage, { displayName })} src={thumbnail}
onError={() => setThumbnailError(true)} 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>
<div className="add-thumbnail" data-testid={`video-thumbnail-${id}`}> <div className="add-thumbnail" data-testid={`video-thumbnail-${id}`}>
<Button <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: { previewNotAvailable: {
id: 'course-authoring.library-authoring.component-comparison.preview-not-available', 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.', description: 'Message shown when preview is not available.',
}, },
}); });