Compare commits

..

1 Commits

Author SHA1 Message Date
Saad Yousaf
f81670547b fix: remove unnecessary call to fetch courses on course authoring page 2025-03-05 20:25:35 +05:00
430 changed files with 7031 additions and 19182 deletions

4
.env
View File

@@ -44,6 +44,4 @@ INVITE_STUDENTS_EMAIL_TO=''
ENABLE_HOME_PAGE_COURSE_API_V2=true
ENABLE_CHECKLIST_QUALITY=''
ENABLE_GRADING_METHOD_IN_PROBLEMS=false
# "Multi-level" blocks are unsupported in libraries
# TODO: Missing support for ORA2
LIBRARY_UNSUPPORTED_BLOCKS="conditional,step-builder,problem-builder,openassessment"
LIBRARY_SUPPORTED_BLOCKS="problem,video,html,drag-and-drop-v2"

View File

@@ -47,5 +47,4 @@ INVITE_STUDENTS_EMAIL_TO="someone@domain.com"
ENABLE_HOME_PAGE_COURSE_API_V2=true
ENABLE_CHECKLIST_QUALITY=true
ENABLE_GRADING_METHOD_IN_PROBLEMS=false
# "Multi-level" blocks are unsupported in libraries
LIBRARY_UNSUPPORTED_BLOCKS="conditional,step-builder,problem-builder"
LIBRARY_SUPPORTED_BLOCKS="problem,video,html,drag-and-drop-v2"

View File

@@ -39,6 +39,5 @@ INVITE_STUDENTS_EMAIL_TO="someone@domain.com"
ENABLE_HOME_PAGE_COURSE_API_V2=true
ENABLE_CHECKLIST_QUALITY=true
ENABLE_GRADING_METHOD_IN_PROBLEMS=false
# "Multi-level" blocks are unsupported in libraries
# TODO: Missing support for ORA2
LIBRARY_UNSUPPORTED_BLOCKS="conditional,step-builder,problem-builder,openassessment"
# "other" is used to test the workflow for creating blocks that aren't supported by the built-in editors
LIBRARY_SUPPORTED_BLOCKS="problem,video,html,drag-and-drop-v2,other"

View File

@@ -38,7 +38,7 @@ Cloning and Setup
git clone https://github.com/openedx/frontend-app-authoring.git
2. Use the version of Node specified in the ``.nvmrc`` file.
2. Use node v20.x.
The current version of the micro-frontend build scripts supports node 20.
Using other major versions of node *may* work, but this is unsupported. For
@@ -315,7 +315,7 @@ In additional to the standard settings, the following local configurations can b
Developing
**********
`Tutor <https://docs.tutor.edly.io/>`_ is the community-supported Open edX development environment. See the `tutor-mfe plugin README <https://github.com/overhangio/tutor-mfe?tab=readme-ov-file#mfe-development>`_ for more information.
`Devstack <https://edx.readthedocs.io/projects/edx-installing-configuring-and-running/en/latest/installation/index.html>`_. If you start Devstack with ``make dev.up.studio`` that should give you everything you need as a companion to this frontend.
If your devstack includes the default Demo course, you can visit the following URLs to see content:

9403
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -23,6 +23,11 @@
"test:ci": "TZ=UTC fedx-scripts jest --silent --coverage --passWithNoTests",
"types": "tsc --noEmit"
},
"husky": {
"hooks": {
"pre-commit": "npm run lint"
}
},
"author": "edX",
"license": "AGPL-3.0",
"homepage": "https://github.com/openedx/frontend-app-authoring#readme",
@@ -44,10 +49,10 @@
"@dnd-kit/utilities": "^3.2.2",
"@edx/brand": "npm:@openedx/brand-openedx@^1.2.3",
"@edx/browserslist-config": "1.2.0",
"@edx/frontend-component-footer": "^14.3.0",
"@edx/frontend-component-header": "^6.2.0",
"@edx/frontend-enterprise-hotjar": "^7.2.0",
"@edx/frontend-platform": "^8.3.1",
"@edx/frontend-component-footer": "^14.1.0",
"@edx/frontend-component-header": "^5.8.3",
"@edx/frontend-enterprise-hotjar": "^2.0.0",
"@edx/frontend-platform": "^8.0.3",
"@edx/openedx-atlas": "^0.6.0",
"@openedx-plugins/course-app-calculator": "file:plugins/course-apps/calculator",
"@openedx-plugins/course-app-edxnotes": "file:plugins/course-apps/edxnotes",
@@ -59,10 +64,9 @@
"@openedx-plugins/course-app-teams": "file:plugins/course-apps/teams",
"@openedx-plugins/course-app-wiki": "file:plugins/course-apps/wiki",
"@openedx-plugins/course-app-xpert_unit_summary": "file:plugins/course-apps/xpert_unit_summary",
"@openedx/frontend-build": "^14.3.3",
"@openedx/frontend-plugin-framework": "^1.6.0",
"@openedx/frontend-slot-footer": "^1.2.0",
"@openedx/paragon": "^22.16.0",
"@openedx/frontend-build": "^14.2.0",
"@openedx/frontend-plugin-framework": "^1.2.1",
"@openedx/paragon": "^22.8.1",
"@redux-devtools/extension": "^3.3.0",
"@reduxjs/toolkit": "1.9.7",
"@tanstack/react-query": "4.36.1",
@@ -81,9 +85,9 @@
"moment-shortformat": "^2.1.0",
"npm": "^10.8.1",
"prop-types": "^15.8.1",
"react": "^18.3.1",
"react": "17.0.2",
"react-datepicker": "^4.13.0",
"react-dom": "^18.3.1",
"react-dom": "17.0.2",
"react-error-boundary": "^4.0.13",
"react-helmet": "^6.1.0",
"react-onclickoutside": "^6.13.0",
@@ -106,19 +110,21 @@
"yup": "0.31.1"
},
"devDependencies": {
"@edx/react-unit-test-utils": "^4.0.0",
"@edx/react-unit-test-utils": "3.0.0",
"@edx/stylelint-config-edx": "2.3.3",
"@edx/typescript-config": "^1.0.1",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.2.0",
"@testing-library/jest-dom": "5.17.0",
"@testing-library/react": "12.1.5",
"@testing-library/react-hooks": "^8.0.1",
"@testing-library/user-event": "^13.2.1",
"@types/lodash": "^4.17.7",
"axios-mock-adapter": "1.22.0",
"eslint-import-resolver-webpack": "^0.13.8",
"fetch-mock-jest": "^1.5.1",
"husky": "7.0.4",
"jest-canvas-mock": "^2.5.2",
"jest-expect-message": "^1.1.3",
"react-test-renderer": "^18.3.1",
"react-test-renderer": "17.0.2",
"redux-mock-store": "^1.5.4"
}
}

View File

@@ -142,7 +142,7 @@ describe('ORASettings', () => {
renderComponent();
await mockStore({ apiStatus: 200, enabled: false });
const label = await screen.findByText(messages.enableFlexPeerGradeLabel.defaultMessage);
const label = screen.getByText(messages.enableFlexPeerGradeLabel.defaultMessage);
const enableBadge = screen.queryByTestId('enable-badge');
expect(label).toBeVisible();

View File

@@ -544,9 +544,12 @@ describe('ProctoredExamSettings', () => {
describe('Connection states', () => {
it('Shows the spinner before the connection is complete', async () => {
render(intlWrapper(<IntlProctoredExamSettings {...defaultProps} />));
const spinner = await screen.findByRole('status');
expect(spinner.textContent).toEqual('Loading...');
await act(async () => {
render(intlWrapper(<IntlProctoredExamSettings {...defaultProps} />));
// This expectation is _inside_ the `act` intentionally, so that it executes immediately.
const spinner = screen.getByRole('status');
expect(spinner.textContent).toEqual('Loading...');
});
});
it('Show connection error message when we suffer studio server side error', async () => {

View File

@@ -26,7 +26,6 @@ const TeamSettings = ({
description: '',
type: GroupTypes.OPEN,
maxTeamSize: null,
userPartitionId: null,
id: null,
key: uuid(),
};
@@ -39,7 +38,6 @@ const TeamSettings = ({
type: group.type,
description: group.description,
max_team_size: group.maxTeamSize,
user_partition_id: group.userPartitionId,
}));
return saveSettings({
team_sets: groups,

View File

@@ -7,7 +7,7 @@ import {
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import { AppProvider, PageWrap } from '@edx/frontend-platform/react';
import {
findByTestId, queryByTestId, render, waitFor, getByText, fireEvent,
queryByTestId, render, waitFor, getByText, fireEvent,
} from '@testing-library/react';
import MockAdapter from 'axios-mock-adapter';
import PagesAndResourcesProvider from 'CourseAuthoring/pages-and-resources/PagesAndResourcesProvider';
@@ -106,9 +106,8 @@ describe('XpertUnitSummarySettings', () => {
});
test('Shows switch on if enabled from backend', async () => {
const enableBadge = await findByTestId(container, 'enable-badge');
expect(container.querySelector('#enable-xpert-unit-summary-toggle').checked).toBeTruthy();
expect(enableBadge).toBeTruthy();
expect(queryByTestId(container, 'enable-badge')).toBeTruthy();
});
test('Shows switch on if disabled from backend', async () => {

View File

@@ -5,13 +5,12 @@ import { useDispatch, useSelector } from 'react-redux';
import {
useLocation,
} from 'react-router-dom';
import { StudioFooterSlot } from '@openedx/frontend-slot-footer';
import { StudioFooter } from '@edx/frontend-component-footer';
import Header from './header';
import { fetchCourseDetail, fetchWaffleFlags } from './data/thunks';
import { useModel } from './generic/model-store';
import NotFoundAlert from './generic/NotFoundAlert';
import PermissionDeniedAlert from './generic/PermissionDeniedAlert';
import { fetchOnlyStudioHomeData } from './studio-home/data/thunks';
import { getCourseAppsApiStatus } from './pages-and-resources/data/selectors';
import { RequestStatus } from './data/constants';
import Loading from './generic/Loading';
@@ -24,10 +23,6 @@ const CourseAuthoringPage = ({ courseId, children }) => {
dispatch(fetchWaffleFlags(courseId));
}, [courseId]);
useEffect(() => {
dispatch(fetchOnlyStudioHomeData());
}, []);
const courseDetail = useModel('courseDetails', courseId);
const courseNumber = courseDetail ? courseDetail.number : null;
@@ -66,7 +61,7 @@ const CourseAuthoringPage = ({ courseId, children }) => {
)
)}
{children}
{!inProgress && !isEditor && <StudioFooterSlot />}
{!inProgress && !isEditor && <StudioFooter />}
</div>
);
};

View File

@@ -17,7 +17,7 @@ import ScheduleAndDetails from './schedule-and-details';
import { GradingSettings } from './grading-settings';
import CourseTeam from './course-team/CourseTeam';
import { CourseUpdates } from './course-updates';
import { CourseUnit } from './course-unit';
import { CourseUnit, IframeProvider } from './course-unit';
import { Certificates } from './certificates';
import CourseExportPage from './export-page/CourseExportPage';
import CourseOptimizerPage from './optimizer-page/CourseOptimizerPage';
@@ -25,8 +25,7 @@ import CourseImportPage from './import-page/CourseImportPage';
import { DECODED_ROUTES } from './constants';
import CourseChecklist from './course-checklist';
import GroupConfigurations from './group-configurations';
import { CourseLibraries } from './course-libraries';
import { IframeProvider } from './generic/hooks/context/iFrameContext';
import CourseLibraries from './course-libraries';
/**
* As of this writing, these routes are mounted at a path prefixed with the following:

View File

@@ -1,16 +0,0 @@
export default {
content: {
id: 67,
userId: 3,
created: '2024-01-16T13:09:11.540615Z',
purpose: 'clipboard',
status: 'ready',
blockType: 'sequential',
blockTypeDisplay: 'Subsection',
olxUrl: 'http://localhost:18010/api/content-staging/v1/staged-content/67/olx',
displayName: 'Sequences',
},
sourceUsageKey: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@sequential_0270f6de40fc',
sourceContextTitle: 'Demonstration Course',
sourceEditUrl: 'http://localhost:18010/container/block-v1:edX+DemoX+Demo_Course+type@sequential+block@sequential_0270f6de40fc',
};

View File

@@ -1,3 +1,2 @@
export { default as clipboardUnit } from './clipboardUnit';
export { default as clipboardSubsection } from './clipboardSubsection';
export { default as clipboardXBlock } from './clipboardXBlock';

View File

@@ -2,7 +2,7 @@ import React from 'react';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { Helmet } from 'react-helmet';
import { Container } from '@openedx/paragon';
import { StudioFooterSlot } from '@openedx/frontend-slot-footer';
import { StudioFooter } from '@edx/frontend-component-footer';
import Header from '../header';
import messages from './messages';
@@ -29,7 +29,7 @@ const AccessibilityPage = ({
<AccessibilityBody {...{ email, communityAccessibilityLink }} />
<AccessibilityForm accessibilityEmail={email} />
</Container>
<StudioFooterSlot />
<StudioFooter />
</>
);
};

View File

@@ -59,7 +59,6 @@ export const COURSE_BLOCK_NAMES = ({
sequential: { id: 'sequential', name: 'Subsection' },
vertical: { id: 'vertical', name: 'Unit' },
libraryContent: { id: 'library_content', name: 'Library content' },
splitTest: { id: 'split_test', name: 'Split Test' },
component: { id: 'component', name: 'Component' },
});
@@ -92,17 +91,3 @@ export const REGEX_RULES = {
export const IFRAME_FEATURE_POLICY = (
'microphone *; camera *; midi *; geolocation *; encrypted-media *; clipboard-write *'
);
export const iframeStateKeys = {
iframeHeight: 'iframeHeight',
hasLoaded: 'hasLoaded',
showError: 'showError',
windowTopOffset: 'windowTopOffset',
};
export const iframeMessageTypes = {
modal: 'plugin.modal',
resize: 'plugin.resize',
videoFullScreen: 'plugin.videoFullScreen',
xblockEvent: 'xblock-event',
};

View File

@@ -699,7 +699,7 @@ describe('<ContentTagsCollapsible />', () => {
const xButtonAppliedTag = within(appliedTag).getByRole('button', {
name: /delete/i,
});
await userEvent.click(xButtonAppliedTag);
xButtonAppliedTag.click();
// Check that the applied tag has been removed
expect(appliedTag).not.toBeInTheDocument();

View File

@@ -1,4 +1,5 @@
import {
act,
fireEvent,
initializeMocks,
render,
@@ -60,15 +61,19 @@ describe('<ContentTagsDrawer />', () => {
});
it('shows spinner before the content data query is complete', async () => {
renderDrawer(stagedTagsId);
const spinner = (await screen.findAllByRole('status'))[0];
expect(spinner.textContent).toEqual('Loading'); // Uses <Spinner />
await act(async () => {
renderDrawer(stagedTagsId);
const spinner = screen.getAllByRole('status')[0];
expect(spinner.textContent).toEqual('Loading'); // Uses <Spinner />
});
});
it('shows spinner before the taxonomy tags query is complete', async () => {
renderDrawer(stagedTagsId);
const spinner = (await screen.findAllByRole('status'))[1];
expect(spinner.textContent).toEqual('Loading...'); // Uses <Loading />
await act(async () => {
renderDrawer(stagedTagsId);
const spinner = screen.getAllByRole('status')[1];
expect(spinner.textContent).toEqual('Loading...'); // Uses <Loading />
});
});
it('shows the content display name after the query is complete in drawer variant', async () => {
@@ -93,12 +98,15 @@ describe('<ContentTagsDrawer />', () => {
});
it('shows the taxonomies data including tag numbers after the query is complete', async () => {
const { container } = renderDrawer(largeTagsId);
await screen.findByText('Taxonomy 1');
await screen.findByText('Taxonomy 2');
const tagCountBadges = container.getElementsByClassName('taxonomy-tags-count-chip');
expect(tagCountBadges[0].textContent).toBe('3');
expect(tagCountBadges[1].textContent).toBe('2');
await act(async () => {
const { container } = renderDrawer(largeTagsId);
await waitFor(() => { expect(screen.getByText('Taxonomy 1')).toBeInTheDocument(); });
expect(screen.getByText('Taxonomy 1')).toBeInTheDocument();
expect(screen.getByText('Taxonomy 2')).toBeInTheDocument();
const tagCountBadges = container.getElementsByClassName('taxonomy-tags-count-chip');
expect(tagCountBadges[0].textContent).toBe('3');
expect(tagCountBadges[1].textContent).toBe('2');
});
});
it('should be read only on first render on drawer variant', async () => {

View File

@@ -227,6 +227,7 @@ interface ContentTagsDrawerProps {
* It is used both in interfaces of this MFE and in edx-platform interfaces such as iframe.
* - If you want to use it as an iframe, the component obtains the `contentId` from the url parameters.
* Functions to close the drawer are handled internally.
* TODO: We can delete this method when is no longer used on edx-platform.
* - If you want to use it as react component, you need to pass the content id and the close functions
* through the component parameters.
*/
@@ -245,7 +246,7 @@ const ContentTagsDrawer = ({
throw new Error('Error: contentId cannot be null.');
}
const context = useCreateContentTagsDrawerContext(contentId, !readOnly, variant === 'drawer');
const context = useCreateContentTagsDrawerContext(contentId, !readOnly);
const { blockingSheet } = useContext(ContentTagsDrawerSheetContext);
const {

View File

@@ -20,11 +20,9 @@ import { ContentTagsDrawerSheetContext } from './common/context';
* To *use* the context, just use `useContext(ContentTagsDrawerContext)`
* @param {string} contentId
* @param {boolean} canTagObject
* @param {boolean} fetchMetadata=false If true, fetches metadata for the contentId. This is used on `edx-platform`
* and the Course/Unit Outline to show the content name as the drawer title.
* @returns {ContentTagsDrawerContextData}
*/
export const useCreateContentTagsDrawerContext = (contentId, canTagObject, fetchMetadata = false) => {
export const useCreateContentTagsDrawerContext = (contentId, canTagObject) => {
const intl = useIntl();
const org = extractOrgFromContentId(contentId);
@@ -50,7 +48,7 @@ export const useCreateContentTagsDrawerContext = (contentId, canTagObject, fetch
const updateTags = useContentTaxonomyTagsUpdater(contentId);
// Fetch from database
const { data: contentData, isSuccess: isContentDataLoaded } = useContentData(contentId, fetchMetadata);
const { data: contentData, isSuccess: isContentDataLoaded } = useContentData(contentId);
const {
data: contentTaxonomyTagsData,
isSuccess: isContentTaxonomyTagsLoaded,

View File

@@ -1,6 +1,7 @@
import React from 'react';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import {
act,
render,
waitFor,
fireEvent,
@@ -73,9 +74,11 @@ describe('<ContentTagsDropDownSelector />', () => {
}
it('should render taxonomy tags drop down selector loading with spinner', async () => {
const { getByRole } = await getComponent();
const spinner = getByRole('status');
expect(spinner.textContent).toEqual('Loading tags'); // Uses <Spinner />
await act(async () => {
const { getByRole } = await getComponent();
const spinner = getByRole('status');
expect(spinner.textContent).toEqual('Loading tags'); // Uses <Spinner />
});
});
it('should render taxonomy tags drop down selector with no sub tags', async () => {
@@ -96,11 +99,13 @@ describe('<ContentTagsDropDownSelector />', () => {
},
});
const { container, getByText } = await getComponent();
await act(async () => {
const { container, getByText } = await getComponent();
await waitFor(() => {
expect(getByText('Tag 1')).toBeInTheDocument();
expect(container.getElementsByClassName('taxonomy-tags-arrow-drop-down').length).toBe(0);
await waitFor(() => {
expect(getByText('Tag 1')).toBeInTheDocument();
expect(container.getElementsByClassName('taxonomy-tags-arrow-drop-down').length).toBe(0);
});
});
});
@@ -122,11 +127,13 @@ describe('<ContentTagsDropDownSelector />', () => {
},
});
const { container, getByText } = await getComponent();
await act(async () => {
const { container, getByText } = await getComponent();
await waitFor(() => {
expect(getByText('Tag 2')).toBeInTheDocument();
expect(container.getElementsByClassName('taxonomy-tags-arrow-drop-down').length).toBe(1);
await waitFor(() => {
expect(getByText('Tag 2')).toBeInTheDocument();
expect(container.getElementsByClassName('taxonomy-tags-arrow-drop-down').length).toBe(1);
});
});
});
@@ -148,45 +155,47 @@ describe('<ContentTagsDropDownSelector />', () => {
},
});
const dataWithTagsTree = {
...data,
tagsTree: {
'Tag 3': {
explicit: false,
children: {},
await act(async () => {
const dataWithTagsTree = {
...data,
tagsTree: {
'Tag 3': {
explicit: false,
children: {},
},
},
},
};
const { container, getByText } = await getComponent(dataWithTagsTree);
await waitFor(() => {
expect(getByText('Tag 2')).toBeInTheDocument();
expect(container.getElementsByClassName('taxonomy-tags-arrow-drop-down').length).toBe(1);
});
};
const { container, getByText } = await getComponent(dataWithTagsTree);
await waitFor(() => {
expect(getByText('Tag 2')).toBeInTheDocument();
expect(container.getElementsByClassName('taxonomy-tags-arrow-drop-down').length).toBe(1);
});
// Mock useTaxonomyTagsData again since it gets called in the recursive call
useTaxonomyTagsData.mockReturnValueOnce({
hasMorePages: false,
tagPages: {
isLoading: false,
isError: false,
data: [{
value: 'Tag 3',
externalId: null,
childCount: 0,
depth: 1,
parentValue: 'Tag 2',
id: 12346,
subTagsUrl: null,
}],
},
});
// Mock useTaxonomyTagsData again since it gets called in the recursive call
useTaxonomyTagsData.mockReturnValueOnce({
hasMorePages: false,
tagPages: {
isLoading: false,
isError: false,
data: [{
value: 'Tag 3',
externalId: null,
childCount: 0,
depth: 1,
parentValue: 'Tag 2',
id: 12346,
subTagsUrl: null,
}],
},
});
// Expand the dropdown to see the subtags selectors
const expandToggle = container.querySelector('.taxonomy-tags-arrow-drop-down span');
fireEvent.click(expandToggle);
// Expand the dropdown to see the subtags selectors
const expandToggle = container.querySelector('.taxonomy-tags-arrow-drop-down span');
fireEvent.click(expandToggle);
await waitFor(() => {
expect(getByText('Tag 3')).toBeInTheDocument();
await waitFor(() => {
expect(getByText('Tag 3')).toBeInTheDocument();
});
});
});
@@ -210,46 +219,48 @@ describe('<ContentTagsDropDownSelector />', () => {
});
const initalSearchTerm = 'test 1';
const { rerender } = await getComponent({ ...data, searchTerm: initalSearchTerm });
await act(async () => {
const { rerender } = await getComponent({ ...data, searchTerm: initalSearchTerm });
await waitFor(() => {
expect(useTaxonomyTagsData).toHaveBeenCalledWith(data.taxonomyId, null, 1, initalSearchTerm);
});
await waitFor(() => {
expect(useTaxonomyTagsData).toBeCalledWith(data.taxonomyId, null, 1, initalSearchTerm);
});
const updatedSearchTerm = 'test 2';
rerender(<ContentTagsDropDownSelectorComponent
key={`selector-${data.taxonomyId}`}
taxonomyId={data.taxonomyId}
level={data.level}
tagsTree={data.tagsTree}
searchTerm={updatedSearchTerm}
appliedContentTagsTree={{}}
stagedContentTagsTree={{}}
/>);
const updatedSearchTerm = 'test 2';
rerender(<ContentTagsDropDownSelectorComponent
key={`selector-${data.taxonomyId}`}
taxonomyId={data.taxonomyId}
level={data.level}
tagsTree={data.tagsTree}
searchTerm={updatedSearchTerm}
appliedContentTagsTree={{}}
stagedContentTagsTree={{}}
/>);
await waitFor(() => {
expect(useTaxonomyTagsData).toHaveBeenCalledWith(data.taxonomyId, null, 1, updatedSearchTerm);
});
await waitFor(() => {
expect(useTaxonomyTagsData).toBeCalledWith(data.taxonomyId, null, 1, updatedSearchTerm);
});
// Clean search term
const cleanSearchTerm = '';
rerender(<ContentTagsDropDownSelectorComponent
key={`selector-${data.taxonomyId}`}
taxonomyId={data.taxonomyId}
level={data.level}
tagsTree={data.tagsTree}
searchTerm={cleanSearchTerm}
appliedContentTagsTree={{}}
stagedContentTagsTree={{}}
/>);
// Clean search term
const cleanSearchTerm = '';
rerender(<ContentTagsDropDownSelectorComponent
key={`selector-${data.taxonomyId}`}
taxonomyId={data.taxonomyId}
level={data.level}
tagsTree={data.tagsTree}
searchTerm={cleanSearchTerm}
appliedContentTagsTree={{}}
stagedContentTagsTree={{}}
/>);
await waitFor(() => {
expect(useTaxonomyTagsData).toHaveBeenCalledWith(data.taxonomyId, null, 1, cleanSearchTerm);
await waitFor(() => {
expect(useTaxonomyTagsData).toBeCalledWith(data.taxonomyId, null, 1, cleanSearchTerm);
});
});
});
it('should render "noTag" message if search doesnt return taxonomies', async () => {
useTaxonomyTagsData.mockReturnValue({
useTaxonomyTagsData.mockReturnValueOnce({
hasMorePages: false,
tagPages: {
isLoading: false,
@@ -260,18 +271,20 @@ describe('<ContentTagsDropDownSelector />', () => {
});
const searchTerm = 'uncommon search term';
const { getByText } = await getComponent({ ...data, searchTerm });
await act(async () => {
const { getByText } = await getComponent({ ...data, searchTerm });
await waitFor(() => {
expect(useTaxonomyTagsData).toHaveBeenCalledWith(data.taxonomyId, null, 1, searchTerm);
await waitFor(() => {
expect(useTaxonomyTagsData).toBeCalledWith(data.taxonomyId, null, 1, searchTerm);
});
const message = `No tags found with the search term "${searchTerm}"`;
expect(getByText(message)).toBeInTheDocument();
});
const message = `No tags found with the search term "${searchTerm}"`;
expect(getByText(message)).toBeInTheDocument();
});
it('should render "noTagsInTaxonomy" message if taxonomy is empty', async () => {
useTaxonomyTagsData.mockReturnValue({
useTaxonomyTagsData.mockReturnValueOnce({
hasMorePages: false,
tagPages: {
isLoading: false,
@@ -282,13 +295,15 @@ describe('<ContentTagsDropDownSelector />', () => {
});
const searchTerm = '';
const { getByText } = await getComponent({ ...data, searchTerm });
await act(async () => {
const { getByText } = await getComponent({ ...data, searchTerm });
await waitFor(() => {
expect(useTaxonomyTagsData).toHaveBeenCalledWith(data.taxonomyId, null, 1, searchTerm);
await waitFor(() => {
expect(useTaxonomyTagsData).toBeCalledWith(data.taxonomyId, null, 1, searchTerm);
});
const message = 'No tags in this taxonomy yet';
expect(getByText(message)).toBeInTheDocument();
});
const message = 'No tags in this taxonomy yet';
expect(getByText(message)).toBeInTheDocument();
});
});

View File

@@ -70,12 +70,17 @@ export async function getContentTaxonomyTagsCount(contentId) {
}
/**
* Fetch meta data (eg: display_name) about the content object (unit/component)
* Fetch meta data (eg: display_name) about the content object (unit/compoenent)
* @param {string} contentId The id of the content object (unit/component)
* @returns {Promise<import("./types.js").ContentData>}
* @returns {Promise<import("./types.js").ContentData | null>}
*/
export async function getContentData(contentId) {
let url;
if (contentId.startsWith('lib-collection:')) {
// This type of usage_key is not used to obtain collections
// is only used in tagging.
return null;
}
if (contentId.startsWith('lb:')) {
url = getLibraryContentDataApiUrl(contentId);

View File

@@ -13,7 +13,6 @@ export async function mockContentTaxonomyTagsData(contentId: string): Promise<an
case thisMock.languageWithTagsId: return thisMock.languageWithTags;
case thisMock.languageWithoutTagsId: return thisMock.languageWithoutTags;
case thisMock.largeTagsId: return thisMock.largeTags;
case thisMock.containerTagsId: return thisMock.largeTags;
case thisMock.emptyTagsId: return thisMock.emptyTags;
default: throw new Error(`No mock has been set up for contentId "${contentId}"`);
}
@@ -205,7 +204,6 @@ mockContentTaxonomyTagsData.emptyTagsId = 'block-v1:EmptyTagsOrg+STC1+2023_1+typ
mockContentTaxonomyTagsData.emptyTags = {
taxonomies: [],
};
mockContentTaxonomyTagsData.containerTagsId = 'lct:org:lib:unit:container_tags';
mockContentTaxonomyTagsData.applyMock = () => jest.spyOn(api, 'getContentTaxonomyTagsData').mockImplementation(mockContentTaxonomyTagsData);
/**

View File

@@ -112,13 +112,11 @@ export const useContentTaxonomyTagsData = (contentId) => (
/**
* Builds the query to get meta data about the content object
* @param {string} contentId The id of the content object (unit/component)
* @param {boolean} enabled Flag to enable/disable the query
*/
export const useContentData = (contentId, enabled) => (
export const useContentData = (contentId) => (
useQuery({
queryKey: ['contentData', contentId],
queryFn: enabled ? () => getContentData(contentId) : undefined,
enabled,
queryFn: () => getContentData(contentId),
})
);
@@ -151,7 +149,7 @@ export const useContentTaxonomyTagsUpdater = (contentId) => {
contentPattern = contentId.replace(/\+type@.*$/, '*');
}
queryClient.invalidateQueries({ queryKey: ['contentTagsCount', contentPattern] });
if (contentId.startsWith('lb:') || contentId.startsWith('lib-collection:') || contentId.startsWith('lct:')) {
if (contentId.startsWith('lb:') || contentId.startsWith('lib-collection:')) {
// Obtain library id from contentId
const libraryId = getLibraryId(contentId);
// Invalidate component metadata to update tags count

View File

@@ -1,5 +1,6 @@
import { useQuery, useMutation, useQueries } from '@tanstack/react-query';
import { act, renderHook } from '@testing-library/react';
import { act } from '@testing-library/react';
import { renderHook } from '@testing-library/react-hooks';
import {
useTaxonomyTagsData,
useContentTaxonomyTagsData,

View File

@@ -1,3 +1,2 @@
export { default as ContentTagsDrawer } from './ContentTagsDrawer';
export { default as ContentTagsDrawerSheet } from './ContentTagsDrawerSheet';
export { useContentTaxonomyTagsData } from './data/apiHooks';

View File

@@ -1,35 +1,22 @@
import fetchMock from 'fetch-mock-jest';
import { cloneDeep } from 'lodash';
import userEvent from '@testing-library/user-event';
import MockAdapter from 'axios-mock-adapter/types';
import { QueryClient } from '@tanstack/react-query';
import {
initializeMocks,
render,
screen,
waitFor,
within,
} from '../testUtils';
import { mockContentSearchConfig } from '../search-manager/data/api.mock';
import { CourseLibraries } from './CourseLibraries';
import {
mockGetEntityLinks,
mockGetEntityLinksSummaryByDownstreamContext,
mockFetchIndexDocuments,
mockUseLibBlockMetadata,
} from './data/api.mocks';
import { libraryBlockChangesUrl } from '../course-unit/data/api';
import { type ToastActionData } from '../generic/toast-context';
import mockInfoResult from './__mocks__/courseBlocksInfo.json';
import CourseLibraries from './CourseLibraries';
import { mockGetEntityLinksByDownstreamContext } from './data/api.mocks';
mockContentSearchConfig.applyMock();
mockGetEntityLinks.applyMock();
mockGetEntityLinksSummaryByDownstreamContext.applyMock();
mockUseLibBlockMetadata.applyMock();
mockGetEntityLinksByDownstreamContext.applyMock();
const searchParamsGetMock = jest.fn();
let axiosMock: MockAdapter;
let mockShowToast: (message: string, action?: ToastActionData | undefined) => void;
let queryClient: QueryClient;
const searchEndpoint = 'http://mock.meilisearch.local/indexes/studio/search';
jest.mock('../studio-home/hooks', () => ({
useStudioHome: () => ({
@@ -39,206 +26,123 @@ jest.mock('../studio-home/hooks', () => ({
}),
}));
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'), // use actual for all non-hook parts
useSearchParams: () => [{
get: searchParamsGetMock,
getAll: () => [],
}],
}));
describe('<CourseLibraries />', () => {
beforeEach(() => {
initializeMocks();
fetchMock.mockReset();
mockFetchIndexDocuments.applyMock();
localStorage.clear();
searchParamsGetMock.mockReturnValue('all');
// The Meilisearch client-side API uses fetch, not Axios.
fetchMock.post(searchEndpoint, (_url, req) => {
const requestData = JSON.parse(req.body?.toString() ?? '');
const filter = requestData?.filter[1];
const mockInfoResultCopy = cloneDeep(mockInfoResult);
const resp = mockInfoResultCopy.filter((o: { filter: string }) => o.filter === filter)[0] || {
result: {
hits: [],
query: '',
processingTimeMs: 0,
limit: 4,
offset: 0,
estimatedTotalHits: 0,
},
};
const { result } = resp;
return result;
});
});
const renderCourseLibrariesPage = async (courseKey?: string) => {
const courseId = courseKey || mockGetEntityLinks.courseKey;
const courseId = courseKey || mockGetEntityLinksByDownstreamContext.courseKey;
render(<CourseLibraries courseId={courseId} />);
};
it('shows the spinner before the query is complete', async () => {
// This mock will never return data (it loads forever):
await renderCourseLibrariesPage(mockGetEntityLinksSummaryByDownstreamContext.courseKeyLoading);
await renderCourseLibrariesPage(mockGetEntityLinksByDownstreamContext.courseKeyLoading);
const spinner = await screen.findByRole('status');
expect(spinner.textContent).toEqual('Loading...');
});
it('shows empty state when no links are present', async () => {
await renderCourseLibrariesPage(mockGetEntityLinks.courseKeyEmpty);
it('shows empty state wheen no links are present', async () => {
await renderCourseLibrariesPage(mockGetEntityLinksByDownstreamContext.courseKeyEmpty);
const emptyMsg = await screen.findByText('This course does not use any content from libraries.');
expect(emptyMsg).toBeInTheDocument();
});
it('shows alert when out of sync components are present', async () => {
await renderCourseLibrariesPage(mockGetEntityLinks.courseKey);
const allTab = await screen.findByRole('tab', { name: 'Libraries' });
const reviewTab = await screen.findByRole('tab', { name: 'Review Content Updates 5' });
// review tab should be open by default as outOfSyncCount is greater than 0
expect(reviewTab).toHaveAttribute('aria-selected', 'true');
userEvent.click(allTab);
await renderCourseLibrariesPage(mockGetEntityLinksByDownstreamContext.courseKey);
const alert = await screen.findByRole('alert');
expect(await within(alert).findByText(
'5 library components are out of sync. Review updates to accept or ignore changes',
'1 library components are out of sync. Review updates to accept or ignore changes',
)).toBeInTheDocument();
const allTab = await screen.findByRole('tab', { name: 'Libraries' });
expect(allTab).toHaveAttribute('aria-selected', 'true');
const reviewBtn = await screen.findByRole('button', { name: 'Review' });
userEvent.click(reviewBtn);
expect(allTab).toHaveAttribute('aria-selected', 'false');
expect(await screen.findByRole('tab', { name: 'Review Content Updates 5' })).toHaveAttribute('aria-selected', 'true');
expect(await screen.findByRole('tab', { name: 'Review Content Updates (1)' })).toHaveAttribute('aria-selected', 'true');
expect(alert).not.toBeInTheDocument();
});
it('hide alert on dismiss', async () => {
await renderCourseLibrariesPage(mockGetEntityLinks.courseKey);
const reviewTab = await screen.findByRole('tab', { name: 'Review Content Updates 5' });
// review tab should be open by default as outOfSyncCount is greater than 0
expect(reviewTab).toHaveAttribute('aria-selected', 'true');
const allTab = await screen.findByRole('tab', { name: 'Libraries' });
// go back to all tab
userEvent.click(allTab);
// alert should not be back
expect(alert).not.toBeInTheDocument();
expect(allTab).toHaveAttribute('aria-selected', 'true');
const alert = await screen.findByRole('alert');
expect(await within(alert).findByText(
'5 library components are out of sync. Review updates to accept or ignore changes',
)).toBeInTheDocument();
const dismissBtn = await screen.findByRole('button', { name: 'Dismiss' });
userEvent.click(dismissBtn);
expect(allTab).toHaveAttribute('aria-selected', 'true');
waitFor(() => expect(alert).not.toBeInTheDocument());
// review updates button
const reviewActionBtn = await screen.findByRole('button', { name: 'Review Updates' });
userEvent.click(reviewActionBtn);
expect(await screen.findByRole('tab', { name: 'Review Content Updates 5' })).toHaveAttribute('aria-selected', 'true');
});
});
describe('<CourseLibraries ReviewTab />', () => {
beforeEach(() => {
const mocks = initializeMocks();
axiosMock = mocks.axiosMock;
mockShowToast = mocks.mockShowToast;
fetchMock.mockReset();
mockFetchIndexDocuments.applyMock();
localStorage.clear();
searchParamsGetMock.mockReturnValue('review');
queryClient = mocks.queryClient;
});
const renderCourseLibrariesReviewPage = async (courseKey?: string) => {
const courseId = courseKey || mockGetEntityLinks.courseKey;
render(<CourseLibraries courseId={courseId} />);
};
it('shows the spinner before the query is complete', async () => {
// This mock will never return data (it loads forever):
await renderCourseLibrariesReviewPage(mockGetEntityLinks.courseKeyLoading);
const spinner = await screen.findByRole('status');
expect(spinner.textContent).toEqual('Loading...');
});
it('shows empty state when no readyToSync links are present', async () => {
await renderCourseLibrariesReviewPage(mockGetEntityLinksSummaryByDownstreamContext.courseKeyUpToDate);
const emptyMsg = await screen.findByText('All components are up to date');
expect(emptyMsg).toBeInTheDocument();
});
it('shows all readyToSync links', async () => {
await renderCourseLibrariesReviewPage(mockGetEntityLinksSummaryByDownstreamContext.courseKey);
const updateBtns = await screen.findAllByRole('button', { name: 'Update' });
expect(updateBtns.length).toEqual(5);
const ignoreBtns = await screen.findAllByRole('button', { name: 'Ignore' });
expect(ignoreBtns.length).toEqual(5);
});
it('update changes works', async () => {
const mockInvalidateQueries = jest.spyOn(queryClient, 'invalidateQueries');
const usageKey = mockGetEntityLinks.response.results[0].downstreamUsageKey;
axiosMock.onPost(libraryBlockChangesUrl(usageKey)).reply(200, {});
await renderCourseLibrariesReviewPage(mockGetEntityLinksSummaryByDownstreamContext.courseKey);
const updateBtns = await screen.findAllByRole('button', { name: 'Update' });
expect(updateBtns.length).toEqual(5);
userEvent.click(updateBtns[0]);
await waitFor(() => {
expect(axiosMock.history.post.length).toEqual(1);
});
expect(axiosMock.history.post[0].url).toEqual(libraryBlockChangesUrl(usageKey));
expect(mockShowToast).toHaveBeenCalledWith('Success! "Dropdown" is updated');
expect(mockInvalidateQueries).toHaveBeenCalledWith(['courseLibraries', 'course-v1:OpenEdx+DemoX+CourseX']);
});
it('update changes works in preview modal', async () => {
const mockInvalidateQueries = jest.spyOn(queryClient, 'invalidateQueries');
const usageKey = mockGetEntityLinks.response.results[0].downstreamUsageKey;
axiosMock.onPost(libraryBlockChangesUrl(usageKey)).reply(200, {});
await renderCourseLibrariesReviewPage(mockGetEntityLinksSummaryByDownstreamContext.courseKey);
const previewBtns = await screen.findAllByRole('button', { name: 'Review Updates' });
expect(previewBtns.length).toEqual(5);
userEvent.click(previewBtns[0]);
const dialog = await screen.findByRole('dialog');
const confirmBtn = await within(dialog).findByRole('button', { name: 'Accept changes' });
userEvent.click(confirmBtn);
await waitFor(() => {
expect(axiosMock.history.post.length).toEqual(1);
});
expect(axiosMock.history.post[0].url).toEqual(libraryBlockChangesUrl(usageKey));
expect(mockShowToast).toHaveBeenCalledWith('Success! "Dropdown" is updated');
expect(mockInvalidateQueries).toHaveBeenCalledWith(['courseLibraries', 'course-v1:OpenEdx+DemoX+CourseX']);
});
it('ignore change works', async () => {
const mockInvalidateQueries = jest.spyOn(queryClient, 'invalidateQueries');
const usageKey = mockGetEntityLinks.response.results[0].downstreamUsageKey;
axiosMock.onDelete(libraryBlockChangesUrl(usageKey)).reply(204, {});
await renderCourseLibrariesReviewPage(mockGetEntityLinksSummaryByDownstreamContext.courseKey);
const ignoreBtns = await screen.findAllByRole('button', { name: 'Ignore' });
expect(ignoreBtns.length).toEqual(5);
// Show confirmation modal on clicking ignore.
userEvent.click(ignoreBtns[0]);
const dialog = await screen.findByRole('dialog', { name: 'Ignore these changes?' });
expect(dialog).toBeInTheDocument();
const confirmBtn = await within(dialog).findByRole('button', { name: 'Ignore' });
userEvent.click(confirmBtn);
await waitFor(() => {
expect(axiosMock.history.delete.length).toEqual(1);
});
expect(axiosMock.history.delete[0].url).toEqual(libraryBlockChangesUrl(usageKey));
expect(mockShowToast).toHaveBeenCalledWith(
'"Dropdown" will remain out of sync with library content. You will be notified when this component is updated again.',
);
expect(mockInvalidateQueries).toHaveBeenCalledWith(['courseLibraries', 'course-v1:OpenEdx+DemoX+CourseX']);
});
it('ignore change works in preview', async () => {
const mockInvalidateQueries = jest.spyOn(queryClient, 'invalidateQueries');
const usageKey = mockGetEntityLinks.response.results[0].downstreamUsageKey;
axiosMock.onDelete(libraryBlockChangesUrl(usageKey)).reply(204, {});
await renderCourseLibrariesReviewPage(mockGetEntityLinksSummaryByDownstreamContext.courseKey);
const previewBtns = await screen.findAllByRole('button', { name: 'Review Updates' });
expect(previewBtns.length).toEqual(5);
userEvent.click(previewBtns[0]);
const previewDialog = await screen.findByRole('dialog');
const ignoreBtn = await within(previewDialog).findByRole('button', { name: 'Ignore changes' });
userEvent.click(ignoreBtn);
// Show confirmation modal on clicking ignore.
const dialog = await screen.findByRole('dialog', { name: 'Ignore these changes?' });
expect(dialog).toBeInTheDocument();
const confirmBtn = await within(dialog).findByRole('button', { name: 'Ignore' });
userEvent.click(confirmBtn);
await waitFor(() => {
expect(axiosMock.history.delete.length).toEqual(1);
});
expect(axiosMock.history.delete[0].url).toEqual(libraryBlockChangesUrl(usageKey));
expect(mockShowToast).toHaveBeenCalledWith(
'"Dropdown" will remain out of sync with library content. You will be notified when this component is updated again.',
);
expect(mockInvalidateQueries).toHaveBeenCalledWith(['courseLibraries', 'course-v1:OpenEdx+DemoX+CourseX']);
expect(await screen.findByRole('tab', { name: 'Review Content Updates (1)' })).toHaveAttribute('aria-selected', 'true');
});
it('hide alert on dismiss', async () => {
await renderCourseLibrariesPage(mockGetEntityLinksByDownstreamContext.courseKey);
const alert = await screen.findByRole('alert');
expect(await within(alert).findByText(
'1 library components are out of sync. Review updates to accept or ignore changes',
)).toBeInTheDocument();
const dismissBtn = await screen.findByRole('button', { name: 'Dismiss' });
userEvent.click(dismissBtn);
const allTab = await screen.findByRole('tab', { name: 'Libraries' });
expect(allTab).toHaveAttribute('aria-selected', 'true');
expect(alert).not.toBeInTheDocument();
});
it('shows links split by library', async () => {
await renderCourseLibrariesPage(mockGetEntityLinksByDownstreamContext.courseKey);
const msg = await screen.findByText('This course contains content from these libraries.');
expect(msg).toBeInTheDocument();
const allButtons = await screen.findAllByRole('button');
// total 3 components used from lib 1
const expectedLib1Blocks = 3;
// total 4 components used from lib 1
const expectedLib2Blocks = 4;
// 1 component has updates.
const expectedLib2ToUpdate = 1;
const libraryCards = allButtons.filter((el) => el.classList.contains('collapsible-trigger'));
expect(libraryCards.length).toEqual(2);
expect(await within(libraryCards[0]).findByText('CS problems 2')).toBeInTheDocument();
expect(await within(libraryCards[0]).findByText(`${expectedLib1Blocks} components applied`)).toBeInTheDocument();
expect(await within(libraryCards[0]).findByText('All components up to date')).toBeInTheDocument();
const libParent1 = libraryCards[0].parentElement;
expect(libParent1).not.toBeNull();
userEvent.click(libraryCards[0]);
const xblockCards1 = libParent1!.querySelectorAll('div.card');
expect(xblockCards1.length).toEqual(expectedLib1Blocks);
expect(await within(libraryCards[1]).findByText('CS problems 3')).toBeInTheDocument();
expect(await within(libraryCards[1]).findByText(`${expectedLib2Blocks} components applied`)).toBeInTheDocument();
expect(await within(libraryCards[1]).findByText(`${expectedLib2ToUpdate} component out of sync`)).toBeInTheDocument();
const libParent2 = libraryCards[1].parentElement;
expect(libParent2).not.toBeNull();
userEvent.click(libraryCards[1]);
const xblockCards2 = libParent2!.querySelectorAll('div.card');
expect(xblockCards2.length).toEqual(expectedLib2Blocks);
});
});

View File

@@ -1,115 +1,241 @@
import React, {
useCallback, useEffect, useMemo, useState,
useCallback, useMemo, useState,
} from 'react';
import { Helmet } from 'react-helmet';
import { getConfig } from '@edx/frontend-platform';
import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n';
import {
Alert,
ActionRow,
Button,
Card,
Container,
Hyperlink,
Icon,
Stack,
Tab,
Tabs,
Breadcrumb, Button, Card, Collapsible, Container, Dropdown, Hyperlink, Icon, IconButton, Layout, Stack, Tab, Tabs,
} from '@openedx/paragon';
import {
Cached, CheckCircle, Launch, Loop,
Cached, CheckCircle, KeyboardArrowDown, KeyboardArrowRight, Loop, MoreVert,
} from '@openedx/paragon/icons';
import sumBy from 'lodash/sumBy';
import { useSearchParams } from 'react-router-dom';
import {
countBy, groupBy, keyBy, tail, uniq,
} from 'lodash';
import classNames from 'classnames';
import getPageHeadTitle from '../generic/utils';
import { useModel } from '../generic/model-store';
import messages from './messages';
import SubHeader from '../generic/sub-header/SubHeader';
import { useEntityLinksSummaryByDownstreamContext } from './data/apiHooks';
import type { PublishableEntityLinkSummary } from './data/api';
import { useEntityLinksByDownstreamContext } from './data/apiHooks';
import type { PublishableEntityLink } from './data/api';
import { useFetchIndexDocuments } from '../search-manager/data/apiHooks';
import { getItemIcon } from '../generic/block-type-utils';
import { BlockTypeLabel } from '../search-manager';
import AlertMessage from '../generic/alert-message';
import type { ContentHit } from '../search-manager/data/api';
import { SearchSortOption } from '../search-manager/data/api';
import Loading from '../generic/Loading';
import { useStudioHome } from '../studio-home/hooks';
import NewsstandIcon from '../generic/NewsstandIcon';
import ReviewTabContent from './ReviewTabContent';
import { OutOfSyncAlert } from './OutOfSyncAlert';
interface Props {
courseId: string;
}
interface LibraryCardProps {
linkSummary: PublishableEntityLinkSummary;
courseId: string;
title: string;
links: PublishableEntityLink[];
}
interface ComponentInfo extends ContentHit {
readyToSync: boolean;
}
interface BlockCardProps {
info: ComponentInfo;
}
export enum CourseLibraryTabs {
all = 'all',
home = '',
review = 'review',
}
const LibraryCard = ({ linkSummary }: LibraryCardProps) => {
const BlockCard: React.FC<BlockCardProps> = ({ info }) => {
const intl = useIntl();
const componentIcon = getItemIcon(info.blockType);
const breadcrumbs = tail(info.breadcrumbs) as Array<{ displayName: string, usageKey: string }>;
const getBlockLink = useCallback(() => {
let key = info.usageKey;
if (breadcrumbs?.length > 1) {
key = breadcrumbs[breadcrumbs.length - 1].usageKey || key;
}
return `${getConfig().STUDIO_BASE_URL}/container/${key}`;
}, [info]);
return (
<Card className="my-3 border-light-500 border shadow-none">
<Card.Header
title={(
<Stack direction="horizontal" gap={2}>
<Icon src={NewsstandIcon} />
{linkSummary.upstreamContextTitle}
<Card
className={classNames(
'my-3 shadow-none border-light-600 border',
{ 'bg-primary-100': info.readyToSync },
)}
orientation="horizontal"
key={info.usageKey}
>
<Card.Section
className="py-2"
>
<Stack direction="vertical" gap={1}>
<Stack direction="horizontal" gap={1} className="micro text-gray-500">
<Icon src={componentIcon} size="xs" />
<BlockTypeLabel blockType={info.blockType} />
<Hyperlink className="lead ml-auto text-black" destination={getBlockLink()} target="_blank">
{' '}
</Hyperlink>
</Stack>
)}
actions={(
<ActionRow>
<Button
destination={`${getConfig().PUBLIC_PATH}library/${linkSummary.upstreamContextKey}`}
target="_blank"
className="border border-light-300"
variant="tertiary"
as={Hyperlink}
size="sm"
showLaunchIcon={false}
iconAfter={Launch}
>
View Library
</Button>
</ActionRow>
)}
size="sm"
/>
<Card.Section>
<Stack
direction="horizontal"
gap={4}
className="x-small"
>
<span>
{intl.formatMessage(messages.totalComponentLabel, { totalComponents: linkSummary.totalCount })}
</span>
{linkSummary.readyToSyncCount > 0 && (
<Stack direction="horizontal" gap={1}>
<Icon src={Loop} size="xs" />
<span>
{intl.formatMessage(messages.outOfSyncCountLabel, { outOfSyncCount: linkSummary.readyToSyncCount })}
</span>
</Stack>
)}
<Stack direction="horizontal" className="small" gap={1}>
{info.readyToSync && <Icon src={Loop} size="xs" />}
{info.formatted?.displayName}
</Stack>
<div className="micro">{info.formatted?.description}</div>
<Breadcrumb
className="micro text-gray-500"
ariaLabel={intl.formatMessage(messages.breadcrumbAriaLabel)}
links={breadcrumbs.map((breadcrumb) => ({ label: breadcrumb.displayName }))}
spacer={<span className="custom-spacer">/</span>}
linkAs="span"
/>
</Stack>
</Card.Section>
</Card>
);
};
export const CourseLibraries: React.FC<Props> = ({ courseId }) => {
const LibraryCard: React.FC<LibraryCardProps> = ({ courseId, title, links }) => {
const intl = useIntl();
const linksInfo = useMemo(() => keyBy(links, 'downstreamUsageKey'), [links]);
const totalComponents = links.length;
const outOfSyncCount = useMemo(() => countBy(links, 'readyToSync').true, [links]);
const downstreamKeys = useMemo(() => uniq(Object.keys(linksInfo)), [links]);
const { data: downstreamInfo } = useFetchIndexDocuments({
filter: [`context_key = "${courseId}"`, `usage_key IN ["${downstreamKeys.join('","')}"]`],
limit: downstreamKeys.length,
attributesToRetrieve: ['usage_key', 'display_name', 'breadcrumbs', 'description', 'block_type'],
attributesToCrop: ['description:30'],
sort: [SearchSortOption.TITLE_AZ],
}) as unknown as { data: ComponentInfo[] };
const renderBlockCards = (info: ComponentInfo) => {
// eslint-disable-next-line no-param-reassign
info.readyToSync = linksInfo[info.usageKey].readyToSync;
return <BlockCard info={info} key={info.usageKey} />;
};
return (
<Collapsible.Advanced>
<Collapsible.Trigger className="bg-white shadow px-2 py-2 my-3 collapsible-trigger d-flex font-weight-normal text-dark">
<Collapsible.Visible whenClosed>
<Icon src={KeyboardArrowRight} />
</Collapsible.Visible>
<Collapsible.Visible whenOpen>
<Icon src={KeyboardArrowDown} />
</Collapsible.Visible>
<Stack direction="vertical" className="flex-grow-1 pl-2 x-small" gap={1}>
<h4>{title}</h4>
<Stack direction="horizontal" gap={2}>
<span>
{intl.formatMessage(messages.totalComponentLabel, { totalComponents })}
</span>
<span>/</span>
{outOfSyncCount ? (
<>
<Icon src={Loop} size="xs" />
<span>
{intl.formatMessage(messages.outOfSyncCountLabel, { outOfSyncCount })}
</span>
</>
) : (
<>
<Icon src={CheckCircle} size="xs" />
<span>
{intl.formatMessage(messages.allUptodateLabel)}
</span>
</>
)}
</Stack>
</Stack>
<Dropdown onClick={(e: { stopPropagation: () => void; }) => e.stopPropagation()}>
<Dropdown.Toggle
id={`dropdown-toggle-${title}`}
alt="dropdown-toggle-menu-items"
as={IconButton}
src={MoreVert}
iconAs={Icon}
variant="primary"
disabled
/>
<Dropdown.Menu>
<Dropdown.Item>TODO 1</Dropdown.Item>
</Dropdown.Menu>
</Dropdown>
</Collapsible.Trigger>
<Collapsible.Body className="collapsible-body border-left border-left-purple px-2">
{downstreamInfo?.map(info => renderBlockCards(info))}
</Collapsible.Body>
</Collapsible.Advanced>
);
};
interface ReviewAlertProps {
show: boolean;
outOfSyncCount: number;
onDismiss: () => void;
onReview: () => void;
}
const ReviewAlert: React.FC<ReviewAlertProps> = ({
show, outOfSyncCount, onDismiss, onReview,
}) => {
const intl = useIntl();
return (
<AlertMessage
title={intl.formatMessage(messages.outOfSyncCountAlertTitle, { outOfSyncCount })}
dismissible
show={show}
icon={Loop}
variant="info"
onClose={onDismiss}
actions={[
<Button
onClick={onReview}
>
{intl.formatMessage(messages.outOfSyncCountAlertReviewBtn)}
</Button>,
]}
/>
);
};
const TabContent = ({ children }: { children: React.ReactNode }) => (
<Layout
lg={[{ span: 9 }, { span: 3 }]}
md={[{ span: 9 }, { span: 3 }]}
sm={[{ span: 12 }, { span: 12 }]}
xs={[{ span: 12 }, { span: 12 }]}
xl={[{ span: 9 }, { span: 3 }]}
>
<Layout.Element>
{children}
</Layout.Element>
<Layout.Element>
Help panel
</Layout.Element>
</Layout>
);
const CourseLibraries: React.FC<Props> = ({ courseId }) => {
const intl = useIntl();
const courseDetails = useModel('courseDetails', courseId);
const [searchParams] = useSearchParams();
const [tabKey, setTabKey] = useState<CourseLibraryTabs>(
() => searchParams.get('tab') as CourseLibraryTabs,
);
const [showReviewAlert, setShowReviewAlert] = useState(false);
const { data: libraries, isLoading } = useEntityLinksSummaryByDownstreamContext(courseId);
const outOfSyncCount = useMemo(() => sumBy(libraries, (lib) => lib.readyToSyncCount), [libraries]);
const [tabKey, setTabKey] = useState<CourseLibraryTabs>(CourseLibraryTabs.home);
const [showReviewAlert, setShowReviewAlert] = useState(true);
const { data: links, isLoading } = useEntityLinksByDownstreamContext(courseId);
const linksByLib = useMemo(() => groupBy(links, 'upstreamContextKey'), [links]);
const outOfSyncCount = useMemo(() => countBy(links, 'readyToSync').true, [links]);
const {
isLoadingPage: isLoadingStudioHome,
isFailedLoadingPage: isFailedLoadingStudioHome,
@@ -118,64 +244,33 @@ export const CourseLibraries: React.FC<Props> = ({ courseId }) => {
const onAlertReview = () => {
setTabKey(CourseLibraryTabs.review);
setShowReviewAlert(false);
};
const onAlertDismiss = () => {
setShowReviewAlert(false);
};
const tabChange = useCallback((selectedTab: CourseLibraryTabs) => {
setTabKey(selectedTab);
}, []);
useEffect(() => {
setTabKey((prev) => {
if (outOfSyncCount > 0) {
return CourseLibraryTabs.review;
}
if (prev) {
return prev;
}
/* istanbul ignore next */
return CourseLibraryTabs.all;
});
}, [outOfSyncCount]);
const renderLibrariesTabContent = useCallback(() => {
if (isLoading) {
return <Loading />;
}
if (libraries?.length === 0) {
if (links?.length === 0) {
return <small><FormattedMessage {...messages.homeTabDescriptionEmpty} /></small>;
}
return (
<>
<small><FormattedMessage {...messages.homeTabDescription} /></small>
{libraries?.map((library) => (
{Object.entries(linksByLib).map(([libKey, libLinks]) => (
<LibraryCard
linkSummary={library}
key={library.upstreamContextKey}
courseId={courseId}
title={libLinks[0].upstreamContextTitle}
links={libLinks}
key={libKey}
/>
))}
</>
);
}, [libraries, isLoading]);
const renderReviewTabContent = useCallback(() => {
if (isLoading) {
return <Loading />;
}
if (tabKey !== CourseLibraryTabs.review) {
return null;
}
if (!outOfSyncCount || outOfSyncCount === 0) {
return (
<Stack direction="horizontal" gap={2}>
<Icon src={CheckCircle} size="xs" />
<small>
<FormattedMessage {...messages.reviewTabDescriptionEmpty} />
</small>
</Stack>
);
}
return <ReviewTabContent courseId={courseId} />;
}, [outOfSyncCount, isLoading, tabKey]);
}, [links, isLoading, linksByLib]);
if (!isLoadingStudioHome && (!librariesV2Enabled || isFailedLoadingStudioHome)) {
return (
@@ -193,16 +288,16 @@ export const CourseLibraries: React.FC<Props> = ({ courseId }) => {
</title>
</Helmet>
<Container size="xl" className="px-4 pt-4 mt-3">
<OutOfSyncAlert
courseId={courseId}
<ReviewAlert
show={outOfSyncCount > 0 && tabKey === CourseLibraryTabs.home && showReviewAlert}
outOfSyncCount={outOfSyncCount}
onDismiss={onAlertDismiss}
onReview={onAlertReview}
showAlert={showReviewAlert && tabKey === CourseLibraryTabs.all}
setShowAlert={setShowReviewAlert}
/>
<SubHeader
title={intl.formatMessage(messages.headingTitle)}
subtitle={intl.formatMessage(messages.headingSubtitle)}
headerActions={!showReviewAlert && outOfSyncCount > 0 && tabKey === CourseLibraryTabs.all && (
headerActions={!showReviewAlert && tabKey === CourseLibraryTabs.home && (
<Button
variant="primary"
onClick={onAlertReview}
@@ -217,27 +312,25 @@ export const CourseLibraries: React.FC<Props> = ({ courseId }) => {
<Tabs
id="course-library-tabs"
activeKey={tabKey}
onSelect={tabChange}
onSelect={(k: CourseLibraryTabs) => setTabKey(k)}
>
<Tab
eventKey={CourseLibraryTabs.all}
eventKey={CourseLibraryTabs.home}
title={intl.formatMessage(messages.homeTabTitle)}
className="px-2 mt-3"
>
{renderLibrariesTabContent()}
<TabContent>
{renderLibrariesTabContent()}
</TabContent>
</Tab>
<Tab
eventKey={CourseLibraryTabs.review}
title={(
<Stack direction="horizontal" gap={1}>
<Icon src={Loop} />
{intl.formatMessage(messages.reviewTabTitle)}
</Stack>
title={intl.formatMessage(
outOfSyncCount > 0 ? messages.reviewTabTitle : messages.reviewTabTitleEmpty,
{ count: outOfSyncCount },
)}
notification={outOfSyncCount}
className="px-2 mt-3"
>
{renderReviewTabContent()}
<TabContent>Help</TabContent>
</Tab>
</Tabs>
</section>
@@ -245,3 +338,5 @@ export const CourseLibraries: React.FC<Props> = ({ courseId }) => {
</>
);
};
export default CourseLibraries;

View File

@@ -1,76 +0,0 @@
import React, { useEffect } from 'react';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Button } from '@openedx/paragon';
import { Loop } from '@openedx/paragon/icons';
import AlertMessage from '../generic/alert-message';
import { useEntityLinksSummaryByDownstreamContext } from './data/apiHooks';
import messages from './messages';
interface OutOfSyncAlertProps {
showAlert: boolean,
setShowAlert: React.Dispatch<React.SetStateAction<boolean>>,
courseId: string,
onDismiss?: () => void;
onReview: () => void;
}
/**
* Shows an alert when library components used in the current course were updated and the blocks
* in course can be updated. Following are the conditions for displaying the alert.
*
* * The alert is displayed if components are out of sync.
* * If the user clicks on dismiss button, the state is stored in localstorage of user
* in this format: outOfSyncCountAlert-${courseId} = <number of out of sync components>.
* * If the number of sync components don't change for the course and the user opens outline
* in the same browser, they don't see the alert again.
* * If the number changes, i.e., if a new component is out of sync or the user updates or ignores
* a component, the alert is displayed again.
*/
export const OutOfSyncAlert: React.FC<OutOfSyncAlertProps> = ({
showAlert,
setShowAlert,
courseId,
onDismiss,
onReview,
}) => {
const intl = useIntl();
const { data, isLoading } = useEntityLinksSummaryByDownstreamContext(courseId);
const outOfSyncCount = data?.reduce((count, lib) => count + lib.readyToSyncCount, 0);
const alertKey = `outOfSyncCountAlert-${courseId}`;
useEffect(() => {
if (isLoading) {
return;
}
if (outOfSyncCount === 0) {
localStorage.removeItem(alertKey);
setShowAlert(false);
return;
}
const dismissedAlert = localStorage.getItem(alertKey);
setShowAlert(parseInt(dismissedAlert || '', 10) !== outOfSyncCount);
}, [outOfSyncCount, isLoading, data]);
const dismissAlert = () => {
setShowAlert(false);
localStorage.setItem(alertKey, String(outOfSyncCount));
onDismiss?.();
};
return (
<AlertMessage
title={intl.formatMessage(messages.outOfSyncCountAlertTitle, { outOfSyncCount })}
dismissible
show={showAlert}
icon={Loop}
variant="info"
onClose={dismissAlert}
actions={[
<Button
onClick={onReview}
>
{intl.formatMessage(messages.outOfSyncCountAlertReviewBtn)}
</Button>,
]}
/>
);
};

View File

@@ -1,393 +0,0 @@
import React, {
useCallback, useContext, useEffect, useMemo, useState,
} from 'react';
import { getConfig } from '@edx/frontend-platform';
import { useIntl } from '@edx/frontend-platform/i18n';
import {
ActionRow,
Breadcrumb,
Button,
Card,
Hyperlink,
Icon,
Stack,
useToggle,
} from '@openedx/paragon';
import {
tail, keyBy, orderBy, merge, omitBy,
} from 'lodash';
import { useQueryClient } from '@tanstack/react-query';
import { Loop, Warning } from '@openedx/paragon/icons';
import messages from './messages';
import previewChangesMessages from '../course-unit/preview-changes/messages';
import { courseLibrariesQueryKeys, useEntityLinks } from './data/apiHooks';
import {
SearchContextProvider, SearchKeywordsField, useSearchContext, BlockTypeLabel, Highlight, SearchSortWidget,
} from '../search-manager';
import { getItemIcon } from '../generic/block-type-utils';
import type { ContentHit } from '../search-manager/data/api';
import { SearchSortOption } from '../search-manager/data/api';
import Loading from '../generic/Loading';
import { useAcceptLibraryBlockChanges, useIgnoreLibraryBlockChanges } from '../course-unit/data/apiHooks';
import { PreviewLibraryXBlockChanges, LibraryChangesMessageData } from '../course-unit/preview-changes';
import LoadingButton from '../generic/loading-button';
import { ToastContext } from '../generic/toast-context';
import { useLoadOnScroll } from '../hooks';
import DeleteModal from '../generic/delete-modal/DeleteModal';
import { PublishableEntityLink } from './data/api';
import AlertError from '../generic/alert-error';
import AlertMessage from '../generic/alert-message';
interface Props {
courseId: string;
}
interface BlockCardProps {
info: ContentHit;
actions?: React.ReactNode;
}
const BlockCard: React.FC<BlockCardProps> = ({ info, actions }) => {
const intl = useIntl();
const componentIcon = getItemIcon(info.blockType);
const breadcrumbs = tail(info.breadcrumbs) as Array<{ displayName: string, usageKey: string }>;
const getBlockLink = useCallback(() => {
let key = info.usageKey;
if (breadcrumbs?.length > 1) {
key = breadcrumbs[breadcrumbs.length - 1].usageKey || key;
}
return `${getConfig().STUDIO_BASE_URL}/container/${key}`;
}, [info]);
return (
<Card
className="my-3 border-light-500 border shadow-none"
orientation="horizontal"
>
<Card.Section
className="py-3"
>
<Stack direction="horizontal" gap={2}>
<Stack direction="vertical" gap={1}>
<Stack direction="horizontal" gap={1} className="micro text-gray-500">
<Icon src={componentIcon} size="xs" />
<BlockTypeLabel blockType={info.blockType} />
</Stack>
<Stack direction="horizontal" className="small" gap={1}>
<strong>
<Highlight text={info.formatted?.displayName ?? ''} />
</strong>
</Stack>
<Stack direction="horizontal" className="micro" gap={3}>
{intl.formatMessage(messages.breadcrumbLabel)}
<Hyperlink showLaunchIcon={false} destination={getBlockLink()} target="_blank">
<Breadcrumb
className="micro text-gray-700 border-bottom"
ariaLabel={intl.formatMessage(messages.breadcrumbLabel)}
links={breadcrumbs.map((breadcrumb) => ({ label: breadcrumb.displayName }))}
spacer={<span className="custom-spacer">/</span>}
linkAs="span"
/>
</Hyperlink>
</Stack>
</Stack>
{actions}
</Stack>
</Card.Section>
</Card>
);
};
const ComponentReviewList = ({
outOfSyncComponents,
onSearchUpdate,
}: {
outOfSyncComponents: PublishableEntityLink[];
onSearchUpdate: () => void;
}) => {
const intl = useIntl();
const { showToast } = useContext(ToastContext);
const [blockData, setBlockData] = useState<LibraryChangesMessageData | undefined>(undefined);
// ignore changes confirmation modal toggle.
const [isConfirmModalOpen, openConfirmModal, closeConfirmModal] = useToggle(false);
const {
hits: downstreamInfo,
isLoading: isIndexDataLoading,
searchKeywords,
searchSortOrder,
hasError,
hasNextPage,
isFetchingNextPage,
fetchNextPage,
} = useSearchContext() as {
hits: ContentHit[];
isLoading: boolean;
searchKeywords: string;
searchSortOrder: SearchSortOption;
hasError: boolean;
hasNextPage: boolean | undefined,
isFetchingNextPage: boolean;
fetchNextPage: () => void;
};
useLoadOnScroll(
hasNextPage,
isFetchingNextPage,
fetchNextPage,
true,
);
const outOfSyncComponentsByKey = useMemo(
() => keyBy(outOfSyncComponents, 'downstreamUsageKey'),
[outOfSyncComponents],
);
const downstreamInfoByKey = useMemo(
() => keyBy(downstreamInfo, 'usageKey'),
[downstreamInfo],
);
const queryClient = useQueryClient();
useEffect(() => {
if (searchKeywords) {
onSearchUpdate();
}
}, [searchKeywords]);
// Toggle preview changes modal
const [isModalOpen, openModal, closeModal] = useToggle(false);
const acceptChangesMutation = useAcceptLibraryBlockChanges();
const ignoreChangesMutation = useIgnoreLibraryBlockChanges();
const setSeletecdBlockData = (info: ContentHit) => {
setBlockData({
displayName: info.displayName,
downstreamBlockId: info.usageKey,
upstreamBlockId: outOfSyncComponentsByKey[info.usageKey].upstreamUsageKey,
upstreamBlockVersionSynced: outOfSyncComponentsByKey[info.usageKey].versionSynced,
isVertical: info.blockType === 'vertical',
});
};
// Show preview changes on review
const onReview = useCallback((info: ContentHit) => {
setSeletecdBlockData(info);
openModal();
}, [setSeletecdBlockData, openModal]);
const onIgnoreClick = useCallback((info: ContentHit) => {
setSeletecdBlockData(info);
openConfirmModal();
}, [setSeletecdBlockData, openConfirmModal]);
const reloadLinks = useCallback((usageKey: string) => {
const courseKey = outOfSyncComponentsByKey[usageKey].downstreamContextKey;
queryClient.invalidateQueries(courseLibrariesQueryKeys.courseLibraries(courseKey));
}, [outOfSyncComponentsByKey]);
const postChange = (accept: boolean) => {
// istanbul ignore if: this should never happen
if (!blockData) {
return;
}
reloadLinks(blockData.downstreamBlockId);
if (accept) {
showToast(intl.formatMessage(
messages.updateSingleBlockSuccess,
{ name: blockData.displayName },
));
} else {
showToast(intl.formatMessage(
messages.ignoreSingleBlockSuccess,
{ name: blockData.displayName },
));
}
};
const updateBlock = useCallback(async (info: ContentHit) => {
try {
await acceptChangesMutation.mutateAsync(info.usageKey);
reloadLinks(info.usageKey);
showToast(intl.formatMessage(
messages.updateSingleBlockSuccess,
{ name: info.displayName },
));
} catch (e) {
showToast(intl.formatMessage(previewChangesMessages.acceptChangesFailure));
}
}, []);
const ignoreBlock = useCallback(async () => {
// istanbul ignore if: this should never happen
if (!blockData) {
return;
}
try {
await ignoreChangesMutation.mutateAsync(blockData.downstreamBlockId);
reloadLinks(blockData.downstreamBlockId);
showToast(intl.formatMessage(
messages.ignoreSingleBlockSuccess,
{ name: blockData.displayName },
));
} catch (e) {
showToast(intl.formatMessage(previewChangesMessages.ignoreChangesFailure));
} finally {
closeConfirmModal();
}
}, [blockData]);
const orderInfo = useMemo(() => {
if (searchSortOrder !== SearchSortOption.RECENTLY_MODIFIED) {
return downstreamInfo;
}
if (isIndexDataLoading) {
return [];
}
let merged = merge(downstreamInfoByKey, outOfSyncComponentsByKey);
merged = omitBy(merged, (o) => !o.displayName);
const ordered = orderBy(Object.values(merged), 'updated', 'desc');
return ordered;
}, [downstreamInfoByKey, outOfSyncComponentsByKey]);
if (isIndexDataLoading) {
return <Loading />;
}
if (hasError) {
return <AlertError error={intl.formatMessage(messages.genericErrorMessage)} />;
}
return (
<>
{orderInfo?.map((info) => (
<BlockCard
key={info.usageKey}
info={info}
actions={(
<ActionRow>
<Button
size="sm"
variant="outline-primary border-light-300"
onClick={() => onReview(info)}
iconBefore={Loop}
className="mr-2"
>
{intl.formatMessage(messages.cardReviewContentBtn)}
</Button>
<span className="border border-dark py-3 ml-4 mr-3" />
<Button
variant="tertiary"
size="sm"
onClick={() => onIgnoreClick(info)}
>
{intl.formatMessage(messages.cardIgnoreContentBtn)}
</Button>
<LoadingButton
label={intl.formatMessage(messages.cardUpdateContentBtn)}
variant="primary"
size="sm"
onClick={() => updateBlock(info)}
className="rounded-0"
/>
</ActionRow>
)}
/>
))}
<PreviewLibraryXBlockChanges
blockData={blockData}
isModalOpen={isModalOpen}
closeModal={closeModal}
postChange={postChange}
alertNode={(
<AlertMessage
show
variant="warning"
icon={Warning}
title={intl.formatMessage(messages.olderVersionPreviewAlert)}
/>
)}
/>
<DeleteModal
isOpen={isConfirmModalOpen}
close={closeConfirmModal}
variant="warning"
title={intl.formatMessage(previewChangesMessages.confirmationTitle)}
description={intl.formatMessage(previewChangesMessages.confirmationDescription)}
onDeleteSubmit={ignoreBlock}
btnLabel={intl.formatMessage(previewChangesMessages.confirmationConfirmBtn)}
/>
</>
);
};
const ReviewTabContent = ({ courseId }: Props) => {
const intl = useIntl();
const {
data: linkPages,
isLoading: isSyncComponentsLoading,
hasNextPage,
isFetchingNextPage,
fetchNextPage,
isError,
error,
} = useEntityLinks({ courseId, readyToSync: true });
const outOfSyncComponents = useMemo(
() => linkPages?.pages?.reduce((links, page) => [...links, ...page.results], []) ?? [],
[linkPages],
);
const downstreamKeys = useMemo(
() => outOfSyncComponents?.map(link => link.downstreamUsageKey),
[outOfSyncComponents],
);
useLoadOnScroll(
hasNextPage,
isFetchingNextPage,
fetchNextPage,
true,
);
const onSearchUpdate = () => {
if (hasNextPage && !isFetchingNextPage) {
fetchNextPage();
}
};
const disableSortOptions = [
SearchSortOption.RELEVANCE,
SearchSortOption.OLDEST,
SearchSortOption.NEWEST,
SearchSortOption.RECENTLY_PUBLISHED,
];
if (isSyncComponentsLoading) {
return <Loading />;
}
if (isError) {
return <AlertError error={error} />;
}
return (
<SearchContextProvider
extraFilter={[`context_key = "${courseId}"`, `usage_key IN ["${downstreamKeys?.join('","')}"]`]}
skipUrlUpdate
skipBlockTypeFetch
>
<ActionRow>
<SearchKeywordsField
placeholder={intl.formatMessage(messages.searchPlaceholder)}
/>
<SearchSortWidget disableOptions={disableSortOptions} />
<ActionRow.Spacer />
</ActionRow>
<ComponentReviewList
outOfSyncComponents={outOfSyncComponents}
onSearchUpdate={onSearchUpdate}
/>
</SearchContextProvider>
);
};
export default ReviewTabContent;

View File

@@ -1,23 +0,0 @@
{
"id": "lb:OpenedX:CSPROB3:problem:d40264d5-80c4-4be8-bfb6-086391de1cd3",
"def_key": null,
"block_type": "problem",
"display_name": "Dropdown 123",
"last_published": "2025-02-19T13:58:49Z",
"published_by": "edx",
"last_draft_created": "2025-02-19T13:58:48Z",
"last_draft_created_by": null,
"has_unpublished_changes": false,
"created": "2024-10-30T10:48:35Z",
"modified": "2025-02-19T13:58:48Z",
"collections": [
{
"key": "second-collection",
"title": "Second collection"
},
{
"key": "test-collection-2",
"title": "Test collection 2"
}
]
}

View File

@@ -1,20 +0,0 @@
[
{
"upstreamContextTitle": "CS problems 3",
"upstreamContextKey": "lib:OpenedX:CSPROB3",
"readyToSyncCount": 5,
"totalCount": 14
},
{
"upstreamContextTitle": "CS problems 2",
"upstreamContextKey": "lib:OpenedX:CSPROB2",
"readyToSyncCount": 0,
"totalCount": 21
},
{
"upstreamContextTitle": "CS problems",
"upstreamContextKey": "lib:OpenedX:CSPROB",
"readyToSyncCount": 0,
"totalCount": 3
}
]

View File

@@ -1,376 +0,0 @@
{
"results": [
{
"indexUid": "tutor_studio_content",
"hits": [
{
"display_name": "Dropdown",
"block_id": "problem3",
"content": {
"problem_types": [
"optionresponse"
],
"capa_content": "asfd sdaf afd"
},
"description": "asfd sdaf afd",
"tags": {},
"id": "block-v1itcracydemoxcoursextypeproblemblockproblem3-31ecf30b",
"type": "course_block",
"breadcrumbs": [
{
"display_name": "OpenedX Demo Course"
},
{
"display_name": "Module 1: Dive into the Open edX® platform!",
"usage_key": "block-v1:OpenEdx+DemoX+CourseX+type@chapter+block@30b3fbb840024953b2d4b2e700a53002"
},
{
"display_name": "Subsection",
"usage_key": "block-v1:OpenEdx+DemoX+CourseX+type@sequential+block@46032ed27e6c47e887782fed91703240"
},
{
"display_name": "Unit",
"usage_key": "block-v1:OpenEdx+DemoX+CourseX+type@vertical+block@43eb5cbc0ea2491aa93f968f5e705ebd"
}
],
"usage_key": "block-v1:OpenEdx+DemoX+CourseX+type@problem+block@problem3",
"block_type": "problem",
"context_key": "course-v1:OpenEdx+DemoX+CourseX",
"org": "OpenEdx",
"access_id": 4,
"_formatted": {
"display_name": "Dropdown",
"block_id": "problem3",
"content": {
"problem_types": [
"optionresponse"
],
"capa_content": "asfd sdaf afd"
},
"description": "asfd sdaf afd",
"tags": {},
"id": "block-v1itcracydemoxcoursextypeproblemblockproblem3-31ecf30b",
"type": "course_block",
"breadcrumbs": [
{
"display_name": "OpenedX Demo Course"
},
{
"display_name": "Module 1: Dive into the Open edX® platform!",
"usage_key": "block-v1:OpenEdx+DemoX+CourseX+type@chapter+block@30b3fbb840024953b2d4b2e700a53002"
},
{
"display_name": "Subsection",
"usage_key": "block-v1:OpenEdx+DemoX+CourseX+type@sequential+block@46032ed27e6c47e887782fed91703240"
},
{
"display_name": "Unit",
"usage_key": "block-v1:OpenEdx+DemoX+CourseX+type@vertical+block@43eb5cbc0ea2491aa93f968f5e705ebd"
}
],
"usage_key": "block-v1:OpenEdx+DemoX+CourseX+type@problem+block@problem3",
"block_type": "problem",
"context_key": "course-v1:OpenEdx+DemoX+CourseX",
"org": "OpenEdx",
"access_id": "4"
}
},
{
"display_name": "Dropdown",
"block_id": "problem6",
"content": {
"problem_types": [
"optionresponse"
],
"capa_content": "asfd sdaf afd"
},
"description": "asfd sdaf afd",
"tags": {},
"id": "block-v1itcracydemoxcoursextypeproblemblockproblem6-5bb64bab",
"type": "course_block",
"breadcrumbs": [
{
"display_name": "OpenedX Demo Course"
},
{
"display_name": "Module 1: Dive into the Open edX® platform!",
"usage_key": "block-v1:OpenEdx+DemoX+CourseX+type@chapter+block@30b3fbb840024953b2d4b2e700a53002"
},
{
"display_name": "Subsection",
"usage_key": "block-v1:OpenEdx+DemoX+CourseX+type@sequential+block@46032ed27e6c47e887782fed91703240"
},
{
"display_name": "Unit",
"usage_key": "block-v1:OpenEdx+DemoX+CourseX+type@vertical+block@43eb5cbc0ea2491aa93f968f5e705ebd"
}
],
"usage_key": "block-v1:OpenEdx+DemoX+CourseX+type@problem+block@problem6",
"block_type": "problem",
"context_key": "course-v1:OpenEdx+DemoX+CourseX",
"org": "OpenEdx",
"access_id": 4,
"_formatted": {
"display_name": "Dropdown",
"block_id": "problem6",
"content": {
"problem_types": [
"optionresponse"
],
"capa_content": "asfd sdaf afd"
},
"description": "asfd sdaf afd",
"tags": {},
"id": "block-v1itcracydemoxcoursextypeproblemblockproblem6-5bb64bab",
"type": "course_block",
"breadcrumbs": [
{
"display_name": "OpenedX Demo Course"
},
{
"display_name": "Module 1: Dive into the Open edX® platform!",
"usage_key": "block-v1:OpenEdx+DemoX+CourseX+type@chapter+block@30b3fbb840024953b2d4b2e700a53002"
},
{
"display_name": "Subsection",
"usage_key": "block-v1:OpenEdx+DemoX+CourseX+type@sequential+block@46032ed27e6c47e887782fed91703240"
},
{
"display_name": "Unit",
"usage_key": "block-v1:OpenEdx+DemoX+CourseX+type@vertical+block@43eb5cbc0ea2491aa93f968f5e705ebd"
}
],
"usage_key": "block-v1:OpenEdx+DemoX+CourseX+type@problem+block@problem6",
"block_type": "problem",
"context_key": "course-v1:OpenEdx+DemoX+CourseX",
"org": "OpenEdx",
"access_id": "4"
}
},
{
"display_name": "Dropdown",
"block_id": "210e356cfa304b0aac591af53f6a6ae0",
"content": {
"problem_types": [
"optionresponse"
],
"capa_content": "asfd sdaf afd"
},
"description": "asfd sdaf afd",
"tags": {},
"id": "block-v1itcracydemoxcoursextypeproblemblock210e356cfa304b0aac591af53f6a6ae0-d02782e3",
"type": "course_block",
"breadcrumbs": [
{
"display_name": "OpenedX Demo Course"
},
{
"display_name": "Module 1: Dive into the Open edX® platform!",
"usage_key": "block-v1:OpenEdx+DemoX+CourseX+type@chapter+block@30b3fbb840024953b2d4b2e700a53002"
},
{
"display_name": "Subsection",
"usage_key": "block-v1:OpenEdx+DemoX+CourseX+type@sequential+block@46032ed27e6c47e887782fed91703240"
},
{
"display_name": "Unit",
"usage_key": "block-v1:OpenEdx+DemoX+CourseX+type@vertical+block@59dc4eea07fa4c20aad677d433ada9f1"
},
{
"display_name": "Problem Bank",
"usage_key": "block-v1:OpenEdx+DemoX+CourseX+type@itembank+block@3ba55d8eae5544aa9d3fb5e3f100ed62"
}
],
"usage_key": "block-v1:OpenEdx+DemoX+CourseX+type@problem+block@210e356cfa304b0aac591af53f6a6ae0",
"block_type": "problem",
"context_key": "course-v1:OpenEdx+DemoX+CourseX",
"org": "OpenEdx",
"access_id": 4,
"_formatted": {
"display_name": "Dropdown",
"block_id": "210e356cfa304b0aac591af53f6a6ae0",
"content": {
"problem_types": [
"optionresponse"
],
"capa_content": "asfd sdaf afd"
},
"description": "asfd sdaf afd",
"tags": {},
"id": "block-v1itcracydemoxcoursextypeproblemblock210e356cfa304b0aac591af53f6a6ae0-d02782e3",
"type": "course_block",
"breadcrumbs": [
{
"display_name": "OpenedX Demo Course"
},
{
"display_name": "Module 1: Dive into the Open edX® platform!",
"usage_key": "block-v1:OpenEdx+DemoX+CourseX+type@chapter+block@30b3fbb840024953b2d4b2e700a53002"
},
{
"display_name": "Subsection",
"usage_key": "block-v1:OpenEdx+DemoX+CourseX+type@sequential+block@46032ed27e6c47e887782fed91703240"
},
{
"display_name": "Unit",
"usage_key": "block-v1:OpenEdx+DemoX+CourseX+type@vertical+block@59dc4eea07fa4c20aad677d433ada9f1"
},
{
"display_name": "Problem Bank",
"usage_key": "block-v1:OpenEdx+DemoX+CourseX+type@itembank+block@3ba55d8eae5544aa9d3fb5e3f100ed62"
}
],
"usage_key": "block-v1:OpenEdx+DemoX+CourseX+type@problem+block@210e356cfa304b0aac591af53f6a6ae0",
"block_type": "problem",
"context_key": "course-v1:OpenEdx+DemoX+CourseX",
"org": "OpenEdx",
"access_id": "4"
}
},
{
"display_name": "HTML 1",
"block_id": "257e68e3386d4a8f8739d45b67e76a9b",
"content": {
"html_content": "A step beyond the simplicity of the WYSIWYG editor is the Raw HTML component which allows you to use HTML, CSS, and Javascript to create highly flexible content that looks good, excites your learners, and keeps them engaged and coming back to your courses. Unlike many learning platforms, the Open edX platform does not restrict authors in terms of what can be used in HTML components. If you can build it, the platform will not stop you. But use judgement and consider consulting with a web design expert. When used correctly, custom HTML, CSS and Javascript can be powerful tools for a course developer. But they can also lead to experiences that are inaccessible, unpleasant or even insecure to the learner and the provider. 1"
},
"description": "A step beyond the simplicity of the WYSIWYG editor is the Raw HTML component which allows you to use HTML, CSS, and Javascript to create highly flexible content that looks good, excites your learners, and keeps them engaged and coming back to your courses. Unlike many learning platforms, the Open edX platform does not restrict authors in terms of what can be used in HTML components. If you can build it, the platform will not stop you. But use judgement and consider consulting with a web design expert. When used correctly, custom HTML, CSS and Javascript can be powerful tools for a course developer. But they can also lead to experiences that are inaccessible, unpleasant or even insecure to the learner and the provider. 1",
"tags": {},
"id": "block-v1itcracydemoxcoursextypehtmlblock257e68e3386d4a8f8739d45b67e76a9b-e5cd0344",
"type": "course_block",
"breadcrumbs": [
{
"display_name": "OpenedX Demo Course"
},
{
"display_name": "Module 1: Dive into the Open edX® platform!",
"usage_key": "block-v1:OpenEdx+DemoX+CourseX+type@chapter+block@30b3fbb840024953b2d4b2e700a53002"
},
{
"display_name": "Subsection",
"usage_key": "block-v1:OpenEdx+DemoX+CourseX+type@sequential+block@46032ed27e6c47e887782fed91703240"
},
{
"display_name": "Unit",
"usage_key": "block-v1:OpenEdx+DemoX+CourseX+type@vertical+block@59dc4eea07fa4c20aad677d433ada9f1"
}
],
"usage_key": "block-v1:OpenEdx+DemoX+CourseX+type@html+block@257e68e3386d4a8f8739d45b67e76a9b",
"block_type": "html",
"context_key": "course-v1:OpenEdx+DemoX+CourseX",
"org": "OpenEdx",
"access_id": 4,
"_formatted": {
"display_name": "HTML 1",
"block_id": "257e68e3386d4a8f8739d45b67e76a9b",
"content": {
"html_content": "A step beyond the simplicity of the WYSIWYG editor is the Raw HTML component which allows you to use HTML, CSS, and Javascript to create highly flexible content that looks good, excites your learners, and keeps them engaged and coming back to your courses. Unlike many learning platforms, the Open edX platform does not restrict authors in terms of what can be used in HTML components. If you can build it, the platform will not stop you. But use judgement and consider consulting with a web design expert. When used correctly, custom HTML, CSS and Javascript can be powerful tools for a course developer. But they can also lead to experiences that are inaccessible, unpleasant or even insecure to the learner and the provider. 1"
},
"description": "A step beyond the simplicity of the WYSIWYG editor is…",
"tags": {},
"id": "block-v1itcracydemoxcoursextypehtmlblock257e68e3386d4a8f8739d45b67e76a9b-e5cd0344",
"type": "course_block",
"breadcrumbs": [
{
"display_name": "OpenedX Demo Course"
},
{
"display_name": "Module 1: Dive into the Open edX® platform!",
"usage_key": "block-v1:OpenEdx+DemoX+CourseX+type@chapter+block@30b3fbb840024953b2d4b2e700a53002"
},
{
"display_name": "Subsection",
"usage_key": "block-v1:OpenEdx+DemoX+CourseX+type@sequential+block@46032ed27e6c47e887782fed91703240"
},
{
"display_name": "Unit",
"usage_key": "block-v1:OpenEdx+DemoX+CourseX+type@vertical+block@59dc4eea07fa4c20aad677d433ada9f1"
}
],
"usage_key": "block-v1:OpenEdx+DemoX+CourseX+type@html+block@257e68e3386d4a8f8739d45b67e76a9b",
"block_type": "html",
"context_key": "course-v1:OpenEdx+DemoX+CourseX",
"org": "OpenEdx",
"access_id": "4"
}
},
{
"display_name": "Dropdown",
"block_id": "a4455860b03647219ff8b01cde49cf37",
"content": {
"problem_types": [
"optionresponse"
],
"capa_content": "asfd sdaf afd"
},
"description": "asfd sdaf afd",
"tags": {},
"id": "block-v1itcracydemoxcoursextypeproblemblocka4455860b03647219ff8b01cde49cf37-709012d2",
"type": "course_block",
"breadcrumbs": [
{
"display_name": "OpenedX Demo Course"
},
{
"display_name": "Module 1: Dive into the Open edX® platform!",
"usage_key": "block-v1:OpenEdx+DemoX+CourseX+type@chapter+block@30b3fbb840024953b2d4b2e700a53002"
},
{
"display_name": "Subsection",
"usage_key": "block-v1:OpenEdx+DemoX+CourseX+type@sequential+block@46032ed27e6c47e887782fed91703240"
},
{
"display_name": "Unit",
"usage_key": "block-v1:OpenEdx+DemoX+CourseX+type@vertical+block@59dc4eea07fa4c20aad677d433ada9f1"
}
],
"usage_key": "block-v1:OpenEdx+DemoX+CourseX+type@problem+block@a4455860b03647219ff8b01cde49cf37",
"block_type": "problem",
"context_key": "course-v1:OpenEdx+DemoX+CourseX",
"org": "OpenEdx",
"access_id": 4,
"_formatted": {
"display_name": "Dropdown",
"block_id": "a4455860b03647219ff8b01cde49cf37",
"content": {
"problem_types": [
"optionresponse"
],
"capa_content": "asfd sdaf afd"
},
"description": "asfd sdaf afd",
"tags": {},
"id": "block-v1itcracydemoxcoursextypeproblemblocka4455860b03647219ff8b01cde49cf37-709012d2",
"type": "course_block",
"breadcrumbs": [
{
"display_name": "OpenedX Demo Course"
},
{
"display_name": "Module 1: Dive into the Open edX® platform!",
"usage_key": "block-v1:OpenEdx+DemoX+CourseX+type@chapter+block@30b3fbb840024953b2d4b2e700a53002"
},
{
"display_name": "Subsection",
"usage_key": "block-v1:OpenEdx+DemoX+CourseX+type@sequential+block@46032ed27e6c47e887782fed91703240"
},
{
"display_name": "Unit",
"usage_key": "block-v1:OpenEdx+DemoX+CourseX+type@vertical+block@59dc4eea07fa4c20aad677d433ada9f1"
}
],
"usage_key": "block-v1:OpenEdx+DemoX+CourseX+type@problem+block@a4455860b03647219ff8b01cde49cf37",
"block_type": "problem",
"context_key": "course-v1:OpenEdx+DemoX+CourseX",
"org": "OpenEdx",
"access_id": "4"
}
}
],
"query": "",
"processingTimeMs": 8,
"limit": 20,
"offset": 0,
"estimatedTotalHits": 5
}
]
}

View File

@@ -1,79 +1,100 @@
{
"count": 7,
"next": null,
"previous": null,
"num_pages": 1,
"current_page": 1,
"results": [
{
"id": 875,
"upstreamContextTitle": "CS problems 3",
"upstreamVersion": 10,
"readyToSync": true,
"upstreamUsageKey": "lb:OpenedX:CSPROB3:problem:d40264d5-80c4-4be8-bfb6-086391de1cd3",
"upstreamContextKey": "lib:OpenedX:CSPROB3",
"downstreamUsageKey": "block-v1:OpenEdx+DemoX+CourseX+type@problem+block@problem3",
"downstreamContextKey": "course-v1:OpenEdx+DemoX+CourseX",
"versionSynced": 2,
"versionDeclined": null,
"created": "2025-02-08T14:07:05.588484Z",
"updated": "2025-02-08T14:07:05.588484Z"
},
{
"id": 876,
"upstreamContextTitle": "CS problems 3",
"upstreamVersion": 10,
"readyToSync": true,
"upstreamUsageKey": "lb:OpenedX:CSPROB3:problem:d40264d5-80c4-4be8-bfb6-086391de1cd3",
"upstreamContextKey": "lib:OpenedX:CSPROB3",
"downstreamUsageKey": "block-v1:OpenEdx+DemoX+CourseX+type@problem+block@problem6",
"downstreamContextKey": "course-v1:OpenEdx+DemoX+CourseX",
"versionSynced": 2,
"versionDeclined": null,
"created": "2025-02-08T14:07:05.588484Z",
"updated": "2025-02-08T14:07:05.588484Z"
},
{
"id": 884,
"upstreamContextTitle": "CS problems 3",
"upstreamVersion": 26,
"readyToSync": true,
"upstreamUsageKey": "lb:OpenedX:CSPROB3:html:ca4d2b1f-0b64-4a2d-88fa-592f7e398477",
"upstreamContextKey": "lib:OpenedX:CSPROB3",
"downstreamUsageKey": "block-v1:OpenEdx+DemoX+CourseX+type@html+block@257e68e3386d4a8f8739d45b67e76a9b",
"downstreamContextKey": "course-v1:OpenEdx+DemoX+CourseX",
"versionSynced": 16,
"versionDeclined": null,
"created": "2025-02-08T14:07:05.588484Z",
"updated": "2025-02-08T14:07:05.588484Z"
},
{
"id": 889,
"upstreamContextTitle": "CS problems 3",
"upstreamVersion": 10,
"readyToSync": true,
"upstreamUsageKey": "lb:OpenedX:CSPROB3:problem:d40264d5-80c4-4be8-bfb6-086391de1cd3",
"upstreamContextKey": "lib:OpenedX:CSPROB3",
"downstreamUsageKey": "block-v1:OpenEdx+DemoX+CourseX+type@problem+block@a4455860b03647219ff8b01cde49cf37",
"downstreamContextKey": "course-v1:OpenEdx+DemoX+CourseX",
"versionSynced": 2,
"versionDeclined": null,
"created": "2025-02-08T14:07:05.588484Z",
"updated": "2025-02-08T14:07:05.588484Z"
},
{
"id": 890,
"upstreamContextTitle": "CS problems 3",
"upstreamVersion": 10,
"readyToSync": true,
"upstreamUsageKey": "lb:OpenedX:CSPROB3:problem:d40264d5-80c4-4be8-bfb6-086391de1cd3",
"upstreamContextKey": "lib:OpenedX:CSPROB3",
"downstreamUsageKey": "block-v1:OpenEdx+DemoX+CourseX+type@problem+block@210e356cfa304b0aac591af53f6a6ae0",
"downstreamContextKey": "course-v1:OpenEdx+DemoX+CourseX",
"versionSynced": 2,
"versionDeclined": null,
"created": "2025-02-08T14:07:05.588484Z",
"updated": "2025-02-08T14:07:05.588484Z"
}
]
}
[
{
"id": 970,
"upstreamContextTitle": "CS problems 2",
"upstreamVersion": 15,
"readyToSync": false,
"upstreamUsageKey": "lb:OpenedX:CSPROB2:html:c0c1ca28-ff25-4757-83bc-3a2c2a0fe9c8",
"upstreamContextKey": "lib:OpenedX:CSPROB2",
"downstreamUsageKey": "block-v1:NewOrg3+TSTCS+2025_T1+type@html+block@0dfbabb8c93140caa3a004ab621757f5",
"downstreamContextKey": "course-v1:NewOrg3+TSTCS+2025_T1",
"versionSynced": 15,
"versionDeclined": 13,
"created": "2025-02-08T14:11:23.650589Z",
"updated": "2025-02-08T14:11:23.650589Z"
},
{
"id": 971,
"upstreamContextTitle": "CS problems 2",
"upstreamVersion": 3,
"readyToSync": false,
"upstreamUsageKey": "lb:OpenedX:CSPROB2:html:fd2d3827-e633-4217-bca9-c6661086b4b2",
"upstreamContextKey": "lib:OpenedX:CSPROB2",
"downstreamUsageKey": "block-v1:NewOrg3+TSTCS+2025_T1+type@html+block@37a916b53c2f430989d33ddeb47e224d",
"downstreamContextKey": "course-v1:NewOrg3+TSTCS+2025_T1",
"versionSynced": 3,
"versionDeclined": null,
"created": "2025-02-08T14:11:23.650589Z",
"updated": "2025-02-08T14:11:23.650589Z"
},
{
"id": 972,
"upstreamContextTitle": "CS problems 2",
"upstreamVersion": 3,
"readyToSync": false,
"upstreamUsageKey": "lb:OpenedX:CSPROB2:video:ba2023d4-b4e4-44a5-bfc8-322203e8737f",
"upstreamContextKey": "lib:OpenedX:CSPROB2",
"downstreamUsageKey": "block-v1:NewOrg3+TSTCS+2025_T1+type@video+block@33518787c3e14844ad823af8ff158a83",
"downstreamContextKey": "course-v1:NewOrg3+TSTCS+2025_T1",
"versionSynced": 3,
"versionDeclined": null,
"created": "2025-02-08T14:11:23.650589Z",
"updated": "2025-02-08T14:11:23.650589Z"
},
{
"id": 974,
"upstreamContextTitle": "CS problems 3",
"upstreamVersion": 18,
"readyToSync": false,
"upstreamUsageKey": "lb:OpenedX:CSPROB3:html:ca4d2b1f-0b64-4a2d-88fa-592f7e398477",
"upstreamContextKey": "lib:OpenedX:CSPROB3",
"downstreamUsageKey": "block-v1:NewOrg3+TSTCS+2025_T1+type@html+block@5be06d70f09c45a581d973d215e7ad62",
"downstreamContextKey": "course-v1:NewOrg3+TSTCS+2025_T1",
"versionSynced": 17,
"versionDeclined": 18,
"created": "2025-02-12T05:38:53.967738Z",
"updated": "2025-02-12T05:41:01.225542Z"
},
{
"id": 975,
"upstreamContextTitle": "CS problems 3",
"upstreamVersion": 1,
"readyToSync": false,
"upstreamUsageKey": "lb:OpenedX:CSPROB3:html:4abdfa10-dd1a-4ebb-bad3-489000671acb",
"upstreamContextKey": "lib:OpenedX:CSPROB3",
"downstreamUsageKey": "block-v1:NewOrg3+TSTCS+2025_T1+type@html+block@d1b006742e0745b0948c8ba8dcd9bc07",
"downstreamContextKey": "course-v1:NewOrg3+TSTCS+2025_T1",
"versionSynced": 1,
"versionDeclined": null,
"created": "2025-02-12T05:38:55.899821Z",
"updated": "2025-02-12T05:38:55.899821Z"
},
{
"id": 976,
"upstreamContextTitle": "CS problems 3",
"upstreamVersion": 1,
"readyToSync": false,
"upstreamUsageKey": "lb:OpenedX:CSPROB3:html:6aff1b41-e406-41ff-9d31-70d02ef42deb",
"upstreamContextKey": "lib:OpenedX:CSPROB3",
"downstreamUsageKey": "block-v1:NewOrg3+TSTCS+2025_T1+type@html+block@276914b632eb49049b2c2568ef24fe4d",
"downstreamContextKey": "course-v1:NewOrg3+TSTCS+2025_T1",
"versionSynced": 1,
"versionDeclined": null,
"created": "2025-02-12T05:38:57.228152Z",
"updated": "2025-02-12T05:38:57.228152Z"
},
{
"id": 977,
"upstreamContextTitle": "CS problems 3",
"upstreamVersion": 3,
"readyToSync": true,
"upstreamUsageKey": "lb:OpenedX:CSPROB3:problem:d40264d5-80c4-4be8-bfb6-086391de1cd3",
"upstreamContextKey": "lib:OpenedX:CSPROB3",
"downstreamUsageKey": "block-v1:NewOrg3+TSTCS+2025_T1+type@problem+block@fcf430ac5b7a44b6ab316b53a7e67fef",
"downstreamContextKey": "course-v1:NewOrg3+TSTCS+2025_T1",
"versionSynced": 2,
"versionDeclined": null,
"created": "2025-02-12T05:38:58.538280Z",
"updated": "2025-02-12T05:38:58.538280Z"
}
]

View File

@@ -1,121 +1,36 @@
/* istanbul ignore file */
// eslint-disable-next-line import/no-extraneous-dependencies
import fetchMock from 'fetch-mock-jest';
import mockLinksResult from '../__mocks__/publishableEntityLinks.json';
import mockSummaryResult from '../__mocks__/linkCourseSummary.json';
import mockLinkDetailsFromIndex from '../__mocks__/linkDetailsFromIndex.json';
import mockLibBlockMetadata from '../__mocks__/libBlockMetadata.json';
import { createAxiosError } from '../../testUtils';
import * as api from './api';
import * as libApi from '../../library-authoring/data/api';
/**
* Mock for `getEntityLinks()`
* Mock for `getEntityLinksByDownstreamContext()`
*
* This mock returns a fixed response for the downstreamContextKey.
*/
export async function mockGetEntityLinks(
downstreamContextKey?: string,
readyToSync?: boolean,
): ReturnType<typeof api.getEntityLinks> {
export async function mockGetEntityLinksByDownstreamContext(
downstreamContextKey: string,
): Promise<api.PublishableEntityLink[]> {
switch (downstreamContextKey) {
case mockGetEntityLinks.invalidCourseKey:
case mockGetEntityLinksByDownstreamContext.invalidCourseKey:
throw createAxiosError({
code: 404,
message: 'Not found.',
path: api.getEntityLinksByDownstreamContextUrl(),
path: api.getEntityLinksByDownstreamContextUrl(downstreamContextKey),
});
case mockGetEntityLinks.courseKeyLoading:
case mockGetEntityLinksByDownstreamContext.courseKeyLoading:
return new Promise(() => {});
case mockGetEntityLinks.courseKeyEmpty:
return Promise.resolve({
next: null,
previous: null,
nextPageNum: null,
previousPageNum: null,
count: 0,
numPages: 0,
currentPage: 0,
results: [],
});
default: {
const { response } = mockGetEntityLinks;
if (readyToSync !== undefined) {
response.results = response.results.filter((o) => o.readyToSync === readyToSync);
response.count = response.results.length;
}
return Promise.resolve(response);
}
}
}
mockGetEntityLinks.courseKey = mockLinksResult.results[0].downstreamContextKey;
mockGetEntityLinks.invalidCourseKey = 'course_key_error';
mockGetEntityLinks.courseKeyLoading = 'courseKeyLoading';
mockGetEntityLinks.courseKeyEmpty = 'courseKeyEmpty';
mockGetEntityLinks.response = mockLinksResult;
/** Apply this mock. Returns a spy object that can tell you if it's been called. */
mockGetEntityLinks.applyMock = () => {
jest.spyOn(api, 'getEntityLinks').mockImplementation(mockGetEntityLinks);
};
/**
* Mock for `getEntityLinksSummaryByDownstreamContext()`
*
* This mock returns a fixed response for the downstreamContextKey.
*/
export async function mockGetEntityLinksSummaryByDownstreamContext(
courseId?: string,
): ReturnType<typeof api.getEntityLinksSummaryByDownstreamContext> {
switch (courseId) {
case mockGetEntityLinksSummaryByDownstreamContext.invalidCourseKey:
throw createAxiosError({
code: 404,
message: 'Not found.',
path: api.getEntityLinksByDownstreamContextUrl(),
});
case mockGetEntityLinksSummaryByDownstreamContext.courseKeyLoading:
return new Promise(() => {});
case mockGetEntityLinksSummaryByDownstreamContext.courseKeyEmpty:
case mockGetEntityLinksByDownstreamContext.courseKeyEmpty:
return Promise.resolve([]);
case mockGetEntityLinksSummaryByDownstreamContext.courseKeyUpToDate:
return Promise.resolve(mockGetEntityLinksSummaryByDownstreamContext.response.filter(
(o: { readyToSyncCount: number }) => o.readyToSyncCount === 0,
));
default:
return Promise.resolve(mockGetEntityLinksSummaryByDownstreamContext.response);
return Promise.resolve(mockGetEntityLinksByDownstreamContext.response);
}
}
mockGetEntityLinksSummaryByDownstreamContext.courseKey = mockLinksResult.results[0].downstreamContextKey;
mockGetEntityLinksSummaryByDownstreamContext.invalidCourseKey = 'course_key_error';
mockGetEntityLinksSummaryByDownstreamContext.courseKeyLoading = 'courseKeySummaryLoading';
mockGetEntityLinksSummaryByDownstreamContext.courseKeyEmpty = 'courseKeyEmpty';
mockGetEntityLinksSummaryByDownstreamContext.courseKeyUpToDate = 'courseKeyUpToDate';
mockGetEntityLinksSummaryByDownstreamContext.response = mockSummaryResult;
mockGetEntityLinksByDownstreamContext.courseKey = mockLinksResult[0].downstreamContextKey;
mockGetEntityLinksByDownstreamContext.invalidCourseKey = 'course_key_error';
mockGetEntityLinksByDownstreamContext.courseKeyLoading = 'courseKeyLoading';
mockGetEntityLinksByDownstreamContext.courseKeyEmpty = 'courseKeyEmpty';
mockGetEntityLinksByDownstreamContext.response = mockLinksResult;
/** Apply this mock. Returns a spy object that can tell you if it's been called. */
mockGetEntityLinksSummaryByDownstreamContext.applyMock = () => {
jest.spyOn(api, 'getEntityLinksSummaryByDownstreamContext').mockImplementation(mockGetEntityLinksSummaryByDownstreamContext);
};
/**
* Mock for multi-search from meilisearch index for link details.
*/
export async function mockFetchIndexDocuments() {
return mockLinkDetailsFromIndex;
}
mockFetchIndexDocuments.applyMock = () => {
fetchMock.post(
'http://mock.meilisearch.local/multi-search',
mockFetchIndexDocuments,
{ overwriteRoutes: true },
);
};
/**
* Mock for library block metadata
*/
export async function mockUseLibBlockMetadata() {
return mockLibBlockMetadata;
}
mockUseLibBlockMetadata.applyMock = () => {
jest.spyOn(libApi, 'getLibraryBlockMetadata').mockImplementation(mockUseLibBlockMetadata);
mockGetEntityLinksByDownstreamContext.applyMock = () => {
jest.spyOn(api, 'getEntityLinksByDownstreamContext').mockImplementation(mockGetEntityLinksByDownstreamContext);
};

View File

@@ -3,84 +3,27 @@ import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
const getApiBaseUrl = () => getConfig().STUDIO_BASE_URL;
export const getEntityLinksByDownstreamContextUrl = () => `${getApiBaseUrl()}/api/contentstore/v2/downstreams/`;
export const getEntityLinksSummaryByDownstreamContextUrl = (downstreamContextKey: string) => `${getApiBaseUrl()}/api/contentstore/v2/downstreams/${downstreamContextKey}/summary`;
export interface PaginatedData<T> {
next: string | null;
previous: string | null;
nextPageNum: number | null;
previousPageNum: number | null;
count: number;
numPages: number;
currentPage: number;
results: T,
}
export const getEntityLinksByDownstreamContextUrl = (downstreamContextKey: string) => `${getApiBaseUrl()}/api/contentstore/v2/upstreams/${downstreamContextKey}`;
export interface PublishableEntityLink {
id: number;
upstreamUsageKey: string;
upstreamContextKey: string;
upstreamContextTitle: string;
upstreamVersion: number;
upstreamVersion: string;
downstreamUsageKey: string;
downstreamContextTitle: string;
downstreamContextKey: string;
versionSynced: number;
versionDeclined: number | null;
versionSynced: string;
versionDeclined: string;
created: string;
updated: string;
readyToSync: boolean;
}
export interface PublishableEntityLinkSummary {
upstreamContextKey: string;
upstreamContextTitle: string;
readyToSyncCount: number;
totalCount: number;
}
export const getEntityLinks = async (
downstreamContextKey?: string,
readyToSync?: boolean,
upstreamUsageKey?: string,
pageParam?: number,
pageSize?: number,
): Promise<PaginatedData<PublishableEntityLink[]>> => {
const { data } = await getAuthenticatedHttpClient()
.get(getEntityLinksByDownstreamContextUrl(), {
params: {
course_id: downstreamContextKey,
ready_to_sync: readyToSync,
upstream_usage_key: upstreamUsageKey,
page_size: pageSize,
page: pageParam,
},
});
return camelCaseObject(data);
};
export const getUnpaginatedEntityLinks = async (
downstreamContextKey?: string,
readyToSync?: boolean,
upstreamUsageKey?: string,
export const getEntityLinksByDownstreamContext = async (
downstreamContextKey: string,
): Promise<PublishableEntityLink[]> => {
const { data } = await getAuthenticatedHttpClient()
.get(getEntityLinksByDownstreamContextUrl(), {
params: {
course_id: downstreamContextKey,
ready_to_sync: readyToSync,
upstream_usage_key: upstreamUsageKey,
no_page: true,
},
});
return camelCaseObject(data);
};
export const getEntityLinksSummaryByDownstreamContext = async (
downstreamContextKey: string,
): Promise<PublishableEntityLinkSummary[]> => {
const { data } = await getAuthenticatedHttpClient()
.get(getEntityLinksSummaryByDownstreamContextUrl(downstreamContextKey));
.get(getEntityLinksByDownstreamContextUrl(downstreamContextKey));
return camelCaseObject(data);
};

View File

@@ -1,10 +1,11 @@
import { initializeMockApp } from '@edx/frontend-platform';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import { renderHook } from '@testing-library/react-hooks';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import MockAdapter from 'axios-mock-adapter';
import { renderHook, waitFor } from '@testing-library/react';
import { waitFor } from '@testing-library/react';
import { getEntityLinksByDownstreamContextUrl } from './api';
import { useEntityLinks, useUnpaginatedEntityLinks } from './apiHooks';
import { useEntityLinksByDownstreamContext } from './apiHooks';
let axiosMock: MockAdapter;
@@ -35,39 +36,15 @@ describe('course libraries api hooks', () => {
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
});
afterEach(() => {
axiosMock.reset();
});
it('should return paginated links for course', async () => {
const courseId = 'course-v1:some+key';
const url = getEntityLinksByDownstreamContextUrl();
const expectedResult = {
next: null, results: [], previous: null, total: 0,
};
axiosMock.onGet(url).reply(200, expectedResult);
const { result } = renderHook(() => useEntityLinks({ courseId }), { wrapper });
await waitFor(() => {
expect(result.current.isLoading).toBeFalsy();
});
expect(result.current.data?.pages).toEqual([expectedResult]);
expect(axiosMock.history.get[0].url).toEqual(url);
});
it('should return links for course', async () => {
const courseId = 'course-v1:some+key';
const url = getEntityLinksByDownstreamContextUrl();
it('should create library block', async () => {
const courseKey = 'course-v1:some+key';
const url = getEntityLinksByDownstreamContextUrl(courseKey);
axiosMock.onGet(url).reply(200, []);
const { result } = renderHook(() => useUnpaginatedEntityLinks({ courseId }), { wrapper });
const { result } = renderHook(() => useEntityLinksByDownstreamContext(courseKey), { wrapper });
await waitFor(() => {
expect(result.current.isLoading).toBeFalsy();
});
expect(result.current.data).toEqual([]);
expect(axiosMock.history.get[0].url).toEqual(url);
expect(axiosMock.history.get[0].params).toEqual({
course_id: courseId,
ready_to_sync: undefined,
upstream_usage_key: undefined,
no_page: true,
});
});
});

View File

@@ -1,95 +1,20 @@
import {
useInfiniteQuery,
useQuery,
} from '@tanstack/react-query';
import { getEntityLinks, getEntityLinksSummaryByDownstreamContext, getUnpaginatedEntityLinks } from './api';
import { getEntityLinksByDownstreamContext } from './api';
export const courseLibrariesQueryKeys = {
all: ['courseLibraries'],
courseLibraries: (courseId?: string) => [...courseLibrariesQueryKeys.all, courseId],
courseReadyToSyncLibraries: ({ courseId, readyToSync, upstreamUsageKey }: {
courseId?: string,
readyToSync?: boolean,
upstreamUsageKey?: string,
pageSize?: number,
}) => {
const key: Array<string | boolean | number> = [...courseLibrariesQueryKeys.all];
if (courseId !== undefined) {
key.push(courseId);
}
if (readyToSync !== undefined) {
key.push(readyToSync);
}
if (upstreamUsageKey !== undefined) {
key.push(upstreamUsageKey);
}
return key;
},
courseLibrariesSummary: (courseId?: string) => [...courseLibrariesQueryKeys.courseLibraries(courseId), 'summary'],
courseLibraries: (courseKey?: string) => [...courseLibrariesQueryKeys.all, courseKey],
};
/**
* Hook to fetch publishable entity links by course key.
* (That is, get a list of the library components used in the given course.)
* Hook to fetch a content library by its ID.
*/
export const useEntityLinks = ({
courseId, readyToSync, upstreamUsageKey, pageSize,
}: {
courseId?: string,
readyToSync?: boolean,
upstreamUsageKey?: string,
pageSize?: number
}) => (
useInfiniteQuery({
queryKey: courseLibrariesQueryKeys.courseReadyToSyncLibraries({
courseId,
readyToSync,
upstreamUsageKey,
}),
queryFn: ({ pageParam }) => getEntityLinks(
courseId,
readyToSync,
upstreamUsageKey,
pageParam,
pageSize,
),
getNextPageParam: (lastPage) => lastPage.nextPageNum,
enabled: courseId !== undefined || upstreamUsageKey !== undefined || readyToSync !== undefined,
})
);
/**
* Hook to fetch unpaginated list of publishable entity links by course key.
*/
export const useUnpaginatedEntityLinks = ({
courseId, readyToSync, upstreamUsageKey,
}: {
courseId?: string,
readyToSync?: boolean,
upstreamUsageKey?: string,
}) => (
export const useEntityLinksByDownstreamContext = (courseKey: string | undefined) => (
useQuery({
queryKey: courseLibrariesQueryKeys.courseReadyToSyncLibraries({
courseId,
readyToSync,
upstreamUsageKey,
}),
queryFn: () => getUnpaginatedEntityLinks(
courseId,
readyToSync,
upstreamUsageKey,
),
enabled: courseId !== undefined || upstreamUsageKey !== undefined || readyToSync !== undefined,
})
);
/**
* Hook to fetch publishable entity links summary by course key.
*/
export const useEntityLinksSummaryByDownstreamContext = (courseId?: string) => (
useQuery({
queryKey: courseLibrariesQueryKeys.courseLibrariesSummary(courseId),
queryFn: () => getEntityLinksSummaryByDownstreamContext(courseId!),
enabled: courseId !== undefined,
queryKey: courseLibrariesQueryKeys.courseLibraries(courseKey),
queryFn: () => getEntityLinksByDownstreamContext(courseKey!),
enabled: courseKey !== undefined,
})
);

View File

@@ -1 +1 @@
export { CourseLibraries } from './CourseLibraries';
export { default } from './CourseLibraries';

View File

@@ -18,7 +18,7 @@ const messages = defineMessages({
},
homeTabDescription: {
id: 'course-authoring.course-libraries.tab.home.description',
defaultMessage: 'Your course contains content from these libraries.',
defaultMessage: 'This course contains content from these libraries.',
description: 'Description text for home tab',
},
homeTabDescriptionEmpty: {
@@ -28,18 +28,18 @@ const messages = defineMessages({
},
reviewTabTitle: {
id: 'course-authoring.course-libraries.tab.review.title',
defaultMessage: 'Review Content Updates',
defaultMessage: 'Review Content Updates ({count})',
description: 'Tab title for review tab',
},
reviewTabDescriptionEmpty: {
id: 'course-authoring.course-libraries.tab.home.description-no-links',
defaultMessage: 'All components are up to date',
description: 'Description text for home tab',
reviewTabTitleEmpty: {
id: 'course-authoring.course-libraries.tab.review.title-no-updates',
defaultMessage: 'Review Content Updates',
description: 'Tab title for review tab when no updates are available',
},
breadcrumbLabel: {
id: 'course-authoring.course-libraries.downstream-block.breadcrumb.label',
defaultMessage: 'Location:',
description: 'label for breadcrumb in component cards in course libraries page.',
breadcrumbAriaLabel: {
id: 'course-authoring.course-libraries.downstream-block.breadcrumb.aria-label',
defaultMessage: 'Component breadcrumb',
description: 'Aria label for breadcrumb in component cards in course libraries page.',
},
totalComponentLabel: {
id: 'course-authoring.course-libraries.libcard.total-component.label',
@@ -58,7 +58,7 @@ const messages = defineMessages({
},
outOfSyncCountAlertTitle: {
id: 'course-authoring.course-libraries.libcard.out-of-sync.alert.title',
defaultMessage: '{outOfSyncCount, plural, one {# library component is} other {# library components are}} out of sync. Review updates to accept or ignore changes',
defaultMessage: '{outOfSyncCount} library components are out of sync. Review updates to accept or ignore changes',
description: 'Alert message shown when library components are out of sync',
},
reviewUpdatesBtn: {
@@ -76,51 +76,6 @@ const messages = defineMessages({
defaultMessage: 'This page cannot be shown: Libraries v2 are disabled.',
description: 'Error message shown to users when trying to load a libraries V2 page while libraries v2 are disabled.',
},
cardReviewContentBtn: {
id: 'course-authoring.course-libraries.review-tab.libcard.review-btn-text',
defaultMessage: 'Review Updates',
description: 'Card review button for component in review tab',
},
cardUpdateContentBtn: {
id: 'course-authoring.course-libraries.review-tab.libcard.update-btn-text',
defaultMessage: 'Update',
description: 'Card update button for component in review tab',
},
cardIgnoreContentBtn: {
id: 'course-authoring.course-libraries.review-tab.libcard.ignore-btn-text',
defaultMessage: 'Ignore',
description: 'Card ignore button for component in review tab',
},
updateSingleBlockSuccess: {
id: 'course-authoring.course-libraries.review-tab.libcard.update-success-toast',
defaultMessage: 'Success! "{name}" is updated',
description: 'Success toast message when a component is updated.',
},
ignoreSingleBlockSuccess: {
id: 'course-authoring.course-libraries.review-tab.libcard.ignore-success-toast',
defaultMessage: '"{name}" will remain out of sync with library content. You will be notified when this component is updated again.',
description: 'Success toast message when a component update is ignored.',
},
searchPlaceholder: {
id: 'course-authoring.course-libraries.review-tab.search.placeholder',
defaultMessage: 'Search',
description: 'Search text box in review tab placeholder text',
},
brokenLinkTooltip: {
id: 'course-authoring.course-libraries.home-tab.broken-link.tooltip',
defaultMessage: 'Sourced from a library - but the upstream link is broken/invalid.',
description: 'Tooltip text describing broken link in component listing.',
},
genericErrorMessage: {
id: 'course-authoring.course-libraries.home-tab.error.message',
defaultMessage: 'Something went wrong! Could not fetch results.',
description: 'Generic error message displayed when fetching link data fails.',
},
olderVersionPreviewAlert: {
id: 'course-authoring.course-libraries.reviw-tab.preview.old-version-alert',
defaultMessage: 'The old version preview is the previous library version',
description: 'Alert message stating that older version in preview is of library block',
},
});
export default messages;

View File

@@ -22,7 +22,6 @@ import {
verticalListSortingStrategy,
} from '@dnd-kit/sortable';
import { useLocation } from 'react-router-dom';
import { CourseAuthoringOutlineSidebarSlot } from '../plugin-slots/CourseAuthoringOutlineSidebarSlot';
import { LoadingSpinner } from '../generic/Loading';
import { getProcessingNotification } from '../generic/processing-notification/data/selectors';
@@ -36,6 +35,8 @@ import AlertMessage from '../generic/alert-message';
import getPageHeadTitle from '../generic/utils';
import { getCurrentItem, getProctoredExamsFlag } from './data/selectors';
import { COURSE_BLOCK_NAMES } from './constants';
import HeaderNavigations from './header-navigations/HeaderNavigations';
import OutlineSideBar from './outline-sidebar/OutlineSidebar';
import StatusBar from './status-bar/StatusBar';
import EnableHighlightsModal from './enable-highlights-modal/EnableHighlightsModal';
import SectionCard from './section-card/SectionCard';
@@ -45,16 +46,15 @@ import HighlightsModal from './highlights-modal/HighlightsModal';
import EmptyPlaceholder from './empty-placeholder/EmptyPlaceholder';
import PublishModal from './publish-modal/PublishModal';
import PageAlerts from './page-alerts/PageAlerts';
import DraggableList from './drag-helper/DraggableList';
import DraggableList from '../generic/drag-helper/DraggableList';
import {
canMoveSection,
possibleUnitMoves,
possibleSubsectionMoves,
} from './drag-helper/utils';
} from '../generic/drag-helper/utils';
import { useCourseOutline } from './hooks';
import messages from './messages';
import { getTagsExportFile } from './data/api';
import CourseOutlineHeaderActionsSlot from '../plugin-slots/CourseOutlineHeaderActionsSlot';
const CourseOutline = ({ courseId }) => {
const intl = useIntl();
@@ -105,9 +105,11 @@ const CourseOutline = ({ courseId }) => {
handleNewUnitSubmit,
getUnitUrl,
handleVideoSharingOptionChange,
handleCopyToClipboardClick,
handlePasteClipboardClick,
notificationDismissUrl,
discussionsSettings,
discussionsIncontextFeedbackUrl,
discussionsIncontextLearnmoreUrl,
deprecatedBlocksInfo,
proctoringErrors,
@@ -240,6 +242,7 @@ const CourseOutline = ({ courseId }) => {
notificationDismissUrl={notificationDismissUrl}
handleDismissNotification={handleDismissNotification}
discussionsSettings={discussionsSettings}
discussionsIncontextFeedbackUrl={discussionsIncontextFeedbackUrl}
discussionsIncontextLearnmoreUrl={discussionsIncontextLearnmoreUrl}
deprecatedBlocksInfo={deprecatedBlocksInfo}
proctoringErrors={proctoringErrors}
@@ -264,6 +267,7 @@ const CourseOutline = ({ courseId }) => {
notificationDismissUrl={notificationDismissUrl}
handleDismissNotification={handleDismissNotification}
discussionsSettings={discussionsSettings}
discussionsIncontextFeedbackUrl={discussionsIncontextFeedbackUrl}
discussionsIncontextLearnmoreUrl={discussionsIncontextLearnmoreUrl}
deprecatedBlocksInfo={deprecatedBlocksInfo}
proctoringErrors={proctoringErrors}
@@ -291,7 +295,7 @@ const CourseOutline = ({ courseId }) => {
title={intl.formatMessage(messages.headingTitle)}
subtitle={intl.formatMessage(messages.headingSubtitle)}
headerActions={(
<CourseOutlineHeaderActionsSlot
<HeaderNavigations
isReIndexShow={isReIndexShow}
isSectionsExpanded={isSectionsExpanded}
headerNavigationsActions={headerNavigationsActions}
@@ -299,7 +303,6 @@ const CourseOutline = ({ courseId }) => {
hasSections={Boolean(sectionsList.length)}
courseActions={courseActions}
errors={errors}
sections={sections}
/>
)}
/>
@@ -416,6 +419,7 @@ const CourseOutline = ({ courseId }) => {
onDuplicateSubmit={handleDuplicateUnitSubmit}
getTitleLink={getUnitUrl}
onOrderChange={updateUnitOrderByIndex}
onCopyToClipboardClick={handleCopyToClipboardClick}
discussionsSettings={discussionsSettings}
/>
))}
@@ -453,7 +457,7 @@ const CourseOutline = ({ courseId }) => {
</article>
</Layout.Element>
<Layout.Element>
<CourseAuthoringOutlineSidebarSlot courseId={courseId} />
<OutlineSideBar courseId={courseId} />
</Layout.Element>
</Layout>
<EnableHighlightsModal

View File

@@ -1,3 +1,4 @@
@import "./header-navigations/HeaderNavigations";
@import "./status-bar/StatusBar";
@import "./section-card/SectionCard";
@import "./subsection-card/SubsectionCard";
@@ -7,4 +8,3 @@
@import "./highlights-modal/HighlightsModal";
@import "./publish-modal/PublishModal";
@import "./xblock-status/XBlockStatus";
@import "./drag-helper/SortableItem";

View File

@@ -11,7 +11,6 @@ import { cloneDeep } from 'lodash';
import { closestCorners } from '@dnd-kit/core';
import { useLocation } from 'react-router-dom';
import userEvent from '@testing-library/user-event';
import {
getCourseBestPracticesApiUrl,
getCourseLaunchApiUrl,
@@ -59,7 +58,7 @@ import {
moveUnitOver,
moveSubsection,
moveUnit,
} from './drag-helper/utils';
} from '../generic/drag-helper/utils';
let axiosMock;
let store;
@@ -68,6 +67,13 @@ const courseId = '123';
window.HTMLElement.prototype.scrollIntoView = jest.fn();
const clipboardBroadcastChannelMock = {
postMessage: jest.fn(),
close: jest.fn(),
};
global.BroadcastChannel = jest.fn(() => clipboardBroadcastChannelMock);
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useLocation: jest.fn(),
@@ -2167,7 +2173,7 @@ describe('<CourseOutline />', () => {
.reply(200, courseSectionMock);
let [subsectionElement] = await within(sectionElement).findAllByTestId('subsection-card');
const expandBtn = await within(subsectionElement).findByTestId('subsection-card-header__expanded-btn');
await userEvent.click(expandBtn);
await act(async () => fireEvent.click(expandBtn));
const [unit] = subsection.childInfo.children;
const [unitElement] = await within(subsectionElement).findAllByTestId('unit-card');
@@ -2176,6 +2182,9 @@ describe('<CourseOutline />', () => {
.onPost(getClipboardUrl(), {
usage_key: unit.id,
}).reply(200, clipboardUnit);
// check that initialUserClipboard state is empty
const { initialUserClipboard } = store.getState().courseOutline;
expect(initialUserClipboard).toBeUndefined();
// find menu button and click on it to open menu
const menu = await within(unitElement).findByTestId('unit-card-header__menu-button');
@@ -2185,6 +2194,9 @@ describe('<CourseOutline />', () => {
const copyButton = await within(unitElement).findByText(cardHeaderMessages.menuCopy.defaultMessage);
await act(async () => fireEvent.click(copyButton));
// check that initialUserClipboard state is updated
expect(store.getState().generic.clipboardData).toEqual(clipboardUnit);
[subsectionElement] = await within(sectionElement).findAllByTestId('subsection-card');
// find clipboard content label
const clipboardLabel = await within(subsectionElement).findByText(
@@ -2248,14 +2260,9 @@ describe('<CourseOutline />', () => {
it('should show toats on export tags', async () => {
const expectedResponse = 'this is a test';
// Delay to ensure we see "Please wait."
// Without the delay the success message renders too quickly
const delayedResponse = axiosMock
axiosMock
.onGet(exportTags(courseId))
.withDelayInMs(500);
delayedResponse(200, expectedResponse);
.reply(200, expectedResponse);
useLocation.mockReturnValue({
pathname: '/foo-bar',
hash: '#export-tags',
@@ -2263,41 +2270,37 @@ describe('<CourseOutline />', () => {
window.URL.createObjectURL = jest.fn().mockReturnValue('http://example.com/archivo');
window.URL.revokeObjectURL = jest.fn();
render(<RootWrapper />);
await screen.findByText('Please wait. Creating export file for course tags...');
expect(await screen.findByText('Please wait. Creating export file for course tags...')).toBeInTheDocument();
const expectedRequest = axiosMock.history.get.filter(request => request.url === exportTags(courseId));
expect(expectedRequest.length).toBe(1);
await screen.findByText('Course tags exported successfully');
expect(await screen.findByText('Course tags exported successfully')).toBeInTheDocument();
});
it('should show toast on export tags error', async () => {
// Delay to ensure we see "Please wait."
// Without the delay the error renders too quickly
const delayedResponse = axiosMock
axiosMock
.onGet(exportTags(courseId))
.withDelayInMs(500);
delayedResponse(404);
.reply(404);
useLocation.mockReturnValue({
pathname: '/foo-bar',
hash: '#export-tags',
});
render(<RootWrapper />);
await screen.findByText('Please wait. Creating export file for course tags...');
await screen.findByText('An error has occurred creating the file');
expect(await screen.findByText('Please wait. Creating export file for course tags...')).toBeInTheDocument();
expect(await screen.findByText('An error has occurred creating the file')).toBeInTheDocument();
});
it('sets status to DENIED when API responds with 403', async () => {
it('displays an alert and sets status to DENIED when API responds with 403', async () => {
axiosMock
.onGet(getCourseOutlineIndexApiUrl(courseId))
.reply(403);
const { getByTestId } = render(<RootWrapper />);
const { getByRole } = render(<RootWrapper />);
await waitFor(() => {
expect(getByTestId('redux-provider')).toBeInTheDocument();
expect(getByRole('alert')).toBeInTheDocument();
const { outlineIndexLoadingStatus } = store.getState().courseOutline.loadingStatus;
expect(outlineIndexLoadingStatus).toEqual(RequestStatus.DENIED);
});

View File

@@ -60,7 +60,7 @@ module.exports = {
highlightsEnabledForMessaging: false,
highlightsEnabled: true,
highlightsPreviewOnly: false,
highlightsDocUrl: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/manage_course_highlight_emails.html',
highlightsDocUrl: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/latest/developing_course/course_sections.html#set-section-highlights-for-weekly-course-highlight-messages',
enableProctoredExams: true,
createZendeskTickets: true,
enableTimedExams: true,
@@ -128,7 +128,7 @@ module.exports = {
],
highlightsEnabled: true,
highlightsPreviewOnly: false,
highlightsDocUrl: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/manage_course_highlight_emails.html',
highlightsDocUrl: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/latest/developing_course/course_sections.html#set-section-highlights-for-weekly-course-highlight-messages',
childInfo: {
category: 'sequential',
displayName: 'Subsection',
@@ -517,7 +517,7 @@ module.exports = {
],
highlightsEnabled: true,
highlightsPreviewOnly: false,
highlightsDocUrl: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/manage_course_highlight_emails.html',
highlightsDocUrl: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/latest/developing_course/course_sections.html#set-section-highlights-for-weekly-course-highlight-messages',
childInfo: {
category: 'sequential',
displayName: 'Subsection',
@@ -1837,7 +1837,7 @@ module.exports = {
highlights: [],
highlightsEnabled: true,
highlightsPreviewOnly: false,
highlightsDocUrl: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/manage_course_highlight_emails.html',
highlightsDocUrl: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/latest/developing_course/course_sections.html#set-section-highlights-for-weekly-course-highlight-messages',
childInfo: {
category: 'sequential',
displayName: 'Subsection',
@@ -2787,7 +2787,7 @@ module.exports = {
highlights: [],
highlightsEnabled: true,
highlightsPreviewOnly: false,
highlightsDocUrl: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/manage_course_highlight_emails.html',
highlightsDocUrl: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/latest/developing_course/course_sections.html#set-section-highlights-for-weekly-course-highlight-messages',
childInfo: {
category: 'sequential',
displayName: 'Subsection',
@@ -3044,6 +3044,7 @@ module.exports = {
blocks: [],
advanceSettingsUrl: '/settings/advanced/course-v1:edx+101+y76',
},
discussionsIncontextFeedbackUrl: '',
discussionsIncontextLearnmoreUrl: '',
initialState: {
expandedLocators: [

View File

@@ -6,6 +6,7 @@ module.exports = {
blocks: [],
advanceSettingsUrl: '/settings/advanced/course-v1:edx+101+y76',
},
discussionsIncontextFeedbackUrl: '',
discussionsIncontextLearnmoreUrl: '',
initialState: {
expandedLocators: [

View File

@@ -55,7 +55,7 @@ module.exports = {
highlights: [],
highlights_enabled: true,
highlights_preview_only: false,
highlights_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/manage_course_highlight_emails.html',
highlights_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/latest/developing_course/course_sections.html#set-section-highlights-for-weekly-course-highlight-messages',
child_info: {
category: 'sequential',
display_name: 'Subsection',

View File

@@ -54,7 +54,6 @@ const CardHeader = ({
discussionEnabled,
discussionsSettings,
parentInfo,
extraActionsComponent,
}) => {
const intl = useIntl();
const [searchParams] = useSearchParams();
@@ -146,7 +145,6 @@ const CardHeader = ({
{ getConfig().ENABLE_TAGGING_TAXONOMY_PAGES === 'true' && !!contentTagCount && (
<TagCount count={contentTagCount} onClick={openManageTagsDrawer} />
)}
{extraActionsComponent}
<Dropdown data-testid={`${namePrefix}-card-header__menu`} onClick={onClickMenuButton}>
<Dropdown.Toggle
className="item-card-header__menu"
@@ -254,7 +252,6 @@ CardHeader.defaultProps = {
discussionsSettings: {},
parentInfo: {},
cardId: '',
extraActionsComponent: null,
};
CardHeader.propTypes = {
@@ -298,9 +295,6 @@ CardHeader.propTypes = {
isTimeLimited: PropTypes.bool,
graded: PropTypes.bool,
}),
// An optional component that is rendered before the dropdown. This is used by the Subsection
// and Unit card components to render their plugin slots.
extraActionsComponent: PropTypes.node,
};
export default CardHeader;

View File

@@ -28,6 +28,7 @@ export const getCourseReindexApiUrl = (reindexLink) => `${getApiBaseUrl()}${rein
export const getXBlockBaseApiUrl = () => `${getApiBaseUrl()}/xblock/`;
export const getCourseItemApiUrl = (itemId) => `${getXBlockBaseApiUrl()}${itemId}`;
export const getXBlockApiUrl = (blockId) => `${getXBlockBaseApiUrl()}outline/${blockId}`;
export const getClipboardUrl = () => `${getApiBaseUrl()}/api/content-staging/v1/clipboard/`;
export const exportTags = (courseId) => `${getApiBaseUrl()}/api/content_tagging/v1/object_tags/${courseId}/export/`;
/**
@@ -35,6 +36,7 @@ export const exportTags = (courseId) => `${getApiBaseUrl()}/api/content_tagging/
* @property {string} courseReleaseDate
* @property {Object} courseStructure
* @property {Object} deprecatedBlocksInfo
* @property {string} discussionsIncontextFeedbackUrl
* @property {string} discussionsIncontextLearnmoreUrl
* @property {Object} initialState
* @property {Object} initialUserClipboard

View File

@@ -1,4 +1,5 @@
import { RequestStatus } from '../../data/constants';
import { updateClipboardData } from '../../generic/data/slice';
import { NOTIFICATION_MESSAGES } from '../../constants';
import { COURSE_BLOCK_NAMES } from '../constants';
import {
@@ -70,6 +71,7 @@ export function fetchCourseOutlineIndexQuery(courseId) {
},
} = outlineIndex;
dispatch(fetchOutlineIndexSuccess(outlineIndex));
dispatch(updateClipboardData(outlineIndex.initialUserClipboard));
dispatch(updateStatusBar({
courseReleaseDate,
highlightsEnabledForMessaging,

View File

@@ -25,7 +25,7 @@ const HeaderNavigations = ({
} = headerNavigationsActions;
return (
<>
<nav className="header-navigations ml-auto">
{courseActions.childAddable && (
<OverlayTrigger
placement="bottom"
@@ -90,7 +90,7 @@ const HeaderNavigations = ({
{intl.formatMessage(messages.viewLiveButton)}
</Button>
</OverlayTrigger>
</>
</nav>
);
};

View File

@@ -0,0 +1,4 @@
.header-navigations {
display: flex;
gap: .75rem;
}

View File

@@ -4,6 +4,7 @@ import { useNavigate } from 'react-router-dom';
import { useToggle } from '@openedx/paragon';
import { getConfig } from '@edx/frontend-platform';
import { copyToClipboard } from '../generic/data/thunks';
import { getSavingStatus as getGenericSavingStatus } from '../generic/data/selectors';
import { getWaffleFlags } from '../data/selectors';
import { RequestStatus } from '../data/constants';
@@ -66,13 +67,13 @@ const useCourseOutline = ({ courseId }) => {
lmsLink,
notificationDismissUrl,
discussionsSettings,
discussionsIncontextFeedbackUrl,
discussionsIncontextLearnmoreUrl,
deprecatedBlocksInfo,
proctoringErrors,
mfeProctoredExamSettingsUrl,
advanceSettingsUrl,
} = useSelector(getOutlineIndexData);
const { outlineIndexLoadingStatus, reIndexLoadingStatus } = useSelector(getLoadingStatus);
const statusBarData = useSelector(getStatusBarData);
const savingStatus = useSelector(getSavingStatus);
@@ -96,6 +97,10 @@ const useCourseOutline = ({ courseId }) => {
const isSavingStatusFailed = savingStatus === RequestStatus.FAILED || genericSavingStatus === RequestStatus.FAILED;
const handleCopyToClipboardClick = (usageKey) => {
dispatch(copyToClipboard(usageKey));
};
const handlePasteClipboardClick = (parentLocator, sectionId) => {
dispatch(pasteClipboardContent(parentLocator, sectionId));
};
@@ -337,9 +342,11 @@ const useCourseOutline = ({ courseId }) => {
openUnitPage,
handleNewUnitSubmit,
handleVideoSharingOptionChange,
handleCopyToClipboardClick,
handlePasteClipboardClick,
notificationDismissUrl,
discussionsSettings,
discussionsIncontextFeedbackUrl,
discussionsIncontextLearnmoreUrl,
deprecatedBlocksInfo,
proctoringErrors,

View File

@@ -26,6 +26,7 @@ const OutlineSideBar = ({ courseId }) => {
return (
<HelpSidebar
intl={intl}
courseId={courseId}
showOtherSettings={false}
className="outline-sidebar mt-4"

View File

@@ -13,7 +13,7 @@ import {
import {
Alert, Button, Hyperlink, Truncate,
} from '@openedx/paragon';
import { Link, useNavigate } from 'react-router-dom';
import { Link } from 'react-router-dom';
import ErrorAlert from '../../editors/sharedComponents/ErrorAlerts/ErrorAlert';
import { RequestStatus } from '../../data/constants';
@@ -24,13 +24,13 @@ import advancedSettingsMessages from '../../advanced-settings/messages';
import { getPasteFileNotices } from '../data/selectors';
import { dismissError, removePasteFileNotices } from '../data/slice';
import { API_ERROR_TYPES } from '../constants';
import { OutOfSyncAlert } from '../../course-libraries/OutOfSyncAlert';
const PageAlerts = ({
courseId,
notificationDismissUrl,
handleDismissNotification,
discussionsSettings,
discussionsIncontextFeedbackUrl,
discussionsIncontextLearnmoreUrl,
deprecatedBlocksInfo,
proctoringErrors,
@@ -48,8 +48,6 @@ const PageAlerts = ({
localStorage.getItem(discussionAlertDismissKey) === null,
);
const { newFiles, conflictingFiles, errorFiles } = useSelector(getPasteFileNotices);
const [showOutOfSyncAlert, setShowOutOfSyncAlert] = useState(false);
const navigate = useNavigate();
const getAssetsUrl = () => {
if (getConfig().ENABLE_ASSETS_PAGE === 'true') {
@@ -113,6 +111,13 @@ const PageAlerts = ({
platformName: process.env.SITE_NAME,
})}
</div>
<Hyperlink
showLaunchIcon={false}
destination={discussionsIncontextFeedbackUrl}
target="_blank"
>
{intl.formatMessage(messages.discussionNotificationFeedback)}
</Hyperlink>
</Alert>
);
};
@@ -414,15 +419,6 @@ const PageAlerts = ({
);
};
const renderOutOfSyncAlert = () => (
<OutOfSyncAlert
courseId={courseId}
onReview={() => navigate(`/course/${courseId}/libraries?tab=review`)}
showAlert={showOutOfSyncAlert}
setShowAlert={setShowOutOfSyncAlert}
/>
);
return (
<>
{configurationErrors()}
@@ -436,7 +432,6 @@ const PageAlerts = ({
{errorFilesPasteAlert()}
{conflictingFilesPasteAlert()}
{newFilesPasteAlert()}
{renderOutOfSyncAlert()}
</>
);
};
@@ -445,6 +440,7 @@ PageAlerts.defaultProps = {
notificationDismissUrl: '',
handleDismissNotification: null,
discussionsSettings: {},
discussionsIncontextFeedbackUrl: '',
discussionsIncontextLearnmoreUrl: '',
deprecatedBlocksInfo: {},
proctoringErrors: [],
@@ -461,6 +457,7 @@ PageAlerts.propTypes = {
discussionsSettings: PropTypes.shape({
providerType: PropTypes.string,
}),
discussionsIncontextFeedbackUrl: PropTypes.string,
discussionsIncontextLearnmoreUrl: PropTypes.string,
deprecatedBlocksInfo: PropTypes.shape({
blocks: PropTypes.arrayOf(PropTypes.arrayOf(PropTypes.string)),

View File

@@ -5,6 +5,7 @@ import {
render,
fireEvent,
screen,
waitFor,
} from '@testing-library/react';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import { AppProvider } from '@edx/frontend-platform/react';
@@ -27,13 +28,6 @@ jest.mock('react-redux', () => ({
useSelector: jest.fn(),
}));
jest.mock('../../course-libraries/data/apiHooks', () => ({
useEntityLinksSummaryByDownstreamContext: () => ({
data: [],
isLoading: false,
}),
}));
let store;
const handleDismissNotification = jest.fn();
@@ -42,6 +36,7 @@ const pageAlertsData = {
notificationDismissUrl: '',
handleDismissNotification: null,
discussionsSettings: {},
discussionsIncontextFeedbackUrl: '',
discussionsIncontextLearnmoreUrl: '',
deprecatedBlocksInfo: {},
proctoringErrors: [],
@@ -75,9 +70,9 @@ describe('<PageAlerts />', () => {
useSelector.mockReturnValue({});
});
it('renders null when no alerts are present', async () => {
it('renders null when no alerts are present', () => {
renderComponent();
expect(await screen.findByTestId('browser-router')).toBeEmptyDOMElement();
expect(screen.queryByTestId('browser-router')).toBeEmptyDOMElement();
});
it('renders configuration alerts', async () => {
@@ -100,6 +95,7 @@ describe('<PageAlerts />', () => {
discussionsSettings: {
providerType: 'openedx',
},
discussionsIncontextFeedbackUrl: 'some-feedback-url',
discussionsIncontextLearnmoreUrl: 'some-learn-more-url',
});
@@ -112,6 +108,12 @@ describe('<PageAlerts />', () => {
fireEvent.click(dismissBtn);
const discussionAlertDismissKey = `discussionAlertDismissed-${pageAlertsData.courseId}`;
expect(localStorage.getItem(discussionAlertDismissKey)).toBe('true');
await waitFor(() => {
const feedbackLink = screen.queryByText(messages.discussionNotificationFeedback.defaultMessage);
expect(feedbackLink).toBeInTheDocument();
expect(feedbackLink).toHaveAttribute('href', 'some-feedback-url');
});
});
it('renders deprecation warning alerts', async () => {

View File

@@ -13,8 +13,8 @@ import classNames from 'classnames';
import { setCurrentItem, setCurrentSection } from '../data/slice';
import { RequestStatus } from '../../data/constants';
import CardHeader from '../card-header/CardHeader';
import SortableItem from '../drag-helper/SortableItem';
import { DragContext } from '../drag-helper/DragContextProvider';
import SortableItem from '../../generic/drag-helper/SortableItem';
import { DragContext } from '../../generic/drag-helper/DragContextProvider';
import TitleButton from '../card-header/TitleButton';
import XBlockStatus from '../xblock-status/XBlockStatus';
import { getItemStatus, getItemStatusBorder, scrollToElement } from '../utils';

View File

@@ -11,13 +11,12 @@ import { Add as IconAdd } from '@openedx/paragon/icons';
import classNames from 'classnames';
import { isEmpty } from 'lodash';
import CourseOutlineSubsectionCardExtraActionsSlot from '../../plugin-slots/CourseOutlineSubsectionCardExtraActionsSlot';
import { setCurrentItem, setCurrentSection, setCurrentSubsection } from '../data/slice';
import { RequestStatus } from '../../data/constants';
import CardHeader from '../card-header/CardHeader';
import SortableItem from '../drag-helper/SortableItem';
import { DragContext } from '../drag-helper/DragContextProvider';
import { useClipboard, PasteComponent } from '../../generic/clipboard';
import SortableItem from '../../generic/drag-helper/SortableItem';
import { DragContext } from '../../generic/drag-helper/DragContextProvider';
import { useCopyToClipboard, PasteComponent } from '../../generic/clipboard';
import TitleButton from '../card-header/TitleButton';
import XBlockStatus from '../xblock-status/XBlockStatus';
import { getItemStatus, getItemStatusBorder, scrollToElement } from '../utils';
@@ -50,7 +49,7 @@ const SubsectionCard = ({
const isScrolledToElement = locatorId === subsection.id;
const [isFormOpen, openForm, closeForm] = useToggle(false);
const namePrefix = 'subsection';
const { sharedClipboardData, showPasteUnit } = useClipboard();
const { sharedClipboardData, showPasteUnit } = useCopyToClipboard();
const {
id,
@@ -128,13 +127,6 @@ const SubsectionCard = ({
/>
);
const extraActionsComponent = (
<CourseOutlineSubsectionCardExtraActionsSlot
subsection={subsection}
section={section}
/>
);
useEffect(() => {
if (activeId === id && isExpanded) {
setIsExpanded(false);
@@ -213,7 +205,6 @@ const SubsectionCard = ({
actions={actions}
proctoringExamConfigurationLink={proctoringExamConfigurationLink}
isSequential
extraActionsComponent={extraActionsComponent}
/>
<div className="subsection-card__content item-children" data-testid="subsection-card__content">
<XBlockStatus
@@ -242,7 +233,7 @@ const SubsectionCard = ({
>
{intl.formatMessage(messages.newUnitButton)}
</Button>
{enableCopyPasteUnits && showPasteUnit && sharedClipboardData && (
{enableCopyPasteUnits && showPasteUnit && (
<PasteComponent
className="mt-4"
text={intl.formatMessage(messages.pasteButton)}

View File

@@ -21,6 +21,13 @@ jest.mock('react-router-dom', () => ({
}),
}));
const clipboardBroadcastChannelMock = {
postMessage: jest.fn(),
close: jest.fn(),
};
global.BroadcastChannel = jest.fn(() => clipboardBroadcastChannelMock);
const unit = {
id: 'unit-1',
};

View File

@@ -6,15 +6,13 @@ import { useToggle } from '@openedx/paragon';
import { isEmpty } from 'lodash';
import { useSearchParams } from 'react-router-dom';
import CourseOutlineUnitCardExtraActionsSlot from '../../plugin-slots/CourseOutlineUnitCardExtraActionsSlot';
import { setCurrentItem, setCurrentSection, setCurrentSubsection } from '../data/slice';
import { RequestStatus } from '../../data/constants';
import CardHeader from '../card-header/CardHeader';
import SortableItem from '../drag-helper/SortableItem';
import SortableItem from '../../generic/drag-helper/SortableItem';
import TitleLink from '../card-header/TitleLink';
import XBlockStatus from '../xblock-status/XBlockStatus';
import { getItemStatus, getItemStatusBorder, scrollToElement } from '../utils';
import { useClipboard } from '../../generic/clipboard';
const UnitCard = ({
unit,
@@ -32,6 +30,7 @@ const UnitCard = ({
onDuplicateSubmit,
getTitleLink,
onOrderChange,
onCopyToClipboardClick,
discussionsSettings,
}) => {
const currentRef = useRef(null);
@@ -42,8 +41,6 @@ const UnitCard = ({
const [isFormOpen, openForm, closeForm] = useToggle(false);
const namePrefix = 'unit';
const { copyToClipboard } = useClipboard();
const {
id,
category,
@@ -101,7 +98,7 @@ const UnitCard = ({
};
const handleCopyClick = () => {
copyToClipboard(id);
onCopyToClipboardClick(unit.id);
};
const titleComponent = (
@@ -112,14 +109,6 @@ const UnitCard = ({
/>
);
const extraActionsComponent = (
<CourseOutlineUnitCardExtraActionsSlot
unit={unit}
subsection={subsection}
section={section}
/>
);
useEffect(() => {
// if this items has been newly added, scroll to it.
// we need to check section.shouldScroll as whole section is fetched when a
@@ -186,7 +175,6 @@ const UnitCard = ({
discussionEnabled={discussionEnabled}
discussionsSettings={discussionsSettings}
parentInfo={parentInfo}
extraActionsComponent={extraActionsComponent}
/>
<div className="unit-card__content item-children" data-testid="unit-card__content">
<XBlockStatus
@@ -253,6 +241,7 @@ UnitCard.propTypes = {
onOrderChange: PropTypes.func.isRequired,
isSelfPaced: PropTypes.bool.isRequired,
isCustomRelativeDatesActive: PropTypes.bool.isRequired,
onCopyToClipboardClick: PropTypes.func.isRequired,
discussionsSettings: PropTypes.shape({
providerType: PropTypes.string,
enableGradedUnits: PropTypes.bool,

View File

@@ -1,3 +1,4 @@
import React from 'react';
import {
act, render, fireEvent, within,
} from '@testing-library/react';
@@ -61,6 +62,7 @@ const renderComponent = (props) => render(
onOpenPublishModal={jest.fn()}
onOpenDeleteModal={jest.fn()}
onOpenConfigureModal={jest.fn()}
onCopyToClipboardClick={jest.fn()}
savingStatus=""
onEditSubmit={jest.fn()}
onDuplicateSubmit={jest.fn()}

View File

@@ -6,7 +6,7 @@ import { initializeMockApp } from '@edx/frontend-platform';
import { IntlProvider, injectIntl } from '@edx/frontend-platform/i18n';
import { AppProvider } from '@edx/frontend-platform/react';
import {
fireEvent, render, waitFor,
act, fireEvent, render, waitFor,
} from '@testing-library/react';
import MockAdapter from 'axios-mock-adapter';
@@ -78,15 +78,16 @@ describe('<CourseRerun />', () => {
it('shows the spinner before the query is complete', async () => {
useSelector.mockReturnValue({ organizationLoadingStatus: RequestStatus.IN_PROGRESS });
const { findByRole } = render(<RootWrapper />);
const spinner = await findByRole('status');
expect(spinner.textContent).toEqual('Loading...');
await act(async () => {
const { getByRole } = render(<RootWrapper />);
const spinner = getByRole('status');
expect(spinner.textContent).toEqual('Loading...');
});
});
it('should show footer', async () => {
const { findByText } = render(<RootWrapper />);
await findByText('Looking for help with Studio?');
const lmsElement = await findByText('LMS');
expect(lmsElement).toHaveAttribute('href', process.env.LMS_BASE_URL);
it('should show footer', () => {
const { getByText } = render(<RootWrapper />);
expect(getByText('Looking for help with Studio?')).toBeInTheDocument();
expect(getByText('LMS')).toHaveAttribute('href', process.env.LMS_BASE_URL);
});
});

View File

@@ -7,8 +7,7 @@ import {
ActionRow,
Button,
} from '@openedx/paragon';
import { StudioFooterSlot } from '@openedx/frontend-slot-footer';
import { StudioFooter } from '@edx/frontend-component-footer';
import { useNavigate, useParams } from 'react-router-dom';
import Header from '../header';
@@ -89,7 +88,7 @@ const CourseRerun = () => {
isQueryPending={savingStatus === RequestStatus.PENDING}
/>
</div>
<StudioFooterSlot />
<StudioFooter />
</>
);
};

View File

@@ -5,12 +5,12 @@ import { useParams } from 'react-router-dom';
import {
Container, Layout, Stack, Button, TransitionReplace,
} from '@openedx/paragon';
import { useIntl } from '@edx/frontend-platform/i18n';
import { getConfig } from '@edx/frontend-platform';
import { useIntl, injectIntl } from '@edx/frontend-platform/i18n';
import {
Warning as WarningIcon,
CheckCircle as CheckCircleIcon,
} from '@openedx/paragon/icons';
import { CourseAuthoringUnitSidebarSlot } from '../plugin-slots/CourseAuthoringUnitSidebarSlot';
import { getProcessingNotification } from '../generic/processing-notification/data/selectors';
import SubHeader from '../generic/sub-header/SubHeader';
@@ -25,16 +25,18 @@ import Loading from '../generic/Loading';
import AddComponent from './add-component/AddComponent';
import HeaderTitle from './header-title/HeaderTitle';
import Breadcrumbs from './breadcrumbs/Breadcrumbs';
import HeaderNavigations from './header-navigations/HeaderNavigations';
import Sequence from './course-sequence';
import Sidebar from './sidebar';
import SplitTestSidebarInfo from './sidebar/SplitTestSidebarInfo';
import { useCourseUnit, useLayoutGrid, useScrollToLastPosition } from './hooks';
import messages from './messages';
import PublishControls from './sidebar/PublishControls';
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';
import IframePreviewLibraryXBlockChanges from './preview-changes';
import CourseUnitHeaderActionsSlot from '../plugin-slots/CourseUnitHeaderActionsSlot';
import PreviewLibraryXBlockChanges from './preview-changes';
const CourseUnit = ({ courseId }) => {
const { blockId } = useParams();
@@ -50,7 +52,6 @@ const CourseUnit = ({ courseId }) => {
isTitleEditFormOpen,
isUnitVerticalType,
isUnitLibraryType,
isSplitTestType,
staticFileNotices,
currentlyVisibleToStudents,
unitXBlockActions,
@@ -71,7 +72,6 @@ const CourseUnit = ({ courseId }) => {
handleRollbackMovedXBlock,
handleCloseXBlockMovedAlert,
handleNavigateToTargetUnit,
addComponentTemplateData,
} = useCourseUnit({ courseId, blockId });
const layoutGrid = useLayoutGrid(unitCategory, isUnitLibraryType);
@@ -154,11 +154,9 @@ const CourseUnit = ({ courseId }) => {
/>
)}
headerActions={(
<CourseUnitHeaderActionsSlot
category={unitCategory}
<HeaderNavigations
unitCategory={unitCategory}
headerNavigationsActions={headerNavigationsActions}
unitTitle={unitTitle}
verticalBlocks={courseVerticalChildren.children}
/>
)}
/>
@@ -190,24 +188,20 @@ const CourseUnit = ({ courseId }) => {
<XBlockContainerIframe
courseId={courseId}
blockId={blockId}
isUnitVerticalType={isUnitVerticalType}
unitXBlockActions={unitXBlockActions}
courseVerticalChildren={courseVerticalChildren.children}
handleConfigureSubmit={handleConfigureSubmit}
/>
<AddComponent
parentLocator={blockId}
isSplitTestType={isSplitTestType}
isUnitVerticalType={isUnitVerticalType}
handleCreateNewCourseXBlock={handleCreateNewCourseXBlock}
addComponentTemplateData={addComponentTemplateData}
/>
{isUnitVerticalType && (
<AddComponent
blockId={blockId}
handleCreateNewCourseXBlock={handleCreateNewCourseXBlock}
/>
)}
{showPasteXBlock && canPasteComponent && isUnitVerticalType && (
<PasteComponent
clipboardData={sharedClipboardData}
onClick={
() => handleCreateNewCourseXBlock({ stagedContent: 'clipboard', parentLocator: blockId })
}
onClick={handleCreateNewCourseXBlock}
text={intl.formatMessage(messages.pasteButtonText)}
/>
)}
@@ -217,21 +211,24 @@ const CourseUnit = ({ courseId }) => {
closeModal={closeMoveModal}
courseId={courseId}
/>
<IframePreviewLibraryXBlockChanges />
<PreviewLibraryXBlockChanges />
</Layout.Element>
<Layout.Element>
<Stack gap={3}>
{isUnitVerticalType && (
<CourseAuthoringUnitSidebarSlot
courseId={courseId}
blockId={blockId}
unitTitle={unitTitle}
/>
)}
{isSplitTestType && (
<Sidebar data-testid="course-split-test-sidebar">
<SplitTestSidebarInfo />
</Sidebar>
<>
<Sidebar data-testid="course-unit-sidebar">
<PublishControls blockId={blockId} />
</Sidebar>
{getConfig().ENABLE_TAGGING_TAXONOMY_PAGES === 'true' && (
<Sidebar className="tags-sidebar">
<TagsSidebarControls />
</Sidebar>
)}
<Sidebar data-testid="course-unit-location-sidebar">
<LocationInfo />
</Sidebar>
</>
)}
</Stack>
</Layout.Element>
@@ -256,4 +253,4 @@ CourseUnit.propTypes = {
courseId: PropTypes.string.isRequired,
};
export default CourseUnit;
export default injectIntl(CourseUnit);

View File

@@ -5,7 +5,6 @@
@import "./header-title/HeaderTitle";
@import "./move-modal";
@import "./preview-changes";
@import "./xblock-container-iframe";
.course-unit {
min-width: 900px;

View File

@@ -1,5 +1,4 @@
import MockAdapter from 'axios-mock-adapter';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import {
act, render, waitFor, within, screen,
} from '@testing-library/react';
@@ -48,8 +47,10 @@ import { executeThunk } from '../utils';
import { IFRAME_FEATURE_POLICY } from '../constants';
import pasteComponentMessages from '../generic/clipboard/paste-component/messages';
import pasteNotificationsMessages from './clipboard/paste-notification/messages';
import headerNavigationsMessages from './header-navigations/messages';
import headerTitleMessages from './header-title/messages';
import courseSequenceMessages from './course-sequence/messages';
import sidebarMessages from './sidebar/messages';
import { extractCourseUnitId } from './sidebar/utils';
import CourseUnit from './CourseUnit';
@@ -59,16 +60,13 @@ import configureModalMessages from '../generic/configure-modal/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 '../generic/hooks/context/iFrameContext';
import { IframeProvider } from './context/iFrameContext';
import moveModalMessages from './move-modal/messages';
import xblockContainerIframeMessages from './xblock-container-iframe/messages';
import headerNavigationsMessages from './header-navigations/messages';
import sidebarMessages from './sidebar/messages';
import messages from './messages';
let axiosMock;
let store;
let queryClient;
const courseId = '123';
const blockId = '567890';
const unitDisplayName = courseUnitIndexMock.metadata.display_name;
@@ -93,6 +91,52 @@ jest.mock('react-router-dom', () => ({
useNavigate: () => mockedUsedNavigate,
}));
jest.mock('@tanstack/react-query', () => ({
useQuery: jest.fn(({ queryKey }) => {
const taxonomyApiHooksModule = jest.requireActual('../taxonomy/data/apiHooks');
const actualQueryKeys = taxonomyApiHooksModule.taxonomyQueryKeys;
if (queryKey[0] === 'contentTaxonomyTags') {
return {
data: {
taxonomies: [],
},
isSuccess: true,
};
} if (queryKey[0] === 'contentTagsCount') {
return {
data: 17,
isSuccess: true,
};
}
if (actualQueryKeys.all.includes(queryKey[0])) {
return {
data: {
results: [],
},
isSuccess: true,
};
}
return {
data: {},
isSuccess: true,
};
}),
useQueryClient: jest.fn(() => ({
setQueryData: jest.fn(),
})),
useMutation: jest.fn(() => ({
mutateAsync: jest.fn(),
})),
}));
const clipboardBroadcastChannelMock = {
postMessage: jest.fn(),
close: jest.fn(),
};
global.BroadcastChannel = jest.fn(() => clipboardBroadcastChannelMock);
/**
* Simulates receiving a post message event for testing purposes.
* This can be used to mimic events like deletion or other actions
@@ -113,9 +157,7 @@ const RootWrapper = () => (
<AppProvider store={store}>
<IntlProvider locale="en">
<IframeProvider>
<QueryClientProvider client={queryClient}>
<CourseUnit courseId={courseId} />
</QueryClientProvider>
<CourseUnit courseId={courseId} />
</IframeProvider>
</IntlProvider>
</AppProvider>
@@ -134,13 +176,6 @@ describe('<CourseUnit />', () => {
window.scrollTo = jest.fn();
global.localStorage.clear();
store = initializeStore();
queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: false,
},
},
});
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
axiosMock
.onGet(getClipboardUrl())
@@ -159,7 +194,7 @@ describe('<CourseUnit />', () => {
await executeThunk(fetchCourseVerticalChildrenData(blockId), store.dispatch);
axiosMock
.onGet(getContentTaxonomyTagsApiUrl(blockId))
.reply(200, { taxonomies: [] });
.reply(200, {});
axiosMock
.onGet(getContentTaxonomyTagsCountApiUrl(blockId))
.reply(200, 17);
@@ -189,7 +224,7 @@ describe('<CourseUnit />', () => {
const iframe = getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
expect(iframe).toHaveAttribute('src', `${getConfig().STUDIO_BASE_URL}/container_embed/${blockId}`);
expect(iframe).toHaveAttribute('allow', IFRAME_FEATURE_POLICY);
expect(iframe).toHaveAttribute('style', 'height: 0px;');
expect(iframe).toHaveAttribute('style', 'width: 100%; height: 0px;');
expect(iframe).toHaveAttribute('scrolling', 'no');
expect(iframe).toHaveAttribute('referrerpolicy', 'origin');
expect(iframe).toHaveAttribute('loading', 'lazy');
@@ -198,15 +233,16 @@ describe('<CourseUnit />', () => {
});
it('adjusts iframe height dynamically based on courseXBlockDropdownHeight postMessage event', async () => {
render(<RootWrapper />);
const { getByTitle } = render(<RootWrapper />);
let iframe = await screen.findByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
expect(iframe).toHaveAttribute('style', 'height: 0px;');
simulatePostMessageEvent(messageTypes.toggleCourseXBlockDropdown, {
courseXBlockDropdownHeight: 200,
await waitFor(() => {
const iframe = getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
expect(iframe).toHaveAttribute('style', 'width: 100%; height: 0px;');
simulatePostMessageEvent(messageTypes.toggleCourseXBlockDropdown, {
courseXBlockDropdownHeight: 200,
});
expect(iframe).toHaveAttribute('style', 'width: 100%; height: 200px;');
});
iframe = await screen.findByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
expect(iframe).toHaveAttribute('style', 'height: 200px;');
});
it('displays an error alert when a studioAjaxError message is received', async () => {
@@ -238,13 +274,16 @@ describe('<CourseUnit />', () => {
});
it('renders the xBlocks iframe and opens the tags drawer on postMessage event', async () => {
render(<RootWrapper />);
const { getByTitle, getByText } = render(<RootWrapper />);
await screen.findByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
await waitFor(() => {
const xblocksIframe = getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
expect(xblocksIframe).toBeInTheDocument();
});
simulatePostMessageEvent(messageTypes.openManageTags, { contentId: blockId });
await screen.findByText(tagsDrawerMessages.headerSubtitle.defaultMessage);
expect(getByText(tagsDrawerMessages.headerSubtitle.defaultMessage)).toBeInTheDocument();
});
it('closes the legacy edit modal when closeXBlockEditorModal message is received', async () => {
@@ -341,10 +380,10 @@ describe('<CourseUnit />', () => {
it('checks whether xblock is removed when the corresponding delete button is clicked and the sidebar is the updated', async () => {
const {
getByTitle, getByText, queryByRole, getByRole,
getByTitle, getByText, queryByRole, getAllByRole, getByRole,
} = render(<RootWrapper />);
await waitFor(async () => {
await waitFor(() => {
const iframe = getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
expect(iframe).toHaveAttribute(
'aria-label',
@@ -359,12 +398,13 @@ describe('<CourseUnit />', () => {
expect(getByText(/Delete this component?/i)).toBeInTheDocument();
expect(getByText(/Deleting this component is permanent and cannot be undone./i)).toBeInTheDocument();
const dialog = getByRole('dialog');
expect(dialog).toBeInTheDocument();
expect(getByRole('dialog')).toBeInTheDocument();
// Find the Cancel and Delete buttons within the iframe by their specific classes
const cancelButton = await within(dialog).findByRole('button', { name: /Cancel/i });
const deleteButton = await within(dialog).findByRole('button', { name: /Delete/i });
const cancelButton = getAllByRole('button', { name: /Cancel/i })
.find(({ classList }) => classList.contains('btn-tertiary'));
const deleteButton = getAllByRole('button', { name: /Delete/i })
.find(({ classList }) => classList.contains('btn-primary'));
expect(cancelButton).toBeInTheDocument();
@@ -1272,6 +1312,13 @@ describe('<CourseUnit />', () => {
enable_copy_paste_units: true,
});
axiosMock
.onGet(getCourseSectionVerticalApiUrl(blockId))
.reply(200, {
...courseSectionVerticalMock,
user_clipboard: clipboardUnit,
});
await executeThunk(fetchCourseUnitQuery(courseId), store.dispatch);
await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch);
@@ -1321,9 +1368,11 @@ describe('<CourseUnit />', () => {
});
axiosMock
.onGet(getClipboardUrl())
.reply(200, clipboardXBlock);
.onGet(getCourseSectionVerticalApiUrl(blockId))
.reply(200, {
...courseSectionVerticalMock,
user_clipboard: clipboardXBlock,
});
axiosMock
.onGet(getCourseUnitApiUrl(courseId))
.reply(200, {
@@ -1394,6 +1443,13 @@ describe('<CourseUnit />', () => {
enable_copy_paste_units: true,
});
axiosMock
.onGet(getCourseSectionVerticalApiUrl(blockId))
.reply(200, {
...courseSectionVerticalMock,
user_clipboard: clipboardUnit,
});
await executeThunk(fetchCourseUnitQuery(courseId), store.dispatch);
await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch);
@@ -1446,6 +1502,13 @@ describe('<CourseUnit />', () => {
enable_copy_paste_units: true,
});
axiosMock
.onGet(getCourseSectionVerticalApiUrl(blockId))
.reply(200, {
...courseSectionVerticalMock,
user_clipboard: clipboardUnit,
});
await executeThunk(fetchCourseUnitQuery(courseId), store.dispatch);
await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch);
@@ -1500,6 +1563,13 @@ describe('<CourseUnit />', () => {
enable_copy_paste_units: true,
});
axiosMock
.onGet(getCourseSectionVerticalApiUrl(blockId))
.reply(200, {
...courseSectionVerticalMock,
user_clipboard: clipboardUnit,
});
await executeThunk(fetchCourseUnitQuery(courseId), store.dispatch);
await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch);
@@ -1587,21 +1657,27 @@ describe('<CourseUnit />', () => {
it('should display "Move Modal" on receive trigger message', async () => {
const {
getByText,
getByRole,
} = render(<RootWrapper />);
await screen.findByText(unitDisplayName);
await act(async () => {
await waitFor(() => {
expect(getByText(unitDisplayName))
.toBeInTheDocument();
});
axiosMock
.onGet(getCourseOutlineInfoUrl(courseId))
.reply(200, courseOutlineInfoMock);
await executeThunk(getCourseOutlineInfoQuery(courseId), store.dispatch);
axiosMock
.onGet(getCourseOutlineInfoUrl(courseId))
.reply(200, courseOutlineInfoMock);
await executeThunk(getCourseOutlineInfoQuery(courseId), store.dispatch);
window.dispatchEvent(messageEvent);
window.dispatchEvent(messageEvent);
});
await screen.findByText(
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();
});
@@ -1612,18 +1688,23 @@ describe('<CourseUnit />', () => {
getByRole,
} = render(<RootWrapper />);
await screen.findByText(unitDisplayName);
await act(async () => {
await waitFor(() => {
expect(getByText(unitDisplayName))
.toBeInTheDocument();
});
axiosMock
.onGet(getCourseOutlineInfoUrl(courseId))
.reply(200, courseOutlineInfoMock);
await executeThunk(getCourseOutlineInfoQuery(courseId), store.dispatch);
axiosMock
.onGet(getCourseOutlineInfoUrl(courseId))
.reply(200, courseOutlineInfoMock);
await executeThunk(getCourseOutlineInfoQuery(courseId), store.dispatch);
window.dispatchEvent(messageEvent);
window.dispatchEvent(messageEvent);
});
await screen.findByText(
expect(getByText(
moveModalMessages.moveModalTitle.defaultMessage.replace('{displayName}', requestData.title),
);
)).toBeInTheDocument();
const currentSection = courseOutlineInfoMock.child_info.children[1];
const currentSectionItemBtn = getByRole('button', {
@@ -1651,6 +1732,7 @@ describe('<CourseUnit />', () => {
it('should allow move operation and handles it successfully', async () => {
const {
getByText,
getByRole,
} = render(<RootWrapper />);
@@ -1662,18 +1744,23 @@ describe('<CourseUnit />', () => {
.onGet(getCourseUnitApiUrl(blockId))
.reply(200, courseUnitIndexMock);
await screen.findByText(unitDisplayName);
await act(async () => {
await waitFor(() => {
expect(getByText(unitDisplayName))
.toBeInTheDocument();
});
axiosMock
.onGet(getCourseOutlineInfoUrl(courseId))
.reply(200, courseOutlineInfoMock);
await executeThunk(getCourseOutlineInfoQuery(courseId), store.dispatch);
axiosMock
.onGet(getCourseOutlineInfoUrl(courseId))
.reply(200, courseOutlineInfoMock);
await executeThunk(getCourseOutlineInfoQuery(courseId), store.dispatch);
window.dispatchEvent(messageEvent);
window.dispatchEvent(messageEvent);
});
await screen.findByText(
expect(getByText(
moveModalMessages.moveModalTitle.defaultMessage.replace('{displayName}', requestData.title),
);
)).toBeInTheDocument();
const currentSection = courseOutlineInfoMock.child_info.children[1];
const currentSectionItemBtn = getByRole('button', {
@@ -1793,7 +1880,7 @@ describe('<CourseUnit />', () => {
});
describe('XBlock restrict access', () => {
it('opens xblock restrict access modal successfully', async () => {
it('opens xblock restrict access modal successfully', () => {
const {
getByTitle, getByTestId,
} = render(<RootWrapper />);
@@ -1802,7 +1889,7 @@ describe('<CourseUnit />', () => {
const modalCancelBtnText = configureModalMessages.cancelButton.defaultMessage;
const modalSaveBtnText = configureModalMessages.saveButton.defaultMessage;
await waitFor(() => {
waitFor(() => {
const iframe = getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
const usageId = courseVerticalChildrenMock.children[0].block_id;
expect(iframe).toBeInTheDocument();
@@ -1812,7 +1899,7 @@ describe('<CourseUnit />', () => {
});
});
await waitFor(() => {
waitFor(() => {
const configureModal = getByTestId('configure-modal');
expect(within(configureModal).getByText(modalSubtitleText)).toBeInTheDocument();
@@ -1826,7 +1913,7 @@ describe('<CourseUnit />', () => {
getByTitle, queryByTestId, getByTestId,
} = render(<RootWrapper />);
await waitFor(() => {
waitFor(() => {
const iframe = getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
expect(iframe).toBeInTheDocument();
simulatePostMessageEvent(messageTypes.manageXBlockAccess, {
@@ -1834,7 +1921,7 @@ describe('<CourseUnit />', () => {
});
});
await waitFor(() => {
waitFor(() => {
const configureModal = getByTestId('configure-modal');
expect(configureModal).toBeInTheDocument();
userEvent.click(within(configureModal).getByRole('button', {
@@ -1855,313 +1942,52 @@ describe('<CourseUnit />', () => {
.reply(200, { dummy: 'value' });
const {
getByTitle, getByRole, getByTestId, queryByTestId,
getByTitle, getByRole, getByTestId,
} = render(<RootWrapper />);
const accessGroupName1 = userPartitionInfoFormatted.selectablePartitions[0].groups[0].name;
const accessGroupName2 = userPartitionInfoFormatted.selectablePartitions[0].groups[1].name;
await waitFor(() => {
waitFor(() => {
const iframe = getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
expect(iframe).toBeInTheDocument();
});
await act(async () => {
simulatePostMessageEvent(messageTypes.manageXBlockAccess, {
usageId: courseVerticalChildrenMock.children[0].block_id,
});
});
const configureModal = await waitFor(() => getByTestId('configure-modal'));
expect(configureModal).toBeInTheDocument();
expect(within(configureModal).queryByText(accessGroupName1)).not.toBeInTheDocument();
expect(within(configureModal).queryByText(accessGroupName2)).not.toBeInTheDocument();
const restrictAccessSelect = getByRole('combobox', {
name: configureModalMessages.restrictAccessTo.defaultMessage,
});
await userEvent.selectOptions(restrictAccessSelect, '0');
await waitFor(() => {
userPartitionInfoFormatted.selectablePartitions[0].groups.forEach((group) => {
const checkbox = within(configureModal).getByRole('checkbox', { name: group.name });
expect(checkbox).not.toBeChecked();
expect(checkbox).toBeInTheDocument();
});
});
const group1Checkbox = within(configureModal).getByRole('checkbox', { name: accessGroupName1 });
await userEvent.click(group1Checkbox);
expect(group1Checkbox).toBeChecked();
const saveModalBtnText = within(configureModal).getByRole('button', {
name: configureModalMessages.saveButton.defaultMessage,
});
expect(saveModalBtnText).toBeInTheDocument();
await userEvent.click(saveModalBtnText);
await waitFor(() => {
expect(axiosMock.history.post.length).toBeGreaterThan(0);
expect(axiosMock.history.post[0].url).toBe(getXBlockBaseApiUrl(id));
});
expect(queryByTestId('configure-modal')).not.toBeInTheDocument();
});
});
const checkLegacyEditModalOnEditMessage = async () => {
const { getByTitle, getByTestId } = render(<RootWrapper />);
await waitFor(() => {
const editButton = getByTestId('header-edit-button');
expect(editButton).toBeInTheDocument();
const xblocksIframe = getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
expect(xblocksIframe).toBeInTheDocument();
userEvent.click(editButton);
});
};
const checkRenderVisibilityModal = async (headingMessageId) => {
const { findByRole, getByTestId } = render(<RootWrapper />);
let configureModal;
let restrictAccessSelect;
const headerConfigureBtn = await findByRole('button', { name: /settings/i });
await userEvent.click(headerConfigureBtn);
await waitFor(() => {
configureModal = getByTestId('configure-modal');
restrictAccessSelect = within(configureModal)
.getByRole('combobox', { name: configureModalMessages.restrictAccessTo.defaultMessage });
expect(within(configureModal)
.getByRole('heading', { name: configureModalMessages[headingMessageId].defaultMessage })).toBeInTheDocument();
expect(within(configureModal)
.queryByText(configureModalMessages.unitVisibility.defaultMessage)).not.toBeInTheDocument();
expect(within(configureModal)
.getByText(configureModalMessages.restrictAccessTo.defaultMessage)).toBeInTheDocument();
expect(restrictAccessSelect).toBeInTheDocument();
expect(restrictAccessSelect).toHaveValue('-1');
});
const modalSaveBtn = within(configureModal)
.getByRole('button', { name: configureModalMessages.saveButton.defaultMessage });
userEvent.click(modalSaveBtn);
};
describe('Library Content page', () => {
const newUnitId = '12345';
const sequenceId = courseSectionVerticalMock.subsection_location;
beforeEach(async () => {
axiosMock
.onGet(getCourseSectionVerticalApiUrl(blockId))
.reply(200, {
...courseSectionVerticalMock,
xblock: {
...courseSectionVerticalMock.xblock,
category: 'library_content',
},
xblock_info: {
...courseSectionVerticalMock.xblock_info,
category: 'library_content',
},
});
await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch);
axiosMock
.onGet(getCourseUnitApiUrl(courseId))
.reply(200, {
...courseUnitIndexMock,
category: 'library_content',
ancestor_info: {
...courseUnitIndexMock.ancestor_info,
child_info: {
...courseUnitIndexMock.ancestor_info.child_info,
category: 'library_content',
},
},
});
await executeThunk(fetchCourseUnitQuery(courseId), store.dispatch);
});
it('navigates to library content page on receive window event', async () => {
render(<RootWrapper />);
await waitFor(() => {
simulatePostMessageEvent(messageTypes.handleViewXBlockContent, { usageId: newUnitId });
expect(mockedUsedNavigate).toHaveBeenCalledWith(`/course/${courseId}/container/${newUnitId}/${sequenceId}`);
});
});
it('should render library content page correctly', async () => {
const {
findByText,
getByRole,
queryByRole,
findByTestId,
} = render(<RootWrapper />);
const currentSectionName = courseUnitIndexMock.ancestor_info.ancestors[1].display_name;
const currentSubSectionName = courseUnitIndexMock.ancestor_info.ancestors[1].display_name;
const unitHeaderTitle = await findByTestId('unit-header-title');
await findByText(unitDisplayName);
await waitFor(() => {
expect(within(unitHeaderTitle).getByRole('button', { name: headerTitleMessages.altButtonEdit.defaultMessage })).toBeInTheDocument();
expect(within(unitHeaderTitle).getByRole('button', { name: headerTitleMessages.altButtonSettings.defaultMessage })).toBeInTheDocument();
expect(getByRole('button', { name: currentSectionName })).toBeInTheDocument();
expect(getByRole('button', { name: currentSubSectionName })).toBeInTheDocument();
expect(queryByRole('heading', { name: addComponentMessages.title.defaultMessage })).not.toBeInTheDocument();
expect(queryByRole('button', { name: headerNavigationsMessages.viewLiveButton.defaultMessage })).not.toBeInTheDocument();
expect(queryByRole('button', { name: headerNavigationsMessages.previewButton.defaultMessage })).not.toBeInTheDocument();
expect(queryByRole('heading', { name: /unit tags/i })).not.toBeInTheDocument();
expect(queryByRole('heading', { name: /unit location/i })).not.toBeInTheDocument();
});
});
it('should display visibility modal correctly', async () => (
checkRenderVisibilityModal('libraryContentAccess')
));
it('opens legacy edit modal on edit button click', checkLegacyEditModalOnEditMessage);
});
describe('Split Test Content page', () => {
const newUnitId = '12345';
const sequenceId = courseSectionVerticalMock.subsection_location;
beforeEach(async () => {
axiosMock
.onGet(getCourseSectionVerticalApiUrl(blockId))
.reply(200, {
...courseSectionVerticalMock,
xblock: {
...courseSectionVerticalMock.xblock,
category: 'split_test',
},
xblock_info: {
...courseSectionVerticalMock.xblock_info,
category: 'split_test',
},
});
await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch);
axiosMock
.onGet(getCourseUnitApiUrl(courseId))
.reply(200, {
...courseUnitIndexMock,
category: 'split_test',
ancestor_info: {
...courseUnitIndexMock.ancestor_info,
child_info: {
...courseUnitIndexMock.ancestor_info.child_info,
category: 'split_test',
},
},
});
await executeThunk(fetchCourseUnitQuery(courseId), store.dispatch);
});
it('navigates to split test content page on receive window event', async () => {
render(<RootWrapper />);
simulatePostMessageEvent(messageTypes.handleViewXBlockContent, { usageId: newUnitId });
expect(mockedUsedNavigate).toHaveBeenCalledWith(`/course/${courseId}/container/${newUnitId}/${sequenceId}`);
});
it('navigates to group configuration page on receive window event', async () => {
const groupId = 12345;
render(<RootWrapper />);
simulatePostMessageEvent(messageTypes.handleViewGroupConfigurations, { usageId: `${courseId}#${groupId}` });
expect(mockedUsedNavigate).toHaveBeenCalledWith(`/course/${courseId}/group_configurations#${groupId}`);
});
it('displays processing notification on receiving post message', async () => {
const { getByText, queryByText } = render(<RootWrapper />);
await waitFor(() => {
simulatePostMessageEvent(messageTypes.addNewComponent);
expect(getByText(('Adding'))).toBeInTheDocument();
});
await waitFor(() => {
simulatePostMessageEvent(messageTypes.hideProcessingNotification);
expect(queryByText(('Adding'))).not.toBeInTheDocument();
});
await waitFor(() => {
simulatePostMessageEvent(messageTypes.pasteNewComponent);
expect(getByText(('Pasting'))).toBeInTheDocument();
});
await waitFor(() => {
simulatePostMessageEvent(messageTypes.hideProcessingNotification);
expect(queryByText(('Pasting'))).not.toBeInTheDocument();
});
});
it('should render split test content page correctly', async () => {
const {
getByText,
getByRole,
queryByRole,
getByTestId,
queryByText,
} = render(<RootWrapper />);
const currentSectionName = courseUnitIndexMock.ancestor_info.ancestors[1].display_name;
const currentSubSectionName = courseUnitIndexMock.ancestor_info.ancestors[1].display_name;
const helpLinkUrl = 'https://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/latest/developing_course/course_components.html#components-that-contain-other-components';
waitFor(() => {
const unitHeaderTitle = getByTestId('unit-header-title');
expect(getByText(unitDisplayName)).toBeInTheDocument();
expect(within(unitHeaderTitle).getByRole('button', { name: headerTitleMessages.altButtonEdit.defaultMessage })).toBeInTheDocument();
expect(within(unitHeaderTitle).getByRole('button', { name: headerTitleMessages.altButtonSettings.defaultMessage })).toBeInTheDocument();
expect(getByRole('button', { name: currentSectionName })).toBeInTheDocument();
expect(getByRole('button', { name: currentSubSectionName })).toBeInTheDocument();
const configureModal = getByTestId('configure-modal');
expect(configureModal).toBeInTheDocument();
expect(queryByRole('heading', { name: addComponentMessages.title.defaultMessage })).not.toBeInTheDocument();
expect(queryByRole('button', { name: headerNavigationsMessages.viewLiveButton.defaultMessage })).not.toBeInTheDocument();
expect(queryByRole('button', { name: headerNavigationsMessages.previewButton.defaultMessage })).not.toBeInTheDocument();
expect(within(configureModal).queryByText(accessGroupName1)).not.toBeInTheDocument();
expect(within(configureModal).queryByText(accessGroupName2)).not.toBeInTheDocument();
expect(queryByRole('heading', { name: /unit tags/i })).not.toBeInTheDocument();
expect(queryByRole('heading', { name: /unit location/i })).not.toBeInTheDocument();
// Sidebar
const sidebarContent = [
{ query: queryByRole, type: 'heading', name: sidebarMessages.sidebarSplitTestAddComponentTitle.defaultMessage },
{ query: queryByText, name: sidebarMessages.sidebarSplitTestSelectComponentType.defaultMessage.replaceAll('{bold_tag}', '') },
{ query: queryByText, name: sidebarMessages.sidebarSplitTestComponentAdded.defaultMessage },
{ query: queryByRole, type: 'heading', name: sidebarMessages.sidebarSplitTestEditComponentTitle.defaultMessage },
{ query: queryByText, name: sidebarMessages.sidebarSplitTestEditComponentInstruction.defaultMessage.replaceAll('{bold_tag}', '') },
{ query: queryByRole, type: 'heading', name: sidebarMessages.sidebarSplitTestReorganizeComponentTitle.defaultMessage },
{ query: queryByText, name: sidebarMessages.sidebarSplitTestReorganizeComponentInstruction.defaultMessage },
{ query: queryByText, name: sidebarMessages.sidebarSplitTestReorganizeGroupsInstruction.defaultMessage },
{ query: queryByRole, type: 'heading', name: sidebarMessages.sidebarSplitTestExperimentComponentTitle.defaultMessage },
{ query: queryByText, name: sidebarMessages.sidebarSplitTestExperimentComponentInstruction.defaultMessage },
{ query: queryByRole, type: 'link', name: sidebarMessages.sidebarSplitTestLearnMoreLinkLabel.defaultMessage },
];
sidebarContent.forEach(({ query, type, name }) => {
expect(type ? query(type, { name }) : query(name)).toBeInTheDocument();
const restrictAccessSelect = getByRole('combobox', {
name: configureModalMessages.restrictAccessTo.defaultMessage,
});
expect(
queryByRole('link', { name: sidebarMessages.sidebarSplitTestLearnMoreLinkLabel.defaultMessage }),
).toHaveAttribute('href', helpLinkUrl);
userEvent.selectOptions(restrictAccessSelect, '0');
// eslint-disable-next-line array-callback-return
userPartitionInfoFormatted.selectablePartitions[0].groups.map((group) => {
expect(within(configureModal).getByRole('checkbox', { name: group.name })).not.toBeChecked();
expect(within(configureModal).queryByText(group.name)).toBeInTheDocument();
});
const group1Checkbox = within(configureModal).getByRole('checkbox', { name: accessGroupName1 });
userEvent.click(group1Checkbox);
expect(group1Checkbox).toBeChecked();
const saveModalBtnText = within(configureModal).getByRole('button', {
name: configureModalMessages.saveButton.defaultMessage,
});
expect(saveModalBtnText).toBeInTheDocument();
userEvent.click(saveModalBtnText);
expect(handleConfigureSubmitMock).toHaveBeenCalledTimes(1);
});
});
it('should display visibility modal correctly', async () => (
checkRenderVisibilityModal('splitTestAccess')
));
it('opens legacy edit modal on edit button click', checkLegacyEditModalOnEditMessage);
});
it('renders and navigates to the new HTML XBlock editor after xblock duplicating', async () => {
@@ -2195,4 +2021,61 @@ describe('<CourseUnit />', () => {
.toHaveBeenCalledWith(`/course/${courseId}/editor/html/${targetBlockId}`, { replace: true });
});
});
describe('Library Content page', () => {
const newUnitId = '12345';
const sequenceId = courseSectionVerticalMock.subsection_location;
beforeEach(async () => {
axiosMock
.onGet(getCourseSectionVerticalApiUrl(blockId))
.reply(200, {
...courseSectionVerticalMock,
xblock: {
...courseSectionVerticalMock.xblock,
category: 'library_content',
},
xblock_info: {
...courseSectionVerticalMock.xblock_info,
category: 'library_content',
},
});
await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch);
});
it('navigates to library content page on receive window event', () => {
render(<RootWrapper />);
simulatePostMessageEvent(messageTypes.handleViewXBlockContent, { usageId: newUnitId });
expect(mockedUsedNavigate).toHaveBeenCalledWith(`/course/${courseId}/container/${newUnitId}/${sequenceId}`);
});
it('should render library content page correctly', async () => {
const {
getByText,
getByRole,
queryByRole,
getByTestId,
} = render(<RootWrapper />);
const currentSectionName = courseUnitIndexMock.ancestor_info.ancestors[1].display_name;
const currentSubSectionName = courseUnitIndexMock.ancestor_info.ancestors[1].display_name;
await waitFor(() => {
const unitHeaderTitle = getByTestId('unit-header-title');
expect(getByText(unitDisplayName)).toBeInTheDocument();
expect(within(unitHeaderTitle).getByRole('button', { name: headerTitleMessages.altButtonEdit.defaultMessage })).toBeInTheDocument();
expect(within(unitHeaderTitle).getByRole('button', { name: headerTitleMessages.altButtonSettings.defaultMessage })).toBeInTheDocument();
expect(getByRole('button', { name: currentSectionName })).toBeInTheDocument();
expect(getByRole('button', { name: currentSubSectionName })).toBeInTheDocument();
expect(queryByRole('heading', { name: addComponentMessages.title.defaultMessage })).not.toBeInTheDocument();
expect(queryByRole('button', { name: headerNavigationsMessages.viewLiveButton.defaultMessage })).not.toBeInTheDocument();
expect(queryByRole('button', { name: headerNavigationsMessages.previewButton.defaultMessage })).not.toBeInTheDocument();
expect(queryByRole('heading', { name: /unit tags/i })).not.toBeInTheDocument();
expect(queryByRole('heading', { name: /unit location/i })).not.toBeInTheDocument();
});
});
});
});

View File

@@ -5,7 +5,7 @@ module.exports = {
has_children: true,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
video_sharing_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',
@@ -18,7 +18,7 @@ module.exports = {
has_children: true,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
video_sharing_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',
@@ -30,7 +30,7 @@ module.exports = {
has_children: true,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
video_sharing_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',
@@ -42,7 +42,7 @@ module.exports = {
has_children: true,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
video_sharing_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: [
{
@@ -52,7 +52,7 @@ module.exports = {
has_children: false,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
video_sharing_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',
@@ -61,7 +61,7 @@ module.exports = {
has_children: false,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
video_sharing_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',
@@ -70,7 +70,7 @@ module.exports = {
has_children: false,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
video_sharing_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',
@@ -79,7 +79,7 @@ module.exports = {
has_children: false,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
},
],
},
@@ -94,7 +94,7 @@ module.exports = {
has_children: true,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
},
],
},
@@ -106,7 +106,7 @@ module.exports = {
has_children: true,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
video_sharing_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',
@@ -118,7 +118,7 @@ module.exports = {
has_children: true,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
video_sharing_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',
@@ -130,7 +130,7 @@ module.exports = {
has_children: true,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
video_sharing_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: [
{
@@ -140,7 +140,7 @@ module.exports = {
has_children: false,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
},
],
},
@@ -152,7 +152,7 @@ module.exports = {
has_children: true,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
video_sharing_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: [
{
@@ -162,7 +162,7 @@ module.exports = {
has_children: false,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
video_sharing_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',
@@ -171,7 +171,7 @@ module.exports = {
has_children: false,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
video_sharing_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',
@@ -180,7 +180,7 @@ module.exports = {
has_children: false,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
},
],
},
@@ -192,7 +192,7 @@ module.exports = {
has_children: true,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
video_sharing_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: [
{
@@ -202,7 +202,7 @@ module.exports = {
has_children: false,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
video_sharing_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',
@@ -211,7 +211,7 @@ module.exports = {
has_children: false,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
video_sharing_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',
@@ -220,7 +220,7 @@ module.exports = {
has_children: false,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
},
],
},
@@ -232,7 +232,7 @@ module.exports = {
has_children: true,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
video_sharing_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: [
{
@@ -242,7 +242,7 @@ module.exports = {
has_children: false,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
video_sharing_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',
@@ -251,7 +251,7 @@ module.exports = {
has_children: false,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
},
],
},
@@ -263,7 +263,7 @@ module.exports = {
has_children: true,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
video_sharing_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: [
{
@@ -273,7 +273,7 @@ module.exports = {
has_children: false,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
video_sharing_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',
@@ -282,7 +282,7 @@ module.exports = {
has_children: false,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
video_sharing_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',
@@ -291,7 +291,7 @@ module.exports = {
has_children: false,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
},
],
},
@@ -303,7 +303,7 @@ module.exports = {
has_children: true,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
video_sharing_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: [
{
@@ -313,7 +313,7 @@ module.exports = {
has_children: false,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
video_sharing_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',
@@ -322,7 +322,7 @@ module.exports = {
has_children: false,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
},
],
},
@@ -334,7 +334,7 @@ module.exports = {
has_children: true,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
video_sharing_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: [
{
@@ -344,7 +344,7 @@ module.exports = {
has_children: false,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
video_sharing_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',
@@ -353,7 +353,7 @@ module.exports = {
has_children: false,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
},
],
},
@@ -365,7 +365,7 @@ module.exports = {
has_children: true,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
video_sharing_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: [
{
@@ -375,7 +375,7 @@ module.exports = {
has_children: false,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
video_sharing_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',
@@ -384,7 +384,7 @@ module.exports = {
has_children: false,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
video_sharing_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',
@@ -393,7 +393,7 @@ module.exports = {
has_children: false,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
video_sharing_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',
@@ -402,7 +402,7 @@ module.exports = {
has_children: false,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
video_sharing_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',
@@ -411,7 +411,7 @@ module.exports = {
has_children: false,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
},
],
},
@@ -423,7 +423,7 @@ module.exports = {
has_children: true,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
video_sharing_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: [
{
@@ -433,7 +433,7 @@ module.exports = {
has_children: false,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
},
],
},
@@ -448,7 +448,7 @@ module.exports = {
has_children: true,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
video_sharing_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',
@@ -460,7 +460,7 @@ module.exports = {
has_children: true,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
video_sharing_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: [
{
@@ -470,7 +470,7 @@ module.exports = {
has_children: false,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
video_sharing_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',
@@ -479,7 +479,7 @@ module.exports = {
has_children: false,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
},
],
},
@@ -491,7 +491,7 @@ module.exports = {
has_children: true,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
video_sharing_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: [
{
@@ -501,7 +501,7 @@ module.exports = {
has_children: false,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
video_sharing_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',
@@ -510,7 +510,7 @@ module.exports = {
has_children: false,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
},
],
},
@@ -522,7 +522,7 @@ module.exports = {
has_children: true,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
video_sharing_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: [
{
@@ -532,7 +532,7 @@ module.exports = {
has_children: false,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
video_sharing_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',
@@ -541,7 +541,7 @@ module.exports = {
has_children: false,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
},
],
},
@@ -553,7 +553,7 @@ module.exports = {
has_children: true,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
video_sharing_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: [
{
@@ -563,7 +563,7 @@ module.exports = {
has_children: false,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
video_sharing_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',
@@ -572,7 +572,7 @@ module.exports = {
has_children: false,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
},
],
},
@@ -584,7 +584,7 @@ module.exports = {
has_children: true,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
video_sharing_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: [
{
@@ -594,7 +594,7 @@ module.exports = {
has_children: false,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
video_sharing_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',
@@ -603,7 +603,7 @@ module.exports = {
has_children: false,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
},
],
},
@@ -615,7 +615,7 @@ module.exports = {
has_children: true,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
video_sharing_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: [
{
@@ -625,7 +625,7 @@ module.exports = {
has_children: false,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
video_sharing_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',
@@ -634,7 +634,7 @@ module.exports = {
has_children: false,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
},
],
},
@@ -646,7 +646,7 @@ module.exports = {
has_children: true,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
video_sharing_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: [
{
@@ -656,7 +656,7 @@ module.exports = {
has_children: false,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
video_sharing_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',
@@ -665,7 +665,7 @@ module.exports = {
has_children: false,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
},
],
},
@@ -677,7 +677,7 @@ module.exports = {
has_children: true,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
video_sharing_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: [],
},
@@ -695,7 +695,7 @@ module.exports = {
has_children: true,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
video_sharing_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',
@@ -707,7 +707,7 @@ module.exports = {
has_children: true,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
video_sharing_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',
@@ -719,7 +719,7 @@ module.exports = {
has_children: true,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
video_sharing_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: [
{
@@ -729,7 +729,7 @@ module.exports = {
has_children: false,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
},
],
},
@@ -741,7 +741,7 @@ module.exports = {
has_children: true,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
video_sharing_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: [
{
@@ -751,7 +751,7 @@ module.exports = {
has_children: false,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
video_sharing_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',
@@ -760,7 +760,7 @@ module.exports = {
has_children: false,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
},
],
},
@@ -772,7 +772,7 @@ module.exports = {
has_children: true,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
video_sharing_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: [
{
@@ -782,7 +782,7 @@ module.exports = {
has_children: false,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
video_sharing_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',
@@ -791,7 +791,7 @@ module.exports = {
has_children: false,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
},
],
},
@@ -803,7 +803,7 @@ module.exports = {
has_children: true,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
video_sharing_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: [
{
@@ -813,7 +813,7 @@ module.exports = {
has_children: false,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
video_sharing_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',
@@ -822,7 +822,7 @@ module.exports = {
has_children: false,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
},
],
},
@@ -834,7 +834,7 @@ module.exports = {
has_children: true,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
video_sharing_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: [
{
@@ -844,7 +844,7 @@ module.exports = {
has_children: false,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
},
],
},
@@ -859,7 +859,7 @@ module.exports = {
has_children: true,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
video_sharing_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',
@@ -871,7 +871,7 @@ module.exports = {
has_children: true,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
video_sharing_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: [
{
@@ -881,7 +881,7 @@ module.exports = {
has_children: false,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
},
],
},
@@ -893,7 +893,7 @@ module.exports = {
has_children: true,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
video_sharing_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: [
{
@@ -903,7 +903,7 @@ module.exports = {
has_children: false,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
video_sharing_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',
@@ -912,7 +912,7 @@ module.exports = {
has_children: false,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
video_sharing_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',
@@ -921,7 +921,7 @@ module.exports = {
has_children: false,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
},
],
},
@@ -933,7 +933,7 @@ module.exports = {
has_children: true,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
video_sharing_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: [
{
@@ -943,7 +943,7 @@ module.exports = {
has_children: false,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
video_sharing_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',
@@ -952,7 +952,7 @@ module.exports = {
has_children: false,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
video_sharing_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',
@@ -961,7 +961,7 @@ module.exports = {
has_children: false,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
video_sharing_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',
@@ -970,7 +970,7 @@ module.exports = {
has_children: false,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
},
],
},
@@ -982,7 +982,7 @@ module.exports = {
has_children: true,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
video_sharing_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: [
{
@@ -992,7 +992,7 @@ module.exports = {
has_children: false,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
video_sharing_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',
@@ -1001,7 +1001,7 @@ module.exports = {
has_children: false,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
video_sharing_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',
@@ -1010,7 +1010,7 @@ module.exports = {
has_children: false,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
},
],
},
@@ -1022,7 +1022,7 @@ module.exports = {
has_children: true,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
video_sharing_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: [
{
@@ -1032,7 +1032,7 @@ module.exports = {
has_children: false,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
},
],
},
@@ -1047,7 +1047,7 @@ module.exports = {
has_children: true,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
video_sharing_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',
@@ -1059,7 +1059,7 @@ module.exports = {
has_children: true,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
video_sharing_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: [
{
@@ -1069,7 +1069,7 @@ module.exports = {
has_children: false,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
video_sharing_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',
@@ -1078,7 +1078,7 @@ module.exports = {
has_children: false,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
},
],
},
@@ -1096,7 +1096,7 @@ module.exports = {
has_children: true,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
video_sharing_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',
@@ -1108,7 +1108,7 @@ module.exports = {
has_children: true,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
video_sharing_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',
@@ -1120,7 +1120,7 @@ module.exports = {
has_children: true,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
video_sharing_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: [
{
@@ -1130,7 +1130,7 @@ module.exports = {
has_children: false,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
},
],
},
@@ -1142,7 +1142,7 @@ module.exports = {
has_children: true,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
video_sharing_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: [
{
@@ -1152,7 +1152,7 @@ module.exports = {
has_children: false,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
video_sharing_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',
@@ -1161,7 +1161,7 @@ module.exports = {
has_children: false,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
},
],
},
@@ -1173,7 +1173,7 @@ module.exports = {
has_children: true,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
video_sharing_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: [
{
@@ -1183,7 +1183,7 @@ module.exports = {
has_children: false,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
},
],
},
@@ -1195,7 +1195,7 @@ module.exports = {
has_children: true,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
video_sharing_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: [
{
@@ -1205,7 +1205,7 @@ module.exports = {
has_children: false,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
},
],
},
@@ -1220,7 +1220,7 @@ module.exports = {
has_children: true,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
video_sharing_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',
@@ -1232,7 +1232,7 @@ module.exports = {
has_children: true,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
video_sharing_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: [
{
@@ -1242,7 +1242,7 @@ module.exports = {
has_children: false,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
},
],
},
@@ -1257,7 +1257,7 @@ module.exports = {
has_children: true,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
video_sharing_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',
@@ -1269,7 +1269,7 @@ module.exports = {
has_children: true,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
video_sharing_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: [
{
@@ -1279,7 +1279,7 @@ module.exports = {
has_children: false,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
video_sharing_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',
@@ -1288,7 +1288,7 @@ module.exports = {
has_children: false,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
video_sharing_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',
@@ -1297,7 +1297,7 @@ module.exports = {
has_children: false,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
},
],
},
@@ -1315,7 +1315,7 @@ module.exports = {
has_children: true,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
video_sharing_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',
@@ -1327,7 +1327,7 @@ module.exports = {
has_children: true,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
video_sharing_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',
@@ -1339,7 +1339,7 @@ module.exports = {
has_children: true,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
video_sharing_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: [
{
@@ -1349,7 +1349,7 @@ module.exports = {
has_children: false,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
},
],
},
@@ -1361,7 +1361,7 @@ module.exports = {
has_children: true,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
video_sharing_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: [
{
@@ -1371,7 +1371,7 @@ module.exports = {
has_children: false,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
video_sharing_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',
@@ -1380,7 +1380,7 @@ module.exports = {
has_children: false,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
},
],
},
@@ -1392,7 +1392,7 @@ module.exports = {
has_children: true,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
video_sharing_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: [
{
@@ -1402,7 +1402,7 @@ module.exports = {
has_children: false,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
video_sharing_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',
@@ -1411,7 +1411,7 @@ module.exports = {
has_children: false,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
},
],
},
@@ -1423,7 +1423,7 @@ module.exports = {
has_children: true,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
video_sharing_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: [
{
@@ -1433,7 +1433,7 @@ module.exports = {
has_children: false,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
video_sharing_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',
@@ -1442,7 +1442,7 @@ module.exports = {
has_children: false,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
},
],
},
@@ -1454,7 +1454,7 @@ module.exports = {
has_children: true,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
video_sharing_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: [
{
@@ -1464,7 +1464,7 @@ module.exports = {
has_children: false,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
video_sharing_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',
@@ -1473,7 +1473,7 @@ module.exports = {
has_children: false,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
video_sharing_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',
@@ -1482,7 +1482,7 @@ module.exports = {
has_children: false,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
},
],
},
@@ -1494,7 +1494,7 @@ module.exports = {
has_children: true,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
video_sharing_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: [
{
@@ -1504,7 +1504,7 @@ module.exports = {
has_children: false,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
video_sharing_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',
@@ -1513,7 +1513,7 @@ module.exports = {
has_children: false,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
},
],
},
@@ -1525,7 +1525,7 @@ module.exports = {
has_children: true,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
video_sharing_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: [
{
@@ -1535,7 +1535,7 @@ module.exports = {
has_children: false,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
video_sharing_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',
@@ -1544,7 +1544,7 @@ module.exports = {
has_children: false,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
},
],
},
@@ -1556,7 +1556,7 @@ module.exports = {
has_children: true,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
video_sharing_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: [
{
@@ -1566,7 +1566,7 @@ module.exports = {
has_children: false,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
video_sharing_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',
@@ -1575,7 +1575,7 @@ module.exports = {
has_children: false,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
},
],
},
@@ -1587,7 +1587,7 @@ module.exports = {
has_children: true,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
video_sharing_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: [
{
@@ -1597,7 +1597,7 @@ module.exports = {
has_children: false,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
video_sharing_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',
@@ -1606,7 +1606,7 @@ module.exports = {
has_children: false,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
},
],
},
@@ -1624,7 +1624,7 @@ module.exports = {
has_children: true,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
video_sharing_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',
@@ -1636,7 +1636,7 @@ module.exports = {
has_children: true,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
video_sharing_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',
@@ -1648,7 +1648,7 @@ module.exports = {
has_children: true,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
video_sharing_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: [
{
@@ -1658,7 +1658,7 @@ module.exports = {
has_children: false,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
},
],
},
@@ -1673,7 +1673,7 @@ module.exports = {
has_children: true,
video_sharing_enabled: true,
video_sharing_options: 'per-video',
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
},
],
},

View File

@@ -1310,7 +1310,7 @@ module.exports = {
highlights: [],
highlights_enabled: true,
highlights_preview_only: false,
highlights_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/manage_course_highlight_emails.html',
highlights_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/latest/developing_course/course_sections.html#set-section-highlights-for-weekly-course-highlight-messages',
ancestor_has_staff_lock: false,
user_partition_info: {
selectable_partitions: [
@@ -1396,7 +1396,7 @@ module.exports = {
highlights_enabled_for_messaging: false,
highlights_enabled: true,
highlights_preview_only: false,
highlights_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/manage_course_highlight_emails.html',
highlights_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/latest/developing_course/course_sections.html#set-section-highlights-for-weekly-course-highlight-messages',
enable_proctored_exams: false,
create_zendesk_tickets: true,
enable_timed_exams: true,

View File

@@ -968,7 +968,7 @@ module.exports = {
highlights: [],
highlights_enabled: true,
highlights_preview_only: false,
highlights_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/manage_course_highlight_emails.html',
highlights_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/latest/developing_course/course_sections.html#set-section-highlights-for-weekly-course-highlight-messages',
ancestor_has_staff_lock: false,
user_partition_info: {
selectable_partitions: [
@@ -1054,7 +1054,7 @@ module.exports = {
highlights_enabled_for_messaging: false,
highlights_enabled: true,
highlights_preview_only: false,
highlights_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/manage_course_highlight_emails.html',
highlights_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/latest/developing_course/course_sections.html#set-section-highlights-for-weekly-course-highlight-messages',
enable_proctored_exams: false,
create_zendesk_tickets: true,
enable_timed_exams: true,

View File

@@ -14,38 +14,26 @@ import AddComponentButton from './add-component-btn';
import messages from './messages';
import { ComponentPicker } from '../../library-authoring/component-picker';
import { messageTypes } from '../constants';
import { useIframe } from '../../generic/hooks/context/hooks';
import { useIframe } from '../context/hooks';
import { useEventListener } from '../../generic/hooks';
const AddComponent = ({
parentLocator,
isSplitTestType,
isUnitVerticalType,
addComponentTemplateData,
handleCreateNewCourseXBlock,
}) => {
const AddComponent = ({ blockId, handleCreateNewCourseXBlock }) => {
const navigate = useNavigate();
const intl = useIntl();
const [isOpenAdvanced, openAdvanced, closeAdvanced] = useToggle(false);
const [isOpenHtml, openHtml, closeHtml] = useToggle(false);
const [isOpenOpenAssessment, openOpenAssessment, closeOpenAssessment] = useToggle(false);
const { componentTemplates = {} } = useSelector(getCourseSectionVertical);
const blockId = addComponentTemplateData.parentLocator || parentLocator;
const [isAddLibraryContentModalOpen, showAddLibraryContentModal, closeAddLibraryContentModal] = useToggle();
const [isSelectLibraryContentModalOpen, showSelectLibraryContentModal, closeSelectLibraryContentModal] = useToggle();
const [selectedComponents, setSelectedComponents] = useState([]);
const [usageId, setUsageId] = useState(null);
const { sendMessageToIframe } = useIframe();
const receiveMessage = useCallback(({ data: { type, payload } }) => {
const receiveMessage = useCallback(({ data: { type } }) => {
if (type === messageTypes.showMultipleComponentPicker) {
showSelectLibraryContentModal();
}
if (type === messageTypes.showSingleComponentPicker) {
setUsageId(payload.usageId);
showAddLibraryContentModal();
}
}, [showSelectLibraryContentModal, showAddLibraryContentModal, setUsageId]);
}, [showSelectLibraryContentModal]);
useEventListener('message', receiveMessage);
@@ -58,11 +46,11 @@ const AddComponent = ({
handleCreateNewCourseXBlock({
type: COMPONENT_TYPES.libraryV2,
category: selection.blockType,
parentLocator: usageId || blockId,
parentLocator: blockId,
libraryContentKey: selection.usageKey,
});
closeAddLibraryContentModal();
}, [usageId]);
}, []);
const handleCreateNewXBlock = (type, moduleName) => {
switch (type) {
@@ -89,10 +77,14 @@ const AddComponent = ({
showAddLibraryContentModal();
break;
case COMPONENT_TYPES.advanced:
handleCreateNewCourseXBlock({ type: moduleName, category: moduleName, parentLocator: blockId });
handleCreateNewCourseXBlock({
type: moduleName, category: moduleName, parentLocator: blockId,
});
break;
case COMPONENT_TYPES.openassessment:
handleCreateNewCourseXBlock({ boilerplate: moduleName, category: type, parentLocator: blockId });
handleCreateNewCourseXBlock({
boilerplate: moduleName, category: type, parentLocator: blockId,
});
break;
case COMPONENT_TYPES.html:
handleCreateNewCourseXBlock({
@@ -108,136 +100,104 @@ const AddComponent = ({
}
};
if (isUnitVerticalType || isSplitTestType) {
return (
<div className="py-4">
{Object.keys(componentTemplates).length && isUnitVerticalType ? (
<>
<h5 className="h3 mb-4 text-center">{intl.formatMessage(messages.title)}</h5>
<ul className="new-component-type list-unstyled m-0 d-flex flex-wrap justify-content-center">
{componentTemplates.map((component) => {
const { type, displayName, beta } = component;
let modalParams;
if (!component.templates.length) {
return null;
}
switch (type) {
case COMPONENT_TYPES.advanced:
modalParams = {
open: openAdvanced,
close: closeAdvanced,
isOpen: isOpenAdvanced,
};
break;
case COMPONENT_TYPES.html:
modalParams = {
open: openHtml,
close: closeHtml,
isOpen: isOpenHtml,
};
break;
case COMPONENT_TYPES.openassessment:
modalParams = {
open: openOpenAssessment,
close: closeOpenAssessment,
isOpen: isOpenOpenAssessment,
};
break;
default:
return (
<li key={type}>
<AddComponentButton
onClick={() => handleCreateNewXBlock(type)}
displayName={displayName}
type={type}
beta={beta}
/>
</li>
);
}
return (
<ComponentModalView
key={type}
component={component}
handleCreateNewXBlock={handleCreateNewXBlock}
modalParams={modalParams}
/>
);
})}
</ul>
</>
) : null}
<StandardModal
title={
isAddLibraryContentModalOpen
? intl.formatMessage(messages.singleComponentPickerModalTitle)
: intl.formatMessage(messages.multipleComponentPickerModalTitle)
}
isOpen={isAddLibraryContentModalOpen || isSelectLibraryContentModalOpen}
onClose={() => {
closeAddLibraryContentModal();
closeSelectLibraryContentModal();
}}
isOverflowVisible={false}
size="xl"
footerNode={
isSelectLibraryContentModalOpen && (
<ActionRow>
<Button onClick={onComponentSelectionSubmit}>
<FormattedMessage {...messages.multipleComponentPickerModalBtn} />
</Button>
</ActionRow>
)
}
>
<ComponentPicker
showOnlyPublished
extraFilter={['NOT block_type = "unit"']}
componentPickerMode={isAddLibraryContentModalOpen ? 'single' : 'multiple'}
onComponentSelected={handleLibraryV2Selection}
onChangeComponentSelection={setSelectedComponents}
/>
</StandardModal>
</div>
);
if (!Object.keys(componentTemplates).length) {
return null;
}
return null;
};
return (
<div className="py-4">
<h5 className="h3 mb-4 text-center">{intl.formatMessage(messages.title)}</h5>
<ul className="new-component-type list-unstyled m-0 d-flex flex-wrap justify-content-center">
{componentTemplates.map((component) => {
const { type, displayName, beta } = component;
let modalParams;
AddComponent.defaultProps = {
addComponentTemplateData: {},
if (!component.templates.length) {
return null;
}
switch (type) {
case COMPONENT_TYPES.advanced:
modalParams = {
open: openAdvanced,
close: closeAdvanced,
isOpen: isOpenAdvanced,
};
break;
case COMPONENT_TYPES.html:
modalParams = {
open: openHtml,
close: closeHtml,
isOpen: isOpenHtml,
};
break;
case COMPONENT_TYPES.openassessment:
modalParams = {
open: openOpenAssessment,
close: closeOpenAssessment,
isOpen: isOpenOpenAssessment,
};
break;
default:
return (
<li key={type}>
<AddComponentButton
onClick={() => handleCreateNewXBlock(type)}
displayName={displayName}
type={type}
beta={beta}
/>
</li>
);
}
return (
<ComponentModalView
key={type}
component={component}
handleCreateNewXBlock={handleCreateNewXBlock}
modalParams={modalParams}
/>
);
})}
</ul>
<StandardModal
title={
isAddLibraryContentModalOpen
? intl.formatMessage(messages.singleComponentPickerModalTitle)
: intl.formatMessage(messages.multipleComponentPickerModalTitle)
}
isOpen={isAddLibraryContentModalOpen || isSelectLibraryContentModalOpen}
onClose={() => {
closeAddLibraryContentModal();
closeSelectLibraryContentModal();
}}
isOverflowVisible={false}
size="xl"
footerNode={
isSelectLibraryContentModalOpen && (
<ActionRow>
<Button variant="primary" onClick={onComponentSelectionSubmit}>
<FormattedMessage {...messages.multipleComponentPickerModalBtn} />
</Button>
</ActionRow>
)
}
>
<ComponentPicker
showOnlyPublished
componentPickerMode={isAddLibraryContentModalOpen ? 'single' : 'multiple'}
onComponentSelected={handleLibraryV2Selection}
onChangeComponentSelection={setSelectedComponents}
/>
</StandardModal>
</div>
);
};
AddComponent.propTypes = {
isSplitTestType: PropTypes.bool.isRequired,
isUnitVerticalType: PropTypes.bool.isRequired,
parentLocator: PropTypes.string.isRequired,
blockId: PropTypes.string.isRequired,
handleCreateNewCourseXBlock: PropTypes.func.isRequired,
addComponentTemplateData: {
blockId: PropTypes.string.isRequired,
model: PropTypes.shape({
displayName: PropTypes.string.isRequired,
category: PropTypes.string,
type: PropTypes.string.isRequired,
templates: PropTypes.arrayOf(
PropTypes.shape({
boilerplateName: PropTypes.string,
category: PropTypes.string,
displayName: PropTypes.string.isRequired,
supportLevel: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]),
}),
),
supportLegend: PropTypes.shape({
allowUnsupportedXblocks: PropTypes.bool,
documentationLabel: PropTypes.string,
showLegend: PropTypes.bool,
}),
}),
},
};
export default AddComponent;

View File

@@ -18,7 +18,7 @@ import { courseSectionVerticalMock } from '../__mocks__';
import { COMPONENT_TYPES } from '../../generic/block-type-utils/constants';
import AddComponent from './AddComponent';
import messages from './messages';
import { IframeProvider } from '../../generic/hooks/context/iFrameContext';
import { IframeProvider } from '../context/iFrameContext';
import { messageTypes } from '../constants';
let store;
@@ -52,7 +52,7 @@ jest.mock('../../library-authoring/component-picker', () => ({
}));
const mockSendMessageToIframe = jest.fn();
jest.mock('../../generic/hooks/context/hooks', () => ({
jest.mock('../context/hooks', () => ({
useIframe: () => ({
sendMessageToIframe: mockSendMessageToIframe,
}),
@@ -64,9 +64,6 @@ const renderComponent = (props) => render(
<IframeProvider>
<AddComponent
blockId={blockId}
isUnitVerticalType
parentLocator={blockId}
addComponentTemplateData={{}}
handleCreateNewCourseXBlock={handleCreateNewCourseXBlockMock}
{...props}
/>

View File

@@ -14,7 +14,6 @@ const ComponentModalView = ({
component,
modalParams,
handleCreateNewXBlock,
isRequestedModalView,
}) => {
const intl = useIntl();
const dispatch = useDispatch();
@@ -31,19 +30,15 @@ const ComponentModalView = ({
setModuleTitle('');
};
const renderAddComponentButton = () => (
<li>
<AddComponentButton
onClick={open}
type={type}
displayName={displayName}
/>
</li>
);
return (
<>
{!isRequestedModalView && renderAddComponentButton()}
<li>
<AddComponentButton
onClick={open}
type={type}
displayName={displayName}
/>
</li>
<ModalContainer
isOpen={isOpen}
close={close}
@@ -97,10 +92,6 @@ const ComponentModalView = ({
);
};
ComponentModalView.defaultProps = {
isRequestedModalView: false,
};
ComponentModalView.propTypes = {
modalParams: PropTypes.shape({
open: PropTypes.func,
@@ -126,7 +117,6 @@ ComponentModalView.propTypes = {
showLegend: PropTypes.bool,
}),
}).isRequired,
isRequestedModalView: PropTypes.bool,
};
export default ComponentModalView;

View File

@@ -58,27 +58,27 @@ const messages = defineMessages({
},
modalComponentSupportTooltipFullySupported: {
id: 'course-authoring.course-unit.modal.component.support.tooltip.fully-supported',
defaultMessage: 'Fully supported tools and features are available for Open edX installations, '
+ 'are fully tested, have user interfaces where applicable, and are documented in the '
+ 'official Open edX guides that are available on docs.openedx.org.',
defaultMessage: 'Fully supported tools and features are available on edX, are '
+ 'fully tested, have user interfaces where applicable, and are documented in the '
+ 'official edX guides that are available on docs.edx.org.',
description: 'Message for support status tooltip for modules with full platform support',
},
modalComponentSupportTooltipNotSupported: {
id: 'course-authoring.course-unit.modal.component.support.tooltip.not-supported',
defaultMessage: 'Tools with no support are not maintained by the Open edX community, '
+ 'and might be deprecated in the future. They are not recommended for use in '
+ 'courses due to non-compliance with one or more of the base requirements, such as '
+ 'testing, accessibility, internationalization, and documentation.',
defaultMessage: 'Tools with no support are not maintained by edX, and might be '
+ 'deprecated in the future. They are not recommended for use in courses due to '
+ 'non-compliance with one or more of the base requirements, such as testing, '
+ 'accessibility, internationalization, and documentation.',
description: 'Message for support status tooltip for modules which is not supported',
},
modalComponentSupportTooltipProvisionallySupported: {
id: 'course-authoring.course-unit.modal.component.support.tooltip.provisionally-support',
defaultMessage: 'Provisionally supported tools might lack the robustness of functionality '
+ 'that your courses require. Open edX does not have control over the quality of the software, '
+ 'that your courses require. edX does not have control over the quality of the software, '
+ 'or of the content that can be provided using these tools. Test these tools thoroughly '
+ 'before using them in your course, especially in graded sections. Complete documentation '
+ 'might not be available for provisionally supported tools, or documentation might be '
+ 'available from sources other than the Open edX community.',
+ 'available from sources other than edX.',
description: 'Message for support status tooltip for modules with provisional platform support',
},
});

View File

@@ -3,7 +3,7 @@
background: transparent;
}
.sub-header-breadcrumbs {
.sub-header-title .sub-header-breadcrumbs {
.dropdown-toggle::after {
display: none;
}

View File

@@ -26,55 +26,43 @@ const Breadcrumbs = ({ courseId, parentUnitId }: { courseId: string, parentUnitI
isOutlinePage ? getPathToCourseOutlinePage(url) : getPathToCourseUnitPage(url)
);
const hasChildWithUrl = (children = []) => (
!!children.filter((child : any) => child?.url).length
);
return (
<nav className="d-flex align-center mb-2.5">
<ol className="p-0 m-0 d-flex align-center flex-wrap">
<ol className="p-0 m-0 d-flex align-center">
{ancestorXblocks.map(({ children, title, isLast }, index) => (
<li
className="d-flex mb-2.5"
className="d-flex"
// eslint-disable-next-line react/no-array-index-key
key={`${title}-${index}`}
>
{hasChildWithUrl(children) ? (
<Dropdown>
<Dropdown.Toggle
id="breadcrumbs-dropdown-section"
variant="link"
className="p-0 text-primary small"
>
<span className="small text-gray-700">
{title}
</span>
<Icon
src={ArrowDropDownIcon}
className="text-primary ml-1"
/>
</Dropdown.Toggle>
<Dropdown.Menu>
{children.map(({ url, displayName }) => (
<Dropdown.Item
as={Link}
key={url}
to={getPathToCoursePage(index < 2, url)}
className="small"
data-testid={`breadcrumbs-dropdown-item-level-${index}`}
>
{displayName}
</Dropdown.Item>
))}
</Dropdown.Menu>
</Dropdown>
) : (
<span className="p-0 text-primary small btn btn-link text-decoration-none">
<Dropdown>
<Dropdown.Toggle
id="breadcrumbs-dropdown-section"
variant="link"
className="p-0 text-primary small"
>
<span className="small text-gray-700">
{title}
</span>
</span>
)}
<Icon
src={ArrowDropDownIcon}
className="text-primary ml-1"
/>
</Dropdown.Toggle>
<Dropdown.Menu>
{children.map(({ url, displayName }) => (
<Dropdown.Item
as={Link}
key={url}
to={getPathToCoursePage(index < 2, url)}
className="small"
data-testid={`breadcrumbs-dropdown-item-level-${index}`}
>
{displayName}
</Dropdown.Item>
))}
</Dropdown.Menu>
</Dropdown>
{!isLast && (
<Icon
src={ChevronRightIcon}

View File

@@ -39,13 +39,22 @@ export const getXBlockSupportMessages = (intl) => ({
},
});
export const stateKeys = {
iframeHeight: 'iframeHeight',
hasLoaded: 'hasLoaded',
showError: 'showError',
windowTopOffset: 'windowTopOffset',
};
export const messageTypes = {
modal: 'plugin.modal',
resize: 'plugin.resize',
videoFullScreen: 'plugin.videoFullScreen',
refreshXBlock: 'refreshXBlock',
showMoveXBlockModal: 'showMoveXBlockModal',
completeXBlockMoving: 'completeXBlockMoving',
rollbackMovedXBlock: 'rollbackMovedXBlock',
showMultipleComponentPicker: 'showMultipleComponentPicker',
showSingleComponentPicker: 'showSingleComponentPicker',
addSelectedComponentsToBank: 'addSelectedComponentsToBank',
showXBlockLibraryChangesPreview: 'showXBlockLibraryChangesPreview',
copyXBlock: 'copyXBlock',
@@ -60,7 +69,6 @@ export const messageTypes = {
addXBlock: 'addXBlock',
scrollToXBlock: 'scrollToXBlock',
handleViewXBlockContent: 'handleViewXBlockContent',
handleViewGroupConfigurations: 'handleViewGroupConfigurations',
editXBlock: 'editXBlock',
closeXBlockEditorModal: 'closeXBlockEditorModal',
saveEditedXBlockData: 'saveEditedXBlockData',
@@ -68,10 +76,4 @@ export const messageTypes = {
studioAjaxError: 'studioAjaxError',
refreshPositions: 'refreshPositions',
openManageTags: 'openManageTags',
showComponentTemplates: 'showComponentTemplates',
addNewComponent: 'addNewComponent',
pasteNewComponent: 'pasteComponent',
copyXBlockLegacy: 'copyXBlockLegacy',
hideProcessingNotification: 'hideProcessingNotification',
handleRedirectToXBlockEditPage: 'handleRedirectToXBlockEditPage',
};

View File

@@ -1,12 +1,13 @@
import { ReactNode } from 'react';
import { renderHook } from '@testing-library/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', () => {
expect(() => { renderHook(() => useIframe()); }).toThrow('useIframe must be used within an 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', () => {

View File

@@ -4,25 +4,23 @@ import React, {
import { logError } from '@edx/frontend-platform/logging';
export interface IframeContextType {
iframeRef: MutableRefObject<HTMLIFrameElement | null>;
setIframeRef: (ref: MutableRefObject<HTMLIFrameElement | null>) => void;
sendMessageToIframe: (messageType: string, payload: unknown, consumerWindow?: Window | null) => void;
sendMessageToIframe: (messageType: string, payload: unknown) => void;
}
export const IframeContext = createContext<IframeContextType | undefined>(undefined);
export const IframeProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
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, consumerWindow?: Window | null) => {
const sendMessageToIframe = useCallback((messageType: string, payload: any) => {
const iframeWindow = iframeRef?.current?.contentWindow;
const targetWindow = consumerWindow || iframeWindow;
if (targetWindow) {
if (iframeWindow) {
try {
targetWindow.postMessage({ type: messageType, payload }, '*');
iframeWindow.postMessage({ type: messageType, payload }, '*');
} catch (error) {
logError('Failed to send message to iframe:', error);
}
@@ -32,7 +30,6 @@ export const IframeProvider: React.FC<{ children: ReactNode }> = ({ children })
}, [iframeRef]);
const value = useMemo(() => ({
iframeRef,
setIframeRef,
sendMessageToIframe,
}), [setIframeRef, sendMessageToIframe]);

View File

@@ -1,4 +1,4 @@
import { renderHook } from '@testing-library/react';
import { renderHook } from '@testing-library/react-hooks';
import { useSelector } from 'react-redux';
import { useSequenceNavigationMetadata } from './hooks';
import { getCourseSectionVertical, getSequenceIds } from '../data/selectors';

View File

@@ -8,6 +8,7 @@ import { handleResponseErrors } from '../../generic/saving-error-alert';
import { RequestStatus } from '../../data/constants';
import { NOTIFICATION_MESSAGES } from '../../constants';
import { updateModel, updateModels } from '../../generic/model-store';
import { updateClipboardData } from '../../generic/data/slice';
import { messageTypes } from '../constants';
import {
getCourseUnitData,
@@ -30,6 +31,7 @@ import {
fetchSequenceSuccess,
fetchCourseSectionVerticalDataSuccess,
updateLoadingCourseSectionVerticalDataStatus,
updateLoadingCourseXblockStatus,
updateCourseVerticalChildren,
updateCourseVerticalChildrenLoadingStatus,
updateQueryPendingStatus,
@@ -75,6 +77,7 @@ export function fetchCourseSectionVerticalData(courseId, sequenceId) {
}));
dispatch(fetchStaticFileNoticesSuccess(JSON.parse(localStorage.getItem('staticFileNotices'))));
localStorage.removeItem('staticFileNotices');
dispatch(updateClipboardData(courseSectionVerticalData.userClipboard));
dispatch(fetchSequenceSuccess({ sequenceId }));
return true;
} catch (error) {
@@ -199,30 +202,18 @@ export function createNewCourseXBlock(body, callback, blockId, sendMessageToIfra
});
} catch (error) {
dispatch(hideProcessingNotification());
dispatch(updateLoadingCourseXblockStatus({ status: RequestStatus.FAILED }));
handleResponseErrors(error, dispatch, updateSavingStatus);
}
};
}
export function fetchCourseVerticalChildrenData(itemId, isSplitTestType, skipPageLoading) {
export function fetchCourseVerticalChildrenData(itemId) {
return async (dispatch) => {
if (!skipPageLoading) {
dispatch(updateCourseVerticalChildrenLoadingStatus({ status: RequestStatus.IN_PROGRESS }));
}
dispatch(updateCourseVerticalChildrenLoadingStatus({ status: RequestStatus.IN_PROGRESS }));
try {
const courseVerticalChildrenData = await getCourseVerticalChildren(itemId);
if (isSplitTestType) {
const blockIds = courseVerticalChildrenData.children.map(child => child.blockId);
const childrenDataArray = await Promise.all(
blockIds.map(blockId => getCourseVerticalChildren(blockId)),
);
const allChildren = childrenDataArray.reduce(
(acc, data) => acc.concat(data.children || []),
[],
);
courseVerticalChildrenData.children = [...courseVerticalChildrenData.children, ...allChildren];
}
dispatch(updateCourseVerticalChildren(courseVerticalChildrenData));
dispatch(updateCourseVerticalChildrenLoadingStatus({ status: RequestStatus.SUCCESSFUL }));
} catch (error) {
@@ -239,6 +230,8 @@ export function deleteUnitItemQuery(itemId, xblockId, sendMessageToIframe) {
try {
await deleteUnitItem(xblockId);
sendMessageToIframe(messageTypes.completeXBlockDeleting, { locator: xblockId });
const { userClipboard } = await getCourseSectionVerticalData(itemId);
dispatch(updateClipboardData(userClipboard));
const courseUnit = await getCourseUnitData(itemId);
dispatch(fetchCourseItemSuccess(courseUnit));
dispatch(hideProcessingNotification());
@@ -311,13 +304,13 @@ export function patchUnitItemQuery({
dispatch(updateMovedXBlockParams(xBlockParams));
dispatch(updateCourseOutlineInfo({}));
dispatch(updateCourseOutlineInfoLoadingStatus({ status: RequestStatus.IN_PROGRESS }));
callbackFn(sourceLocator);
try {
const courseUnit = await getCourseUnitData(currentParentLocator);
dispatch(fetchCourseItemSuccess(courseUnit));
} catch (error) {
handleResponseErrors(error, dispatch, updateSavingStatus);
}
callbackFn(sourceLocator);
} catch (error) {
handleResponseErrors(error, dispatch, updateSavingStatus);
} finally {

View File

@@ -6,13 +6,13 @@ import { Edit as EditIcon } from '@openedx/paragon/icons';
import { COURSE_BLOCK_NAMES } from '../../constants';
import messages from './messages';
const HeaderNavigations = ({ headerNavigationsActions, category }) => {
const HeaderNavigations = ({ headerNavigationsActions, unitCategory }) => {
const intl = useIntl();
const { handleViewLive, handlePreview, handleEdit } = headerNavigationsActions;
return (
<nav className="header-navigations ml-auto flex-shrink-0">
{category === COURSE_BLOCK_NAMES.vertical.id && (
{unitCategory === COURSE_BLOCK_NAMES.vertical.id && (
<>
<Button
variant="outline-primary"
@@ -28,12 +28,11 @@ const HeaderNavigations = ({ headerNavigationsActions, category }) => {
</Button>
</>
)}
{[COURSE_BLOCK_NAMES.libraryContent.id, COURSE_BLOCK_NAMES.splitTest.id].includes(category) && (
{unitCategory === COURSE_BLOCK_NAMES.libraryContent.id && (
<Button
iconBefore={EditIcon}
variant="outline-primary"
onClick={handleEdit}
data-testid="header-edit-button"
>
{intl.formatMessage(messages.editButton)}
</Button>
@@ -48,7 +47,7 @@ HeaderNavigations.propTypes = {
handlePreview: PropTypes.func.isRequired,
handleEdit: PropTypes.func.isRequired,
}).isRequired,
category: PropTypes.string.isRequired,
unitCategory: PropTypes.string.isRequired,
};
export default HeaderNavigations;

View File

@@ -18,7 +18,6 @@ const headerNavigationsActions = {
const renderComponent = (props) => render(
<IntlProvider locale="en">
<HeaderNavigations
category={COURSE_BLOCK_NAMES.vertical.id}
headerNavigationsActions={headerNavigationsActions}
{...props}
/>
@@ -48,17 +47,17 @@ describe('<HeaderNavigations />', () => {
expect(editButton).not.toBeInTheDocument();
});
['libraryContent', 'splitTest'].forEach((category) => {
it(`calls the correct handlers when clicking buttons for ${category} page`, () => {
const { getByRole, queryByRole } = renderComponent({ category: COURSE_BLOCK_NAMES[category].id });
it('calls the correct handlers when clicking buttons for library page', () => {
const { getByRole, queryByRole } = renderComponent({ unitCategory: COURSE_BLOCK_NAMES.libraryContent.id });
const editButton = getByRole('button', { name: messages.editButton.defaultMessage });
fireEvent.click(editButton);
expect(handleViewLiveFn).toHaveBeenCalledTimes(1);
const editButton = getByRole('button', { name: messages.editButton.defaultMessage });
fireEvent.click(editButton);
expect(handleViewLiveFn).toHaveBeenCalledTimes(1);
[messages.viewLiveButton.defaultMessage, messages.previewButton.defaultMessage].forEach((btnName) => {
expect(queryByRole('button', { name: btnName })).not.toBeInTheDocument();
});
});
const viewLiveButton = queryByRole('button', { name: messages.viewLiveButton.defaultMessage });
expect(viewLiveButton).not.toBeInTheDocument();
const previewButton = queryByRole('button', { name: messages.previewButton.defaultMessage });
expect(previewButton).not.toBeInTheDocument();
});
});

View File

@@ -26,13 +26,7 @@ const HeaderTitle = ({
const [titleValue, setTitleValue] = useState(unitTitle);
const currentItemData = useSelector(getCourseUnitData);
const [isConfigureModalOpen, openConfigureModal, closeConfigureModal] = useToggle(false);
const { selectedPartitionIndex, selectedGroupsLabel } = currentItemData.userPartitionInfo ?? {};
const isXBlockComponent = [
COURSE_BLOCK_NAMES.libraryContent.id,
COURSE_BLOCK_NAMES.splitTest.id,
COURSE_BLOCK_NAMES.component.id,
].includes(currentItemData.category);
const { selectedPartitionIndex, selectedGroupsLabel } = currentItemData.userPartitionInfo;
const onConfigureSubmit = (...arg) => {
handleConfigureSubmit(currentItemData.id, ...arg, closeConfigureModal);
@@ -93,8 +87,9 @@ const HeaderTitle = ({
onConfigureSubmit={onConfigureSubmit}
currentItemData={currentItemData}
isSelfPaced={false}
isXBlockComponent={isXBlockComponent}
userPartitionInfo={currentItemData?.userPartitionInfo || {}}
isXBlockComponent={
[COURSE_BLOCK_NAMES.libraryContent.id, COURSE_BLOCK_NAMES.component.id].includes(currentItemData.category)
}
/>
</div>
{getVisibilityMessage()}

View File

@@ -4,12 +4,11 @@ import {
import { useDispatch, useSelector } from 'react-redux';
import { useNavigate, useSearchParams } from 'react-router-dom';
import { useToggle } from '@openedx/paragon';
import { camelCaseObject } from '@edx/frontend-platform/utils';
import { RequestStatus } from '../data/constants';
import { useClipboard } from '../generic/clipboard';
import { useCopyToClipboard } from '../generic/clipboard';
import { useEventListener } from '../generic/hooks';
import { COURSE_BLOCK_NAMES, iframeMessageTypes } from '../constants';
import { COURSE_BLOCK_NAMES } from '../constants';
import { messageTypes, PUBLISH_TYPES } from './constants';
import {
createNewCourseXBlock,
@@ -41,13 +40,12 @@ import {
updateMovedXBlockParams,
updateQueryPendingStatus,
} from './data/slice';
import { useIframe } from '../generic/hooks/context/hooks';
import { useIframe } from './context/hooks';
export const useCourseUnit = ({ courseId, blockId }) => {
const dispatch = useDispatch();
const [searchParams] = useSearchParams();
const { sendMessageToIframe } = useIframe();
const [addComponentTemplateData, setAddComponentTemplateData] = useState({});
const [isMoveModalOpen, openMoveModal, closeMoveModal] = useToggle(false);
const courseUnit = useSelector(getCourseUnitData);
@@ -64,13 +62,12 @@ export const useCourseUnit = ({ courseId, blockId }) => {
const courseOutlineInfo = useSelector(getCourseOutlineInfo);
const movedXBlockParams = useSelector(getMovedXBlockParams);
const { currentlyVisibleToStudents } = courseUnit;
const { sharedClipboardData, showPasteXBlock, showPasteUnit } = useClipboard(canEdit);
const { sharedClipboardData, showPasteXBlock, showPasteUnit } = useCopyToClipboard(canEdit);
const { canPasteComponent } = courseVerticalChildren;
const { displayName: unitTitle, category: unitCategory } = xblockInfo;
const sequenceId = courseUnit.ancestorInfo?.ancestors[0].id;
const isUnitVerticalType = unitCategory === COURSE_BLOCK_NAMES.vertical.id;
const isUnitLibraryType = unitCategory === COURSE_BLOCK_NAMES.libraryContent.id;
const isSplitTestType = unitCategory === COURSE_BLOCK_NAMES.splitTest.id;
const headerNavigationsActions = {
handleViewLive: () => {
@@ -79,9 +76,7 @@ export const useCourseUnit = ({ courseId, blockId }) => {
handlePreview: () => {
window.open(draftPreviewLink, '_blank');
},
handleEdit: () => {
sendMessageToIframe(messageTypes.editXBlock, { id: courseUnit.id }, window);
},
handleEdit: () => {},
};
const handleTitleEdit = () => {
@@ -175,16 +170,6 @@ export const useCourseUnit = ({ courseId, blockId }) => {
const { usageId } = payload;
navigate(`/course/${courseId}/container/${usageId}/${sequenceId}`);
}
if (type === messageTypes.handleViewGroupConfigurations) {
const { usageId } = payload;
const groupId = usageId.split('#').pop();
navigate(`/course/${courseId}/group_configurations#${groupId}`);
}
if (type === messageTypes.showComponentTemplates) {
setAddComponentTemplateData(camelCaseObject(payload));
}
}, [courseId, sequenceId]);
useEventListener('message', receiveMessage);
@@ -198,17 +183,12 @@ export const useCourseUnit = ({ courseId, blockId }) => {
useEffect(() => {
dispatch(fetchCourseUnitQuery(blockId));
dispatch(fetchCourseSectionVerticalData(blockId, sequenceId));
dispatch(fetchCourseVerticalChildrenData(blockId, isSplitTestType));
dispatch(fetchCourseVerticalChildrenData(blockId));
handleNavigate(sequenceId);
dispatch(updateMovedXBlockParams({ isSuccess: false }));
}, [courseId, blockId, sequenceId]);
useEffect(() => {
if (isSplitTestType) {
dispatch(fetchCourseVerticalChildrenData(blockId, isSplitTestType));
}
}, [isSplitTestType, blockId]);
useEffect(() => {
if (isMoveModalOpen && !Object.keys(courseOutlineInfo).length) {
dispatch(getCourseOutlineInfoQuery(courseId));
@@ -229,7 +209,6 @@ export const useCourseUnit = ({ courseId, blockId }) => {
isTitleEditFormOpen,
isUnitVerticalType,
isUnitLibraryType,
isSplitTestType,
sharedClipboardData,
showPasteXBlock,
showPasteUnit,
@@ -248,8 +227,6 @@ export const useCourseUnit = ({ courseId, blockId }) => {
handleCloseXBlockMovedAlert,
movedXBlockParams,
handleNavigateToTargetUnit,
addComponentTemplateData,
setAddComponentTemplateData,
};
};
@@ -313,7 +290,7 @@ export const useScrollToLastPosition = (storageKey = 'createXBlockLastYPosition'
}, [storageKey]);
const handleMessage = useCallback((event) => {
if (event.data?.type === iframeMessageTypes.resize) {
if (event.data?.type === messageTypes.resize) {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}

View File

@@ -1,7 +1,7 @@
import React from 'react';
import { act, renderHook } from '@testing-library/react';
import { renderHook, act } from '@testing-library/react-hooks';
import { useScrollToLastPosition, useLayoutGrid } from './hooks';
import { iframeMessageTypes } from '../constants';
import { messageTypes } from './constants';
jest.useFakeTimers();
@@ -108,7 +108,7 @@ describe('useScrollToLastPosition', () => {
const { unmount } = renderHook(() => useScrollToLastPosition(storageKey));
act(() => {
window.dispatchEvent(new MessageEvent('message', { data: { type: iframeMessageTypes.resize } }));
window.dispatchEvent(new MessageEvent('message', { data: { type: messageTypes.resize } }));
jest.advanceTimersByTime(1000);
});
@@ -136,8 +136,8 @@ describe('useScrollToLastPosition', () => {
renderHook(() => useScrollToLastPosition(storageKey));
act(() => {
window.dispatchEvent(new MessageEvent('message', { data: { type: iframeMessageTypes.resize } }));
window.dispatchEvent(new MessageEvent('message', { data: { type: iframeMessageTypes.resize } }));
window.dispatchEvent(new MessageEvent('message', { data: { type: messageTypes.resize } }));
window.dispatchEvent(new MessageEvent('message', { data: { type: messageTypes.resize } }));
});
expect(clearTimeoutSpy).toHaveBeenCalled();
@@ -150,9 +150,9 @@ describe('useScrollToLastPosition', () => {
renderHook(() => useScrollToLastPosition(storageKey));
act(() => {
window.dispatchEvent(new MessageEvent('message', { data: { type: iframeMessageTypes.resize } }));
window.dispatchEvent(new MessageEvent('message', { data: { type: messageTypes.resize } }));
jest.advanceTimersByTime(500);
window.dispatchEvent(new MessageEvent('message', { data: { type: iframeMessageTypes.resize } }));
window.dispatchEvent(new MessageEvent('message', { data: { type: messageTypes.resize } }));
});
expect(window.scrollTo).not.toHaveBeenCalled();
@@ -164,7 +164,7 @@ describe('useScrollToLastPosition', () => {
renderHook(() => useScrollToLastPosition(storageKey));
act(() => {
window.dispatchEvent(new MessageEvent('message', { data: { type: iframeMessageTypes.resize } }));
window.dispatchEvent(new MessageEvent('message', { data: { type: messageTypes.resize } }));
jest.advanceTimersByTime(1000);
});

View File

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

View File

@@ -11,7 +11,7 @@ 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 '../../generic/hooks/context/hooks';
import { useIframe } from '../context/hooks';
import { messageTypes } from '../constants';
import { CATEGORIES, MOVE_DIRECTIONS } from './constants';
import {

View File

@@ -105,7 +105,6 @@ const MoveModal: FC<IUseMoveModalParams> = ({
title={intl.formatMessage(messages.moveModalTitle, { displayName })}
hasCloseButton
isFullscreenOnMobile
isOverflowVisible
>
<ModalDialog.Header>
<ModalDialog.Title>

View File

@@ -11,7 +11,7 @@ import { getCourseOutlineInfoUrl } from '../data/api';
import { courseOutlineInfoMock } from '../__mocks__';
import { executeThunk } from '../../utils';
import { getCourseOutlineInfoQuery } from '../data/thunk';
import { IframeProvider } from '../../generic/hooks/context/iFrameContext';
import { IframeProvider } from '../context/iFrameContext';
import { IXBlock } from './interfaces';
import MoveModal from './index';
import messages from './messages';

View File

@@ -8,8 +8,9 @@ import {
waitFor,
} from '../../testUtils';
import IframePreviewLibraryXBlockChanges, { LibraryChangesMessageData } from '.';
import PreviewLibraryXBlockChanges, { LibraryChangesMessageData } from '.';
import { messageTypes } from '../constants';
import { IframeProvider } from '../context/iFrameContext';
import { libraryBlockChangesUrl } from '../data/api';
import { ToastActionData } from '../../generic/toast-context';
import { getLibraryBlockMetadataUrl } from '../../library-authoring/data/api';
@@ -24,15 +25,15 @@ const defaultEventData: LibraryChangesMessageData = {
};
const mockSendMessageToIframe = jest.fn();
jest.mock('../../generic/hooks/context/hooks', () => ({
jest.mock('../context/hooks', () => ({
useIframe: () => ({
iframeRef: { current: { contentWindow: {} as HTMLIFrameElement } },
setIframeRef: () => {},
sendMessageToIframe: mockSendMessageToIframe,
}),
}));
const render = (eventData?: LibraryChangesMessageData) => {
baseRender(<IframePreviewLibraryXBlockChanges />);
baseRender(<PreviewLibraryXBlockChanges />, {
extraWrapper: ({ children }) => <IframeProvider>{ children }</IframeProvider>,
});
const message = {
data: {
type: messageTypes.showXBlockLibraryChangesPreview,
@@ -48,7 +49,7 @@ const render = (eventData?: LibraryChangesMessageData) => {
let axiosMock: MockAdapter;
let mockShowToast: (message: string, action?: ToastActionData | undefined) => void;
describe('<IframePreviewLibraryXBlockChanges />', () => {
describe('<PreviewLibraryXBlockChanges />', () => {
beforeEach(() => {
const mocks = initializeMocks();
axiosMock = mocks.axiosMock;

View File

@@ -1,4 +1,4 @@
import React, { useCallback, useContext, useState } from 'react';
import { useCallback, useContext, useState } from 'react';
import {
ActionRow, Button, ModalDialog, useToggle,
} from '@openedx/paragon';
@@ -8,7 +8,7 @@ import { useEventListener } from '../../generic/hooks';
import { messageTypes } from '../constants';
import CompareChangesWidget from '../../library-authoring/component-comparison/CompareChangesWidget';
import { useAcceptLibraryBlockChanges, useIgnoreLibraryBlockChanges } from '../data/apiHooks';
import { useIframe } from '../../generic/hooks/context/hooks';
import { useIframe } from '../context/hooks';
import DeleteModal from '../../generic/delete-modal/DeleteModal';
import messages from './messages';
import { ToastContext } from '../../generic/toast-context';
@@ -24,34 +24,36 @@ export interface LibraryChangesMessageData {
isVertical: boolean,
}
export interface PreviewLibraryXBlockChangesProps {
blockData?: LibraryChangesMessageData,
isModalOpen: boolean,
closeModal: () => void,
postChange: (accept: boolean) => void,
alertNode?: React.ReactNode,
}
/**
* Component to preview two xblock versions in a modal that depends on params
* to display blocks, open-close modal, accept-ignore changes and post change triggers
*/
export const PreviewLibraryXBlockChanges = ({
blockData,
isModalOpen,
closeModal,
postChange,
alertNode,
}: PreviewLibraryXBlockChangesProps) => {
const PreviewLibraryXBlockChanges = () => {
const { showToast } = useContext(ToastContext);
const intl = useIntl();
const [blockData, setBlockData] = useState<LibraryChangesMessageData | undefined>(undefined);
// Main preview library modal toggle.
const [isModalOpen, openModal, closeModal] = useToggle(false);
// ignore changes confirmation modal toggle.
const [isConfirmModalOpen, openConfirmModal, closeConfirmModal] = useToggle(false);
const { data: componentMetadata } = useLibraryBlockMetadata(blockData?.upstreamBlockId);
const acceptChangesMutation = useAcceptLibraryBlockChanges();
const ignoreChangesMutation = useIgnoreLibraryBlockChanges();
const { data: componentMetadata } = useLibraryBlockMetadata(blockData?.upstreamBlockId);
const { sendMessageToIframe } = useIframe();
const receiveMessage = useCallback(({ data }: { data: {
payload: LibraryChangesMessageData;
type: string;
} }) => {
const { payload, type } = data;
if (type === messageTypes.showXBlockLibraryChangesPreview) {
setBlockData(payload);
openModal();
}
}, [openModal]);
useEventListener('message', receiveMessage);
const getTitle = useCallback(() => {
const oldName = blockData?.displayName;
@@ -93,7 +95,7 @@ export const PreviewLibraryXBlockChanges = ({
try {
await mutation.mutateAsync(blockData.downstreamBlockId);
postChange(accept);
sendMessageToIframe(messageTypes.refreshXBlock, null);
} catch (e) {
showToast(intl.formatMessage(failureMsg));
} finally {
@@ -110,7 +112,6 @@ export const PreviewLibraryXBlockChanges = ({
className="lib-preview-xblock-changes-modal"
hasCloseButton
isFullscreenOnMobile
isOverflowVisible={false}
>
<ModalDialog.Header>
<ModalDialog.Title>
@@ -118,7 +119,6 @@ export const PreviewLibraryXBlockChanges = ({
</ModalDialog.Title>
</ModalDialog.Header>
<ModalDialog.Body>
{alertNode}
{getBody()}
</ModalDialog.Body>
<ModalDialog.Footer>
@@ -151,42 +151,4 @@ export const PreviewLibraryXBlockChanges = ({
);
};
/**
* Wrapper over PreviewLibraryXBlockChanges to preview two xblock versions in a modal
* that depends on iframe message events to setBlockData and display modal.
*/
const IframePreviewLibraryXBlockChanges = () => {
const [blockData, setBlockData] = useState<LibraryChangesMessageData | undefined>(undefined);
// Main preview library modal toggle.
const [isModalOpen, openModal, closeModal] = useToggle(false);
const { sendMessageToIframe } = useIframe();
const receiveMessage = useCallback(({ data }: {
data: {
payload: LibraryChangesMessageData;
type: string;
}
}) => {
const { payload, type } = data;
if (type === messageTypes.showXBlockLibraryChangesPreview) {
setBlockData(payload);
openModal();
}
}, [openModal]);
useEventListener('message', receiveMessage);
return (
<PreviewLibraryXBlockChanges
blockData={blockData}
isModalOpen={isModalOpen}
closeModal={closeModal}
postChange={() => sendMessageToIframe(messageTypes.refreshXBlock, null)}
/>
);
};
export default IframePreviewLibraryXBlockChanges;
export default PreviewLibraryXBlockChanges;

View File

@@ -4,7 +4,7 @@ import { useToggle } from '@openedx/paragon';
import { InfoOutline as InfoOutlineIcon } from '@openedx/paragon/icons';
import { useIntl } from '@edx/frontend-platform/i18n';
import useCourseUnitData from './hooks';
import { useIframe } from '../../generic/hooks/context/hooks';
import { useIframe } from '../context/hooks';
import { editCourseUnitVisibilityAndData } from '../data/thunk';
import { SidebarBody, SidebarFooter, SidebarHeader } from './components';
import { PUBLISH_TYPES, messageTypes } from '../constants';

View File

@@ -81,19 +81,4 @@
text-decoration: line-through;
}
}
.course-split-test-sidebar {
padding: $spacer;
@extend %base-font-params;
.course-split-test-sidebar-title {
font-size: $font-size-base;
line-height: $line-height-base;
}
.course-split-test-sidebar-devider {
width: 100%;
}
}
}

View File

@@ -1,58 +0,0 @@
import React from 'react';
import { Card, Hyperlink, Stack } from '@openedx/paragon';
import { useIntl } from '@edx/frontend-platform/i18n';
import messages from './messages';
const SplitTestSidebarInfo = () => {
const intl = useIntl();
const boldTagWrapper = (chunks: React.ReactNode) => <strong>{chunks}</strong>;
return (
<Card.Body className="course-split-test-sidebar">
<Stack>
<h3 className="course-split-test-sidebar-title">
{intl.formatMessage(messages.sidebarSplitTestAddComponentTitle)}
</h3>
<p>
{intl.formatMessage(messages.sidebarSplitTestSelectComponentType, { bold_tag: boldTagWrapper })}
</p>
<p>
{intl.formatMessage(messages.sidebarSplitTestComponentAdded)}
</p>
<h3 className="course-split-test-sidebar-title">
{intl.formatMessage(messages.sidebarSplitTestEditComponentTitle)}
</h3>
<p>
{intl.formatMessage(messages.sidebarSplitTestEditComponentInstruction, { bold_tag: boldTagWrapper })}
</p>
<h3 className="course-split-test-sidebar-title">
{intl.formatMessage(messages.sidebarSplitTestReorganizeComponentTitle)}
</h3>
<p>
{intl.formatMessage(messages.sidebarSplitTestReorganizeComponentInstruction)}
</p>
<p>
{intl.formatMessage(messages.sidebarSplitTestReorganizeGroupsInstruction)}
</p>
<h3 className="course-split-test-sidebar-title">
{intl.formatMessage(messages.sidebarSplitTestExperimentComponentTitle)}
</h3>
<p className="mb-0">
{intl.formatMessage(messages.sidebarSplitTestExperimentComponentInstruction)}
</p>
<hr className="course-split-test-sidebar-devider my-4" />
<Hyperlink
showLaunchIcon={false}
destination="https://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/latest/developing_course/course_components.html#components-that-contain-other-components"
className="btn btn-outline-primary btn-sm"
target="_blank"
>
{intl.formatMessage(messages.sidebarSplitTestLearnMoreLinkLabel)}
</Hyperlink>
</Stack>
</Card.Body>
);
};
export default SplitTestSidebarInfo;

View File

@@ -1,14 +1,15 @@
import PropTypes from 'prop-types';
import { useSelector } from 'react-redux';
import { useDispatch, useSelector } from 'react-redux';
import { Button } from '@openedx/paragon';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Divider } from '../../../../generic/divider';
import { getCanEdit, getCourseUnitData } from '../../../data/selectors';
import { useClipboard } from '../../../../generic/clipboard';
import { copyToClipboard } from '../../../../generic/data/thunks';
import messages from '../../messages';
const ActionButtons = ({ openDiscardModal, handlePublishing }) => {
const dispatch = useDispatch();
const intl = useIntl();
const {
id,
@@ -17,7 +18,6 @@ const ActionButtons = ({ openDiscardModal, handlePublishing }) => {
enableCopyPasteUnits,
} = useSelector(getCourseUnitData);
const canEdit = useSelector(getCanEdit);
const { copyToClipboard } = useClipboard();
return (
<>
@@ -40,7 +40,7 @@ const ActionButtons = ({ openDiscardModal, handlePublishing }) => {
<>
<Divider className="course-unit-sidebar-footer__divider" />
<Button
onClick={() => copyToClipboard(id)}
onClick={() => dispatch(copyToClipboard(id))}
variant="outline-primary"
size="sm"
>

View File

@@ -1,4 +1,3 @@
import { QueryClientProvider, QueryClient } from '@tanstack/react-query';
import { render } from '@testing-library/react';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import { AppProvider } from '@edx/frontend-platform/react';
@@ -13,21 +12,24 @@ import { clipboardUnit } from '../../../../__mocks__';
import { getCourseUnitApiUrl } from '../../../data/api';
import { getClipboardUrl } from '../../../../generic/data/api';
import { fetchCourseUnitQuery } from '../../../data/thunk';
import { copyToClipboard } from '../../../../generic/data/thunks';
import { courseUnitIndexMock } from '../../../__mocks__';
import messages from '../../messages';
import ActionButtons from './ActionButtons';
jest.mock('../../../../generic/data/thunks', () => ({
...jest.requireActual('../../../../generic/data/thunks'),
copyToClipboard: jest.fn().mockImplementation(() => () => {}),
}));
let store;
let axiosMock;
let queryClient;
const courseId = '123';
const renderComponent = (props = {}) => render(
<AppProvider store={store}>
<IntlProvider locale="en">
<QueryClientProvider client={queryClient}>
<ActionButtons {...props} />
</QueryClientProvider>
<ActionButtons {...props} />
</IntlProvider>
</AppProvider>,
);
@@ -55,8 +57,6 @@ describe('<ActionButtons />', () => {
.onGet(getClipboardUrl())
.reply(200, clipboardUnit);
queryClient = new QueryClient();
await executeThunk(fetchCourseUnitQuery(courseId), store.dispatch);
});
@@ -73,8 +73,8 @@ describe('<ActionButtons />', () => {
const copyXBlockBtn = getByRole('button', { name: messages.actionButtonCopyUnitTitle.defaultMessage });
userEvent.click(copyXBlockBtn);
expect(axiosMock.history.post.length).toBe(1);
expect(axiosMock.history.post[0].data).toBe(JSON.stringify({ usage_key: courseUnitIndexMock.id }));
expect(copyToClipboard).toHaveBeenCalledWith(courseUnitIndexMock.id);
jest.resetAllMocks();
});
});

View File

@@ -0,0 +1,24 @@
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { Card } from '@openedx/paragon';
const Sidebar = ({ className, children, ...props }) => (
<Card
className={classNames('course-unit-sidebar', className)}
{...props}
>
{children}
</Card>
);
Sidebar.propTypes = {
className: PropTypes.string,
children: PropTypes.node,
};
Sidebar.defaultProps = {
className: null,
children: null,
};
export default Sidebar;

View File

@@ -1,18 +0,0 @@
import classNames from 'classnames';
import { Card } from '@openedx/paragon';
const Sidebar = ({ className = null, children = null, ...props }:SidebarProps) => (
<Card
className={classNames('course-unit-sidebar', className)}
{...props}
>
{children}
</Card>
);
interface SidebarProps {
className?: string | null;
children?: React.ReactNode | null;
}
export default Sidebar;

View File

@@ -137,61 +137,6 @@ const messages = defineMessages({
id: 'course-authoring.course-unit.modal.make-visibility.description',
defaultMessage: 'If the unit was previously published and released to students, any changes you made to the unit when it was hidden will now be visible to students. Do you want to proceed?',
},
sidebarSplitTestAddComponentTitle: {
id: 'course-authoring.course-unit.split-test.sidebar.add-component.title',
defaultMessage: 'Adding components',
description: 'Title for the section that explains how to add components to a split test',
},
sidebarSplitTestSelectComponentType: {
id: 'course-authoring.course-unit.split-test.sidebar.add-component.select-type',
defaultMessage: 'Select a component type under {bold_tag}Add New Component{bold_tag}. Then select a template.',
description: 'Instruction text for selecting a component type and template when adding new components',
},
sidebarSplitTestComponentAdded: {
id: 'course-authoring.course-unit.split-test.sidebar.add-component.component-added',
defaultMessage: 'The new component is added at the bottom of the page or group. You can then edit and move the component.',
description: 'Instruction text indicating that the component has been added and can be moved or edited',
},
sidebarSplitTestEditComponentTitle: {
id: 'course-authoring.course-unit.split-test.sidebar.edit-component.title',
defaultMessage: 'Editing components',
description: 'Title for the section that explains how to edit components in a split test',
},
sidebarSplitTestEditComponentInstruction: {
id: 'course-authoring.course-unit.split-test.sidebar.edit-component.instruction',
defaultMessage: 'Click the {bold_tag}Edit{bold_tag} icon in a component to edit its content.',
description: 'Instruction text for editing a component by clicking the edit icon',
},
sidebarSplitTestReorganizeComponentTitle: {
id: 'course-authoring.course-unit.split-test.sidebar.reorganize-component.title',
defaultMessage: 'Reorganizing components',
description: 'Title for the section that explains how to reorganize components within a split test',
},
sidebarSplitTestReorganizeComponentInstruction: {
id: 'course-authoring.course-unit.split-test.sidebar.reorganize-component.instruction',
defaultMessage: 'Drag components to new locations within this component.',
description: 'Instruction text for reorganizing components by dragging them to new locations within a split test',
},
sidebarSplitTestReorganizeGroupsInstruction: {
id: 'course-authoring.course-unit.split-test.sidebar.reorganize-component.drag-to-groups',
defaultMessage: 'For content experiments, you can drag components to other groups.',
description: 'Instruction text for dragging components to other groups for content experiments',
},
sidebarSplitTestExperimentComponentTitle: {
id: 'course-authoring.course-unit.split-test.sidebar.experiment-component.title',
defaultMessage: 'Working with content experiments',
description: 'Title for the section that explains how to work with content experiments',
},
sidebarSplitTestExperimentComponentInstruction: {
id: 'course-authoring.course-unit.split-test.sidebar.experiment-component.confirm-config',
defaultMessage: 'Confirm that you have properly configured content in each of your experiment groups.',
description: 'Instruction text reminding users to check content configuration in each experiment group',
},
sidebarSplitTestLearnMoreLinkLabel: {
id: 'course-authoring.course-unit.split-test.sidebar.learn-more-link.label',
defaultMessage: 'Learn more about component containers',
description: 'Text for a link that directs users to more information about component containers in the split test setup.',
},
});
export default messages;

View File

@@ -1 +1,5 @@
export { useIframeMessages } from './useIframeMessages';
export { useIframeContent } from './useIframeContent';
export { useMessageHandlers } from './useMessageHandlers';
export { useIFrameBehavior } from './useIFrameBehavior';
export { useLoadBearingHook } from './useLoadBearingHook';

Some files were not shown because too many files have changed in this diff Show More