feat: [FC-0070] implement move xblock modal (#1422)
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 },
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
1683
src/course-unit/__mocks__/courseOutlineInfo.js
Normal file
1683
src/course-unit/__mocks__/courseOutlineInfo.js
Normal 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',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
@@ -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';
|
||||
|
||||
@@ -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 = (
|
||||
|
||||
24
src/course-unit/context/hooks.test.tsx
Normal file
24
src/course-unit/context/hooks.test.tsx
Normal 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');
|
||||
});
|
||||
});
|
||||
12
src/course-unit/context/hooks.tsx
Normal file
12
src/course-unit/context/hooks.tsx
Normal 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;
|
||||
};
|
||||
42
src/course-unit/context/iFrameContext.tsx
Normal file
42
src/course-unit/context/iFrameContext.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -85,6 +85,7 @@ const HeaderTitle = ({
|
||||
onClose={closeConfigureModal}
|
||||
onConfigureSubmit={onConfigureSubmit}
|
||||
currentItemData={currentItemData}
|
||||
isSelfPaced={false}
|
||||
/>
|
||||
</div>
|
||||
{getVisibilityMessage()}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
export { default as CourseUnit } from './CourseUnit';
|
||||
export { IframeProvider } from './context/iFrameContext';
|
||||
|
||||
@@ -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;
|
||||
|
||||
26
src/course-unit/move-modal/components/CategoryIndicator.tsx
Normal file
26
src/course-unit/move-modal/components/CategoryIndicator.tsx
Normal 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;
|
||||
17
src/course-unit/move-modal/components/EmptyMessage.tsx
Normal file
17
src/course-unit/move-modal/components/EmptyMessage.tsx
Normal 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;
|
||||
9
src/course-unit/move-modal/components/ModalLoader.tsx
Normal file
9
src/course-unit/move-modal/components/ModalLoader.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
import { LoadingSpinner } from '../../../generic/Loading';
|
||||
|
||||
const ModalLoader = () => (
|
||||
<div className="move-xblock-modal-loading">
|
||||
<LoadingSpinner />
|
||||
</div>
|
||||
);
|
||||
|
||||
export default ModalLoader;
|
||||
3
src/course-unit/move-modal/components/index.ts
Normal file
3
src/course-unit/move-modal/components/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export { default as EmptyMessage } from './EmptyMessage';
|
||||
export { default as ModalLoader } from './ModalLoader';
|
||||
export { default as CategoryIndicator } from './CategoryIndicator';
|
||||
41
src/course-unit/move-modal/constants.ts
Normal file
41
src/course-unit/move-modal/constants.ts
Normal 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;
|
||||
234
src/course-unit/move-modal/hooks.tsx
Normal file
234
src/course-unit/move-modal/hooks.tsx
Normal 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,
|
||||
};
|
||||
};
|
||||
79
src/course-unit/move-modal/index.scss
Normal file
79
src/course-unit/move-modal/index.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
164
src/course-unit/move-modal/index.tsx
Normal file
164
src/course-unit/move-modal/index.tsx
Normal 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;
|
||||
82
src/course-unit/move-modal/interfaces.ts
Normal file
82
src/course-unit/move-modal/interfaces.ts
Normal 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;
|
||||
}
|
||||
81
src/course-unit/move-modal/messages.ts
Normal file
81
src/course-unit/move-modal/messages.ts
Normal 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;
|
||||
182
src/course-unit/move-modal/moveModal.test.tsx
Normal file
182
src/course-unit/move-modal/moveModal.test.tsx
Normal 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();
|
||||
});
|
||||
});
|
||||
});
|
||||
175
src/course-unit/move-modal/utils.test.ts
Normal file
175
src/course-unit/move-modal/utils.test.ts
Normal 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([]);
|
||||
});
|
||||
});
|
||||
116
src/course-unit/move-modal/utils.ts
Normal file
116
src/course-unit/move-modal/utils.ts
Normal 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 || '';
|
||||
});
|
||||
};
|
||||
@@ -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}
|
||||
|
||||
@@ -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');
|
||||
|
||||
Reference in New Issue
Block a user