feat: [FC-0070] implement move xblock modal (#1422)

This commit is contained in:
Ihor Romaniuk
2024-11-21 19:22:41 +01:00
committed by GitHub
parent 66577b0d59
commit b5419acd74
34 changed files with 3536 additions and 23 deletions

View File

@@ -17,7 +17,7 @@ import ScheduleAndDetails from './schedule-and-details';
import { GradingSettings } from './grading-settings';
import CourseTeam from './course-team/CourseTeam';
import { CourseUpdates } from './course-updates';
import { CourseUnit } from './course-unit';
import { CourseUnit, IframeProvider } from './course-unit';
import { Certificates } from './certificates';
import CourseExportPage from './export-page/CourseExportPage';
import CourseImportPage from './import-page/CourseImportPage';
@@ -79,7 +79,7 @@ const CourseAuthoringRoutes = () => {
<Route
key={path}
path={path}
element={<PageWrap><CourseUnit courseId={courseId} /></PageWrap>}
element={<PageWrap><IframeProvider><CourseUnit courseId={courseId} /></IframeProvider></PageWrap>}
/>
))}
<Route

View File

@@ -27,6 +27,8 @@ export const NOTIFICATION_MESSAGES = {
copying: 'Copying',
pasting: 'Pasting',
discardChanges: 'Discarding changes',
moving: 'Moving',
undoMoving: 'Undo moving',
publishing: 'Publishing',
hidingFromStudents: 'Hiding from students',
makingVisibleToStudents: 'Making visible to students',

View File

@@ -2,10 +2,15 @@ import { useEffect } from 'react';
import PropTypes from 'prop-types';
import { useSelector } from 'react-redux';
import { useParams } from 'react-router-dom';
import { Container, Layout, Stack } from '@openedx/paragon';
import {
Container, Layout, Stack, Button, TransitionReplace,
} from '@openedx/paragon';
import { getConfig } from '@edx/frontend-platform';
import { useIntl, injectIntl } from '@edx/frontend-platform/i18n';
import { Warning as WarningIcon } from '@openedx/paragon/icons';
import {
Warning as WarningIcon,
CheckCircle as CheckCircleIcon,
} from '@openedx/paragon/icons';
import { getProcessingNotification } from '../generic/processing-notification/data/selectors';
import SubHeader from '../generic/sub-header/SubHeader';
@@ -30,6 +35,7 @@ import LocationInfo from './sidebar/LocationInfo';
import TagsSidebarControls from '../content-tags-drawer/tags-sidebar-controls';
import { PasteNotificationAlert } from './clipboard';
import XBlockContainerIframe from './xblock-container-iframe';
import MoveModal from './move-modal';
const CourseUnit = ({ courseId }) => {
const { blockId } = useParams();
@@ -55,6 +61,13 @@ const CourseUnit = ({ courseId }) => {
handleConfigureSubmit,
courseVerticalChildren,
canPasteComponent,
isMoveModalOpen,
openMoveModal,
closeMoveModal,
movedXBlockParams,
handleRollbackMovedXBlock,
handleCloseXBlockMovedAlert,
handleNavigateToTargetUnit,
} = useCourseUnit({ courseId, blockId });
useEffect(() => {
@@ -82,6 +95,40 @@ const CourseUnit = ({ courseId }) => {
<>
<Container size="xl" className="course-unit px-4">
<section className="course-unit-container mb-4 mt-5">
<TransitionReplace>
{movedXBlockParams.isSuccess ? (
<AlertMessage
key="xblock-moved-alert"
data-testid="xblock-moved-alert"
show={movedXBlockParams.isSuccess}
variant="success"
icon={CheckCircleIcon}
title={movedXBlockParams.isUndo
? intl.formatMessage(messages.alertMoveCancelTitle)
: intl.formatMessage(messages.alertMoveSuccessTitle)}
description={movedXBlockParams.isUndo
? intl.formatMessage(messages.alertMoveCancelDescription, { title: movedXBlockParams.title })
: intl.formatMessage(messages.alertMoveSuccessDescription, { title: movedXBlockParams.title })}
aria-hidden={movedXBlockParams.isSuccess}
dismissible
actions={movedXBlockParams.isUndo ? null : [
<Button
onClick={handleRollbackMovedXBlock}
key="xblock-moved-alert-undo-move-button"
>
{intl.formatMessage(messages.undoMoveButton)}
</Button>,
<Button
onClick={handleNavigateToTargetUnit}
key="xblock-moved-alert-new-location-button"
>
{intl.formatMessage(messages.newLocationButton)}
</Button>,
]}
onClose={handleCloseXBlockMovedAlert}
/>
) : null}
</TransitionReplace>
<SubHeader
hideBorder
title={(
@@ -147,6 +194,12 @@ const CourseUnit = ({ courseId }) => {
text={intl.formatMessage(messages.pasteButtonText)}
/>
)}
<MoveModal
isOpenModal={isMoveModalOpen}
openModal={openMoveModal}
closeModal={closeMoveModal}
courseId={courseId}
/>
</Layout.Element>
<Layout.Element>
<Stack gap={3}>

View File

@@ -3,6 +3,7 @@
@import "./add-component/AddComponent";
@import "./sidebar/Sidebar";
@import "./header-title/HeaderTitle";
@import "./move-modal";
.course-unit__alert {
margin-bottom: 1.75rem;

View File

@@ -1,6 +1,6 @@
import MockAdapter from 'axios-mock-adapter';
import {
act, render, waitFor, fireEvent, within, screen,
act, render, waitFor, within, screen,
} from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { IntlProvider } from '@edx/frontend-platform/i18n';
@@ -18,6 +18,7 @@ import {
getCourseSectionVerticalApiUrl,
getCourseUnitApiUrl,
getCourseVerticalChildrenApiUrl,
getCourseOutlineInfoUrl,
getXBlockBaseApiUrl,
postXBlockBaseApiUrl,
} from './data/api';
@@ -27,6 +28,8 @@ import {
fetchCourseSectionVerticalData,
fetchCourseUnitQuery,
fetchCourseVerticalChildrenData,
getCourseOutlineInfoQuery,
patchUnitItemQuery,
} from './data/thunk';
import initializeStore from '../store';
import {
@@ -36,6 +39,7 @@ import {
courseUnitMock,
courseVerticalChildrenMock,
clipboardMockResponse,
courseOutlineInfoMock,
} from './__mocks__';
import { clipboardUnit } from '../__mocks__';
import { executeThunk } from '../utils';
@@ -49,10 +53,12 @@ import { extractCourseUnitId } from './sidebar/utils';
import CourseUnit from './CourseUnit';
import configureModalMessages from '../generic/configure-modal/messages';
import addComponentMessages from './add-component/messages';
import { PUBLISH_TYPES, UNIT_VISIBILITY_STATES } from './constants';
import messages from './messages';
import { getContentTaxonomyTagsApiUrl, getContentTaxonomyTagsCountApiUrl } from '../content-tags-drawer/data/api';
import addComponentMessages from './add-component/messages';
import { messageTypes, PUBLISH_TYPES, UNIT_VISIBILITY_STATES } from './constants';
import { IframeProvider } from './context/iFrameContext';
import moveModalMessages from './move-modal/messages';
import messages from './messages';
let axiosMock;
let store;
@@ -108,7 +114,9 @@ global.BroadcastChannel = jest.fn(() => clipboardBroadcastChannelMock);
const RootWrapper = () => (
<AppProvider store={store}>
<IntlProvider locale="en">
<CourseUnit courseId={courseId} />
<IframeProvider>
<CourseUnit courseId={courseId} />
</IframeProvider>
</IntlProvider>
</AppProvider>
);
@@ -123,6 +131,7 @@ describe('<CourseUnit />', () => {
roles: [],
},
});
window.scrollTo = jest.fn();
global.localStorage.clear();
store = initializeStore();
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
@@ -223,12 +232,13 @@ describe('<CourseUnit />', () => {
.queryByRole('textbox', { name: headerTitleMessages.ariaLabelButtonEdit.defaultMessage });
});
expect(titleEditField).not.toBeInTheDocument();
fireEvent.click(editTitleButton);
userEvent.click(editTitleButton);
titleEditField = getByRole('textbox', { name: headerTitleMessages.ariaLabelButtonEdit.defaultMessage });
fireEvent.change(titleEditField, { target: { value: newDisplayName } });
await act(async () => {
fireEvent.blur(titleEditField);
});
await userEvent.clear(titleEditField);
await userEvent.type(titleEditField, newDisplayName);
await userEvent.tab();
expect(titleEditField).toHaveValue(newDisplayName);
titleEditField = queryByRole('textbox', { name: headerTitleMessages.ariaLabelButtonEdit.defaultMessage });
@@ -393,12 +403,13 @@ describe('<CourseUnit />', () => {
const unitHeaderTitle = getByTestId('unit-header-title');
const editTitleButton = within(unitHeaderTitle).getByRole('button', { name: headerTitleMessages.altButtonEdit.defaultMessage });
fireEvent.click(editTitleButton);
userEvent.click(editTitleButton);
const titleEditField = within(unitHeaderTitle).getByRole('textbox', { name: headerTitleMessages.ariaLabelButtonEdit.defaultMessage });
fireEvent.change(titleEditField, { target: { value: newDisplayName } });
await act(async () => fireEvent.blur(titleEditField));
await userEvent.clear(titleEditField);
await userEvent.type(titleEditField, newDisplayName);
await userEvent.tab();
await waitFor(async () => {
const units = getAllByTestId('course-unit-btn');
@@ -1061,4 +1072,253 @@ describe('<CourseUnit />', () => {
)).not.toBeInTheDocument();
});
});
describe('Move functionality', () => {
const requestData = {
sourceLocator: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@867dddb6f55d410caaa9c1eb9c6743ec',
targetParentLocator: 'block-v1:edX+DemoX+Demo_Course+type@course+block@course',
title: 'Getting Started',
currentParentLocator: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@19a30717eff543078a5d94ae9d6c18a5',
isMoving: true,
callbackFn: jest.fn(),
};
const messageEvent = new MessageEvent('message', {
data: {
type: messageTypes.showMoveXBlockModal,
payload: {
sourceXBlockInfo: {
id: requestData.sourceLocator,
displayName: requestData.title,
},
sourceParentXBlockInfo: {
id: requestData.currentParentLocator,
category: 'vertical',
hasChildren: true,
},
},
},
origin: '*',
});
it('should display "Move Modal" on receive trigger message', async () => {
const {
getByText,
getByRole,
} = render(<RootWrapper />);
await act(async () => {
await waitFor(() => {
expect(getByText(unitDisplayName))
.toBeInTheDocument();
});
axiosMock
.onGet(getCourseOutlineInfoUrl(courseId))
.reply(200, courseOutlineInfoMock);
await executeThunk(getCourseOutlineInfoQuery(courseId), store.dispatch);
window.dispatchEvent(messageEvent);
});
expect(getByText(
moveModalMessages.moveModalTitle.defaultMessage.replace('{displayName}', requestData.title),
)).toBeInTheDocument();
expect(getByRole('button', { name: moveModalMessages.moveModalSubmitButton.defaultMessage })).toBeInTheDocument();
expect(getByRole('button', { name: moveModalMessages.moveModalCancelButton.defaultMessage })).toBeInTheDocument();
});
it('should navigates to xBlock current unit', async () => {
const {
getByText,
getByRole,
} = render(<RootWrapper />);
await act(async () => {
await waitFor(() => {
expect(getByText(unitDisplayName))
.toBeInTheDocument();
});
axiosMock
.onGet(getCourseOutlineInfoUrl(courseId))
.reply(200, courseOutlineInfoMock);
await executeThunk(getCourseOutlineInfoQuery(courseId), store.dispatch);
window.dispatchEvent(messageEvent);
});
expect(getByText(
moveModalMessages.moveModalTitle.defaultMessage.replace('{displayName}', requestData.title),
)).toBeInTheDocument();
const currentSection = courseOutlineInfoMock.child_info.children[1];
const currentSectionItemBtn = getByRole('button', {
name: `${currentSection.display_name} ${moveModalMessages.moveModalOutlineItemCurrentLocationText.defaultMessage} ${moveModalMessages.moveModalOutlineItemViewText.defaultMessage}`,
});
expect(currentSectionItemBtn).toBeInTheDocument();
userEvent.click(currentSectionItemBtn);
await waitFor(() => {
const currentSubsection = currentSection.child_info.children[0];
const currentSubsectionItemBtn = getByRole('button', {
name: `${currentSubsection.display_name} ${moveModalMessages.moveModalOutlineItemCurrentLocationText.defaultMessage} ${moveModalMessages.moveModalOutlineItemViewText.defaultMessage}`,
});
expect(currentSubsectionItemBtn).toBeInTheDocument();
userEvent.click(currentSubsectionItemBtn);
});
await waitFor(() => {
const currentComponentLocationText = getByText(
moveModalMessages.moveModalOutlineItemCurrentComponentLocationText.defaultMessage,
);
expect(currentComponentLocationText).toBeInTheDocument();
});
});
it('should allow move operation and handles it successfully', async () => {
const {
getByText,
getByRole,
} = render(<RootWrapper />);
axiosMock
.onPatch(postXBlockBaseApiUrl())
.reply(200, {});
axiosMock
.onGet(getCourseUnitApiUrl(blockId))
.reply(200, {});
await act(async () => {
await waitFor(() => {
expect(getByText(unitDisplayName))
.toBeInTheDocument();
});
axiosMock
.onGet(getCourseOutlineInfoUrl(courseId))
.reply(200, courseOutlineInfoMock);
await executeThunk(getCourseOutlineInfoQuery(courseId), store.dispatch);
window.dispatchEvent(messageEvent);
});
expect(getByText(
moveModalMessages.moveModalTitle.defaultMessage.replace('{displayName}', requestData.title),
)).toBeInTheDocument();
const currentSection = courseOutlineInfoMock.child_info.children[1];
const currentSectionItemBtn = getByRole('button', {
name: `${currentSection.display_name} ${moveModalMessages.moveModalOutlineItemCurrentLocationText.defaultMessage} ${moveModalMessages.moveModalOutlineItemViewText.defaultMessage}`,
});
expect(currentSectionItemBtn).toBeInTheDocument();
userEvent.click(currentSectionItemBtn);
const currentSubsection = currentSection.child_info.children[1];
await waitFor(() => {
const currentSubsectionItemBtn = getByRole('button', {
name: `${currentSubsection.display_name} ${moveModalMessages.moveModalOutlineItemViewText.defaultMessage}`,
});
expect(currentSubsectionItemBtn).toBeInTheDocument();
userEvent.click(currentSubsectionItemBtn);
});
await waitFor(() => {
const currentUnit = currentSubsection.child_info.children[0];
const currentUnitItemBtn = getByRole('button', {
name: `${currentUnit.display_name} ${moveModalMessages.moveModalOutlineItemViewText.defaultMessage}`,
});
expect(currentUnitItemBtn).toBeInTheDocument();
userEvent.click(currentUnitItemBtn);
});
const moveModalBtn = getByRole('button', {
name: moveModalMessages.moveModalSubmitButton.defaultMessage,
});
expect(moveModalBtn).toBeInTheDocument();
expect(moveModalBtn).not.toBeDisabled();
userEvent.click(moveModalBtn);
await waitFor(() => {
expect(window.scrollTo).toHaveBeenCalledWith({ top: 0, behavior: 'smooth' });
expect(window.scrollTo).toHaveBeenCalledTimes(1);
});
});
it('should display "Move Confirmation" alert after moving and undo operations', async () => {
const {
queryByRole,
getByText,
} = render(<RootWrapper />);
axiosMock
.onPatch(postXBlockBaseApiUrl())
.reply(200, {});
await executeThunk(patchUnitItemQuery({
sourceLocator: requestData.sourceLocator,
targetParentLocator: requestData.targetParentLocator,
title: requestData.title,
currentParentLocator: requestData.currentParentLocator,
isMoving: requestData.isMoving,
callbackFn: requestData.callbackFn,
}), store.dispatch);
const dismissButton = queryByRole('button', {
name: /dismiss/i, hidden: true,
});
const undoButton = queryByRole('button', {
name: messages.undoMoveButton.defaultMessage, hidden: true,
});
const newLocationButton = queryByRole('button', {
name: messages.newLocationButton.defaultMessage, hidden: true,
});
expect(getByText(messages.alertMoveSuccessTitle.defaultMessage)).toBeInTheDocument();
expect(getByText(`${requestData.title} has been moved`)).toBeInTheDocument();
expect(dismissButton).toBeInTheDocument();
expect(undoButton).toBeInTheDocument();
expect(newLocationButton).toBeInTheDocument();
userEvent.click(undoButton);
await waitFor(() => {
expect(getByText(messages.alertMoveCancelTitle.defaultMessage)).toBeInTheDocument();
});
expect(getByText(
messages.alertMoveCancelDescription.defaultMessage.replace('{title}', requestData.title),
)).toBeInTheDocument();
expect(dismissButton).toBeInTheDocument();
expect(undoButton).not.toBeInTheDocument();
expect(newLocationButton).not.toBeInTheDocument();
});
it('should navigate to new location by button click', async () => {
const {
queryByRole,
} = render(<RootWrapper />);
axiosMock
.onPatch(postXBlockBaseApiUrl())
.reply(200, {});
await executeThunk(patchUnitItemQuery({
sourceLocator: requestData.sourceLocator,
targetParentLocator: requestData.targetParentLocator,
title: requestData.title,
currentParentLocator: requestData.currentParentLocator,
isMoving: requestData.isMoving,
callbackFn: requestData.callbackFn,
}), store.dispatch);
const newLocationButton = queryByRole('button', {
name: messages.newLocationButton.defaultMessage, hidden: true,
});
userEvent.click(newLocationButton);
expect(mockedUsedNavigate).toHaveBeenCalledWith(
`/course/${courseId}/container/${blockId}/${requestData.currentParentLocator}`,
{ replace: true },
);
});
});
});

View File

@@ -0,0 +1,1683 @@
module.exports = {
id: 'block-v1:edX+DemoX+Demo_Course+type@course+block@course',
display_name: 'Demonstration Course',
category: 'course',
has_children: true,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
unit_level_discussions: false,
child_info: {
category: 'chapter',
display_name: 'Section',
children: [
{
id: 'block-v1:edX+DemoX+Demo_Course+type@chapter+block@d8a6192ade314473a78242dfeedfbf5b',
display_name: 'Introduction',
category: 'chapter',
has_children: true,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
child_info: {
category: 'sequential',
display_name: 'Subsection',
children: [
{
id: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@edx_introduction',
display_name: 'Demo Course Overview',
category: 'sequential',
has_children: true,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
child_info: {
category: 'vertical',
display_name: 'Unit',
children: [
{
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@vertical_0270f6de40fc',
display_name: 'Introduction: Video and Sequences',
category: 'vertical',
has_children: true,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
child_info: {
children: [
{
id: 'block-v1:edX+DemoX+Demo_Course+type@html+block@030e35c4756a4ddc8d40b95fbbfff4d4',
display_name: 'Blank HTML Page',
category: 'html',
has_children: false,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
},
{
id: 'block-v1:edX+DemoX+Demo_Course+type@html+block@f7cc083ff66d442eafafd48152881276',
display_name: '“Blank HTML Page”的副本',
category: 'html',
has_children: false,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
},
{
id: 'block-v1:edX+DemoX+Demo_Course+type@video+block@0b9e39477cf34507a7a48f74be381fdd',
display_name: 'Welcome!',
category: 'video',
has_children: false,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
},
{
id: 'block-v1:edX+DemoX+Demo_Course+type@video+block@6e72ebc448694e42ac56553af74304e7',
display_name: 'Video',
category: 'video',
has_children: false,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
},
],
},
},
],
},
},
{
id: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@8c964a36521a42e3a221e7b8cf6c94fc',
display_name: 'Subsection',
category: 'sequential',
has_children: true,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
},
],
},
},
{
id: 'block-v1:edX+DemoX+Demo_Course+type@chapter+block@interactive_demonstrations',
display_name: 'Example Week 1: Getting Started',
category: 'chapter',
has_children: true,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
child_info: {
category: 'sequential',
display_name: 'Subsection',
children: [
{
id: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@19a30717eff543078a5d94ae9d6c18a5',
display_name: 'Lesson 1 - Getting Started',
category: 'sequential',
has_children: true,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
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,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
child_info: {
children: [
{
id: 'block-v1:edX+DemoX+Demo_Course+type@html+block@82d599b014b246c7a9b5dfc750dc08a9',
display_name: 'Getting Started',
category: 'html',
has_children: false,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
},
],
},
},
{
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@4f6c1b4e316a419ab5b6bf30e6c708e9',
display_name: 'Working with Videos',
category: 'vertical',
has_children: true,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
child_info: {
children: [
{
id: 'block-v1:edX+DemoX+Demo_Course+type@html+block@6bcccc2d7343416e9e03fd7325b2f232',
display_name: '',
category: 'html',
has_children: false,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
},
{
id: 'block-v1:edX+DemoX+Demo_Course+type@video+block@7e9b434e6de3435ab99bd3fb25bde807',
display_name: 'A Shared Culture',
category: 'video',
has_children: false,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
},
{
id: 'block-v1:edX+DemoX+Demo_Course+type@discussion+block@412dc8dbb6674014862237b23c1f643f',
display_name: 'Working with Videos',
category: 'discussion',
has_children: false,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
},
],
},
},
{
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@3dc16db8d14842e38324e95d4030b8a0',
display_name: 'Videos on edX',
category: 'vertical',
has_children: true,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
child_info: {
children: [
{
id: 'block-v1:edX+DemoX+Demo_Course+type@html+block@0a3b4139f51a4917a3aff9d519b1eeb6',
display_name: 'Videos on edX',
category: 'html',
has_children: false,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
},
{
id: 'block-v1:edX+DemoX+Demo_Course+type@video+block@5c90cffecd9b48b188cbfea176bf7fe9',
display_name: 'Video',
category: 'video',
has_children: false,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
},
{
id: 'block-v1:edX+DemoX+Demo_Course+type@discussion+block@722085be27c84ac693cfebc8ac5da700',
display_name: 'Videos on edX',
category: 'discussion',
has_children: false,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
},
],
},
},
{
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@4a1bba2a403f40bca5ec245e945b0d76',
display_name: 'Video Demonstrations',
category: 'vertical',
has_children: true,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
child_info: {
children: [
{
id: 'block-v1:edX+DemoX+Demo_Course+type@html+block@ed5dccf14ae94353961f46fa07217491',
display_name: '',
category: 'html',
has_children: false,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
},
{
id: 'block-v1:edX+DemoX+Demo_Course+type@discussion+block@9f9e1373cc8243b985c8750cc8acec7d',
display_name: 'Video Demonstrations',
category: 'discussion',
has_children: false,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
},
],
},
},
{
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@256f17a44983429fb1a60802203ee4e0',
display_name: 'Video Presentation Styles',
category: 'vertical',
has_children: true,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
child_info: {
children: [
{
id: 'block-v1:edX+DemoX+Demo_Course+type@html+block@c2f7008c9ccf4bd09d5d800c98fb0722',
display_name: '',
category: 'html',
has_children: false,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
},
{
id: 'block-v1:edX+DemoX+Demo_Course+type@video+block@636541acbae448d98ab484b028c9a7f6',
display_name: 'Connecting a Circuit and a Circuit Diagram',
category: 'video',
has_children: false,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
},
{
id: 'block-v1:edX+DemoX+Demo_Course+type@discussion+block@e2cb0e0994f84b0abfa5f4ae42ed9d44',
display_name: 'Video Presentation Styles',
category: 'discussion',
has_children: false,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
},
],
},
},
{
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@e3601c0abee6427d8c17e6d6f8fdddd1',
display_name: 'Interactive Questions',
category: 'vertical',
has_children: true,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
child_info: {
children: [
{
id: 'block-v1:edX+DemoX+Demo_Course+type@problem+block@9cee77a606ea4c1aa5440e0ea5d0f618',
display_name: 'Interactive Questions',
category: 'problem',
has_children: false,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
},
{
id: 'block-v1:edX+DemoX+Demo_Course+type@discussion+block@3169f89efde2452993f2f2d9bc74f5b2',
display_name: '',
category: 'discussion',
has_children: false,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
},
],
},
},
{
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@a79d59cd72034188a71d388f4954a606',
display_name: 'Exciting Labs and Tools',
category: 'vertical',
has_children: true,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
child_info: {
children: [
{
id: 'block-v1:edX+DemoX+Demo_Course+type@html+block@ffcd6351126d4ca984409180e41d1b51',
display_name: 'Exciting Labs and Tools',
category: 'html',
has_children: false,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
},
{
id: 'block-v1:edX+DemoX+Demo_Course+type@discussion+block@1c8d47c425724346a7968fa1bc745dcd',
display_name: 'Labs and Tools',
category: 'discussion',
has_children: false,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
},
],
},
},
{
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@134df56c516a4a0dbb24dd5facef746e',
display_name: 'Reading Assignments',
category: 'vertical',
has_children: true,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
child_info: {
children: [
{
id: 'block-v1:edX+DemoX+Demo_Course+type@html+block@e0254b911fa246218bd98bbdadffef06',
display_name: 'Reading Assignments',
category: 'html',
has_children: false,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
},
{
id: 'block-v1:edX+DemoX+Demo_Course+type@html+block@2574c523e97b477a9d72fbb37bfb995f',
display_name: 'Text',
category: 'html',
has_children: false,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
},
{
id: 'block-v1:edX+DemoX+Demo_Course+type@problem+block@932e6f2ce8274072a355a94560216d1a',
display_name: 'Perchance to Dream',
category: 'problem',
has_children: false,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
},
{
id: 'block-v1:edX+DemoX+Demo_Course+type@problem+block@303034da25524878a2e66fb57c91cf85',
display_name: 'Attributing Blame',
category: 'problem',
has_children: false,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
},
{
id: 'block-v1:edX+DemoX+Demo_Course+type@discussion+block@ffa5817d49e14fec83ad6187cbe16358',
display_name: 'Reading Sample',
category: 'discussion',
has_children: false,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
},
],
},
},
{
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@d91b9e5d8bc64d57a1332d06bf2f2193',
display_name: 'When Are Your Exams? ',
category: 'vertical',
has_children: true,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
child_info: {
children: [
{
id: 'block-v1:edX+DemoX+Demo_Course+type@html+block@9d5104b502f24ee89c3d2f4ce9d347cf',
display_name: 'When Are Your Exams? ',
category: 'html',
has_children: false,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
},
],
},
},
],
},
},
{
id: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@basic_questions',
display_name: 'Homework - Question Styles',
category: 'sequential',
has_children: true,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
child_info: {
category: 'vertical',
display_name: 'Unit',
children: [
{
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@2152d4a4aadc4cb0af5256394a3d1fc7',
display_name: 'Pointing on a Picture',
category: 'vertical',
has_children: true,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
child_info: {
children: [
{
id: 'block-v1:edX+DemoX+Demo_Course+type@problem+block@c554538a57664fac80783b99d9d6da7c',
display_name: 'Pointing on a Picture Component',
category: 'problem',
has_children: false,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
},
{
id: 'block-v1:edX+DemoX+Demo_Course+type@discussion+block@e5eac7e1a5a24f5fa7ed77bb6d136591',
display_name: '',
category: 'discussion',
has_children: false,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
},
],
},
},
{
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@47dbd5f836544e61877a483c0b75606c',
display_name: 'Drag and Drop',
category: 'vertical',
has_children: true,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
child_info: {
children: [
{
id: 'block-v1:edX+DemoX+Demo_Course+type@problem+block@d2e35c1d294b4ba0b3b1048615605d2a',
display_name: 'Drag and Drop',
category: 'problem',
has_children: false,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
},
{
id: 'block-v1:edX+DemoX+Demo_Course+type@discussion+block@5ab88e67d46049b9aa694cb240c39cef',
display_name: '',
category: 'discussion',
has_children: false,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
},
],
},
},
{
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@54bb9b142c6c4c22afc62bcb628f0e68',
display_name: 'Multiple Choice Questions',
category: 'vertical',
has_children: true,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
child_info: {
children: [
{
id: 'block-v1:edX+DemoX+Demo_Course+type@problem+block@a0effb954cca4759994f1ac9e9434bf4',
display_name: 'Multiple Choice Questions',
category: 'problem',
has_children: false,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
},
{
id: 'block-v1:edX+DemoX+Demo_Course+type@discussion+block@67c26b1e826e47aaa29757f62bcd1ad0',
display_name: '',
category: 'discussion',
has_children: false,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
},
],
},
},
{
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@vertical_0c92347a5c00',
display_name: 'Mathematical Expressions',
category: 'vertical',
has_children: true,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
child_info: {
children: [
{
id: 'block-v1:edX+DemoX+Demo_Course+type@problem+block@Sample_Algebraic_Problem',
display_name: 'Mathematical Expressions',
category: 'problem',
has_children: false,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
},
{
id: 'block-v1:edX+DemoX+Demo_Course+type@discussion+block@870371212ba04dcf9536d7c7b8f3109e',
display_name: '',
category: 'discussion',
has_children: false,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
},
],
},
},
{
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@vertical_1fef54c2b23b',
display_name: 'Chemical Equations',
category: 'vertical',
has_children: true,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
child_info: {
children: [
{
id: 'block-v1:edX+DemoX+Demo_Course+type@problem+block@Sample_ChemFormula_Problem',
display_name: 'Chemical Equations',
category: 'problem',
has_children: false,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
},
{
id: 'block-v1:edX+DemoX+Demo_Course+type@discussion+block@4d672c5893cb4f1dad0de67d2008522e',
display_name: '',
category: 'discussion',
has_children: false,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
},
],
},
},
{
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@2889db1677a549abb15eb4d886f95d1c',
display_name: 'Numerical Input',
category: 'vertical',
has_children: true,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
child_info: {
children: [
{
id: 'block-v1:edX+DemoX+Demo_Course+type@problem+block@75f9562c77bc4858b61f907bb810d974',
display_name: 'Numerical Input',
category: 'problem',
has_children: false,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
},
{
id: 'block-v1:edX+DemoX+Demo_Course+type@discussion+block@501aed9d902349eeb2191fa505548de2',
display_name: '',
category: 'discussion',
has_children: false,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
},
],
},
},
{
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@e8a5cc2aed424838853defab7be45e42',
display_name: 'Text input',
category: 'vertical',
has_children: true,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
child_info: {
children: [
{
id: 'block-v1:edX+DemoX+Demo_Course+type@problem+block@0d759dee4f9d459c8956136dbde55f02',
display_name: 'Text Input',
category: 'problem',
has_children: false,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
},
{
id: 'block-v1:edX+DemoX+Demo_Course+type@discussion+block@6244918637ed4ff4b5f94a840a7e4b43',
display_name: '',
category: 'discussion',
has_children: false,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
},
],
},
},
{
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@fb6b62dbec4348528629cf2232b86aea',
display_name: 'Instructor Programmed Responses',
category: 'vertical',
has_children: true,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
child_info: {
children: [],
},
},
],
},
},
],
},
},
{
id: 'block-v1:edX+DemoX+Demo_Course+type@chapter+block@graded_interactions',
display_name: 'Example Week 2: Get Interactive',
category: 'chapter',
has_children: true,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
child_info: {
category: 'sequential',
display_name: 'Subsection',
children: [
{
id: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@simulations',
display_name: "Lesson 2 - Let's Get Interactive!",
category: 'sequential',
has_children: true,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
child_info: {
category: 'vertical',
display_name: 'Unit',
children: [
{
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@d0d804e8863c4a95a659c04d8a2b2bc0',
display_name: "Lesson 2 - Let's Get Interactive! ",
category: 'vertical',
has_children: true,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
child_info: {
children: [
{
id: 'block-v1:edX+DemoX+Demo_Course+type@html+block@78d7d3642f3a4dbabbd1b017861aa5f2',
display_name: "Lesson 2: Let's Get Interactive!",
category: 'html',
has_children: false,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
},
],
},
},
{
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@vertical_2dbb0072785e',
display_name: 'An Interactive Reference Table',
category: 'vertical',
has_children: true,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
child_info: {
children: [
{
id: 'block-v1:edX+DemoX+Demo_Course+type@html+block@html_07d547513285',
display_name: 'An Interactive Reference Table',
category: 'html',
has_children: false,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
},
{
id: 'block-v1:edX+DemoX+Demo_Course+type@discussion+block@6f7a6670f87147149caeff6afa07a526',
display_name: '',
category: 'discussion',
has_children: false,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
},
],
},
},
{
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@vertical_98cf62510471',
display_name: 'Zooming Diagrams',
category: 'vertical',
has_children: true,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
child_info: {
children: [
{
id: 'block-v1:edX+DemoX+Demo_Course+type@html+block@700x_pathways',
display_name: 'Zooming Diagrams',
category: 'html',
has_children: false,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
},
{
id: 'block-v1:edX+DemoX+Demo_Course+type@discussion+block@e0d7423118ab432582d03e8e8dad8e36',
display_name: '',
category: 'discussion',
has_children: false,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
},
],
},
},
{
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@vertical_d32bf9b2242c',
display_name: 'Electronic Sound Experiment',
category: 'vertical',
has_children: true,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
child_info: {
children: [
{
id: 'block-v1:edX+DemoX+Demo_Course+type@html+block@Lab_5B_Mosfet_Amplifier_Experiment',
display_name: 'Electronic Sound Experiment',
category: 'html',
has_children: false,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
},
{
id: 'block-v1:edX+DemoX+Demo_Course+type@discussion+block@03f051f9a8814881a3783d2511613aa6',
display_name: '',
category: 'discussion',
has_children: false,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
},
],
},
},
{
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@4e592689563243c484af947465eaef0d',
display_name: 'New Unit',
category: 'vertical',
has_children: true,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
child_info: {
children: [
{
id: 'block-v1:edX+DemoX+Demo_Course+type@video+block@af7fe1335eb841cd81ce31c7ee8eb069',
display_name: 'Video',
category: 'video',
has_children: false,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
},
],
},
},
],
},
},
{
id: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@graded_simulations',
display_name: 'Homework - Labs and Demos',
category: 'sequential',
has_children: true,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
child_info: {
category: 'vertical',
display_name: 'Unit',
children: [
{
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@d6cee45205a449369d7ef8f159b22bdf',
display_name: 'Labs and Demos',
category: 'vertical',
has_children: true,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
child_info: {
children: [
{
id: 'block-v1:edX+DemoX+Demo_Course+type@html+block@2bee8c4248e842a19ba1e73ed8d426c2',
display_name: 'Labs and Demos',
category: 'html',
has_children: false,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
},
],
},
},
{
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@vertical_aae927868e55',
display_name: 'Code Grader',
category: 'vertical',
has_children: true,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
child_info: {
children: [
{
id: 'block-v1:edX+DemoX+Demo_Course+type@html+block@891211e17f9a472290a5f12c7a6626d7',
display_name: 'Code Grader',
category: 'html',
has_children: false,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
},
{
id: 'block-v1:edX+DemoX+Demo_Course+type@problem+block@python_grader',
display_name: 'problem',
category: 'problem',
has_children: false,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
},
{
id: 'block-v1:edX+DemoX+Demo_Course+type@discussion+block@c6cd4bea43454aaea60ad01beb0cf213',
display_name: '',
category: 'discussion',
has_children: false,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
},
],
},
},
{
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@vertical_c037f3757df1',
display_name: 'Electric Circuit Simulator',
category: 'vertical',
has_children: true,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
child_info: {
children: [
{
id: 'block-v1:edX+DemoX+Demo_Course+type@html+block@d5a5caaf35e84ebc9a747038465dcfb4',
display_name: 'Electronic Circuit Simulator',
category: 'html',
has_children: false,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
},
{
id: 'block-v1:edX+DemoX+Demo_Course+type@problem+block@free_form_simulation',
display_name: 'problem',
category: 'problem',
has_children: false,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
},
{
id: 'block-v1:edX+DemoX+Demo_Course+type@problem+block@logic_gate_problem',
display_name: 'problem',
category: 'problem',
has_children: false,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
},
{
id: 'block-v1:edX+DemoX+Demo_Course+type@discussion+block@4f06b358a96f4d1dae57d6d81acd06f2',
display_name: '',
category: 'discussion',
has_children: false,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
},
],
},
},
{
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@vertical_bc69a47c6fae',
display_name: 'Protein Creator',
category: 'vertical',
has_children: true,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
child_info: {
children: [
{
id: 'block-v1:edX+DemoX+Demo_Course+type@html+block@78e3719e864e45f3bee938461f3c3de6',
display_name: 'Protein Builder',
category: 'html',
has_children: false,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
},
{
id: 'block-v1:edX+DemoX+Demo_Course+type@problem+block@700x_proteinmake',
display_name: 'Designing Proteins in Two Dimensions',
category: 'problem',
has_children: false,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
},
{
id: 'block-v1:edX+DemoX+Demo_Course+type@discussion+block@ed01bcd164e64038a78964a16eac3edc',
display_name: '',
category: 'discussion',
has_children: false,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
},
],
},
},
{
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@8f89194410954e768bde1764985454a7',
display_name: 'Molecule Structures',
category: 'vertical',
has_children: true,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
child_info: {
children: [
{
id: 'block-v1:edX+DemoX+Demo_Course+type@problem+block@9b9687073e904ae197799dc415df899f',
display_name: 'Molecule Structures',
category: 'problem',
has_children: false,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
},
],
},
},
],
},
},
{
id: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@175e76c4951144a29d46211361266e0e',
display_name: 'Homework - Essays',
category: 'sequential',
has_children: true,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
child_info: {
category: 'vertical',
display_name: 'Unit',
children: [
{
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@fb79dcbad35b466a8c6364f8ffee9050',
display_name: 'Peer Assessed Essays',
category: 'vertical',
has_children: true,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
child_info: {
children: [
{
id: 'block-v1:edX+DemoX+Demo_Course+type@openassessment+block@b24c33ea35954c7889e1d2944d3fe397',
display_name: 'Open Response Assessment',
category: 'openassessment',
has_children: false,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
},
{
id: 'block-v1:edX+DemoX+Demo_Course+type@discussion+block@12ad4f3ff4c14114a6e629b00e000976',
display_name: 'Peer Grading',
category: 'discussion',
has_children: false,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
},
],
},
},
],
},
},
],
},
},
{
id: 'block-v1:edX+DemoX+Demo_Course+type@chapter+block@social_integration',
display_name: 'Example Week 3: Be Social',
category: 'chapter',
has_children: true,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
child_info: {
category: 'sequential',
display_name: 'Subsection',
children: [
{
id: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@48ecb924d7fe4b66a230137626bfa93e',
display_name: 'Lesson 3 - Be Social',
category: 'sequential',
has_children: true,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
child_info: {
category: 'vertical',
display_name: 'Unit',
children: [
{
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@3c4b575924bf4b75a2f3542df5c354fc',
display_name: 'Be Social',
category: 'vertical',
has_children: true,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
child_info: {
children: [
{
id: 'block-v1:edX+DemoX+Demo_Course+type@html+block@f9f3a25e7bab46e583fd1fbbd7a2f6a0',
display_name: 'Be Social',
category: 'html',
has_children: false,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
},
],
},
},
{
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@vertical_3888db0bc286',
display_name: 'Discussion Forums',
category: 'vertical',
has_children: true,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
child_info: {
children: [
{
id: 'block-v1:edX+DemoX+Demo_Course+type@html+block@html_49b4494da2f7',
display_name: 'Discussion Forums',
category: 'html',
has_children: false,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
},
{
id: 'block-v1:edX+DemoX+Demo_Course+type@discussion+block@discussion_5deb6081620d',
display_name: 'Discussion Forums',
category: 'discussion',
has_children: false,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
},
],
},
},
{
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@312cb4faed17420e82ab3178fc3e251a',
display_name: 'Getting Help',
category: 'vertical',
has_children: true,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
child_info: {
children: [
{
id: 'block-v1:edX+DemoX+Demo_Course+type@html+block@8bb218cccf8d40519a971ff0e4901ccf',
display_name: 'Getting Help',
category: 'html',
has_children: false,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
},
],
},
},
{
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@7efc7bf4a47b4a6cb6595c32cde7712a',
display_name: 'Homework - Find Your Study Buddy',
category: 'vertical',
has_children: true,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
child_info: {
children: [
{
id: 'block-v1:edX+DemoX+Demo_Course+type@html+block@87fa6792d79f4862be098e5169e93339',
display_name: 'Blank HTML Page',
category: 'html',
has_children: false,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
},
],
},
},
],
},
},
{
id: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@dbe8fc027bcb4fe9afb744d2e8415855',
display_name: 'Homework - Find Your Study Buddy',
category: 'sequential',
has_children: true,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
child_info: {
category: 'vertical',
display_name: 'Unit',
children: [
{
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@26d89b08f75d48829a63520ed8b0037d',
display_name: 'Homework - Find Your Study Buddy',
category: 'vertical',
has_children: true,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
child_info: {
children: [
{
id: 'block-v1:edX+DemoX+Demo_Course+type@html+block@6018785795994726950614ce7d0f38c5',
display_name: 'Find Your Study Buddy',
category: 'html',
has_children: false,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
},
],
},
},
],
},
},
{
id: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@6ab9c442501d472c8ed200e367b4edfa',
display_name: 'More Ways to Connect',
category: 'sequential',
has_children: true,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
child_info: {
category: 'vertical',
display_name: 'Unit',
children: [
{
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@3f2c11aba9434e459676a7d7acc4d960',
display_name: 'Google Hangout',
category: 'vertical',
has_children: true,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
child_info: {
children: [
{
id: 'block-v1:edX+DemoX+Demo_Course+type@html+block@d45779ad3d024a40a09ad8cc317c0970',
display_name: 'Text',
category: 'html',
has_children: false,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
},
{
id: 'block-v1:edX+DemoX+Demo_Course+type@html+block@55cbc99f262443d886a25cf84594eafb',
display_name: 'Text',
category: 'html',
has_children: false,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
},
{
id: 'block-v1:edX+DemoX+Demo_Course+type@discussion+block@ade92343df3d4953a40ab3adc8805390',
display_name: 'Google Hangout',
category: 'discussion',
has_children: false,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
},
],
},
},
],
},
},
],
},
},
{
id: 'block-v1:edX+DemoX+Demo_Course+type@chapter+block@1414ffd5143b4b508f739b563ab468b7',
display_name: 'About Exams and Certificates',
category: 'chapter',
has_children: true,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
child_info: {
category: 'sequential',
display_name: 'Subsection',
children: [
{
id: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@workflow',
display_name: 'edX Exams',
category: 'sequential',
has_children: true,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
child_info: {
category: 'vertical',
display_name: 'Unit',
children: [
{
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@934cc32c177d41b580c8413e561346b3',
display_name: 'EdX Exams',
category: 'vertical',
has_children: true,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
child_info: {
children: [
{
id: 'block-v1:edX+DemoX+Demo_Course+type@html+block@8293139743f34377817d537b69911530',
display_name: 'EdX Exams',
category: 'html',
has_children: false,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
},
],
},
},
{
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@vertical_f04afeac0131',
display_name: 'Immediate Feedback',
category: 'vertical',
has_children: true,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
child_info: {
children: [
{
id: 'block-v1:edX+DemoX+Demo_Course+type@problem+block@ex_practice_2',
display_name: 'Immediate Feedback',
category: 'problem',
has_children: false,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
},
{
id: 'block-v1:edX+DemoX+Demo_Course+type@discussion+block@4aba537a78774bd5a862485a8563c345',
display_name: '',
category: 'discussion',
has_children: false,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
},
],
},
},
{
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@b6662b497c094bcc9b870d8270c90c93',
display_name: 'Getting Answers',
category: 'vertical',
has_children: true,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
child_info: {
children: [
{
id: 'block-v1:edX+DemoX+Demo_Course+type@problem+block@45d46192272c4f6db6b63586520bbdf4',
display_name: 'Getting Answers',
category: 'problem',
has_children: false,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
},
{
id: 'block-v1:edX+DemoX+Demo_Course+type@discussion+block@f480df4ce91347c5ae4301ddf6146238',
display_name: '',
category: 'discussion',
has_children: false,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
},
],
},
},
{
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@f91d8d31f7cf48ce990f8d8745ae4cfa',
display_name: 'Answering More Than Once',
category: 'vertical',
has_children: true,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
child_info: {
children: [
{
id: 'block-v1:edX+DemoX+Demo_Course+type@problem+block@651e0945b77f42e0a4c89b8c3e6f5b3b',
display_name: 'Answering More Than Once',
category: 'problem',
has_children: false,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
},
{
id: 'block-v1:edX+DemoX+Demo_Course+type@discussion+block@b8cec2a19ebf463f90cd3544c7927b0e',
display_name: '',
category: 'discussion',
has_children: false,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
},
],
},
},
{
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@vertical_ac391cde8a91',
display_name: 'Limited Checks',
category: 'vertical',
has_children: true,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
child_info: {
children: [
{
id: 'block-v1:edX+DemoX+Demo_Course+type@problem+block@ex_practice_limited_checks',
display_name: 'Limited Checks',
category: 'problem',
has_children: false,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
},
{
id: 'block-v1:edX+DemoX+Demo_Course+type@problem+block@d1b84dcd39b0423d9e288f27f0f7f242',
display_name: 'Few Checks',
category: 'problem',
has_children: false,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
},
{
id: 'block-v1:edX+DemoX+Demo_Course+type@discussion+block@cd177caa62444fbca48aa8f843f09eac',
display_name: '',
category: 'discussion',
has_children: false,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
},
],
},
},
{
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@vertical_36e0beb03f0a',
display_name: 'Randomized Questions',
category: 'vertical',
has_children: true,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
child_info: {
children: [
{
id: 'block-v1:edX+DemoX+Demo_Course+type@problem+block@ex_practice_3',
display_name: 'Randomized Questions',
category: 'problem',
has_children: false,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
},
{
id: 'block-v1:edX+DemoX+Demo_Course+type@discussion+block@ddede76df71045ffa16de9d1481d2119',
display_name: '',
category: 'discussion',
has_children: false,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
},
],
},
},
{
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@1b0e2c2c84884b95b1c99fb678cc964c',
display_name: 'Overall Grade Performance',
category: 'vertical',
has_children: true,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
child_info: {
children: [
{
id: 'block-v1:edX+DemoX+Demo_Course+type@html+block@f4a39219742149f781a1dda6f43a623c',
display_name: 'Overall Grade',
category: 'html',
has_children: false,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
},
{
id: 'block-v1:edX+DemoX+Demo_Course+type@discussion+block@1a810b1a3b2447b998f0917d0e5a802b',
display_name: '',
category: 'discussion',
has_children: false,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
},
],
},
},
{
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@c7e98fd39a6944edb6b286c32e1150ff',
display_name: 'Passing a Course',
category: 'vertical',
has_children: true,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
child_info: {
children: [
{
id: 'block-v1:edX+DemoX+Demo_Course+type@html+block@5e009378f0b64585baa0a14b155974b9',
display_name: 'Passing a Course',
category: 'html',
has_children: false,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
},
{
id: 'block-v1:edX+DemoX+Demo_Course+type@discussion+block@23e6eda482c04335af2bb265beacaf59',
display_name: '',
category: 'discussion',
has_children: false,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
},
],
},
},
{
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@d6eaa391d2be41dea20b8b1bfbcb1c45',
display_name: 'Getting Your edX Certificate',
category: 'vertical',
has_children: true,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
child_info: {
children: [
{
id: 'block-v1:edX+DemoX+Demo_Course+type@html+block@148ae8fa73ea460eb6f05505da0ba6e6',
display_name: 'Getting Your edX Certificate',
category: 'html',
has_children: false,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
},
{
id: 'block-v1:edX+DemoX+Demo_Course+type@html+block@6b6bee43c7c641509da71c9299cc9f5a',
display_name: 'Blank HTML Page',
category: 'html',
has_children: false,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
},
],
},
},
],
},
},
],
},
},
{
id: 'block-v1:edX+DemoX+Demo_Course+type@chapter+block@59666313a79946079f5ef4fff36e45f0',
display_name: 'IFrame',
category: 'chapter',
has_children: true,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
child_info: {
category: 'sequential',
display_name: 'Subsection',
children: [
{
id: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@f9fd819dfb224d118e4df4d46c648179',
display_name: 'Subsection',
category: 'sequential',
has_children: true,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
child_info: {
category: 'vertical',
display_name: 'Unit',
children: [
{
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@c8165538b5f04283879efc8e8deb2d92',
display_name: 'Iframe',
category: 'vertical',
has_children: true,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
child_info: {
children: [
{
id: 'block-v1:edX+DemoX+Demo_Course+type@html+block@fd3d0a72d0d344af9a53de144d83af1f',
display_name: 'IFrame Tool',
category: 'html',
has_children: false,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
},
],
},
},
],
},
},
{
id: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@a7deaeb85ee24470871c912536534a59',
display_name: 'Subsection',
category: 'sequential',
has_children: true,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
},
],
},
},
],
},
};

View File

@@ -4,3 +4,4 @@ export { default as courseUnitMock } from './courseUnit';
export { default as courseCreateXblockMock } from './courseCreateXblock';
export { default as courseVerticalChildrenMock } from './courseVerticalChildren';
export { default as clipboardMockResponse } from './clipboardResponse';
export { default as courseOutlineInfoMock } from './courseOutlineInfo';

View File

@@ -50,6 +50,8 @@ export const messageTypes = {
modal: 'plugin.modal',
resize: 'plugin.resize',
videoFullScreen: 'plugin.videoFullScreen',
refreshXBlock: 'refreshXBlock',
showMoveXBlockModal: 'showMoveXBlockModal',
};
export const IFRAME_FEATURE_POLICY = (

View File

@@ -0,0 +1,24 @@
import { ReactNode } from 'react';
import { renderHook } from '@testing-library/react-hooks';
import { useIframe } from './hooks';
import { IframeProvider } from './iFrameContext';
describe('useIframe hook', () => {
it('throws an error when used outside of IframeProvider', () => {
const { result } = renderHook(() => useIframe());
expect(result.error).toEqual(new Error('useIframe must be used within an IframeProvider'));
});
it('returns context value when used inside IframeProvider', () => {
const wrapper = ({ children }: { children: ReactNode }) => (
<IframeProvider>
{children}
</IframeProvider>
);
const { result } = renderHook(() => useIframe(), { wrapper });
expect(result.current).toHaveProperty('setIframeRef');
expect(result.current).toHaveProperty('sendMessageToIframe');
});
});

View File

@@ -0,0 +1,12 @@
import { useContext } from 'react';
import { IframeContext, IframeContextType } from './iFrameContext';
// eslint-disable-next-line import/prefer-default-export
export const useIframe = (): IframeContextType => {
const context = useContext(IframeContext);
if (!context) {
throw new Error('useIframe must be used within an IframeProvider');
}
return context;
};

View File

@@ -0,0 +1,42 @@
import {
createContext, MutableRefObject, useRef, useCallback, useMemo, ReactNode,
} from 'react';
import { logError } from '@edx/frontend-platform/logging';
export interface IframeContextType {
setIframeRef: (ref: MutableRefObject<HTMLIFrameElement | null>) => void;
sendMessageToIframe: (messageType: string, payload: unknown) => void;
}
export const IframeContext = createContext<IframeContextType | undefined>(undefined);
export const IframeProvider: React.FC = ({ children }: { children: ReactNode }) => {
const iframeRef = useRef<HTMLIFrameElement | null>(null);
const setIframeRef = useCallback((ref: MutableRefObject<HTMLIFrameElement | null>) => {
iframeRef.current = ref.current;
}, []);
const sendMessageToIframe = useCallback((messageType: string, payload: any) => {
const iframeWindow = iframeRef?.current?.contentWindow;
if (iframeWindow) {
try {
iframeWindow.postMessage({ type: messageType, payload }, '*');
} catch (error) {
logError('Failed to send message to iframe:', error);
}
} else {
logError('Iframe is not accessible or loaded yet.');
}
}, [iframeRef]);
const value = useMemo(() => ({
setIframeRef,
sendMessageToIframe,
}), [setIframeRef, sendMessageToIframe]);
return (
<IframeContext.Provider value={value}>
{children}
</IframeContext.Provider>
);
};

View File

@@ -11,6 +11,7 @@ export const getCourseUnitApiUrl = (itemId) => `${getStudioBaseUrl()}/xblock/con
export const getXBlockBaseApiUrl = (itemId) => `${getStudioBaseUrl()}/xblock/${itemId}`;
export const getCourseSectionVerticalApiUrl = (itemId) => `${getStudioBaseUrl()}/api/contentstore/v1/container_handler/${itemId}`;
export const getCourseVerticalChildrenApiUrl = (itemId) => `${getStudioBaseUrl()}/api/contentstore/v1/container/vertical/${itemId}/children`;
export const getCourseOutlineInfoUrl = (courseId) => `${getStudioBaseUrl()}/course/${courseId}?format=concise`;
export const postXBlockBaseApiUrl = () => `${getStudioBaseUrl()}/xblock/`;
/**
@@ -157,3 +158,51 @@ export async function duplicateUnitItem(itemId, XBlockId) {
return data;
}
/**
* @typedef {Object} courseOutline
* @property {string} id - The unique identifier of the course.
* @property {string} displayName - The display name of the course.
* @property {string} category - The category of the course (e.g., "course").
* @property {boolean} hasChildren - Whether the course has child items.
* @property {boolean} unitLevelDiscussions - Indicates if unit-level discussions are available.
* @property {Object} childInfo - Information about the child elements of the course.
* @property {string} childInfo.category - The category of the child (e.g., "chapter").
* @property {string} childInfo.display_name - The display name of the child element.
* @property {Array<Object>} childInfo.children - List of children within the child_info (could be empty).
*/
/**
* Get an object containing course outline data.
* @param {string} courseId - The identifier of the course.
* @returns {Promise<courseOutline>} - The course outline data.
*/
export async function getCourseOutlineInfo(courseId) {
const { data } = await getAuthenticatedHttpClient()
.get(getCourseOutlineInfoUrl(courseId));
return camelCaseObject(data);
}
/**
* @typedef {Object} moveInfo
* @property {string} moveSourceLocator - The locator of the source block being moved.
* @property {string} parentLocator - The locator of the parent block where the source is being moved to.
* @property {number} sourceIndex - The index position of the source block.
*/
/**
* Move a unit item to new unit.
* @param {string} sourceLocator - The ID of the item to be moved.
* @param {string} targetParentLocator - The ID of the XBlock associated with the item.
* @returns {Promise<moveInfo>} - The move information.
*/
export async function patchUnitItem(sourceLocator, targetParentLocator) {
const { data } = await getAuthenticatedHttpClient()
.patch(postXBlockBaseApiUrl(), {
parent_locator: targetParentLocator,
move_source_locator: sourceLocator,
});
return camelCaseObject(data);
}

View File

@@ -13,6 +13,9 @@ export const getCourseSectionVertical = (state) => state.courseUnit.courseSectio
export const getCourseId = (state) => state.courseDetail.courseId;
export const getSequenceId = (state) => state.courseUnit.sequenceId;
export const getCourseVerticalChildren = (state) => state.courseUnit.courseVerticalChildren;
export const getCourseOutlineInfo = (state) => state.courseUnit.courseOutlineInfo;
export const getCourseOutlineInfoLoadingStatus = (state) => state.courseUnit.courseOutlineInfoLoadingStatus;
export const getMovedXBlockParams = (state) => state.courseUnit.movedXBlockParams;
const getLoadingStatuses = (state) => state.courseUnit.loadingStatus;
export const getIsLoading = createSelector(
[getLoadingStatuses],

View File

@@ -20,6 +20,15 @@ const slice = createSlice({
courseSectionVertical: {},
courseVerticalChildren: { children: [], isPublished: true },
staticFileNotices: {},
courseOutlineInfo: {},
courseOutlineInfoLoadingStatus: RequestStatus.IN_PROGRESS,
movedXBlockParams: {
isSuccess: false,
isUndo: false,
title: '',
sourceLocator: '',
targetParentLocator: '',
},
},
reducers: {
fetchCourseItemSuccess: (state, { payload }) => {
@@ -103,6 +112,15 @@ const slice = createSlice({
fetchStaticFileNoticesSuccess: (state, { payload }) => {
state.staticFileNotices = payload;
},
updateCourseOutlineInfo: (state, { payload }) => {
state.courseOutlineInfo = payload;
},
updateCourseOutlineInfoLoadingStatus: (state, { payload }) => {
state.courseOutlineInfoLoadingStatus = payload.status;
},
updateMovedXBlockParams: (state, { payload }) => {
state.movedXBlockParams = { ...state.movedXBlockParams, ...payload };
},
},
});
@@ -124,6 +142,9 @@ export const {
deleteXBlock,
duplicateXBlock,
fetchStaticFileNoticesSuccess,
updateCourseOutlineInfo,
updateCourseOutlineInfoLoadingStatus,
updateMovedXBlockParams,
} = slice.actions;
export const {

View File

@@ -18,6 +18,8 @@ import {
handleCourseUnitVisibilityAndData,
deleteUnitItem,
duplicateUnitItem,
getCourseOutlineInfo,
patchUnitItem,
} from './api';
import {
updateLoadingCourseUnitStatus,
@@ -35,6 +37,9 @@ import {
deleteXBlock,
duplicateXBlock,
fetchStaticFileNoticesSuccess,
updateCourseOutlineInfo,
updateCourseOutlineInfoLoadingStatus,
updateMovedXBlockParams,
} from './slice';
import { getNotificationMessage } from './utils';
@@ -260,3 +265,56 @@ export function duplicateUnitItemQuery(itemId, xblockId) {
}
};
}
export function getCourseOutlineInfoQuery(courseId) {
return async (dispatch) => {
dispatch(updateCourseOutlineInfoLoadingStatus({ status: RequestStatus.IN_PROGRESS }));
try {
const result = await getCourseOutlineInfo(courseId);
if (result) {
dispatch(updateCourseOutlineInfo(result));
dispatch(updateCourseOutlineInfoLoadingStatus({ status: RequestStatus.SUCCESSFUL }));
}
} catch (error) {
handleResponseErrors(error, dispatch, updateSavingStatus);
dispatch(updateCourseOutlineInfoLoadingStatus({ status: RequestStatus.FAILED }));
}
};
}
export function patchUnitItemQuery({
sourceLocator = '',
targetParentLocator = '',
title,
currentParentLocator = '',
isMoving,
callbackFn,
}) {
return async (dispatch) => {
dispatch(updateSavingStatus({ status: RequestStatus.PENDING }));
dispatch(showProcessingNotification(NOTIFICATION_MESSAGES[isMoving ? 'moving' : 'undoMoving']));
try {
await patchUnitItem(sourceLocator, isMoving ? targetParentLocator : currentParentLocator);
const xBlockParams = {
title,
isSuccess: true,
isUndo: !isMoving,
sourceLocator,
targetParentLocator,
currentParentLocator,
};
dispatch(updateMovedXBlockParams(xBlockParams));
dispatch(updateCourseOutlineInfo({}));
dispatch(updateCourseOutlineInfoLoadingStatus({ status: RequestStatus.IN_PROGRESS }));
const courseUnit = await getCourseUnitData(currentParentLocator);
dispatch(fetchCourseItemSuccess(courseUnit));
callbackFn();
} catch (error) {
handleResponseErrors(error, dispatch, updateSavingStatus);
} finally {
dispatch(hideProcessingNotification());
}
};
}

View File

@@ -85,6 +85,7 @@ const HeaderTitle = ({
onClose={closeConfigureModal}
onConfigureSubmit={onConfigureSubmit}
currentItemData={currentItemData}
isSelfPaced={false}
/>
</div>
{getVisibilityMessage()}

View File

@@ -1,8 +1,10 @@
import { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { useNavigate, useSearchParams } from 'react-router-dom';
import { useToggle } from '@openedx/paragon';
import { RequestStatus } from '../data/constants';
import { useCopyToClipboard } from '../generic/clipboard';
import {
createNewCourseXBlock,
fetchCourseUnitQuery,
@@ -12,6 +14,8 @@ import {
deleteUnitItemQuery,
duplicateUnitItemQuery,
editCourseUnitVisibilityAndData,
getCourseOutlineInfoQuery,
patchUnitItemQuery,
} from './data/thunk';
import {
getCourseSectionVertical,
@@ -23,15 +27,22 @@ import {
getSequenceStatus,
getStaticFileNotices,
getCanEdit,
getCourseOutlineInfo,
getMovedXBlockParams,
} from './data/selectors';
import { changeEditTitleFormOpen, updateQueryPendingStatus } from './data/slice';
import { PUBLISH_TYPES } from './constants';
import { useCopyToClipboard } from '../generic/clipboard';
import {
changeEditTitleFormOpen,
updateQueryPendingStatus,
updateMovedXBlockParams,
} from './data/slice';
import { useIframe } from './context/hooks';
import { messageTypes, PUBLISH_TYPES } from './constants';
export const useCourseUnit = ({ courseId, blockId }) => {
const dispatch = useDispatch();
const [searchParams] = useSearchParams();
const { sendMessageToIframe } = useIframe();
const [isMoveModalOpen, openMoveModal, closeMoveModal] = useToggle(false);
const courseUnit = useSelector(getCourseUnitData);
const savingStatus = useSelector(getSavingStatus);
@@ -44,6 +55,8 @@ export const useCourseUnit = ({ courseId, blockId }) => {
const navigate = useNavigate();
const isTitleEditFormOpen = useSelector(state => state.courseUnit.isTitleEditFormOpen);
const canEdit = useSelector(getCanEdit);
const courseOutlineInfo = useSelector(getCourseOutlineInfo);
const movedXBlockParams = useSelector(getMovedXBlockParams);
const { currentlyVisibleToStudents } = courseUnit;
const { sharedClipboardData, showPasteXBlock, showPasteUnit } = useCopyToClipboard(canEdit);
const { canPasteComponent } = courseVerticalChildren;
@@ -112,6 +125,31 @@ export const useCourseUnit = ({ courseId, blockId }) => {
},
};
const handleRollbackMovedXBlock = () => {
const {
sourceLocator, targetParentLocator, title, currentParentLocator,
} = movedXBlockParams;
dispatch(patchUnitItemQuery({
sourceLocator,
targetParentLocator,
title,
currentParentLocator,
isMoving: false,
callbackFn: () => {
sendMessageToIframe(messageTypes.refreshXBlock, null);
window.scrollTo({ top: 0, behavior: 'smooth' });
},
}));
};
const handleCloseXBlockMovedAlert = () => {
dispatch(updateMovedXBlockParams({ isSuccess: false }));
};
const handleNavigateToTargetUnit = () => {
navigate(`/course/${courseId}/container/${movedXBlockParams.targetParentLocator}`);
};
useEffect(() => {
if (savingStatus === RequestStatus.SUCCESSFUL) {
dispatch(updateQueryPendingStatus(true));
@@ -124,8 +162,15 @@ export const useCourseUnit = ({ courseId, blockId }) => {
dispatch(fetchCourseVerticalChildrenData(blockId));
handleNavigate(sequenceId);
dispatch(updateMovedXBlockParams({ isSuccess: false }));
}, [courseId, blockId, sequenceId]);
useEffect(() => {
if (isMoveModalOpen && !Object.keys(courseOutlineInfo).length) {
dispatch(getCourseOutlineInfoQuery(courseId));
}
}, [isMoveModalOpen]);
return {
sequenceId,
courseUnit,
@@ -148,5 +193,12 @@ export const useCourseUnit = ({ courseId, blockId }) => {
handleConfigureSubmit,
courseVerticalChildren,
canPasteComponent,
isMoveModalOpen,
openMoveModal,
closeMoveModal,
handleRollbackMovedXBlock,
handleCloseXBlockMovedAlert,
movedXBlockParams,
handleNavigateToTargetUnit,
};
};

View File

@@ -1 +1,2 @@
export { default as CourseUnit } from './CourseUnit';
export { IframeProvider } from './context/iFrameContext';

View File

@@ -13,6 +13,36 @@ const messages = defineMessages({
id: 'course-authoring.course-unit.paste-component.btn.text',
defaultMessage: 'Paste component',
},
alertMoveSuccessTitle: {
id: 'course-authoring.course-unit.alert.xblock.move.success.title',
defaultMessage: 'Success!',
description: 'Title for the success alert when an XBlock is moved successfully',
},
alertMoveSuccessDescription: {
id: 'course-authoring.course-unit.alert.xblock.move.success.description',
defaultMessage: '{title} has been moved',
description: 'Description for the success alert when an XBlock is moved successfully',
},
alertMoveCancelTitle: {
id: 'course-authoring.course-unit.alert.xblock.move.cancel.title',
defaultMessage: 'Move cancelled',
description: 'Title for the alert when moving an XBlock is cancelled',
},
alertMoveCancelDescription: {
id: 'course-authoring.course-unit.alert.xblock.move.cancel.description',
defaultMessage: '{title} has been moved back to its original location',
description: 'Description for the alert when moving an XBlock is cancelled and the XBlock is moved back to its original location',
},
undoMoveButton: {
id: 'course-authoring.course-unit.alert.xblock.move.undo.btn.text',
defaultMessage: 'Undo move',
description: 'Text for the button allowing users to undo a move action of an XBlock',
},
newLocationButton: {
id: 'course-authoring.course-unit.alert.xblock.new.location.btn.text',
defaultMessage: 'Take me to the new location',
description: 'Text for the button allowing users to navigate to the new location after an XBlock has been moved',
},
});
export default messages;

View File

@@ -0,0 +1,26 @@
import { useIntl } from '@edx/frontend-platform/i18n';
import messages from '../messages';
const CategoryIndicator = ({ categoryText, displayName }: { categoryText: string, displayName: string }) => {
const intl = useIntl();
return (
<div className="xblock-items-category small text-gray-500">
<span className="sr-only">
{intl.formatMessage(messages.moveModalCategoryIndicatorAccessibilityText, {
categoryText,
displayName,
})}
</span>
<span
className="category-text"
aria-hidden="true"
data-testid="move-xblock-modal-category"
>
{categoryText}
</span>
</div>
);
};
export default CategoryIndicator;

View File

@@ -0,0 +1,17 @@
import { useIntl } from '@edx/frontend-platform/i18n';
import messages from '../messages';
const EmptyMessage = ({ category, categoryText }: { category: string, categoryText: string }) => {
const intl = useIntl();
return (
<li className="xblock-no-child-message">
{intl.formatMessage(messages.moveModalEmptyCategoryText, {
category,
categoryText: categoryText.toLowerCase(),
})}
</li>
);
};
export default EmptyMessage;

View File

@@ -0,0 +1,9 @@
import { LoadingSpinner } from '../../../generic/Loading';
const ModalLoader = () => (
<div className="move-xblock-modal-loading">
<LoadingSpinner />
</div>
);
export default ModalLoader;

View File

@@ -0,0 +1,3 @@
export { default as EmptyMessage } from './EmptyMessage';
export { default as ModalLoader } from './ModalLoader';
export { default as CategoryIndicator } from './CategoryIndicator';

View File

@@ -0,0 +1,41 @@
import messages from './messages';
export const CATEGORIES = {
TEXT: {
section: messages.moveModalBreadcrumbsSections,
subsection: messages.moveModalBreadcrumbsSubsections,
unit: messages.moveModalBreadcrumbsUnits,
component: messages.moveModalBreadcrumbsComponents,
group: messages.moveModalBreadcrumbsGroups,
},
KEYS: {
course: 'course',
chapter: 'chapter',
section: 'section',
sequential: 'sequential',
subsection: 'subsection',
vertical: 'vertical',
unit: 'unit',
component: 'component',
split_test: 'split_test',
group: 'group',
},
RELATION_MAP: {
course: 'section',
section: 'subsection',
subsection: 'unit',
unit: 'component',
},
} as const;
export const MOVE_DIRECTIONS = {
forward: 'forward',
backward: 'backward',
} as const;
export const BASIC_BLOCK_TYPES = [
'course',
'chapter',
'sequential',
'vertical',
] as const;

View File

@@ -0,0 +1,234 @@
import {
useCallback, useEffect, useState, useMemo,
} from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { useParams } from 'react-router-dom';
import { useMediaQuery } from 'react-responsive';
import { breakpoints } from '@openedx/paragon';
import { useIntl } from '@edx/frontend-platform/i18n';
import { RequestStatus } from '../../data/constants';
import { useEventListener } from '../../generic/hooks';
import { getCourseOutlineInfo, getCourseOutlineInfoLoadingStatus } from '../data/selectors';
import { getCourseOutlineInfoQuery, patchUnitItemQuery } from '../data/thunk';
import { useIframe } from '../context/hooks';
import { messageTypes } from '../constants';
import { CATEGORIES, MOVE_DIRECTIONS } from './constants';
import {
findParentIds, getBreadcrumbs, getXBlockType, isValidCategory,
} from './utils';
import {
IState, IUseMoveModalParams, IUseMoveModalReturn, IXBlockInfo,
} from './interfaces';
// eslint-disable-next-line import/prefer-default-export
export const useMoveModal = ({
isOpenModal, closeModal, openModal, courseId,
}: IUseMoveModalParams): IUseMoveModalReturn => {
const { blockId } = useParams<{ blockId: string }>();
const intl = useIntl();
const dispatch = useDispatch();
const { sendMessageToIframe } = useIframe();
const courseOutlineInfo = useSelector(getCourseOutlineInfo);
const courseOutlineInfoLoadingStatus = useSelector(getCourseOutlineInfoLoadingStatus);
const initialValues = useMemo<IState>(() => ({
childrenInfo: { children: courseOutlineInfo.childInfo?.children ?? [], category: CATEGORIES.KEYS.section },
parentInfo: { parent: courseOutlineInfo, category: CATEGORIES.KEYS.course },
isValidMove: false,
sourceXBlockInfo: { current: {} as IXBlockInfo, parent: {} as IXBlockInfo },
visitedAncestors: [courseOutlineInfo],
}), [courseOutlineInfo]);
const [state, setState] = useState<IState>(initialValues);
const isExtraSmall = useMediaQuery({ maxWidth: breakpoints.extraSmall.maxWidth });
const currentXBlockParentIds = useMemo(
() => findParentIds(courseOutlineInfo, state.sourceXBlockInfo.current.id as string),
[courseOutlineInfo, state.sourceXBlockInfo.current.id],
);
const receiveMessage = useCallback(({ data }: { data: any }) => {
const { payload, type } = data;
if (type === messageTypes.showMoveXBlockModal) {
setState((prevState) => ({
...prevState,
sourceXBlockInfo: {
current: payload.sourceXBlockInfo,
parent: payload.sourceParentXBlockInfo,
},
}));
openModal();
}
}, [openModal]);
useEventListener('message', receiveMessage);
const updateParentItemsData = useCallback((direction?: string, newParentIndex?: string) => {
setState((prevState: IState) => {
if (!direction) {
return {
...prevState,
parentInfo: {
parent: initialValues.parentInfo.parent,
category: initialValues.parentInfo.category,
},
visitedAncestors: [initialValues.parentInfo.parent],
};
}
if (
direction === MOVE_DIRECTIONS.forward && newParentIndex !== undefined
&& prevState.childrenInfo.children[newParentIndex]
) {
const newParent = prevState.childrenInfo.children[newParentIndex];
return {
...prevState,
parentInfo: {
parent: newParent,
category: prevState.parentInfo.category,
},
visitedAncestors: [...prevState.visitedAncestors, newParent],
};
}
if (
direction === MOVE_DIRECTIONS.backward && newParentIndex !== undefined
&& prevState.visitedAncestors[newParentIndex]
) {
return {
...prevState,
parentInfo: {
parent: prevState.visitedAncestors[newParentIndex],
category: prevState.parentInfo.category,
},
visitedAncestors: prevState.visitedAncestors.slice(0, parseInt(newParentIndex, 10) + 1),
};
}
return prevState;
});
}, [initialValues]);
const handleXBlockClick = useCallback((newParentIndex: string) => {
updateParentItemsData(MOVE_DIRECTIONS.forward, newParentIndex);
}, [updateParentItemsData]);
const handleBreadcrumbsClick = useCallback((newParentIndex: string) => {
updateParentItemsData(MOVE_DIRECTIONS.backward, newParentIndex);
}, [updateParentItemsData]);
const updateChildrenItemsData = useCallback(() => {
setState((prevState: IState) => ({
...prevState,
childrenInfo: {
...prevState.childrenInfo,
children: prevState.parentInfo.parent?.childInfo?.children || [],
},
}));
}, []);
const getCategoryText = useCallback(() => (
intl.formatMessage(CATEGORIES.TEXT[state.childrenInfo.category]) || ''
), [intl, state.childrenInfo.category]);
const breadcrumbs = useMemo(() => (
getBreadcrumbs(state.visitedAncestors, intl.formatMessage)
), [state.visitedAncestors]);
const setDisplayedXBlocksCategories = useCallback(() => {
setState((prevState) => {
const childCategory = CATEGORIES.KEYS.component;
const newParentCategory = getXBlockType(prevState.parentInfo.parent?.category || '');
if (prevState.parentInfo.category !== newParentCategory) {
return {
...prevState,
parentInfo: {
...prevState.parentInfo,
category: newParentCategory,
},
childrenInfo: {
...prevState.childrenInfo,
category: CATEGORIES.RELATION_MAP[newParentCategory] || childCategory,
},
};
}
return prevState;
});
}, []);
const handleCLoseModal = useCallback(() => {
setState(initialValues);
closeModal();
}, [initialValues, closeModal]);
const enableMoveOperation = useCallback((targetParentXBlockInfo: IXBlockInfo) => {
const isValid = isValidCategory(state.sourceXBlockInfo.parent, targetParentXBlockInfo)
&& state.sourceXBlockInfo.parent.id !== targetParentXBlockInfo.id // different parent
&& state.sourceXBlockInfo.current.id !== targetParentXBlockInfo.id; // different source item
setState((prevState) => ({
...prevState,
isValidMove: isValid,
}));
}, [isValidCategory, state.sourceXBlockInfo]);
const handleMoveXBlock = useCallback(() => {
const lastAncestor = state.visitedAncestors[state.visitedAncestors.length - 1];
dispatch(patchUnitItemQuery({
sourceLocator: state.sourceXBlockInfo.current.id,
targetParentLocator: lastAncestor.id,
title: state.sourceXBlockInfo.current.displayName,
currentParentLocator: blockId,
isMoving: true,
callbackFn: () => {
sendMessageToIframe(messageTypes.refreshXBlock, null);
closeModal();
window.scrollTo({ top: 0, behavior: 'smooth' });
},
}));
}, [state, dispatch, blockId, closeModal]);
useEffect(() => {
if (isOpenModal && !Object.keys(courseOutlineInfo).length) {
dispatch(getCourseOutlineInfoQuery(courseId));
}
}, [isOpenModal, courseOutlineInfo, courseId, dispatch]);
useEffect(() => {
if (isOpenModal && courseOutlineInfoLoadingStatus === RequestStatus.SUCCESSFUL) {
updateParentItemsData();
}
}, [courseOutlineInfoLoadingStatus, isOpenModal, updateParentItemsData]);
useEffect(() => {
if (isOpenModal && courseOutlineInfoLoadingStatus === RequestStatus.SUCCESSFUL) {
updateChildrenItemsData();
setDisplayedXBlocksCategories();
enableMoveOperation(state.parentInfo.parent);
}
}, [
state.parentInfo, isOpenModal, courseOutlineInfoLoadingStatus, updateChildrenItemsData,
setDisplayedXBlocksCategories, enableMoveOperation,
]);
return {
isLoading: courseOutlineInfoLoadingStatus === RequestStatus.IN_PROGRESS,
isValidMove: state.isValidMove,
isExtraSmall,
parentInfo: state.parentInfo,
childrenInfo: state.childrenInfo,
displayName: state.sourceXBlockInfo.current.displayName,
sourceXBlockId: state.sourceXBlockInfo.current.id,
categoryText: getCategoryText(),
breadcrumbs,
currentXBlockParentIds,
handleXBlockClick,
handleBreadcrumbsClick,
handleCLoseModal,
handleMoveXBlock,
};
};

View File

@@ -0,0 +1,79 @@
.move-xblock-modal {
max-width: 57.5rem;
.move-xblock-modal-loading {
min-height: 10rem;
display: flex;
align-items: center;
justify-content: center;
}
.pgn__modal-header,
.pgn__modal-footer {
z-index: 2;
}
.pgn__modal-header {
@include pgn-box-shadow(2, "centered");
}
.pgn__modal-footer {
@include pgn-box-shadow(2, "down");
}
.pgn__modal-body {
background: $white;
padding-left: 0;
padding-right: 0;
}
.pgn__breadcrumb {
border-bottom: 1px solid $light-300;
padding: map-get($spacers, 1) map-get($spacers, 4) $spacer;
.list-inline {
flex-wrap: wrap;
}
.list-inline-item {
&.active,
a.link-muted {
color: $dark-500;
}
a.link-muted {
cursor: pointer;
}
}
}
.xblock-items-category {
padding: $spacer map-get($spacers, 4) map-get($spacers, 2\.5);
}
.xblock-items-container {
list-style: none;
}
.xblock-item {
.btn,
.component {
display: flex;
border-radius: 0;
width: 100%;
gap: map-get($spacers, 2);
padding: .5625rem $spacer .5625rem map-get($spacers, 4);
}
.btn {
&:hover {
background: $light-300;
text-decoration: none;
}
}
}
.xblock-no-child-message {
text-align: center;
}
}

View File

@@ -0,0 +1,164 @@
import React, { FC, useCallback } from 'react';
import PropTypes from 'prop-types';
import { useIntl } from '@edx/frontend-platform/i18n';
import {
ActionRow,
Breadcrumb,
Button,
ModalDialog,
} from '@openedx/paragon';
import {
ArrowForwardIos as ArrowForwardIosIcon,
} from '@openedx/paragon/icons';
import { CATEGORIES } from './constants';
import { IUseMoveModalParams, IXBlock, IXBlockInfo } from './interfaces';
import { useMoveModal } from './hooks';
import { EmptyMessage, ModalLoader, CategoryIndicator } from './components';
import messages from './messages';
const MoveModal: FC<IUseMoveModalParams> = ({
isOpenModal, closeModal, openModal, courseId,
}) => {
const intl = useIntl();
const {
isLoading,
isValidMove,
isExtraSmall,
parentInfo,
childrenInfo,
displayName,
categoryText,
breadcrumbs,
sourceXBlockId,
currentXBlockParentIds,
handleXBlockClick,
handleBreadcrumbsClick,
handleCLoseModal,
handleMoveXBlock,
} = useMoveModal({
isOpenModal, closeModal, openModal, courseId,
});
const renderBreadcrumbs = useCallback(() => (
<Breadcrumb
ariaLabel={intl.formatMessage(messages.moveModalBreadcrumbsLabel)}
data-testid="move-xblock-modal-breadcrumbs"
isMobile={isExtraSmall}
links={breadcrumbs.slice(0, -1).map((breadcrumb, index) => (
{ label: breadcrumb, 'data-parent-index': index }
))}
activeLabel={breadcrumbs[breadcrumbs.length - 1]}
clickHandler={({ target }) => handleBreadcrumbsClick(target.dataset.parentIndex)}
/>
), [isExtraSmall, breadcrumbs, handleBreadcrumbsClick]);
const getCourseStructureItemButton = useCallback((xBlock: IXBlock, index: number) => (
<Button
variant="link"
className="button-forward text-left justify-content-start text-gray-700"
onClick={() => handleXBlockClick(index)}
>
<span className="xblock-display-name text-truncate">
{xBlock?.displayName}
</span>
{currentXBlockParentIds.includes(xBlock.id) && (
<span className="current-location text-nowrap mr-3">
{intl.formatMessage(messages.moveModalOutlineItemCurrentLocationText)}
</span>
)}
<ArrowForwardIosIcon className="ml-auto flex-shrink-0" />
<span className="sr-only">
{intl.formatMessage(messages.moveModalOutlineItemViewText)}
</span>
</Button>
), [currentXBlockParentIds, handleXBlockClick]);
const renderCourseStructureItemSpan = useCallback((xBlock: IXBlock) => (
<span className="component text-left justify-content-start text-gray-700">
<span className="xblock-display-name text-truncate">
{xBlock?.displayName}
</span>
{currentXBlockParentIds.includes(xBlock.id) && (
<span className="current-location text-nowrap mr-3">
{intl.formatMessage(messages.moveModalOutlineItemCurrentComponentLocationText)}
</span>
)}
</span>
), [currentXBlockParentIds]);
const renderCourseStructureListItem = useCallback((xBlock: IXBlock, index: number) => (
<li key={xBlock.id} className="xblock-item">
{sourceXBlockId !== xBlock.id && (xBlock?.childInfo || childrenInfo.category !== CATEGORIES.KEYS.component)
? getCourseStructureItemButton(xBlock, index)
: renderCourseStructureItemSpan(xBlock)}
</li>
), [sourceXBlockId, childrenInfo.category, getCourseStructureItemButton, renderCourseStructureItemSpan]);
return (
<ModalDialog
isOpen={isOpenModal}
onClose={handleCLoseModal}
size="xl"
className="move-xblock-modal"
hasCloseButton
isFullscreenOnMobile
>
<ModalDialog.Header>
<ModalDialog.Title>
{intl.formatMessage(messages.moveModalTitle, { displayName })}
</ModalDialog.Title>
</ModalDialog.Header>
<ModalDialog.Body>
{isLoading ? <ModalLoader /> : (
<>
{renderBreadcrumbs()}
<div className="xblock-list-container">
<CategoryIndicator
categoryText={categoryText}
displayName={displayName}
/>
<ul className="xblock-items-container p-0 m-0">
{!childrenInfo.children?.length
? (
<EmptyMessage
category={parentInfo.category}
categoryText={categoryText.toLowerCase()}
/>
)
: childrenInfo.children.map(
(xBlock: IXBlock | IXBlockInfo, index: number) => (
renderCourseStructureListItem(xBlock as IXBlock, index)
),
)}
</ul>
</div>
</>
)}
</ModalDialog.Body>
<ModalDialog.Footer>
<ActionRow>
<ModalDialog.CloseButton variant="tertiary">
{intl.formatMessage(messages.moveModalCancelButton)}
</ModalDialog.CloseButton>
<Button
disabled={!isValidMove}
onClick={handleMoveXBlock}
>
{intl.formatMessage(messages.moveModalSubmitButton)}
</Button>
</ActionRow>
</ModalDialog.Footer>
</ModalDialog>
);
};
MoveModal.propTypes = {
isOpenModal: PropTypes.bool.isRequired,
closeModal: PropTypes.func.isRequired,
openModal: PropTypes.func.isRequired,
courseId: PropTypes.string.isRequired,
};
export default MoveModal;

View File

@@ -0,0 +1,82 @@
export interface IXBlockInfo {
id: string;
displayName: string;
childInfo?: {
children?: IXBlockInfo[];
};
category?: string;
hasChildren?: boolean;
}
export interface IUseMoveModalParams {
isOpenModal: boolean;
closeModal: () => void;
openModal: () => void;
courseId: string;
}
export interface IUseMoveModalReturn {
isLoading: boolean;
isValidMove: boolean;
isExtraSmall: boolean;
parentInfo: {
parent: IXBlockInfo;
category: string;
};
childrenInfo: {
children: IXBlockInfo[];
category: string;
};
displayName: string;
sourceXBlockId: string;
categoryText: string;
breadcrumbs: string[];
currentXBlockParentIds: string[];
handleXBlockClick: (newParentIndex: string | number) => void;
handleBreadcrumbsClick: (newParentIndex: string | number) => void;
handleCLoseModal: () => void;
handleMoveXBlock: () => void;
}
export interface IState {
sourceXBlockInfo: {
current: IXBlockInfo;
parent: IXBlockInfo;
};
childrenInfo: {
children: IXBlockInfo[];
category: string;
};
parentInfo: {
parent: IXBlockInfo;
category: string;
};
visitedAncestors: IXBlockInfo[];
isValidMove: boolean;
}
export interface ITreeNode {
id: string;
childInfo?: {
children?: ITreeNode[];
};
}
export interface IAncestor {
category?: string;
displayName?: string;
}
export interface IXBlockChildInfo {
category?: string;
displayName?: string;
children?: IXBlock[];
}
export interface IXBlock {
id: string;
displayName: string;
category: string;
hasChildren: boolean;
childInfo?: IXBlockChildInfo;
}

View File

@@ -0,0 +1,81 @@
import { defineMessages } from '@edx/frontend-platform/i18n';
const messages = defineMessages({
moveModalTitle: {
id: 'course-authoring.course-unit.xblock.move.modal.title',
defaultMessage: 'Move: {displayName}',
description: 'Text for the move modal heading',
},
moveModalCancelButton: {
id: 'course-authoring.course-unit.xblock.move.modal.cancel.btn.text',
defaultMessage: 'Cancel',
description: 'Text for the button closing move modal of an XBlock',
},
moveModalSubmitButton: {
id: 'course-authoring.course-unit.xblock.move.modal.submit.btn.text',
defaultMessage: 'Move',
description: 'Text for the button submitting move modal of an XBlock',
},
moveModalBreadcrumbsBaseCategory: {
id: 'course-authoring.course-unit.xblock.move.modal.breadcrumbs.core.category.text',
defaultMessage: 'Course Outline',
description: 'Text for the core breadcrumbs item in move modal of an XBlock',
},
moveModalBreadcrumbsSections: {
id: 'course-authoring.course-unit.xblock.move.modal.breadcrumbs.sections.text',
defaultMessage: 'Sections',
description: 'Text for the sections breadcrumbs item in move modal of an XBlock',
},
moveModalBreadcrumbsSubsections: {
id: 'course-authoring.course-unit.xblock.move.modal.breadcrumbs.subsections.text',
defaultMessage: 'Subsections',
description: 'Text for the subsections breadcrumbs item in move modal of an XBlock',
},
moveModalBreadcrumbsUnits: {
id: 'course-authoring.course-unit.xblock.move.modal.breadcrumbs.units.text',
defaultMessage: 'Units',
description: 'Text for the units breadcrumbs item in move modal of an XBlock',
},
moveModalBreadcrumbsComponents: {
id: 'course-authoring.course-unit.xblock.move.modal.breadcrumbs.components.text',
defaultMessage: 'Components',
description: 'Text for the components breadcrumbs item in move modal of an XBlock',
},
moveModalBreadcrumbsGroups: {
id: 'course-authoring.course-unit.xblock.move.modal.breadcrumbs.groups.text',
defaultMessage: 'Groups',
description: 'Text for the groups breadcrumbs item in move modal of an XBlock',
},
moveModalBreadcrumbsLabel: {
id: 'course-authoring.course-unit.xblock.move.modal.breadcrumbs.label.text',
defaultMessage: 'Course Outline breadcrumb',
description: 'Text for the breadcrumbs label in move modal of an XBlock',
},
moveModalEmptyCategoryText: {
id: 'course-authoring.course-unit.xblock.move.modal.category.empty.text',
defaultMessage: 'This {category} has no {categoryText}',
description: 'Text for the category with empty children in move modal of an XBlock',
},
moveModalCategoryIndicatorAccessibilityText: {
id: 'course-authoring.course-unit.xblock.move.modal.category.accessibility.text',
defaultMessage: '{categoryText} in {displayName}',
description: 'Text for the category indicator accessibility in move modal of an XBlock',
},
moveModalOutlineItemCurrentLocationText: {
id: 'course-authoring.course-unit.xblock.move.modal.outline.item.location.text',
defaultMessage: '(Current location)',
description: 'Text for the outline item that indicates the current location in move modal of an XBlock',
},
moveModalOutlineItemCurrentComponentLocationText: {
id: 'course-authoring.course-unit.xblock.move.modal.outline.item.component.location.text',
defaultMessage: '(Currently selected)',
description: 'Text for the outline item that indicates the current component location in move modal of an XBlock',
},
moveModalOutlineItemViewText: {
id: 'course-authoring.course-unit.xblock.move.modal.outline.item.view.text',
defaultMessage: 'View child items',
description: 'Text for the outline item action description in move modal of an XBlock',
},
});
export default messages;

View File

@@ -0,0 +1,182 @@
import MockAdapter from 'axios-mock-adapter';
import { render, waitFor, within } from '@testing-library/react';
import { AppProvider } from '@edx/frontend-platform/react';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import { camelCaseObject, initializeMockApp } from '@edx/frontend-platform';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import userEvent from '@testing-library/user-event';
import initializeStore from '../../store';
import { getCourseOutlineInfoUrl } from '../data/api';
import { courseOutlineInfoMock } from '../__mocks__';
import { executeThunk } from '../../utils';
import { getCourseOutlineInfoQuery } from '../data/thunk';
import { IframeProvider } from '../context/iFrameContext';
import { IXBlock } from './interfaces';
import MoveModal from './index';
import messages from './messages';
let store;
let axiosMock: MockAdapter;
const courseId = '1234567890';
const closeModalMockFn = jest.fn() as jest.MockedFunction<() => void>;
const openModalMockFn = jest.fn() as jest.MockedFunction<() => void>;
const scrollToMockFn = jest.fn() as jest.MockedFunction<() => void>;
const sections: IXBlock[] | any = camelCaseObject(courseOutlineInfoMock)?.childInfo.children || [];
const subsections: IXBlock[] = sections[1]?.childInfo?.children || [];
const units: IXBlock[] = subsections[1]?.childInfo?.children || [];
const components: IXBlock[] = units[0]?.childInfo?.children || [];
const renderComponent = (props?: any) => render(
<AppProvider store={store}>
<IntlProvider locale="en">
<IframeProvider>
<MoveModal
isOpenModal
closeModal={closeModalMockFn}
openModal={openModalMockFn}
courseId={courseId}
{...props}
/>
</IframeProvider>
</IntlProvider>
</AppProvider>,
);
describe('<MoveModal />', () => {
beforeEach(async () => {
initializeMockApp({
authenticatedUser: {
userId: 3,
username: 'abc123',
administrator: true,
roles: [],
},
});
window.scrollTo = scrollToMockFn;
store = initializeStore();
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
axiosMock
.onGet(getCourseOutlineInfoUrl(courseId))
.reply(200, courseOutlineInfoMock);
await executeThunk(getCourseOutlineInfoQuery(courseId), store.dispatch);
});
it('renders loading indicator correctly', async () => {
axiosMock
.onGet(getCourseOutlineInfoUrl(courseId))
.reply(200, null);
await executeThunk(getCourseOutlineInfoQuery(courseId), store.dispatch);
const { getByText } = renderComponent();
expect(getByText('Loading...')).toBeInTheDocument();
});
it('renders component properly', () => {
const { getByText, getByRole, getByTestId } = renderComponent();
const breadcrumbs: HTMLElement = getByTestId('move-xblock-modal-breadcrumbs');
const categoryIndicator: HTMLElement = getByTestId('move-xblock-modal-category');
expect(getByText(messages.moveModalTitle.defaultMessage.replace(' {displayName}', ''))).toBeInTheDocument();
expect(within(breadcrumbs).getByText(messages.moveModalBreadcrumbsBaseCategory.defaultMessage)).toBeInTheDocument();
expect(
within(categoryIndicator).getByText(messages.moveModalBreadcrumbsSections.defaultMessage),
).toBeInTheDocument();
expect(getByRole('button', { name: messages.moveModalSubmitButton.defaultMessage })).toBeInTheDocument();
expect(getByRole('button', { name: messages.moveModalCancelButton.defaultMessage })).toBeInTheDocument();
userEvent.click(getByRole('button', { name: messages.moveModalCancelButton.defaultMessage }));
expect(closeModalMockFn).toHaveBeenCalledTimes(1);
});
it('correctly navigates through the structure list', async () => {
const { getByText, getByRole, getByTestId } = renderComponent();
const breadcrumbs: HTMLElement = getByTestId('move-xblock-modal-breadcrumbs');
const categoryIndicator: HTMLElement = getByTestId('move-xblock-modal-category');
expect(within(breadcrumbs).getByText(messages.moveModalBreadcrumbsBaseCategory.defaultMessage)).toBeInTheDocument();
expect(
within(categoryIndicator).getByText(messages.moveModalBreadcrumbsSections.defaultMessage),
).toBeInTheDocument();
sections.forEach((section) => {
expect(getByText(section.displayName)).toBeInTheDocument();
});
userEvent.click(getByRole('button', { name: new RegExp(sections[1].displayName, 'i') }));
await waitFor(() => {
expect(
within(categoryIndicator).getByText(messages.moveModalBreadcrumbsSubsections.defaultMessage),
).toBeInTheDocument();
expect(within(breadcrumbs).getByText(sections[1].displayName)).toBeInTheDocument();
subsections.forEach((subsection) => {
expect(getByRole('button', { name: new RegExp(subsection.displayName, 'i') })).toBeInTheDocument();
});
});
userEvent.click(getByRole('button', { name: new RegExp(subsections[1].displayName, 'i') }));
await waitFor(() => {
expect(
within(categoryIndicator).getByText(messages.moveModalBreadcrumbsUnits.defaultMessage),
).toBeInTheDocument();
expect(within(breadcrumbs).getByText(subsections[1].displayName)).toBeInTheDocument();
units.forEach((unit) => {
expect(getByRole('button', { name: new RegExp(unit.displayName, 'i') })).toBeInTheDocument();
});
});
userEvent.click(getByRole('button', { name: new RegExp(units[0].displayName, 'i') }));
await waitFor(() => {
expect(
within(categoryIndicator).getByText(messages.moveModalBreadcrumbsComponents.defaultMessage),
).toBeInTheDocument();
expect(within(breadcrumbs).getByText(units[0].displayName)).toBeInTheDocument();
components.forEach((component) => {
if (component.displayName) {
expect(getByText(component.displayName)).toBeInTheDocument();
}
});
});
});
it('correctly navigates using breadcrumbs', async () => {
const { getByRole, getByTestId } = renderComponent();
const breadcrumbs: HTMLElement = getByTestId('move-xblock-modal-breadcrumbs');
const categoryIndicator: HTMLElement = getByTestId('move-xblock-modal-category');
await waitFor(() => {
userEvent.click(getByRole('button', { name: new RegExp(sections[1].displayName, 'i') }));
userEvent.click(getByRole('button', { name: new RegExp(subsections[1].displayName, 'i') }));
userEvent.click(within(breadcrumbs).getByText(sections[1].displayName));
});
await waitFor(() => {
expect(
within(categoryIndicator).getByText(messages.moveModalBreadcrumbsSubsections.defaultMessage),
).toBeInTheDocument();
expect(within(breadcrumbs).getByText(sections[1].displayName)).toBeInTheDocument();
subsections.forEach((subsection) => (
expect(getByRole('button', { name: new RegExp(subsection.displayName, 'i') })).toBeInTheDocument()
));
});
});
it('renders empty message when no components are provided', async () => {
const { getByText, getByRole } = renderComponent();
await waitFor(() => {
userEvent.click(getByRole('button', { name: new RegExp(sections[1].displayName, 'i') }));
userEvent.click(getByRole('button', { name: new RegExp(subsections[1].displayName, 'i') }));
});
await waitFor(() => {
const unitBtn = getByRole('button', { name: new RegExp(units[7].displayName, 'i') });
userEvent.click(unitBtn);
});
await waitFor(() => {
expect(getByText(
messages.moveModalEmptyCategoryText.defaultMessage
.replace('{category}', 'unit')
.replace('{categoryText}', 'components'),
)).toBeInTheDocument();
});
});
});

View File

@@ -0,0 +1,175 @@
import { CATEGORIES } from './constants';
import { ITreeNode, IXBlockInfo, IAncestor } from './interfaces';
import {
getXBlockType, findParentIds, isValidCategory, getBreadcrumbs,
} from './utils';
import messages from './messages';
const mockFormatMessage = jest.fn((message) => message.defaultMessage);
const tree: ITreeNode = {
id: 'root',
childInfo: {
children: [
{
id: 'child-1',
childInfo: {
children: [
{
id: 'grandchild-1',
childInfo: {
children: [],
},
},
{
id: 'grandchild-2',
childInfo: {
children: [],
},
},
],
},
},
{
id: 'child-2',
childInfo: {
children: [],
},
},
],
},
};
describe('getXBlockType utility', () => {
it('returns section for chapter category', () => {
const result = getXBlockType(CATEGORIES.KEYS.chapter);
expect(result).toBe(CATEGORIES.KEYS.section);
});
it('returns subsection for sequential category', () => {
const result = getXBlockType(CATEGORIES.KEYS.sequential);
expect(result).toBe(CATEGORIES.KEYS.subsection);
});
it('returns unit for vertical category', () => {
const result = getXBlockType(CATEGORIES.KEYS.vertical);
expect(result).toBe(CATEGORIES.KEYS.unit);
});
it('returns the same category if no match is found', () => {
const customCategory = 'custom-category';
const result = getXBlockType(customCategory);
expect(result).toBe(customCategory);
});
});
describe('findParentIds utility', () => {
it('returns path to target ID in the tree', () => {
const result = findParentIds(tree, 'grandchild-2');
expect(result).toEqual(['root', 'child-1', 'grandchild-2']);
});
it('returns empty array if target ID is not found', () => {
const result = findParentIds(tree, 'non-existent-id');
expect(result).toEqual([]);
});
it('returns path with only root when target ID is the root', () => {
const result = findParentIds(tree, 'root');
expect(result).toEqual(['root']);
});
it('returns empty array if tree is undefined', () => {
const result = findParentIds(undefined, 'some-id');
expect(result).toEqual([]);
});
});
describe('isValidCategory utility', () => {
const sourceParentInfo: IXBlockInfo = {
displayName: 'test-source-parent-name',
id: '12345',
category: 'chapter',
hasChildren: true,
};
const targetParentInfo: IXBlockInfo = {
displayName: 'test-target-parent-name',
id: '67890',
category: 'chapter',
hasChildren: true,
};
it('returns true when target and source categories are the same', () => {
const result = isValidCategory(sourceParentInfo, targetParentInfo);
expect(result).toBe(true);
});
it('returns false when categories are different', () => {
const result = isValidCategory(sourceParentInfo, { ...targetParentInfo, category: 'unit' });
expect(result).toBe(false);
});
it('converts source category to vertical if it has children and is not basic block type', () => {
const result = isValidCategory(
{ ...sourceParentInfo, category: 'section' },
{ ...targetParentInfo, category: 'vertical' },
);
expect(result).toBe(true);
});
it('converts target category to vertical if it has children and is not basic block type or split_test', () => {
const result = isValidCategory(
{ ...sourceParentInfo, category: 'vertical' },
{ ...targetParentInfo, category: 'section' },
);
expect(result).toBe(true);
});
it('returns false when categories are different after conversion', () => {
const result = isValidCategory(
{ ...sourceParentInfo, category: 'chapter' },
{ ...targetParentInfo, category: 'section' },
);
expect(result).toBe(false);
});
});
describe('getBreadcrumbs utility', () => {
it('returns correct breadcrumb labels for visited ancestors', () => {
const visitedAncestors: IAncestor[] = [
{ category: 'chapter', displayName: 'Chapter 1' },
{ category: 'section', displayName: 'Section 1' },
];
const result = getBreadcrumbs(visitedAncestors, mockFormatMessage);
expect(result).toEqual(['Chapter 1', 'Section 1']);
});
it('returns base category label when category is course', () => {
const visitedAncestors: IAncestor[] = [
{ category: CATEGORIES.KEYS.course, displayName: 'Course Name' },
];
const result = getBreadcrumbs(visitedAncestors, mockFormatMessage);
expect(result).toEqual(['Course Outline']);
expect(mockFormatMessage).toHaveBeenCalledWith(messages.moveModalBreadcrumbsBaseCategory);
});
it('returns empty string if displayName is missing', () => {
const visitedAncestors: IAncestor[] = [
{ category: 'chapter', displayName: '' },
];
const result = getBreadcrumbs(visitedAncestors, mockFormatMessage);
expect(result).toEqual(['']);
});
it('returns empty array if visitedAncestors is not an array', () => {
const result = getBreadcrumbs(undefined as any, mockFormatMessage);
expect(result).toEqual([]);
});
});

View File

@@ -0,0 +1,116 @@
import { BASIC_BLOCK_TYPES, CATEGORIES } from './constants';
import { ITreeNode, IXBlockInfo, IAncestor } from './interfaces';
import messages from './messages';
/**
* Determines the XBlock type based on the provided category and parent information.
*
* @param {string} category - The category of the XBlock (e.g., 'chapter', 'sequential', 'vertical').
* @returns {string} - The determined XBlock type (e.g., 'section', 'subsection', 'unit').
*/
export const getXBlockType = (category: string): string => {
const categoryMap: { [key: string]: string } = {
[CATEGORIES.KEYS.chapter]: CATEGORIES.KEYS.section,
[CATEGORIES.KEYS.sequential]: CATEGORIES.KEYS.subsection,
[CATEGORIES.KEYS.vertical]: CATEGORIES.KEYS.unit,
};
return categoryMap[category] || category;
};
/**
* Recursively finds the parent IDs of the target ID in a hierarchical object structure.
* It returns an array of IDs leading to the target, including the target's own ID.
*
* @param {Object} tree - The hierarchical object to search through.
* @param {string} targetId - The ID of the target element for which to find the parent IDs.
* @returns {string[]} - An array of IDs representing the path from the root to the target element.
*/
export const findParentIds = (
tree: ITreeNode | undefined,
targetId: string,
): string[] => {
let path: string[] = [];
function traverse(node: ITreeNode | undefined, id: string, currentPath: string[]): boolean {
if (!node) {
return false;
}
currentPath.push(node.id);
if (node.id === id) {
path = currentPath.slice();
return true;
}
for (const child of node.childInfo?.children ?? []) {
if (traverse(child, id, currentPath)) {
return true;
}
}
currentPath.pop();
return false;
}
traverse(tree, targetId, []);
return path;
};
/**
* Checks if the target category is valid for moving.
* @param {Object} sourceParentInfo - Current parent information.
* @param {Object} targetParentInfo - Target parent information.
* @returns {boolean} - Returns true if moving is valid.
*/
export const isValidCategory = (
sourceParentInfo: IXBlockInfo,
targetParentInfo: IXBlockInfo,
): boolean => {
let { category: sourceParentCategory } = sourceParentInfo;
let { category: targetParentCategory } = targetParentInfo;
const { hasChildren: sourceParentHasChildren } = sourceParentInfo;
const { hasChildren: targetParentHasChildren } = targetParentInfo;
if (
sourceParentHasChildren
&& sourceParentCategory
&& !(BASIC_BLOCK_TYPES as readonly string[]).includes(sourceParentCategory)
) {
sourceParentCategory = CATEGORIES.KEYS.vertical;
}
if (
targetParentHasChildren
&& targetParentCategory
&& !(BASIC_BLOCK_TYPES as readonly string[]).includes(targetParentCategory)
&& targetParentCategory !== CATEGORIES.KEYS.split_test
) {
targetParentCategory = CATEGORIES.KEYS.vertical;
}
return targetParentCategory === sourceParentCategory;
};
/**
* Builds breadcrumbs based on visited ancestors.
* @param {Array} visitedAncestors - Array of ancestors.
* @param {Function} formatMessage - Intl formatting function.
* @returns {Array} - Array of breadcrumb elements.
*/
export const getBreadcrumbs = (
visitedAncestors: IAncestor[],
formatMessage: any,
): string[] => {
if (!Array.isArray(visitedAncestors)) {
return [];
}
return visitedAncestors.map((ancestor) => {
if (ancestor?.category === CATEGORIES.KEYS.course) {
return formatMessage(messages.moveModalBreadcrumbsBaseCategory);
}
return ancestor?.displayName || '';
});
};

View File

@@ -1,9 +1,10 @@
import { useRef, FC } from 'react';
import { useRef, useEffect, FC } from 'react';
import PropTypes from 'prop-types';
import { useIntl } from '@edx/frontend-platform/i18n';
import { getConfig } from '@edx/frontend-platform';
import { IFRAME_FEATURE_POLICY } from '../constants';
import { useIframe } from '../context/hooks';
import { useIFrameBehavior } from './hooks';
import messages from './messages';
@@ -20,6 +21,7 @@ interface XBlockContainerIframeProps {
const XBlockContainerIframe: FC<XBlockContainerIframeProps> = ({ blockId }) => {
const intl = useIntl();
const iframeRef = useRef<HTMLIFrameElement>(null);
const { setIframeRef } = useIframe();
const iframeUrl = `${getConfig().STUDIO_BASE_URL}/container_embed/${blockId}`;
@@ -28,6 +30,10 @@ const XBlockContainerIframe: FC<XBlockContainerIframeProps> = ({ blockId }) => {
iframeUrl,
});
useEffect(() => {
setIframeRef(iframeRef);
}, [setIframeRef]);
return (
<iframe
ref={iframeRef}

View File

@@ -5,6 +5,7 @@ import { IntlProvider } from '@edx/frontend-platform/i18n';
import { IFRAME_FEATURE_POLICY } from '../../constants';
import { useIFrameBehavior } from '../hooks';
import XBlockContainerIframe from '..';
import { IframeProvider } from '../../context/iFrameContext';
jest.mock('@edx/frontend-platform', () => ({
getConfig: jest.fn(),
@@ -27,7 +28,9 @@ describe('<XBlockContainerIframe />', () => {
it('renders correctly with the given blockId', () => {
const { getByTitle } = render(
<IntlProvider locale="en">
<XBlockContainerIframe blockId={blockId} />
<IframeProvider>
<XBlockContainerIframe blockId={blockId} />
</IframeProvider>
</IntlProvider>,
);
const iframe = getByTitle('Course unit iframe');