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
565 changed files with 8382 additions and 24533 deletions

3
.env
View File

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

View File

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

View File

@@ -39,5 +39,5 @@ INVITE_STUDENTS_EMAIL_TO="someone@domain.com"
ENABLE_HOME_PAGE_COURSE_API_V2=true ENABLE_HOME_PAGE_COURSE_API_V2=true
ENABLE_CHECKLIST_QUALITY=true ENABLE_CHECKLIST_QUALITY=true
ENABLE_GRADING_METHOD_IN_PROBLEMS=false ENABLE_GRADING_METHOD_IN_PROBLEMS=false
# "Multi-level" blocks are unsupported in libraries # "other" is used to test the workflow for creating blocks that aren't supported by the built-in editors
LIBRARY_UNSUPPORTED_BLOCKS="conditional,step-builder,problem-builder" 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 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. 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 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 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: If your devstack includes the default Demo course, you can visit the following URLs to see content:

View File

@@ -10,5 +10,4 @@ coverage:
threshold: 0% threshold: 0%
ignore: ignore:
- "src/grading-settings/grading-scale/react-ranger.js" - "src/grading-settings/grading-scale/react-ranger.js"
- "src/generic/DraggableList/verticalSortableList.ts"
- "src/index.js" - "src/index.js"

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

View File

@@ -1,6 +1,6 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { getConfig } from '@edx/frontend-platform'; import { getConfig } from '@edx/frontend-platform';
import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n'; import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { Form, Hyperlink } from '@openedx/paragon'; import { Form, Hyperlink } from '@openedx/paragon';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import AppConfigFormDivider from 'CourseAuthoring/pages-and-resources/discussions/app-config-form/apps/shared/AppConfigFormDivider'; import AppConfigFormDivider from 'CourseAuthoring/pages-and-resources/discussions/app-config-form/apps/shared/AppConfigFormDivider';
@@ -11,10 +11,10 @@ import LiveCommonFields from './LiveCommonFields';
import messages from './messages'; import messages from './messages';
const BbbSettings = ({ const BbbSettings = ({
intl,
values, values,
setFieldValue, setFieldValue,
}) => { }) => {
const intl = useIntl();
const [bbbPlan, setBbbPlan] = useState(values.tierType); const [bbbPlan, setBbbPlan] = useState(values.tierType);
useEffect(() => { useEffect(() => {
@@ -107,10 +107,12 @@ const BbbSettings = ({
)} )}
</> </>
</> </>
); );
}; };
BbbSettings.propTypes = { BbbSettings.propTypes = {
intl: intlShape.isRequired,
values: PropTypes.shape({ values: PropTypes.shape({
consumerKey: PropTypes.string, consumerKey: PropTypes.string,
consumerSecret: PropTypes.string, consumerSecret: PropTypes.string,
@@ -125,4 +127,4 @@ BbbSettings.propTypes = {
setFieldValue: PropTypes.func.isRequired, setFieldValue: PropTypes.func.isRequired,
}; };
export default BbbSettings; export default injectIntl(BbbSettings);

View File

@@ -1,43 +1,42 @@
import React from 'react'; import React from 'react';
import { useIntl } from '@edx/frontend-platform/i18n'; import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import FormikControl from 'CourseAuthoring/generic/FormikControl'; import FormikControl from 'CourseAuthoring/generic/FormikControl';
import messages from './messages'; import messages from './messages';
const LiveCommonFields = ({ const LiveCommonFields = ({
intl,
values, values,
}) => { }) => (
const intl = useIntl(); <>
return ( <p className="pb-2">{intl.formatMessage(messages.formInstructions)}</p>
<> <FormikControl
<p className="pb-2">{intl.formatMessage(messages.formInstructions)}</p> name="consumerKey"
<FormikControl value={values.consumerKey}
name="consumerKey" floatingLabel={intl.formatMessage(messages.consumerKey)}
value={values.consumerKey} className="pb-1"
floatingLabel={intl.formatMessage(messages.consumerKey)} type="input"
className="pb-1" />
type="input" <FormikControl
/> name="consumerSecret"
<FormikControl value={values.consumerSecret}
name="consumerSecret" floatingLabel={intl.formatMessage(messages.consumerSecret)}
value={values.consumerSecret} className="pb-1"
floatingLabel={intl.formatMessage(messages.consumerSecret)} type="password"
className="pb-1" />
type="password" <FormikControl
/> name="launchUrl"
<FormikControl value={values.launchUrl}
name="launchUrl" floatingLabel={intl.formatMessage(messages.launchUrl)}
value={values.launchUrl} className="pb-1"
floatingLabel={intl.formatMessage(messages.launchUrl)} type="input"
className="pb-1" />
type="input" </>
/> );
</>
);
};
LiveCommonFields.propTypes = { LiveCommonFields.propTypes = {
intl: intlShape.isRequired,
values: PropTypes.shape({ values: PropTypes.shape({
consumerKey: PropTypes.string, consumerKey: PropTypes.string,
consumerSecret: PropTypes.string, consumerSecret: PropTypes.string,
@@ -46,4 +45,4 @@ LiveCommonFields.propTypes = {
}).isRequired, }).isRequired,
}; };
export default LiveCommonFields; export default injectIntl(LiveCommonFields);

View File

@@ -2,7 +2,7 @@ import React, { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import { camelCase } from 'lodash'; import { camelCase } from 'lodash';
import { Icon } from '@openedx/paragon'; import { Icon } from '@openedx/paragon';
import { useIntl } from '@edx/frontend-platform/i18n'; import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import * as Yup from 'yup'; import * as Yup from 'yup';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
@@ -20,9 +20,9 @@ import ZoomSettings from './ZoomSettings';
import BBBSettings from './BBBSettings'; import BBBSettings from './BBBSettings';
const LiveSettings = ({ const LiveSettings = ({
intl,
onClose, onClose,
}) => { }) => {
const intl = useIntl();
const navigate = useNavigate(); const navigate = useNavigate();
const dispatch = useDispatch(); const dispatch = useDispatch();
const courseId = useSelector(state => state.courseDetail.courseId); const courseId = useSelector(state => state.courseDetail.courseId);
@@ -130,7 +130,8 @@ const LiveSettings = ({
}; };
LiveSettings.propTypes = { LiveSettings.propTypes = {
intl: intlShape.isRequired,
onClose: PropTypes.func.isRequired, onClose: PropTypes.func.isRequired,
}; };
export default LiveSettings; export default injectIntl(LiveSettings);

View File

@@ -1,5 +1,5 @@
import React from 'react'; import React from 'react';
import { useIntl } from '@edx/frontend-platform/i18n'; import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import FormikControl from 'CourseAuthoring/generic/FormikControl'; import FormikControl from 'CourseAuthoring/generic/FormikControl';
@@ -8,38 +8,37 @@ import { providerNames } from './constants';
import LiveCommonFields from './LiveCommonFields'; import LiveCommonFields from './LiveCommonFields';
const ZoomSettings = ({ const ZoomSettings = ({
intl,
values, values,
}) => { }) => (
const intl = useIntl(); // eslint-disable-next-line react/jsx-no-useless-fragment
return ( <>
// eslint-disable-next-line react/jsx-no-useless-fragment {!values.piiSharingEnable ? (
<> <p data-testid="request-pii-sharing">
{!values.piiSharingEnable ? ( {intl.formatMessage(messages.requestPiiSharingEnable, { provider: providerNames[values.provider] })}
<p data-testid="request-pii-sharing"> </p>
{intl.formatMessage(messages.requestPiiSharingEnable, { provider: providerNames[values.provider] })} ) : (
</p> <>
) : ( {(values.piiSharingEmail || values.piiSharingUsername)
<> && (
{(values.piiSharingEmail || values.piiSharingUsername) <p data-testid="helper-text">
&& ( {intl.formatMessage(messages.providerHelperText, { providerName: providerNames[values.provider] })}
<p data-testid="helper-text"> </p>
{intl.formatMessage(messages.providerHelperText, { providerName: providerNames[values.provider] })} )}
</p> <LiveCommonFields values={values} />
)} <FormikControl
<LiveCommonFields values={values} /> name="launchEmail"
<FormikControl value={values.launchEmail}
name="launchEmail" floatingLabel={intl.formatMessage(messages.launchEmail)}
value={values.launchEmail} type="input"
floatingLabel={intl.formatMessage(messages.launchEmail)} />
type="input" </>
/> )}
</> </>
)} );
</>
);
};
ZoomSettings.propTypes = { ZoomSettings.propTypes = {
intl: intlShape.isRequired,
values: PropTypes.shape({ values: PropTypes.shape({
consumerKey: PropTypes.string, consumerKey: PropTypes.string,
consumerSecret: PropTypes.string, consumerSecret: PropTypes.string,
@@ -52,4 +51,4 @@ ZoomSettings.propTypes = {
}).isRequired, }).isRequired,
}; };
export default ZoomSettings; export default injectIntl(ZoomSettings);

View File

@@ -142,7 +142,7 @@ describe('ORASettings', () => {
renderComponent(); renderComponent();
await mockStore({ apiStatus: 200, enabled: false }); 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'); const enableBadge = screen.queryByTestId('enable-badge');
expect(label).toBeVisible(); expect(label).toBeVisible();

View File

@@ -8,7 +8,7 @@ import PropTypes from 'prop-types';
import { getConfig } from '@edx/frontend-platform'; import { getConfig } from '@edx/frontend-platform';
import { getAuthenticatedUser } from '@edx/frontend-platform/auth'; import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
import { useIntl, FormattedMessage } from '@edx/frontend-platform/i18n'; import { injectIntl, intlShape, FormattedMessage } from '@edx/frontend-platform/i18n';
import { import {
ActionRow, Alert, Badge, Form, Hyperlink, ModalDialog, StatefulButton, ActionRow, Alert, Badge, Form, Hyperlink, ModalDialog, StatefulButton,
} from '@openedx/paragon'; } from '@openedx/paragon';
@@ -25,8 +25,7 @@ import { PagesAndResourcesContext } from 'CourseAuthoring/pages-and-resources/Pa
import messages from './messages'; import messages from './messages';
const ProctoringSettings = ({ onClose }) => { const ProctoringSettings = ({ intl, onClose }) => {
const intl = useIntl();
const initialFormValues = { const initialFormValues = {
enableProctoredExams: false, enableProctoredExams: false,
proctoringProvider: false, proctoringProvider: false,
@@ -653,9 +652,10 @@ const ProctoringSettings = ({ onClose }) => {
}; };
ProctoringSettings.propTypes = { ProctoringSettings.propTypes = {
intl: intlShape.isRequired,
onClose: PropTypes.func.isRequired, onClose: PropTypes.func.isRequired,
}; };
ProctoringSettings.defaultProps = {}; ProctoringSettings.defaultProps = {};
export default ProctoringSettings; export default injectIntl(ProctoringSettings);

View File

@@ -544,9 +544,12 @@ describe('ProctoredExamSettings', () => {
describe('Connection states', () => { describe('Connection states', () => {
it('Shows the spinner before the connection is complete', async () => { it('Shows the spinner before the connection is complete', async () => {
render(intlWrapper(<IntlProctoredExamSettings {...defaultProps} />)); await act(async () => {
const spinner = await screen.findByRole('status'); render(intlWrapper(<IntlProctoredExamSettings {...defaultProps} />));
expect(spinner.textContent).toEqual('Loading...'); // 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 () => { it('Show connection error message when we suffer studio server side error', async () => {

View File

@@ -1,4 +1,4 @@
import { useIntl } from '@edx/frontend-platform/i18n'; import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import * as Yup from 'yup'; import * as Yup from 'yup';
@@ -8,8 +8,7 @@ import { useAppSetting } from 'CourseAuthoring/utils';
import AppSettingsModal from 'CourseAuthoring/pages-and-resources/app-settings-modal/AppSettingsModal'; import AppSettingsModal from 'CourseAuthoring/pages-and-resources/app-settings-modal/AppSettingsModal';
import messages from './messages'; import messages from './messages';
const ProgressSettings = ({ onClose }) => { const ProgressSettings = ({ intl, onClose }) => {
const intl = useIntl();
const [disableProgressGraph, saveSetting] = useAppSetting('disableProgressGraph'); const [disableProgressGraph, saveSetting] = useAppSetting('disableProgressGraph');
const showProgressGraphSetting = getConfig().ENABLE_PROGRESS_GRAPH_SETTINGS.toString().toLowerCase() === 'true'; const showProgressGraphSetting = getConfig().ENABLE_PROGRESS_GRAPH_SETTINGS.toString().toLowerCase() === 'true';
@@ -49,7 +48,8 @@ const ProgressSettings = ({ onClose }) => {
}; };
ProgressSettings.propTypes = { ProgressSettings.propTypes = {
intl: intlShape.isRequired,
onClose: PropTypes.func.isRequired, onClose: PropTypes.func.isRequired,
}; };
export default ProgressSettings; export default injectIntl(ProgressSettings);

View File

@@ -1,4 +1,4 @@
import { useIntl } from '@edx/frontend-platform/i18n'; import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { Button, Form, TransitionReplace } from '@openedx/paragon'; import { Button, Form, TransitionReplace } from '@openedx/paragon';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React, { useState } from 'react'; import React, { useState } from 'react';
@@ -30,9 +30,8 @@ const TeamTypeNameMessage = {
}; };
const GroupEditor = ({ const GroupEditor = ({
group, onDelete, onChange, onBlur, fieldNameCommonBase, errors, intl, group, onDelete, onChange, onBlur, fieldNameCommonBase, errors,
}) => { }) => {
const intl = useIntl();
const [isDeleting, setDeleting] = useState(false); const [isDeleting, setDeleting] = useState(false);
const [isOpen, setOpen] = useState(group.id === null); const [isOpen, setOpen] = useState(group.id === null);
const initiateDeletion = () => setDeleting(true); const initiateDeletion = () => setDeleting(true);
@@ -150,6 +149,7 @@ export const groupShape = PropTypes.shape({
}); });
GroupEditor.propTypes = { GroupEditor.propTypes = {
intl: intlShape.isRequired,
fieldNameCommonBase: PropTypes.string.isRequired, fieldNameCommonBase: PropTypes.string.isRequired,
errors: PropTypes.shape({ errors: PropTypes.shape({
name: PropTypes.string, name: PropTypes.string,
@@ -170,4 +170,4 @@ GroupEditor.defaultProps = {
}, },
}; };
export default GroupEditor; export default injectIntl(GroupEditor);

View File

@@ -1,4 +1,4 @@
import { useIntl } from '@edx/frontend-platform/i18n'; import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { Button, Form } from '@openedx/paragon'; import { Button, Form } from '@openedx/paragon';
import { Add } from '@openedx/paragon/icons'; import { Add } from '@openedx/paragon/icons';
@@ -17,16 +17,15 @@ import messages from './messages';
setupYupExtensions(); setupYupExtensions();
const TeamSettings = ({ const TeamSettings = ({
intl,
onClose, onClose,
}) => { }) => {
const intl = useIntl();
const [teamsConfiguration, saveSettings] = useAppSetting('teamsConfiguration'); const [teamsConfiguration, saveSettings] = useAppSetting('teamsConfiguration');
const blankNewGroup = { const blankNewGroup = {
name: '', name: '',
description: '', description: '',
type: GroupTypes.OPEN, type: GroupTypes.OPEN,
maxTeamSize: null, maxTeamSize: null,
userPartitionId: null,
id: null, id: null,
key: uuid(), key: uuid(),
}; };
@@ -39,7 +38,6 @@ const TeamSettings = ({
type: group.type, type: group.type,
description: group.description, description: group.description,
max_team_size: group.maxTeamSize, max_team_size: group.maxTeamSize,
user_partition_id: group.userPartitionId,
})); }));
return saveSettings({ return saveSettings({
team_sets: groups, team_sets: groups,
@@ -166,7 +164,8 @@ const TeamSettings = ({
}; };
TeamSettings.propTypes = { TeamSettings.propTypes = {
intl: intlShape.isRequired,
onClose: PropTypes.func.isRequired, onClose: PropTypes.func.isRequired,
}; };
export default TeamSettings; export default injectIntl(TeamSettings);

View File

@@ -1,4 +1,4 @@
import { useIntl } from '@edx/frontend-platform/i18n'; import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import * as Yup from 'yup'; import * as Yup from 'yup';
@@ -8,8 +8,7 @@ import { useAppSetting } from 'CourseAuthoring/utils';
import AppSettingsModal from 'CourseAuthoring/pages-and-resources/app-settings-modal/AppSettingsModal'; import AppSettingsModal from 'CourseAuthoring/pages-and-resources/app-settings-modal/AppSettingsModal';
import messages from './messages'; import messages from './messages';
const WikiSettings = ({ onClose }) => { const WikiSettings = ({ intl, onClose }) => {
const intl = useIntl();
const [enablePublicWiki, saveSetting] = useAppSetting('allowPublicWikiAccess'); const [enablePublicWiki, saveSetting] = useAppSetting('allowPublicWikiAccess');
const handleSettingsSave = (values) => saveSetting(values.enablePublicWiki); const handleSettingsSave = (values) => saveSetting(values.enablePublicWiki);
@@ -33,7 +32,7 @@ const WikiSettings = ({ onClose }) => {
label={intl.formatMessage(messages.enablePublicWikiLabel)} label={intl.formatMessage(messages.enablePublicWikiLabel)}
helpText={intl.formatMessage(messages.enablePublicWikiHelp)} helpText={intl.formatMessage(messages.enablePublicWikiHelp)}
onChange={handleChange} onChange={handleChange}
onBlur={handleBlur} onBlue={handleBlur}
checked={values.enablePublicWiki} checked={values.enablePublicWiki}
/> />
) )
@@ -43,7 +42,8 @@ const WikiSettings = ({ onClose }) => {
}; };
WikiSettings.propTypes = { WikiSettings.propTypes = {
intl: intlShape.isRequired,
onClose: PropTypes.func.isRequired, onClose: PropTypes.func.isRequired,
}; };
export default WikiSettings; export default injectIntl(WikiSettings);

View File

@@ -1,6 +1,7 @@
import React, { useCallback, useContext, useEffect } from 'react'; import React, { useCallback, useContext, useEffect } from 'react';
import { useDispatch } from 'react-redux'; import { useDispatch } from 'react-redux';
import { useIntl } from '@edx/frontend-platform/i18n';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { PagesAndResourcesContext } from 'CourseAuthoring/pages-and-resources/PagesAndResourcesProvider'; import { PagesAndResourcesContext } from 'CourseAuthoring/pages-and-resources/PagesAndResourcesProvider';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
@@ -9,8 +10,7 @@ import messages from './messages';
import { fetchXpertSettings } from './data/thunks'; import { fetchXpertSettings } from './data/thunks';
const XpertUnitSummarySettings = () => { const XpertUnitSummarySettings = ({ intl }) => {
const intl = useIntl();
const { path: pagesAndResourcesPath, courseId } = useContext(PagesAndResourcesContext); const { path: pagesAndResourcesPath, courseId } = useContext(PagesAndResourcesContext);
const dispatch = useDispatch(); const dispatch = useDispatch();
const navigate = useNavigate(); const navigate = useNavigate();
@@ -38,4 +38,8 @@ const XpertUnitSummarySettings = () => {
); );
}; };
export default XpertUnitSummarySettings; XpertUnitSummarySettings.propTypes = {
intl: intlShape.isRequired,
};
export default injectIntl(XpertUnitSummarySettings);

View File

@@ -7,7 +7,7 @@ import {
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import { AppProvider, PageWrap } from '@edx/frontend-platform/react'; import { AppProvider, PageWrap } from '@edx/frontend-platform/react';
import { import {
findByTestId, queryByTestId, render, waitFor, getByText, fireEvent, queryByTestId, render, waitFor, getByText, fireEvent,
} from '@testing-library/react'; } from '@testing-library/react';
import MockAdapter from 'axios-mock-adapter'; import MockAdapter from 'axios-mock-adapter';
import PagesAndResourcesProvider from 'CourseAuthoring/pages-and-resources/PagesAndResourcesProvider'; import PagesAndResourcesProvider from 'CourseAuthoring/pages-and-resources/PagesAndResourcesProvider';
@@ -106,9 +106,8 @@ describe('XpertUnitSummarySettings', () => {
}); });
test('Shows switch on if enabled from backend', async () => { 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(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 () => { test('Shows switch on if disabled from backend', async () => {

View File

@@ -1,4 +1,4 @@
import { useIntl } from '@edx/frontend-platform/i18n'; import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { import {
ActionRow, ActionRow,
Alert, Alert,
@@ -70,40 +70,38 @@ AppSettingsForm.defaultProps = {
}; };
const SettingsModalBase = ({ const SettingsModalBase = ({
title, onClose, variant, isMobile, children, footer, intl, title, onClose, variant, isMobile, children, footer,
}) => { }) => (
const intl = useIntl(); <ModalDialog
return ( title={title}
<ModalDialog isOpen
title={title} onClose={onClose}
isOpen size="lg"
onClose={onClose} variant={variant}
size="lg" hasCloseButton={isMobile}
variant={variant} isFullscreenOnMobile
hasCloseButton={isMobile} >
isFullscreenOnMobile <ModalDialog.Header>
> <ModalDialog.Title data-testid="modal-title">
<ModalDialog.Header> {title}
<ModalDialog.Title data-testid="modal-title"> </ModalDialog.Title>
{title} </ModalDialog.Header>
</ModalDialog.Title> <ModalDialog.Body>
</ModalDialog.Header> {children}
<ModalDialog.Body> </ModalDialog.Body>
{children} <ModalDialog.Footer className="p-4">
</ModalDialog.Body> <ActionRow>
<ModalDialog.Footer className="p-4"> <ModalDialog.CloseButton variant="tertiary">
<ActionRow> {intl.formatMessage(messages.cancel)}
<ModalDialog.CloseButton variant="tertiary"> </ModalDialog.CloseButton>
{intl.formatMessage(messages.cancel)} {footer}
</ModalDialog.CloseButton> </ActionRow>
{footer} </ModalDialog.Footer>
</ActionRow> </ModalDialog>
</ModalDialog.Footer> );
</ModalDialog>
);
};
SettingsModalBase.propTypes = { SettingsModalBase.propTypes = {
intl: intlShape.isRequired,
title: PropTypes.string.isRequired, title: PropTypes.string.isRequired,
onClose: PropTypes.func.isRequired, onClose: PropTypes.func.isRequired,
variant: PropTypes.oneOf(['default', 'dark']).isRequired, variant: PropTypes.oneOf(['default', 'dark']).isRequired,
@@ -117,11 +115,11 @@ SettingsModalBase.defaultProps = {
}; };
const ResetUnitsButton = ({ const ResetUnitsButton = ({
intl,
courseId, courseId,
checked, checked,
visible, visible,
}) => { }) => {
const intl = useIntl();
const resetStatusRequestStatus = useSelector(getResetStatus); const resetStatusRequestStatus = useSelector(getResetStatus);
const dispatch = useDispatch(); const dispatch = useDispatch();
@@ -187,6 +185,7 @@ const ResetUnitsButton = ({
}; };
ResetUnitsButton.propTypes = { ResetUnitsButton.propTypes = {
intl: intlShape.isRequired,
courseId: PropTypes.string.isRequired, courseId: PropTypes.string.isRequired,
checked: PropTypes.oneOf(['true', 'false']).isRequired, checked: PropTypes.oneOf(['true', 'false']).isRequired,
visible: PropTypes.bool, visible: PropTypes.bool,
@@ -197,6 +196,7 @@ ResetUnitsButton.defaultProps = {
}; };
const SettingsModal = ({ const SettingsModal = ({
intl,
appId, appId,
title, title,
children, children,
@@ -213,7 +213,6 @@ const SettingsModal = ({
allUnitsEnabledText, allUnitsEnabledText,
noUnitsEnabledText, noUnitsEnabledText,
}) => { }) => {
const intl = useIntl();
const { courseId } = useContext(PagesAndResourcesContext); const { courseId } = useContext(PagesAndResourcesContext);
const loadingStatus = useSelector(getLoadingStatus); const loadingStatus = useSelector(getLoadingStatus);
const updateSettingsRequestStatus = useSelector(getSavingStatus); const updateSettingsRequestStatus = useSelector(getSavingStatus);
@@ -373,6 +372,7 @@ const SettingsModal = ({
> >
{allUnitsEnabledText} {allUnitsEnabledText}
<ResetUnitsButton <ResetUnitsButton
intl={intl}
courseId={courseId} courseId={courseId}
checked={formikProps.values.checked} checked={formikProps.values.checked}
visible={formikProps.values.checked === 'true'} visible={formikProps.values.checked === 'true'}
@@ -385,6 +385,7 @@ const SettingsModal = ({
> >
{noUnitsEnabledText} {noUnitsEnabledText}
<ResetUnitsButton <ResetUnitsButton
intl={intl}
courseId={courseId} courseId={courseId}
checked={formikProps.values.checked} checked={formikProps.values.checked}
visible={formikProps.values.checked === 'false'} visible={formikProps.values.checked === 'false'}
@@ -422,6 +423,7 @@ const SettingsModal = ({
}; };
SettingsModal.propTypes = { SettingsModal.propTypes = {
intl: intlShape.isRequired,
title: PropTypes.string.isRequired, title: PropTypes.string.isRequired,
appId: PropTypes.string.isRequired, appId: PropTypes.string.isRequired,
children: PropTypes.func, children: PropTypes.func,
@@ -448,4 +450,4 @@ SettingsModal.defaultProps = {
enableReinitialize: false, enableReinitialize: false,
}; };
export default SettingsModal; export default injectIntl(SettingsModal);

View File

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

View File

@@ -4,7 +4,7 @@ import {
} from 'react-router-dom'; } from 'react-router-dom';
import { getConfig } from '@edx/frontend-platform'; import { getConfig } from '@edx/frontend-platform';
import { PageWrap } from '@edx/frontend-platform/react'; import { PageWrap } from '@edx/frontend-platform/react';
import { Textbooks } from './textbooks'; import { Textbooks } from 'CourseAuthoring/textbooks';
import CourseAuthoringPage from './CourseAuthoringPage'; import CourseAuthoringPage from './CourseAuthoringPage';
import { PagesAndResources } from './pages-and-resources'; import { PagesAndResources } from './pages-and-resources';
import EditorContainer from './editors/EditorContainer'; import EditorContainer from './editors/EditorContainer';
@@ -17,7 +17,7 @@ import ScheduleAndDetails from './schedule-and-details';
import { GradingSettings } from './grading-settings'; import { GradingSettings } from './grading-settings';
import CourseTeam from './course-team/CourseTeam'; import CourseTeam from './course-team/CourseTeam';
import { CourseUpdates } from './course-updates'; import { CourseUpdates } from './course-updates';
import { CourseUnit } from './course-unit'; import { CourseUnit, IframeProvider } from './course-unit';
import { Certificates } from './certificates'; import { Certificates } from './certificates';
import CourseExportPage from './export-page/CourseExportPage'; import CourseExportPage from './export-page/CourseExportPage';
import CourseOptimizerPage from './optimizer-page/CourseOptimizerPage'; import CourseOptimizerPage from './optimizer-page/CourseOptimizerPage';
@@ -25,8 +25,7 @@ import CourseImportPage from './import-page/CourseImportPage';
import { DECODED_ROUTES } from './constants'; import { DECODED_ROUTES } from './constants';
import CourseChecklist from './course-checklist'; import CourseChecklist from './course-checklist';
import GroupConfigurations from './group-configurations'; import GroupConfigurations from './group-configurations';
import { CourseLibraries } from './course-libraries'; import CourseLibraries from './course-libraries';
import { IframeProvider } from './generic/hooks/context/iFrameContext';
/** /**
* As of this writing, these routes are mounted at a path prefixed with the following: * 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 clipboardUnit } from './clipboardUnit';
export { default as clipboardSubsection } from './clipboardSubsection';
export { default as clipboardXBlock } from './clipboardXBlock'; 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 { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { Helmet } from 'react-helmet'; import { Helmet } from 'react-helmet';
import { Container } from '@openedx/paragon'; import { Container } from '@openedx/paragon';
import { StudioFooterSlot } from '@edx/frontend-component-footer'; import { StudioFooter } from '@edx/frontend-component-footer';
import Header from '../header'; import Header from '../header';
import messages from './messages'; import messages from './messages';
@@ -29,7 +29,7 @@ const AccessibilityPage = ({
<AccessibilityBody {...{ email, communityAccessibilityLink }} /> <AccessibilityBody {...{ email, communityAccessibilityLink }} />
<AccessibilityForm accessibilityEmail={email} /> <AccessibilityForm accessibilityEmail={email} />
</Container> </Container>
<StudioFooterSlot /> <StudioFooter />
</> </>
); );
}; };

View File

@@ -1,10 +1,5 @@
/* eslint-disable import/prefer-default-export */ import { camelCaseObject, getConfig } from '@edx/frontend-platform';
import {
camelCaseObject,
getConfig,
} from '@edx/frontend-platform';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import { camelCase } from 'lodash';
import { convertObjectToSnakeCase } from '../../utils'; import { convertObjectToSnakeCase } from '../../utils';
const getApiBaseUrl = () => getConfig().STUDIO_BASE_URL; const getApiBaseUrl = () => getConfig().STUDIO_BASE_URL;
@@ -19,19 +14,7 @@ const getProctoringErrorsApiUrl = () => `${getApiBaseUrl()}/api/contentstore/v1/
export async function getCourseAdvancedSettings(courseId) { export async function getCourseAdvancedSettings(courseId) {
const { data } = await getAuthenticatedHttpClient() const { data } = await getAuthenticatedHttpClient()
.get(`${getCourseAdvancedSettingsApiUrl(courseId)}?fetch_all=0`); .get(`${getCourseAdvancedSettingsApiUrl(courseId)}?fetch_all=0`);
const keepValues = {}; return camelCaseObject(data);
Object.keys(data).forEach((key) => {
keepValues[camelCase(key)] = { value: data[key].value };
});
const formattedData = {};
const formattedCamelCaseData = camelCaseObject(data);
Object.keys(formattedCamelCaseData).forEach((key) => {
formattedData[key] = {
...formattedCamelCaseData[key],
value: keepValues[key]?.value,
};
});
return formattedData;
} }
/** /**
@@ -43,19 +26,7 @@ export async function getCourseAdvancedSettings(courseId) {
export async function updateCourseAdvancedSettings(courseId, settings) { export async function updateCourseAdvancedSettings(courseId, settings) {
const { data } = await getAuthenticatedHttpClient() const { data } = await getAuthenticatedHttpClient()
.patch(`${getCourseAdvancedSettingsApiUrl(courseId)}`, convertObjectToSnakeCase(settings)); .patch(`${getCourseAdvancedSettingsApiUrl(courseId)}`, convertObjectToSnakeCase(settings));
const keepValues = {}; return camelCaseObject(data);
Object.keys(data).forEach((key) => {
keepValues[camelCase(key)] = { value: data[key].value };
});
const formattedData = {};
const formattedCamelCaseData = camelCaseObject(data);
Object.keys(formattedCamelCaseData).forEach((key) => {
formattedData[key] = {
...formattedCamelCaseData[key],
value: keepValues[key]?.value,
};
});
return formattedData;
} }
/** /**
@@ -65,17 +36,5 @@ export async function updateCourseAdvancedSettings(courseId, settings) {
*/ */
export async function getProctoringExamErrors(courseId) { export async function getProctoringExamErrors(courseId) {
const { data } = await getAuthenticatedHttpClient().get(`${getProctoringErrorsApiUrl()}${courseId}`); const { data } = await getAuthenticatedHttpClient().get(`${getProctoringErrorsApiUrl()}${courseId}`);
const keepValues = {}; return camelCaseObject(data);
Object.keys(data).forEach((key) => {
keepValues[camelCase(key)] = { value: data[key].value };
});
const formattedData = {};
const formattedCamelCaseData = camelCaseObject(data);
Object.keys(formattedCamelCaseData).forEach((key) => {
formattedData[key] = {
...formattedCamelCaseData[key],
value: keepValues[key]?.value,
};
});
return formattedData;
} }

View File

@@ -1,236 +0,0 @@
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import {
getCourseAdvancedSettings,
updateCourseAdvancedSettings,
getProctoringExamErrors,
} from './api';
jest.mock('@edx/frontend-platform/auth', () => ({
getAuthenticatedHttpClient: jest.fn(),
}));
describe('courseSettings API', () => {
const mockHttpClient = {
get: jest.fn(),
patch: jest.fn(),
};
beforeEach(() => {
jest.clearAllMocks();
getAuthenticatedHttpClient.mockReturnValue(mockHttpClient);
});
describe('getCourseAdvancedSettings', () => {
it('should fetch and unformat course advanced settings', async () => {
const fakeData = {
key_snake_case: {
display_name: 'To come camelCase',
testCamelCase: 'This key must not be formatted',
PascalCase: 'To come camelCase',
'kebab-case': 'To come camelCase',
UPPER_CASE: 'To come camelCase',
lowercase: 'This key must not be formatted',
UPPERCASE: 'To come lowercase',
'Title Case': 'To come camelCase',
'dot.case': 'To come camelCase',
SCREAMING_SNAKE_CASE: 'To come camelCase',
MixedCase: 'To come camelCase',
'Train-Case': 'To come camelCase',
nestedOption: {
anotherOption: 'To come camelCase',
},
// value is an object with various cases
// this contain must not be formatted to camelCase
value: {
snake_case: 'snake_case',
camelCase: 'camelCase',
PascalCase: 'PascalCase',
'kebab-case': 'kebab-case',
UPPER_CASE: 'UPPER_CASE',
lowercase: 'lowercase',
UPPERCASE: 'UPPERCASE',
'Title Case': 'Title Case',
'dot.case': 'dot.case',
SCREAMING_SNAKE_CASE: 'SCREAMING_SNAKE_CASE',
MixedCase: 'MixedCase',
'Train-Case': 'Train-Case',
nestedOption: {
anotherOption: 'nestedContent',
},
},
},
};
const expected = {
keySnakeCase: {
displayName: 'To come camelCase',
testCamelCase: 'This key must not be formatted',
pascalCase: 'To come camelCase',
kebabCase: 'To come camelCase',
upperCase: 'To come camelCase',
lowercase: 'This key must not be formatted',
uppercase: 'To come lowercase',
titleCase: 'To come camelCase',
dotCase: 'To come camelCase',
screamingSnakeCase: 'To come camelCase',
mixedCase: 'To come camelCase',
trainCase: 'To come camelCase',
nestedOption: {
anotherOption: 'To come camelCase',
},
value: fakeData.key_snake_case.value,
},
};
mockHttpClient.get.mockResolvedValue({ data: fakeData });
const result = await getCourseAdvancedSettings('course-v1:Test+T101+2024');
expect(mockHttpClient.get).toHaveBeenCalledWith(
`${process.env.STUDIO_BASE_URL}/api/contentstore/v0/advanced_settings/course-v1:Test+T101+2024?fetch_all=0`,
);
expect(result).toEqual(expected);
});
});
describe('updateCourseAdvancedSettings', () => {
it('should update and unformat course advanced settings', async () => {
const fakeData = {
key_snake_case: {
display_name: 'To come camelCase',
testCamelCase: 'This key must not be formatted', // because already be camelCase
PascalCase: 'To come camelCase',
'kebab-case': 'To come camelCase',
UPPER_CASE: 'To come camelCase',
lowercase: 'This key must not be formatted', // because camelCase in lowercase not formatted
UPPERCASE: 'To come lowercase', // because camelCase in UPPERCASE format to lowercase
'Title Case': 'To come camelCase',
'dot.case': 'To come camelCase',
SCREAMING_SNAKE_CASE: 'To come camelCase',
MixedCase: 'To come camelCase',
'Train-Case': 'To come camelCase',
nestedOption: {
anotherOption: 'To come camelCase',
},
// value is an object with various cases
// this contain must not be formatted to camelCase
value: {
snake_case: 'snake_case',
camelCase: 'camelCase',
PascalCase: 'PascalCase',
'kebab-case': 'kebab-case',
UPPER_CASE: 'UPPER_CASE',
lowercase: 'lowercase',
UPPERCASE: 'UPPERCASE',
'Title Case': 'Title Case',
'dot.case': 'dot.case',
SCREAMING_SNAKE_CASE: 'SCREAMING_SNAKE_CASE',
MixedCase: 'MixedCase',
'Train-Case': 'Train-Case',
nestedOption: {
anotherOption: 'nestedContent',
},
},
},
};
const expected = {
keySnakeCase: {
displayName: 'To come camelCase',
testCamelCase: 'This key must not be formatted',
pascalCase: 'To come camelCase',
kebabCase: 'To come camelCase',
upperCase: 'To come camelCase',
lowercase: 'This key must not be formatted',
uppercase: 'To come lowercase',
titleCase: 'To come camelCase',
dotCase: 'To come camelCase',
screamingSnakeCase: 'To come camelCase',
mixedCase: 'To come camelCase',
trainCase: 'To come camelCase',
nestedOption: {
anotherOption: 'To come camelCase',
},
value: fakeData.key_snake_case.value,
},
};
mockHttpClient.patch.mockResolvedValue({ data: fakeData });
const result = await updateCourseAdvancedSettings('course-v1:Test+T101+2024', {});
expect(mockHttpClient.patch).toHaveBeenCalledWith(
`${process.env.STUDIO_BASE_URL}/api/contentstore/v0/advanced_settings/course-v1:Test+T101+2024`,
{},
);
expect(result).toEqual(expected);
});
});
describe('getProctoringExamErrors', () => {
it('should fetch proctoring errors and return unformat object', async () => {
const fakeData = {
key_snake_case: {
display_name: 'To come camelCase',
testCamelCase: 'This key must not be formatted',
PascalCase: 'To come camelCase',
'kebab-case': 'To come camelCase',
UPPER_CASE: 'To come camelCase',
lowercase: 'This key must not be formatted',
UPPERCASE: 'To come lowercase',
'Title Case': 'To come camelCase',
'dot.case': 'To come camelCase',
SCREAMING_SNAKE_CASE: 'To come camelCase',
MixedCase: 'To come camelCase',
'Train-Case': 'To come camelCase',
nestedOption: {
anotherOption: 'To come camelCase',
},
// value is an object with various cases
// this contain must not be formatted to camelCase
value: {
snake_case: 'snake_case',
camelCase: 'camelCase',
PascalCase: 'PascalCase',
'kebab-case': 'kebab-case',
UPPER_CASE: 'UPPER_CASE',
lowercase: 'lowercase',
UPPERCASE: 'UPPERCASE',
'Title Case': 'Title Case',
'dot.case': 'dot.case',
SCREAMING_SNAKE_CASE: 'SCREAMING_SNAKE_CASE',
MixedCase: 'MixedCase',
'Train-Case': 'Train-Case',
nestedOption: {
anotherOption: 'nestedContent',
},
},
},
};
const expected = {
keySnakeCase: {
displayName: 'To come camelCase',
testCamelCase: 'This key must not be formatted',
pascalCase: 'To come camelCase',
kebabCase: 'To come camelCase',
upperCase: 'To come camelCase',
lowercase: 'This key must not be formatted',
uppercase: 'To come lowercase',
titleCase: 'To come camelCase',
dotCase: 'To come camelCase',
screamingSnakeCase: 'To come camelCase',
mixedCase: 'To come camelCase',
trainCase: 'To come camelCase',
nestedOption: {
anotherOption: 'To come camelCase',
},
value: fakeData.key_snake_case.value,
},
};
mockHttpClient.get.mockResolvedValue({ data: fakeData });
const result = await getProctoringExamErrors('course-v1:Test+T101+2024');
expect(mockHttpClient.get).toHaveBeenCalledWith(
`${process.env.STUDIO_BASE_URL}/api/contentstore/v1/proctoring_errors/course-v1:Test+T101+2024`,
);
expect(result).toEqual(expected);
});
});
});

View File

@@ -59,7 +59,6 @@ export const COURSE_BLOCK_NAMES = ({
sequential: { id: 'sequential', name: 'Subsection' }, sequential: { id: 'sequential', name: 'Subsection' },
vertical: { id: 'vertical', name: 'Unit' }, vertical: { id: 'vertical', name: 'Unit' },
libraryContent: { id: 'library_content', name: 'Library content' }, libraryContent: { id: 'library_content', name: 'Library content' },
splitTest: { id: 'split_test', name: 'Split Test' },
component: { id: 'component', name: 'Component' }, component: { id: 'component', name: 'Component' },
}); });
@@ -92,17 +91,3 @@ export const REGEX_RULES = {
export const IFRAME_FEATURE_POLICY = ( export const IFRAME_FEATURE_POLICY = (
'microphone *; camera *; midi *; geolocation *; encrypted-media *; clipboard-write *' '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', { const xButtonAppliedTag = within(appliedTag).getByRole('button', {
name: /delete/i, name: /delete/i,
}); });
await userEvent.click(xButtonAppliedTag); xButtonAppliedTag.click();
// Check that the applied tag has been removed // Check that the applied tag has been removed
expect(appliedTag).not.toBeInTheDocument(); expect(appliedTag).not.toBeInTheDocument();

View File

@@ -1,4 +1,5 @@
import { import {
act,
fireEvent, fireEvent,
initializeMocks, initializeMocks,
render, render,
@@ -21,7 +22,6 @@ const path = '/content/:contentId?/*';
const mockOnClose = jest.fn(); const mockOnClose = jest.fn();
const mockSetBlockingSheet = jest.fn(); const mockSetBlockingSheet = jest.fn();
const mockNavigate = jest.fn(); const mockNavigate = jest.fn();
const mockSidebarAction = jest.fn();
mockContentTaxonomyTagsData.applyMock(); mockContentTaxonomyTagsData.applyMock();
mockTaxonomyListData.applyMock(); mockTaxonomyListData.applyMock();
mockTaxonomyTagsData.applyMock(); mockTaxonomyTagsData.applyMock();
@@ -41,11 +41,6 @@ jest.mock('react-router-dom', () => ({
useNavigate: () => mockNavigate, useNavigate: () => mockNavigate,
})); }));
jest.mock('../library-authoring/common/context/SidebarContext', () => ({
...jest.requireActual('../library-authoring/common/context/SidebarContext'),
useSidebarContext: () => ({ sidebarAction: mockSidebarAction() }),
}));
const renderDrawer = (contentId, drawerParams = {}) => ( const renderDrawer = (contentId, drawerParams = {}) => (
render( render(
<ContentTagsDrawerSheetContext.Provider value={drawerParams}> <ContentTagsDrawerSheetContext.Provider value={drawerParams}>
@@ -66,15 +61,19 @@ describe('<ContentTagsDrawer />', () => {
}); });
it('shows spinner before the content data query is complete', async () => { it('shows spinner before the content data query is complete', async () => {
renderDrawer(stagedTagsId); await act(async () => {
const spinner = (await screen.findAllByRole('status'))[0]; renderDrawer(stagedTagsId);
expect(spinner.textContent).toEqual('Loading'); // Uses <Spinner /> const spinner = screen.getAllByRole('status')[0];
expect(spinner.textContent).toEqual('Loading'); // Uses <Spinner />
});
}); });
it('shows spinner before the taxonomy tags query is complete', async () => { it('shows spinner before the taxonomy tags query is complete', async () => {
renderDrawer(stagedTagsId); await act(async () => {
const spinner = (await screen.findAllByRole('status'))[1]; renderDrawer(stagedTagsId);
expect(spinner.textContent).toEqual('Loading...'); // Uses <Loading /> 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 () => { it('shows the content display name after the query is complete in drawer variant', async () => {
@@ -99,12 +98,15 @@ describe('<ContentTagsDrawer />', () => {
}); });
it('shows the taxonomies data including tag numbers after the query is complete', async () => { it('shows the taxonomies data including tag numbers after the query is complete', async () => {
const { container } = renderDrawer(largeTagsId); await act(async () => {
await screen.findByText('Taxonomy 1'); const { container } = renderDrawer(largeTagsId);
await screen.findByText('Taxonomy 2'); await waitFor(() => { expect(screen.getByText('Taxonomy 1')).toBeInTheDocument(); });
const tagCountBadges = container.getElementsByClassName('taxonomy-tags-count-chip'); expect(screen.getByText('Taxonomy 1')).toBeInTheDocument();
expect(tagCountBadges[0].textContent).toBe('3'); expect(screen.getByText('Taxonomy 2')).toBeInTheDocument();
expect(tagCountBadges[1].textContent).toBe('2'); 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 () => { it('should be read only on first render on drawer variant', async () => {
@@ -190,26 +192,6 @@ describe('<ContentTagsDrawer />', () => {
expect(screen.getByRole('button', { name: /save/i })).toBeInTheDocument(); expect(screen.getByRole('button', { name: /save/i })).toBeInTheDocument();
}); });
it('should change to edit mode sidebar action is set to JumpToManageTags', async () => {
mockSidebarAction.mockReturnValueOnce('jump-to-manage-tags');
renderDrawer(stagedTagsId, { variant: 'component' });
expect(await screen.findByText('Taxonomy 1')).toBeInTheDocument();
// Show delete tag buttons
expect(screen.getAllByRole('button', {
name: /delete/i,
}).length).toBe(2);
// Show add a tag select
expect(screen.getByText(/add a tag/i)).toBeInTheDocument();
// Show cancel button
expect(screen.getByRole('button', { name: /cancel/i })).toBeInTheDocument();
// Show save button
expect(screen.getByRole('button', { name: /save/i })).toBeInTheDocument();
});
it('should change to read mode when click on `Cancel` on drawer variant', async () => { it('should change to read mode when click on `Cancel` on drawer variant', async () => {
renderDrawer(stagedTagsId); renderDrawer(stagedTagsId);
expect(await screen.findByText('Taxonomy 1')).toBeInTheDocument(); expect(await screen.findByText('Taxonomy 1')).toBeInTheDocument();

View File

@@ -14,7 +14,6 @@ import ContentTagsCollapsible from './ContentTagsCollapsible';
import Loading from '../generic/Loading'; import Loading from '../generic/Loading';
import { useCreateContentTagsDrawerContext } from './ContentTagsDrawerHelper'; import { useCreateContentTagsDrawerContext } from './ContentTagsDrawerHelper';
import { ContentTagsDrawerContext, ContentTagsDrawerSheetContext } from './common/context'; import { ContentTagsDrawerContext, ContentTagsDrawerSheetContext } from './common/context';
import { SidebarActions, useSidebarContext } from '../library-authoring/common/context/SidebarContext';
interface TaxonomyListProps { interface TaxonomyListProps {
contentId: string; contentId: string;
@@ -228,6 +227,7 @@ interface ContentTagsDrawerProps {
* It is used both in interfaces of this MFE and in edx-platform interfaces such as iframe. * 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. * - 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. * 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 * - If you want to use it as react component, you need to pass the content id and the close functions
* through the component parameters. * through the component parameters.
*/ */
@@ -245,9 +245,8 @@ const ContentTagsDrawer = ({
if (contentId === undefined) { if (contentId === undefined) {
throw new Error('Error: contentId cannot be null.'); throw new Error('Error: contentId cannot be null.');
} }
const { sidebarAction } = useSidebarContext();
const context = useCreateContentTagsDrawerContext(contentId, !readOnly, variant === 'drawer'); const context = useCreateContentTagsDrawerContext(contentId, !readOnly);
const { blockingSheet } = useContext(ContentTagsDrawerSheetContext); const { blockingSheet } = useContext(ContentTagsDrawerSheetContext);
const { const {
@@ -262,7 +261,6 @@ const ContentTagsDrawer = ({
closeToast, closeToast,
setCollapsibleToInitalState, setCollapsibleToInitalState,
otherTaxonomies, otherTaxonomies,
toEditMode,
} = context; } = context;
let onCloseDrawer: () => void; let onCloseDrawer: () => void;
@@ -305,13 +303,8 @@ const ContentTagsDrawer = ({
// First call of the initial collapsible states // First call of the initial collapsible states
React.useEffect(() => { React.useEffect(() => {
// Open tag edit mode when sidebarAction is JumpToManageTags setCollapsibleToInitalState();
if (sidebarAction === SidebarActions.JumpToManageTags) { }, [isTaxonomyListLoaded, isContentTaxonomyTagsLoaded]);
toEditMode();
} else {
setCollapsibleToInitalState();
}
}, [isTaxonomyListLoaded, isContentTaxonomyTagsLoaded, sidebarAction, toEditMode]);
const renderFooter = () => { const renderFooter = () => {
if (isTaxonomyListLoaded && isContentTaxonomyTagsLoaded) { if (isTaxonomyListLoaded && isContentTaxonomyTagsLoaded) {

View File

@@ -20,11 +20,9 @@ import { ContentTagsDrawerSheetContext } from './common/context';
* To *use* the context, just use `useContext(ContentTagsDrawerContext)` * To *use* the context, just use `useContext(ContentTagsDrawerContext)`
* @param {string} contentId * @param {string} contentId
* @param {boolean} canTagObject * @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} * @returns {ContentTagsDrawerContextData}
*/ */
export const useCreateContentTagsDrawerContext = (contentId, canTagObject, fetchMetadata = false) => { export const useCreateContentTagsDrawerContext = (contentId, canTagObject) => {
const intl = useIntl(); const intl = useIntl();
const org = extractOrgFromContentId(contentId); const org = extractOrgFromContentId(contentId);
@@ -50,7 +48,7 @@ export const useCreateContentTagsDrawerContext = (contentId, canTagObject, fetch
const updateTags = useContentTaxonomyTagsUpdater(contentId); const updateTags = useContentTaxonomyTagsUpdater(contentId);
// Fetch from database // Fetch from database
const { data: contentData, isSuccess: isContentDataLoaded } = useContentData(contentId, fetchMetadata); const { data: contentData, isSuccess: isContentDataLoaded } = useContentData(contentId);
const { const {
data: contentTaxonomyTagsData, data: contentTaxonomyTagsData,
isSuccess: isContentTaxonomyTagsLoaded, isSuccess: isContentTaxonomyTagsLoaded,

View File

@@ -1,6 +1,7 @@
import React from 'react'; import React from 'react';
import { IntlProvider } from '@edx/frontend-platform/i18n'; import { IntlProvider } from '@edx/frontend-platform/i18n';
import { import {
act,
render, render,
waitFor, waitFor,
fireEvent, fireEvent,
@@ -73,9 +74,11 @@ describe('<ContentTagsDropDownSelector />', () => {
} }
it('should render taxonomy tags drop down selector loading with spinner', async () => { it('should render taxonomy tags drop down selector loading with spinner', async () => {
const { getByRole } = await getComponent(); await act(async () => {
const spinner = getByRole('status'); const { getByRole } = await getComponent();
expect(spinner.textContent).toEqual('Loading tags'); // Uses <Spinner /> 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 () => { 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(() => { await waitFor(() => {
expect(getByText('Tag 1')).toBeInTheDocument(); expect(getByText('Tag 1')).toBeInTheDocument();
expect(container.getElementsByClassName('taxonomy-tags-arrow-drop-down').length).toBe(0); 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(() => { await waitFor(() => {
expect(getByText('Tag 2')).toBeInTheDocument(); expect(getByText('Tag 2')).toBeInTheDocument();
expect(container.getElementsByClassName('taxonomy-tags-arrow-drop-down').length).toBe(1); expect(container.getElementsByClassName('taxonomy-tags-arrow-drop-down').length).toBe(1);
});
}); });
}); });
@@ -148,45 +155,47 @@ describe('<ContentTagsDropDownSelector />', () => {
}, },
}); });
const dataWithTagsTree = { await act(async () => {
...data, const dataWithTagsTree = {
tagsTree: { ...data,
'Tag 3': { tagsTree: {
explicit: false, 'Tag 3': {
children: {}, explicit: false,
children: {},
},
}, },
}, };
}; const { container, getByText } = await getComponent(dataWithTagsTree);
const { container, getByText } = await getComponent(dataWithTagsTree); await waitFor(() => {
await waitFor(() => { expect(getByText('Tag 2')).toBeInTheDocument();
expect(getByText('Tag 2')).toBeInTheDocument(); expect(container.getElementsByClassName('taxonomy-tags-arrow-drop-down').length).toBe(1);
expect(container.getElementsByClassName('taxonomy-tags-arrow-drop-down').length).toBe(1); });
});
// Mock useTaxonomyTagsData again since it gets called in the recursive call // Mock useTaxonomyTagsData again since it gets called in the recursive call
useTaxonomyTagsData.mockReturnValueOnce({ useTaxonomyTagsData.mockReturnValueOnce({
hasMorePages: false, hasMorePages: false,
tagPages: { tagPages: {
isLoading: false, isLoading: false,
isError: false, isError: false,
data: [{ data: [{
value: 'Tag 3', value: 'Tag 3',
externalId: null, externalId: null,
childCount: 0, childCount: 0,
depth: 1, depth: 1,
parentValue: 'Tag 2', parentValue: 'Tag 2',
id: 12346, id: 12346,
subTagsUrl: null, subTagsUrl: null,
}], }],
}, },
}); });
// Expand the dropdown to see the subtags selectors // Expand the dropdown to see the subtags selectors
const expandToggle = container.querySelector('.taxonomy-tags-arrow-drop-down span'); const expandToggle = container.querySelector('.taxonomy-tags-arrow-drop-down span');
fireEvent.click(expandToggle); fireEvent.click(expandToggle);
await waitFor(() => { await waitFor(() => {
expect(getByText('Tag 3')).toBeInTheDocument(); expect(getByText('Tag 3')).toBeInTheDocument();
});
}); });
}); });
@@ -210,46 +219,48 @@ describe('<ContentTagsDropDownSelector />', () => {
}); });
const initalSearchTerm = 'test 1'; const initalSearchTerm = 'test 1';
const { rerender } = await getComponent({ ...data, searchTerm: initalSearchTerm }); await act(async () => {
const { rerender } = await getComponent({ ...data, searchTerm: initalSearchTerm });
await waitFor(() => { await waitFor(() => {
expect(useTaxonomyTagsData).toHaveBeenCalledWith(data.taxonomyId, null, 1, initalSearchTerm); expect(useTaxonomyTagsData).toBeCalledWith(data.taxonomyId, null, 1, initalSearchTerm);
}); });
const updatedSearchTerm = 'test 2'; const updatedSearchTerm = 'test 2';
rerender(<ContentTagsDropDownSelectorComponent rerender(<ContentTagsDropDownSelectorComponent
key={`selector-${data.taxonomyId}`} key={`selector-${data.taxonomyId}`}
taxonomyId={data.taxonomyId} taxonomyId={data.taxonomyId}
level={data.level} level={data.level}
tagsTree={data.tagsTree} tagsTree={data.tagsTree}
searchTerm={updatedSearchTerm} searchTerm={updatedSearchTerm}
appliedContentTagsTree={{}} appliedContentTagsTree={{}}
stagedContentTagsTree={{}} stagedContentTagsTree={{}}
/>); />);
await waitFor(() => { await waitFor(() => {
expect(useTaxonomyTagsData).toHaveBeenCalledWith(data.taxonomyId, null, 1, updatedSearchTerm); expect(useTaxonomyTagsData).toBeCalledWith(data.taxonomyId, null, 1, updatedSearchTerm);
}); });
// Clean search term // Clean search term
const cleanSearchTerm = ''; const cleanSearchTerm = '';
rerender(<ContentTagsDropDownSelectorComponent rerender(<ContentTagsDropDownSelectorComponent
key={`selector-${data.taxonomyId}`} key={`selector-${data.taxonomyId}`}
taxonomyId={data.taxonomyId} taxonomyId={data.taxonomyId}
level={data.level} level={data.level}
tagsTree={data.tagsTree} tagsTree={data.tagsTree}
searchTerm={cleanSearchTerm} searchTerm={cleanSearchTerm}
appliedContentTagsTree={{}} appliedContentTagsTree={{}}
stagedContentTagsTree={{}} stagedContentTagsTree={{}}
/>); />);
await waitFor(() => { await waitFor(() => {
expect(useTaxonomyTagsData).toHaveBeenCalledWith(data.taxonomyId, null, 1, cleanSearchTerm); expect(useTaxonomyTagsData).toBeCalledWith(data.taxonomyId, null, 1, cleanSearchTerm);
});
}); });
}); });
it('should render "noTag" message if search doesnt return taxonomies', async () => { it('should render "noTag" message if search doesnt return taxonomies', async () => {
useTaxonomyTagsData.mockReturnValue({ useTaxonomyTagsData.mockReturnValueOnce({
hasMorePages: false, hasMorePages: false,
tagPages: { tagPages: {
isLoading: false, isLoading: false,
@@ -260,18 +271,20 @@ describe('<ContentTagsDropDownSelector />', () => {
}); });
const searchTerm = 'uncommon search term'; const searchTerm = 'uncommon search term';
const { getByText } = await getComponent({ ...data, searchTerm }); await act(async () => {
const { getByText } = await getComponent({ ...data, searchTerm });
await waitFor(() => { await waitFor(() => {
expect(useTaxonomyTagsData).toHaveBeenCalledWith(data.taxonomyId, null, 1, searchTerm); 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 () => { it('should render "noTagsInTaxonomy" message if taxonomy is empty', async () => {
useTaxonomyTagsData.mockReturnValue({ useTaxonomyTagsData.mockReturnValueOnce({
hasMorePages: false, hasMorePages: false,
tagPages: { tagPages: {
isLoading: false, isLoading: false,
@@ -282,13 +295,15 @@ describe('<ContentTagsDropDownSelector />', () => {
}); });
const searchTerm = ''; const searchTerm = '';
const { getByText } = await getComponent({ ...data, searchTerm }); await act(async () => {
const { getByText } = await getComponent({ ...data, searchTerm });
await waitFor(() => { await waitFor(() => {
expect(useTaxonomyTagsData).toHaveBeenCalledWith(data.taxonomyId, null, 1, searchTerm); 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) * @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) { export async function getContentData(contentId) {
let url; 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:')) { if (contentId.startsWith('lb:')) {
url = getLibraryContentDataApiUrl(contentId); 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.languageWithTagsId: return thisMock.languageWithTags;
case thisMock.languageWithoutTagsId: return thisMock.languageWithoutTags; case thisMock.languageWithoutTagsId: return thisMock.languageWithoutTags;
case thisMock.largeTagsId: return thisMock.largeTags; case thisMock.largeTagsId: return thisMock.largeTags;
case thisMock.containerTagsId: return thisMock.largeTags;
case thisMock.emptyTagsId: return thisMock.emptyTags; case thisMock.emptyTagsId: return thisMock.emptyTags;
default: throw new Error(`No mock has been set up for contentId "${contentId}"`); 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 = { mockContentTaxonomyTagsData.emptyTags = {
taxonomies: [], taxonomies: [],
}; };
mockContentTaxonomyTagsData.containerTagsId = 'lct:org:lib:unit:container_tags';
mockContentTaxonomyTagsData.applyMock = () => jest.spyOn(api, 'getContentTaxonomyTagsData').mockImplementation(mockContentTaxonomyTagsData); mockContentTaxonomyTagsData.applyMock = () => jest.spyOn(api, 'getContentTaxonomyTagsData').mockImplementation(mockContentTaxonomyTagsData);
/** /**

View File

@@ -7,7 +7,6 @@ import {
useMutation, useMutation,
useQueryClient, useQueryClient,
} from '@tanstack/react-query'; } from '@tanstack/react-query';
import { useParams } from 'react-router';
import { import {
getTaxonomyTagsData, getTaxonomyTagsData,
getContentTaxonomyTagsData, getContentTaxonomyTagsData,
@@ -15,7 +14,7 @@ import {
updateContentTaxonomyTags, updateContentTaxonomyTags,
getContentTaxonomyTagsCount, getContentTaxonomyTagsCount,
} from './api'; } from './api';
import { libraryAuthoringQueryKeys, libraryQueryPredicate, xblockQueryKeys } from '../../library-authoring/data/apiHooks'; import { libraryQueryPredicate, xblockQueryKeys } from '../../library-authoring/data/apiHooks';
import { getLibraryId } from '../../generic/key-utils'; import { getLibraryId } from '../../generic/key-utils';
/** @typedef {import("../../taxonomy/data/types.js").TagListData} TagListData */ /** @typedef {import("../../taxonomy/data/types.js").TagListData} TagListData */
@@ -113,13 +112,11 @@ export const useContentTaxonomyTagsData = (contentId) => (
/** /**
* Builds the query to get meta data about the content object * Builds the query to get meta data about the content object
* @param {string} contentId The id of the content object (unit/component) * @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({ useQuery({
queryKey: ['contentData', contentId], queryKey: ['contentData', contentId],
queryFn: enabled ? () => getContentData(contentId) : undefined, queryFn: () => getContentData(contentId),
enabled,
}) })
); );
@@ -130,7 +127,6 @@ export const useContentData = (contentId, enabled) => (
export const useContentTaxonomyTagsUpdater = (contentId) => { export const useContentTaxonomyTagsUpdater = (contentId) => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const unitIframe = window.frames['xblock-iframe']; const unitIframe = window.frames['xblock-iframe'];
const { unitId } = useParams();
return useMutation({ return useMutation({
/** /**
@@ -153,17 +149,13 @@ export const useContentTaxonomyTagsUpdater = (contentId) => {
contentPattern = contentId.replace(/\+type@.*$/, '*'); contentPattern = contentId.replace(/\+type@.*$/, '*');
} }
queryClient.invalidateQueries({ queryKey: ['contentTagsCount', contentPattern] }); 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 // Obtain library id from contentId
const libraryId = getLibraryId(contentId); const libraryId = getLibraryId(contentId);
// Invalidate component metadata to update tags count // Invalidate component metadata to update tags count
queryClient.invalidateQueries(xblockQueryKeys.componentMetadata(contentId)); queryClient.invalidateQueries(xblockQueryKeys.componentMetadata(contentId));
// Invalidate content search to update tags count // Invalidate content search to update tags count
queryClient.invalidateQueries(['content_search'], { predicate: (query) => libraryQueryPredicate(query, libraryId) }); queryClient.invalidateQueries(['content_search'], { predicate: (query) => libraryQueryPredicate(query, libraryId) });
// If the tags for a compoent were edited from Unit page, invalidate children query to fetch count again.
if (unitId) {
queryClient.invalidateQueries(libraryAuthoringQueryKeys.containerChildren(unitId));
}
} }
}, },
onSuccess: /* istanbul ignore next */ () => { onSuccess: /* istanbul ignore next */ () => {

View File

@@ -1,5 +1,6 @@
import { useQuery, useMutation, useQueries } from '@tanstack/react-query'; 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 { import {
useTaxonomyTagsData, useTaxonomyTagsData,
useContentTaxonomyTagsData, useContentTaxonomyTagsData,
@@ -157,7 +158,7 @@ describe('useContentTaxonomyTagsUpdater', () => {
const contentId = 'testerContent'; const contentId = 'testerContent';
const taxonomyId = 123; const taxonomyId = 123;
const mutation = renderHook(() => useContentTaxonomyTagsUpdater(contentId)).result.current; const mutation = useContentTaxonomyTagsUpdater(contentId);
const tagsData = [{ const tagsData = [{
taxonomy: taxonomyId, taxonomy: taxonomyId,
tags: ['tag1', 'tag2'], tags: ['tag1', 'tag2'],

View File

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

View File

@@ -1,4 +1,5 @@
import { useState, useMemo } from 'react'; // @ts-check
import React, { useState, useMemo } from 'react';
import { import {
Card, Stack, Button, Collapsible, Icon, Card, Stack, Button, Collapsible, Icon,
} from '@openedx/paragon'; } from '@openedx/paragon';
@@ -9,19 +10,10 @@ import { ContentTagsDrawerSheet } from '..';
import messages from '../messages'; import messages from '../messages';
import { useContentTaxonomyTagsData } from '../data/apiHooks'; import { useContentTaxonomyTagsData } from '../data/apiHooks';
import type { ContentTaxonomyTagData, Tag } from '../data/types';
import { LoadingSpinner } from '../../generic/Loading'; import { LoadingSpinner } from '../../generic/Loading';
import TagsTree from '../TagsTree'; import TagsTree from '../TagsTree';
interface TagsSidebarBodyProps { const TagsSidebarBody = () => {
readOnly: boolean
}
type TagTree = {
[key: string]: { children: TagTree, canChangeObjecttag: boolean, canDeleteObjecttag: boolean }
};
const TagsSidebarBody = ({ readOnly }: TagsSidebarBodyProps) => {
const intl = useIntl(); const intl = useIntl();
const [showManageTags, setShowManageTags] = useState(false); const [showManageTags, setShowManageTags] = useState(false);
const contentId = useParams().blockId; const contentId = useParams().blockId;
@@ -32,8 +24,8 @@ const TagsSidebarBody = ({ readOnly }: TagsSidebarBodyProps) => {
isSuccess: isContentTaxonomyTagsLoaded, isSuccess: isContentTaxonomyTagsLoaded,
} = useContentTaxonomyTagsData(contentId || ''); } = useContentTaxonomyTagsData(contentId || '');
const buildTagsTree = (contentTags: Tag[]) => { const buildTagsTree = (contentTags) => {
const resultTree: TagTree = {}; const resultTree = {};
contentTags.forEach(item => { contentTags.forEach(item => {
let currentLevel = resultTree; let currentLevel = resultTree;
@@ -54,7 +46,7 @@ const TagsSidebarBody = ({ readOnly }: TagsSidebarBodyProps) => {
}; };
const tree = useMemo(() => { const tree = useMemo(() => {
const result: (Omit<ContentTaxonomyTagData, 'tags'> & { tags: TagTree })[] = []; const result = [];
if (isContentTaxonomyTagsLoaded && contentTaxonomyTagsData) { if (isContentTaxonomyTagsLoaded && contentTaxonomyTagsData) {
contentTaxonomyTagsData.taxonomies.forEach((taxonomy) => { contentTaxonomyTagsData.taxonomies.forEach((taxonomy) => {
result.push({ result.push({
@@ -96,13 +88,7 @@ const TagsSidebarBody = ({ readOnly }: TagsSidebarBodyProps) => {
</div> </div>
)} )}
<Button <Button className="mt-3 ml-2" variant="outline-primary" size="sm" onClick={() => setShowManageTags(true)}>
className="mt-3 ml-2"
variant="outline-primary"
size="sm"
onClick={() => setShowManageTags(true)}
disabled={readOnly}
>
{intl.formatMessage(messages.manageTagsButton)} {intl.formatMessage(messages.manageTagsButton)}
</Button> </Button>
</Stack> </Stack>
@@ -116,4 +102,6 @@ const TagsSidebarBody = ({ readOnly }: TagsSidebarBodyProps) => {
); );
}; };
TagsSidebarBody.propTypes = {};
export default TagsSidebarBody; export default TagsSidebarBody;

View File

@@ -1,14 +1,10 @@
import TagsSidebarHeader from './TagsSidebarHeader'; import TagsSidebarHeader from './TagsSidebarHeader';
import TagsSidebarBody from './TagsSidebarBody'; import TagsSidebarBody from './TagsSidebarBody';
interface TagsSidebarControlsProps { const TagsSidebarControls = () => (
readOnly: boolean,
}
const TagsSidebarControls = ({ readOnly }: TagsSidebarControlsProps) => (
<> <>
<TagsSidebarHeader /> <TagsSidebarHeader />
<TagsSidebarBody readOnly={readOnly} /> <TagsSidebarBody />
</> </>
); );

View File

@@ -1,35 +1,22 @@
import fetchMock from 'fetch-mock-jest'; import fetchMock from 'fetch-mock-jest';
import { cloneDeep } from 'lodash';
import userEvent from '@testing-library/user-event'; import userEvent from '@testing-library/user-event';
import MockAdapter from 'axios-mock-adapter/types';
import { QueryClient } from '@tanstack/react-query';
import { import {
initializeMocks, initializeMocks,
render, render,
screen, screen,
waitFor,
within, within,
} from '../testUtils'; } from '../testUtils';
import { mockContentSearchConfig } from '../search-manager/data/api.mock'; import { mockContentSearchConfig } from '../search-manager/data/api.mock';
import { CourseLibraries } from './CourseLibraries'; import mockInfoResult from './__mocks__/courseBlocksInfo.json';
import { import CourseLibraries from './CourseLibraries';
mockGetEntityLinks, import { mockGetEntityLinksByDownstreamContext } from './data/api.mocks';
mockGetEntityLinksSummaryByDownstreamContext,
mockFetchIndexDocuments,
mockUseLibBlockMetadata,
} from './data/api.mocks';
import { libraryBlockChangesUrl } from '../course-unit/data/api';
import { type ToastActionData } from '../generic/toast-context';
mockContentSearchConfig.applyMock(); mockContentSearchConfig.applyMock();
mockGetEntityLinks.applyMock(); mockGetEntityLinksByDownstreamContext.applyMock();
mockGetEntityLinksSummaryByDownstreamContext.applyMock();
mockUseLibBlockMetadata.applyMock();
const searchParamsGetMock = jest.fn(); const searchEndpoint = 'http://mock.meilisearch.local/indexes/studio/search';
let axiosMock: MockAdapter;
let mockShowToast: (message: string, action?: ToastActionData | undefined) => void;
let queryClient: QueryClient;
jest.mock('../studio-home/hooks', () => ({ jest.mock('../studio-home/hooks', () => ({
useStudioHome: () => ({ useStudioHome: () => ({
@@ -39,246 +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 />', () => { describe('<CourseLibraries />', () => {
beforeEach(() => { beforeEach(() => {
initializeMocks(); initializeMocks();
fetchMock.mockReset(); fetchMock.mockReset();
mockFetchIndexDocuments.applyMock();
localStorage.clear(); // The Meilisearch client-side API uses fetch, not Axios.
searchParamsGetMock.mockReturnValue('all'); 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 renderCourseLibrariesPage = async (courseKey?: string) => {
const courseId = courseKey || mockGetEntityLinks.courseKey; const courseId = courseKey || mockGetEntityLinksByDownstreamContext.courseKey;
render(<CourseLibraries courseId={courseId} />); render(<CourseLibraries courseId={courseId} />);
}; };
it('shows the spinner before the query is complete', async () => { it('shows the spinner before the query is complete', async () => {
// This mock will never return data (it loads forever): // This mock will never return data (it loads forever):
await renderCourseLibrariesPage(mockGetEntityLinksSummaryByDownstreamContext.courseKeyLoading); await renderCourseLibrariesPage(mockGetEntityLinksByDownstreamContext.courseKeyLoading);
const spinner = await screen.findByRole('status'); const spinner = await screen.findByRole('status');
expect(spinner.textContent).toEqual('Loading...'); expect(spinner.textContent).toEqual('Loading...');
}); });
it('shows empty state when no links are present', async () => { it('shows empty state wheen no links are present', async () => {
await renderCourseLibrariesPage(mockGetEntityLinks.courseKeyEmpty); await renderCourseLibrariesPage(mockGetEntityLinksByDownstreamContext.courseKeyEmpty);
const emptyMsg = await screen.findByText('This course does not use any content from libraries.'); const emptyMsg = await screen.findByText('This course does not use any content from libraries.');
expect(emptyMsg).toBeInTheDocument(); expect(emptyMsg).toBeInTheDocument();
}); });
it('shows alert when out of sync components are present', async () => { it('shows alert when out of sync components are present', async () => {
await renderCourseLibrariesPage(mockGetEntityLinks.courseKey); await renderCourseLibrariesPage(mockGetEntityLinksByDownstreamContext.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);
const alert = await screen.findByRole('alert'); const alert = await screen.findByRole('alert');
expect(await within(alert).findByText( 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(); )).toBeInTheDocument();
const allTab = await screen.findByRole('tab', { name: 'Libraries' });
expect(allTab).toHaveAttribute('aria-selected', 'true'); expect(allTab).toHaveAttribute('aria-selected', 'true');
const reviewBtn = await screen.findByRole('button', { name: 'Review' }); const reviewBtn = await screen.findByRole('button', { name: 'Review' });
userEvent.click(reviewBtn); userEvent.click(reviewBtn);
expect(allTab).toHaveAttribute('aria-selected', 'false'); 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(); expect(alert).not.toBeInTheDocument();
});
it('hide alert on dismiss', async () => { // go back to all tab
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' });
userEvent.click(allTab); userEvent.click(allTab);
// alert should not be back
expect(alert).not.toBeInTheDocument();
expect(allTab).toHaveAttribute('aria-selected', 'true'); 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 // review updates button
const reviewActionBtn = await screen.findByRole('button', { name: 'Review Updates' }); const reviewActionBtn = await screen.findByRole('button', { name: 'Review Updates' });
userEvent.click(reviewActionBtn); userEvent.click(reviewActionBtn);
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');
}); });
it('show alert if max lastPublishedDate is greated than the local storage value', async () => { it('hide alert on dismiss', async () => {
const lastPublishedDate = new Date('2025-05-01T22:20:44.989042Z'); await renderCourseLibrariesPage(mockGetEntityLinksByDownstreamContext.courseKey);
localStorage.setItem(
`outOfSyncCountAlert-${mockGetEntityLinks.courseKey}`,
String(lastPublishedDate.getTime() - 1000),
);
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);
const alert = await screen.findByRole('alert'); const alert = await screen.findByRole('alert');
expect(await within(alert).findByText( 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(); )).toBeInTheDocument();
}); const dismissBtn = await screen.findByRole('button', { name: 'Dismiss' });
userEvent.click(dismissBtn);
it('doesnt show alert if max lastPublishedDate is less than the local storage value', async () => {
const lastPublishedDate = new Date('2025-05-01T22:20:44.989042Z');
localStorage.setItem(
`outOfSyncCountAlert-${mockGetEntityLinks.courseKey}`,
String(lastPublishedDate.getTime() + 1000),
);
await renderCourseLibrariesPage(mockGetEntityLinks.courseKey);
const allTab = await screen.findByRole('tab', { name: 'Libraries' }); 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);
expect(allTab).toHaveAttribute('aria-selected', 'true'); expect(allTab).toHaveAttribute('aria-selected', 'true');
screen.logTestingPlaygroundURL(); expect(alert).not.toBeInTheDocument();
});
expect(screen.queryByRole('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();
describe('<CourseLibraries ReviewTab />', () => { const allButtons = await screen.findAllByRole('button');
beforeEach(() => { // total 3 components used from lib 1
const mocks = initializeMocks(); const expectedLib1Blocks = 3;
axiosMock = mocks.axiosMock; // total 4 components used from lib 1
mockShowToast = mocks.mockShowToast; const expectedLib2Blocks = 4;
fetchMock.mockReset(); // 1 component has updates.
mockFetchIndexDocuments.applyMock(); const expectedLib2ToUpdate = 1;
localStorage.clear();
searchParamsGetMock.mockReturnValue('review'); const libraryCards = allButtons.filter((el) => el.classList.contains('collapsible-trigger'));
queryClient = mocks.queryClient; 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();
const renderCourseLibrariesReviewPage = async (courseKey?: string) => { expect(await within(libraryCards[0]).findByText('All components up to date')).toBeInTheDocument();
const courseId = courseKey || mockGetEntityLinks.courseKey;
render(<CourseLibraries courseId={courseId} />); const libParent1 = libraryCards[0].parentElement;
}; expect(libParent1).not.toBeNull();
userEvent.click(libraryCards[0]);
it('shows the spinner before the query is complete', async () => { const xblockCards1 = libParent1!.querySelectorAll('div.card');
// This mock will never return data (it loads forever): expect(xblockCards1.length).toEqual(expectedLib1Blocks);
await renderCourseLibrariesReviewPage(mockGetEntityLinks.courseKeyLoading);
const spinner = await screen.findByRole('status'); expect(await within(libraryCards[1]).findByText('CS problems 3')).toBeInTheDocument();
expect(spinner.textContent).toEqual('Loading...'); expect(await within(libraryCards[1]).findByText(`${expectedLib2Blocks} components applied`)).toBeInTheDocument();
}); expect(await within(libraryCards[1]).findByText(`${expectedLib2ToUpdate} component out of sync`)).toBeInTheDocument();
it('shows empty state when no readyToSync links are present', async () => { const libParent2 = libraryCards[1].parentElement;
await renderCourseLibrariesReviewPage(mockGetEntityLinksSummaryByDownstreamContext.courseKeyUpToDate); expect(libParent2).not.toBeNull();
const emptyMsg = await screen.findByText('All components are up to date'); userEvent.click(libraryCards[1]);
expect(emptyMsg).toBeInTheDocument(); const xblockCards2 = libParent2!.querySelectorAll('div.card');
}); expect(xblockCards2.length).toEqual(expectedLib2Blocks);
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[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[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[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[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']);
}); });
}); });

View File

@@ -1,115 +1,241 @@
import React, { import React, {
useCallback, useEffect, useMemo, useState, useCallback, useMemo, useState,
} from 'react'; } from 'react';
import { Helmet } from 'react-helmet'; import { Helmet } from 'react-helmet';
import { getConfig } from '@edx/frontend-platform'; import { getConfig } from '@edx/frontend-platform';
import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n'; import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n';
import { import {
Alert, Alert,
ActionRow, Breadcrumb, Button, Card, Collapsible, Container, Dropdown, Hyperlink, Icon, IconButton, Layout, Stack, Tab, Tabs,
Button,
Card,
Container,
Hyperlink,
Icon,
Stack,
Tab,
Tabs,
} from '@openedx/paragon'; } from '@openedx/paragon';
import { import {
Cached, CheckCircle, Launch, Loop, Cached, CheckCircle, KeyboardArrowDown, KeyboardArrowRight, Loop, MoreVert,
} from '@openedx/paragon/icons'; } from '@openedx/paragon/icons';
import sumBy from 'lodash/sumBy'; import {
import { useSearchParams } from 'react-router-dom'; countBy, groupBy, keyBy, tail, uniq,
} from 'lodash';
import classNames from 'classnames';
import getPageHeadTitle from '../generic/utils'; import getPageHeadTitle from '../generic/utils';
import { useModel } from '../generic/model-store'; import { useModel } from '../generic/model-store';
import messages from './messages'; import messages from './messages';
import SubHeader from '../generic/sub-header/SubHeader'; import SubHeader from '../generic/sub-header/SubHeader';
import { useEntityLinksSummaryByDownstreamContext } from './data/apiHooks'; import { useEntityLinksByDownstreamContext } from './data/apiHooks';
import type { PublishableEntityLinkSummary } from './data/api'; 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 Loading from '../generic/Loading';
import { useStudioHome } from '../studio-home/hooks'; import { useStudioHome } from '../studio-home/hooks';
import NewsstandIcon from '../generic/NewsstandIcon';
import ReviewTabContent from './ReviewTabContent';
import { OutOfSyncAlert } from './OutOfSyncAlert';
interface Props { interface Props {
courseId: string; courseId: string;
} }
interface LibraryCardProps { interface LibraryCardProps {
linkSummary: PublishableEntityLinkSummary; courseId: string;
title: string;
links: PublishableEntityLink[];
}
interface ComponentInfo extends ContentHit {
readyToSync: boolean;
}
interface BlockCardProps {
info: ComponentInfo;
} }
export enum CourseLibraryTabs { export enum CourseLibraryTabs {
all = 'all', home = '',
review = 'review', review = 'review',
} }
const LibraryCard = ({ linkSummary }: LibraryCardProps) => { const BlockCard: React.FC<BlockCardProps> = ({ info }) => {
const intl = useIntl(); 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 ( return (
<Card className="my-3 border-light-500 border shadow-none"> <Card
<Card.Header className={classNames(
title={( 'my-3 shadow-none border-light-600 border',
<Stack direction="horizontal" gap={2}> { 'bg-primary-100': info.readyToSync },
<Icon src={NewsstandIcon} /> )}
{linkSummary.upstreamContextTitle} 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> </Stack>
)} <Stack direction="horizontal" className="small" gap={1}>
actions={( {info.readyToSync && <Icon src={Loop} size="xs" />}
<ActionRow> {info.formatted?.displayName}
<Button </Stack>
destination={`${getConfig().PUBLIC_PATH}library/${linkSummary.upstreamContextKey}`} <div className="micro">{info.formatted?.description}</div>
target="_blank" <Breadcrumb
className="border border-light-300" className="micro text-gray-500"
variant="tertiary" ariaLabel={intl.formatMessage(messages.breadcrumbAriaLabel)}
as={Hyperlink} links={breadcrumbs.map((breadcrumb) => ({ label: breadcrumb.displayName }))}
size="sm" spacer={<span className="custom-spacer">/</span>}
showLaunchIcon={false} linkAs="span"
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> </Stack>
</Card.Section> </Card.Section>
</Card> </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 intl = useIntl();
const courseDetails = useModel('courseDetails', courseId); const courseDetails = useModel('courseDetails', courseId);
const [searchParams] = useSearchParams(); const [tabKey, setTabKey] = useState<CourseLibraryTabs>(CourseLibraryTabs.home);
const [tabKey, setTabKey] = useState<CourseLibraryTabs>( const [showReviewAlert, setShowReviewAlert] = useState(true);
() => searchParams.get('tab') as CourseLibraryTabs, const { data: links, isLoading } = useEntityLinksByDownstreamContext(courseId);
); const linksByLib = useMemo(() => groupBy(links, 'upstreamContextKey'), [links]);
const [showReviewAlert, setShowReviewAlert] = useState(false); const outOfSyncCount = useMemo(() => countBy(links, 'readyToSync').true, [links]);
const { data: libraries, isLoading } = useEntityLinksSummaryByDownstreamContext(courseId);
const outOfSyncCount = useMemo(() => sumBy(libraries, (lib) => lib.readyToSyncCount), [libraries]);
const { const {
isLoadingPage: isLoadingStudioHome, isLoadingPage: isLoadingStudioHome,
isFailedLoadingPage: isFailedLoadingStudioHome, isFailedLoadingPage: isFailedLoadingStudioHome,
@@ -118,64 +244,33 @@ export const CourseLibraries: React.FC<Props> = ({ courseId }) => {
const onAlertReview = () => { const onAlertReview = () => {
setTabKey(CourseLibraryTabs.review); 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(() => { const renderLibrariesTabContent = useCallback(() => {
if (isLoading) { if (isLoading) {
return <Loading />; return <Loading />;
} }
if (libraries?.length === 0) { if (links?.length === 0) {
return <small><FormattedMessage {...messages.homeTabDescriptionEmpty} /></small>; return <small><FormattedMessage {...messages.homeTabDescriptionEmpty} /></small>;
} }
return ( return (
<> <>
<small><FormattedMessage {...messages.homeTabDescription} /></small> <small><FormattedMessage {...messages.homeTabDescription} /></small>
{libraries?.map((library) => ( {Object.entries(linksByLib).map(([libKey, libLinks]) => (
<LibraryCard <LibraryCard
linkSummary={library} courseId={courseId}
key={library.upstreamContextKey} title={libLinks[0].upstreamContextTitle}
links={libLinks}
key={libKey}
/> />
))} ))}
</> </>
); );
}, [libraries, isLoading]); }, [links, isLoading, linksByLib]);
const renderReviewTabContent = useCallback(() => {
if (isLoading) {
return <Loading />;
}
if (tabKey !== CourseLibraryTabs.review) {
return null;
}
if (!outOfSyncCount) {
return (
<Stack direction="horizontal" gap={2}>
<Icon src={CheckCircle} size="xs" />
<small>
<FormattedMessage {...messages.reviewTabDescriptionEmpty} />
</small>
</Stack>
);
}
return <ReviewTabContent courseId={courseId} />;
}, [outOfSyncCount, isLoading, tabKey]);
if (!isLoadingStudioHome && (!librariesV2Enabled || isFailedLoadingStudioHome)) { if (!isLoadingStudioHome && (!librariesV2Enabled || isFailedLoadingStudioHome)) {
return ( return (
@@ -193,16 +288,16 @@ export const CourseLibraries: React.FC<Props> = ({ courseId }) => {
</title> </title>
</Helmet> </Helmet>
<Container size="xl" className="px-4 pt-4 mt-3"> <Container size="xl" className="px-4 pt-4 mt-3">
<OutOfSyncAlert <ReviewAlert
courseId={courseId} show={outOfSyncCount > 0 && tabKey === CourseLibraryTabs.home && showReviewAlert}
outOfSyncCount={outOfSyncCount}
onDismiss={onAlertDismiss}
onReview={onAlertReview} onReview={onAlertReview}
showAlert={showReviewAlert && tabKey === CourseLibraryTabs.all}
setShowAlert={setShowReviewAlert}
/> />
<SubHeader <SubHeader
title={intl.formatMessage(messages.headingTitle)} title={intl.formatMessage(messages.headingTitle)}
subtitle={intl.formatMessage(messages.headingSubtitle)} subtitle={intl.formatMessage(messages.headingSubtitle)}
headerActions={!showReviewAlert && outOfSyncCount > 0 && tabKey === CourseLibraryTabs.all && ( headerActions={!showReviewAlert && tabKey === CourseLibraryTabs.home && (
<Button <Button
variant="primary" variant="primary"
onClick={onAlertReview} onClick={onAlertReview}
@@ -217,27 +312,25 @@ export const CourseLibraries: React.FC<Props> = ({ courseId }) => {
<Tabs <Tabs
id="course-library-tabs" id="course-library-tabs"
activeKey={tabKey} activeKey={tabKey}
onSelect={tabChange} onSelect={(k: CourseLibraryTabs) => setTabKey(k)}
> >
<Tab <Tab
eventKey={CourseLibraryTabs.all} eventKey={CourseLibraryTabs.home}
title={intl.formatMessage(messages.homeTabTitle)} title={intl.formatMessage(messages.homeTabTitle)}
className="px-2 mt-3" className="px-2 mt-3"
> >
{renderLibrariesTabContent()} <TabContent>
{renderLibrariesTabContent()}
</TabContent>
</Tab> </Tab>
<Tab <Tab
eventKey={CourseLibraryTabs.review} eventKey={CourseLibraryTabs.review}
title={( title={intl.formatMessage(
<Stack direction="horizontal" gap={1}> outOfSyncCount > 0 ? messages.reviewTabTitle : messages.reviewTabTitleEmpty,
<Icon src={Loop} /> { count: outOfSyncCount },
{intl.formatMessage(messages.reviewTabTitle)}
</Stack>
)} )}
notification={outOfSyncCount}
className="px-2 mt-3"
> >
{renderReviewTabContent()} <TabContent>Help</TabContent>
</Tab> </Tab>
</Tabs> </Tabs>
</section> </section>
@@ -245,3 +338,5 @@ export const CourseLibraries: React.FC<Props> = ({ courseId }) => {
</> </>
); );
}; };
export default CourseLibraries;

View File

@@ -1,78 +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 of dismiss is stored in localstorage of user
* in this format: outOfSyncCountAlert-${courseId} = <datetime value in milliseconds>.
* * If there are not new published components for the course and the user opens outline
* in the same browser, they don't see the alert again.
* * If there is a new published component upstream, 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), 0);
const lastPublishedDate = data?.map(lib => new Date(lib.lastPublishedAt || 0).getTime())
.reduce((acc, lastPublished) => Math.max(lastPublished, acc), 0);
const alertKey = `outOfSyncCountAlert-${courseId}`;
useEffect(() => {
if (isLoading) {
return;
}
if (outOfSyncCount === 0) {
localStorage.removeItem(alertKey);
setShowAlert(false);
return;
}
const dismissedAlertDate = parseInt(localStorage.getItem(alertKey) ?? '0', 10);
setShowAlert((lastPublishedDate ?? 0) > dismissedAlertDate);
}, [outOfSyncCount, lastPublishedDate, isLoading, data]);
const dismissAlert = () => {
setShowAlert(false);
localStorage.setItem(alertKey, Date.now().toString());
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,330 +0,0 @@
import React, {
useCallback, useContext, 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 } from 'lodash';
import { useQueryClient } from '@tanstack/react-query';
import { Loop } 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';
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,
}: {
outOfSyncComponents: PublishableEntityLink[];
}) => {
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,
isLoading: isIndexDataLoading,
hasError,
hasNextPage,
isFetchingNextPage,
fetchNextPage,
} = useSearchContext();
const downstreamInfo = hits as ContentHit[];
useLoadOnScroll(
hasNextPage,
isFetchingNextPage,
fetchNextPage,
true,
);
const outOfSyncComponentsByKey = useMemo(
() => keyBy(outOfSyncComponents, 'downstreamUsageKey'),
[outOfSyncComponents],
);
const queryClient = useQueryClient();
// Toggle preview changes modal
const [isModalOpen, openModal, closeModal] = useToggle(false);
const acceptChangesMutation = useAcceptLibraryBlockChanges();
const ignoreChangesMutation = useIgnoreLibraryBlockChanges();
const setSelectedBlockData = useCallback((info: ContentHit) => {
setBlockData({
displayName: info.displayName,
downstreamBlockId: info.usageKey,
upstreamBlockId: outOfSyncComponentsByKey[info.usageKey].upstreamUsageKey,
upstreamBlockVersionSynced: outOfSyncComponentsByKey[info.usageKey].versionSynced,
isVertical: info.blockType === 'vertical',
});
}, [outOfSyncComponentsByKey]);
// Show preview changes on review
const onReview = useCallback((info: ContentHit) => {
setSelectedBlockData(info);
openModal();
}, [setSelectedBlockData, openModal]);
const onIgnoreClick = useCallback((info: ContentHit) => {
setSelectedBlockData(info);
openConfirmModal();
}, [setSelectedBlockData, 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]);
if (isIndexDataLoading) {
return <Loading />;
}
if (hasError) {
return <AlertError error={intl.formatMessage(messages.genericErrorMessage)} />;
}
return (
<>
{downstreamInfo?.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>
)}
/>
))}
{blockData && (
<PreviewLibraryXBlockChanges
blockData={blockData}
isModalOpen={isModalOpen}
closeModal={closeModal}
postChange={postChange}
/>
)}
<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: outOfSyncComponents,
isLoading: isSyncComponentsLoading,
isError,
error,
} = useEntityLinks({ courseId, readyToSync: true });
const downstreamKeys = useMemo(
() => outOfSyncComponents?.map(link => link.downstreamUsageKey),
[outOfSyncComponents],
);
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}
/>
</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,22 +0,0 @@
[
{
"upstreamContextTitle": "CS problems 3",
"upstreamContextKey": "lib:OpenedX:CSPROB3",
"readyToSyncCount": 5,
"totalCount": 14,
"lastPublishedAt": "2025-05-01T20:20:44.989042Z"
},
{
"upstreamContextTitle": "CS problems 2",
"upstreamContextKey": "lib:OpenedX:CSPROB2",
"readyToSyncCount": 0,
"totalCount": 21,
"lastPublishedAt": "2025-05-01T21:20:44.989042Z"
},
{
"upstreamContextTitle": "CS problems",
"upstreamContextKey": "lib:OpenedX:CSPROB",
"totalCount": 3,
"lastPublishedAt": "2025-05-01T22:20:44.989042Z"
}
]

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,72 +1,100 @@
[ [
{ {
"id": 875, "id": 970,
"upstreamContextTitle": "CS problems 3", "upstreamContextTitle": "CS problems 2",
"upstreamVersion": 10, "upstreamVersion": 15,
"readyToSync": true, "readyToSync": false,
"upstreamUsageKey": "lb:OpenedX:CSPROB3:problem:d40264d5-80c4-4be8-bfb6-086391de1cd3", "upstreamUsageKey": "lb:OpenedX:CSPROB2:html:c0c1ca28-ff25-4757-83bc-3a2c2a0fe9c8",
"upstreamContextKey": "lib:OpenedX:CSPROB3", "upstreamContextKey": "lib:OpenedX:CSPROB2",
"downstreamUsageKey": "block-v1:OpenEdx+DemoX+CourseX+type@problem+block@problem3", "downstreamUsageKey": "block-v1:NewOrg3+TSTCS+2025_T1+type@html+block@0dfbabb8c93140caa3a004ab621757f5",
"downstreamContextKey": "course-v1:OpenEdx+DemoX+CourseX", "downstreamContextKey": "course-v1:NewOrg3+TSTCS+2025_T1",
"versionSynced": 2, "versionSynced": 15,
"versionDeclined": null, "versionDeclined": 13,
"created": "2025-02-08T14:07:05.588484Z", "created": "2025-02-08T14:11:23.650589Z",
"updated": "2025-02-08T14:07:05.588484Z" "updated": "2025-02-08T14:11:23.650589Z"
}, },
{ {
"id": 876, "id": 971,
"upstreamContextTitle": "CS problems 3", "upstreamContextTitle": "CS problems 2",
"upstreamVersion": 10, "upstreamVersion": 3,
"readyToSync": true, "readyToSync": false,
"upstreamUsageKey": "lb:OpenedX:CSPROB3:problem:d40264d5-80c4-4be8-bfb6-086391de1cd3", "upstreamUsageKey": "lb:OpenedX:CSPROB2:html:fd2d3827-e633-4217-bca9-c6661086b4b2",
"upstreamContextKey": "lib:OpenedX:CSPROB3", "upstreamContextKey": "lib:OpenedX:CSPROB2",
"downstreamUsageKey": "block-v1:OpenEdx+DemoX+CourseX+type@problem+block@problem6", "downstreamUsageKey": "block-v1:NewOrg3+TSTCS+2025_T1+type@html+block@37a916b53c2f430989d33ddeb47e224d",
"downstreamContextKey": "course-v1:OpenEdx+DemoX+CourseX", "downstreamContextKey": "course-v1:NewOrg3+TSTCS+2025_T1",
"versionSynced": 2, "versionSynced": 3,
"versionDeclined": null, "versionDeclined": null,
"created": "2025-02-08T14:07:05.588484Z", "created": "2025-02-08T14:11:23.650589Z",
"updated": "2025-02-08T14:07:05.588484Z" "updated": "2025-02-08T14:11:23.650589Z"
}, },
{ {
"id": 884, "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", "upstreamContextTitle": "CS problems 3",
"upstreamVersion": 26, "upstreamVersion": 18,
"readyToSync": true, "readyToSync": false,
"upstreamUsageKey": "lb:OpenedX:CSPROB3:html:ca4d2b1f-0b64-4a2d-88fa-592f7e398477", "upstreamUsageKey": "lb:OpenedX:CSPROB3:html:ca4d2b1f-0b64-4a2d-88fa-592f7e398477",
"upstreamContextKey": "lib:OpenedX:CSPROB3", "upstreamContextKey": "lib:OpenedX:CSPROB3",
"downstreamUsageKey": "block-v1:OpenEdx+DemoX+CourseX+type@html+block@257e68e3386d4a8f8739d45b67e76a9b", "downstreamUsageKey": "block-v1:NewOrg3+TSTCS+2025_T1+type@html+block@5be06d70f09c45a581d973d215e7ad62",
"downstreamContextKey": "course-v1:OpenEdx+DemoX+CourseX", "downstreamContextKey": "course-v1:NewOrg3+TSTCS+2025_T1",
"versionSynced": 16, "versionSynced": 17,
"versionDeclined": null, "versionDeclined": 18,
"created": "2025-02-08T14:07:05.588484Z", "created": "2025-02-12T05:38:53.967738Z",
"updated": "2025-02-08T14:07:05.588484Z" "updated": "2025-02-12T05:41:01.225542Z"
}, },
{ {
"id": 889, "id": 975,
"upstreamContextTitle": "CS problems 3", "upstreamContextTitle": "CS problems 3",
"upstreamVersion": 10, "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, "readyToSync": true,
"upstreamUsageKey": "lb:OpenedX:CSPROB3:problem:d40264d5-80c4-4be8-bfb6-086391de1cd3", "upstreamUsageKey": "lb:OpenedX:CSPROB3:problem:d40264d5-80c4-4be8-bfb6-086391de1cd3",
"upstreamContextKey": "lib:OpenedX:CSPROB3", "upstreamContextKey": "lib:OpenedX:CSPROB3",
"downstreamUsageKey": "block-v1:OpenEdx+DemoX+CourseX+type@problem+block@a4455860b03647219ff8b01cde49cf37", "downstreamUsageKey": "block-v1:NewOrg3+TSTCS+2025_T1+type@problem+block@fcf430ac5b7a44b6ab316b53a7e67fef",
"downstreamContextKey": "course-v1:OpenEdx+DemoX+CourseX", "downstreamContextKey": "course-v1:NewOrg3+TSTCS+2025_T1",
"versionSynced": 2, "versionSynced": 2,
"versionDeclined": null, "versionDeclined": null,
"created": "2025-02-08T14:07:05.588484Z", "created": "2025-02-12T05:38:58.538280Z",
"updated": "2025-02-08T14:07:05.588484Z" "updated": "2025-02-12T05:38:58.538280Z"
},
{
"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"
} }
] ]

View File

@@ -1,111 +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 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 { createAxiosError } from '../../testUtils';
import * as api from './api'; 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. * This mock returns a fixed response for the downstreamContextKey.
*/ */
export async function mockGetEntityLinks( export async function mockGetEntityLinksByDownstreamContext(
downstreamContextKey?: string, downstreamContextKey: string,
readyToSync?: boolean, ): Promise<api.PublishableEntityLink[]> {
): ReturnType<typeof api.getEntityLinks> {
switch (downstreamContextKey) { switch (downstreamContextKey) {
case mockGetEntityLinks.invalidCourseKey: case mockGetEntityLinksByDownstreamContext.invalidCourseKey:
throw createAxiosError({ throw createAxiosError({
code: 404, code: 404,
message: 'Not found.', message: 'Not found.',
path: api.getEntityLinksByDownstreamContextUrl(), path: api.getEntityLinksByDownstreamContextUrl(downstreamContextKey),
}); });
case mockGetEntityLinks.courseKeyLoading: case mockGetEntityLinksByDownstreamContext.courseKeyLoading:
return new Promise(() => {}); return new Promise(() => {});
case mockGetEntityLinks.courseKeyEmpty: case mockGetEntityLinksByDownstreamContext.courseKeyEmpty:
return Promise.resolve([]); return Promise.resolve([]);
default: {
let { response } = mockGetEntityLinks;
if (readyToSync !== undefined) {
response = response.filter((o) => o.readyToSync === readyToSync);
}
return Promise.resolve(response);
}
}
}
mockGetEntityLinks.courseKey = mockLinksResult[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:
return Promise.resolve([]);
case mockGetEntityLinksSummaryByDownstreamContext.courseKeyUpToDate:
return Promise.resolve(mockGetEntityLinksSummaryByDownstreamContext.response.filter(
(o: { readyToSyncCount: number }) => o.readyToSyncCount === 0,
));
default: default:
return Promise.resolve(mockGetEntityLinksSummaryByDownstreamContext.response); return Promise.resolve(mockGetEntityLinksByDownstreamContext.response);
} }
} }
mockGetEntityLinksSummaryByDownstreamContext.courseKey = mockLinksResult[0].downstreamContextKey; mockGetEntityLinksByDownstreamContext.courseKey = mockLinksResult[0].downstreamContextKey;
mockGetEntityLinksSummaryByDownstreamContext.invalidCourseKey = 'course_key_error'; mockGetEntityLinksByDownstreamContext.invalidCourseKey = 'course_key_error';
mockGetEntityLinksSummaryByDownstreamContext.courseKeyLoading = 'courseKeySummaryLoading'; mockGetEntityLinksByDownstreamContext.courseKeyLoading = 'courseKeyLoading';
mockGetEntityLinksSummaryByDownstreamContext.courseKeyEmpty = 'courseKeyEmpty'; mockGetEntityLinksByDownstreamContext.courseKeyEmpty = 'courseKeyEmpty';
mockGetEntityLinksSummaryByDownstreamContext.courseKeyUpToDate = 'courseKeyUpToDate'; mockGetEntityLinksByDownstreamContext.response = mockLinksResult;
mockGetEntityLinksSummaryByDownstreamContext.response = mockSummaryResult;
/** Apply this mock. Returns a spy object that can tell you if it's been called. */ /** Apply this mock. Returns a spy object that can tell you if it's been called. */
mockGetEntityLinksSummaryByDownstreamContext.applyMock = () => { mockGetEntityLinksByDownstreamContext.applyMock = () => {
jest.spyOn(api, 'getEntityLinksSummaryByDownstreamContext').mockImplementation(mockGetEntityLinksSummaryByDownstreamContext); jest.spyOn(api, 'getEntityLinksByDownstreamContext').mockImplementation(mockGetEntityLinksByDownstreamContext);
};
/**
* 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);
}; };

View File

@@ -3,65 +3,27 @@ import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
const getApiBaseUrl = () => getConfig().STUDIO_BASE_URL; const getApiBaseUrl = () => getConfig().STUDIO_BASE_URL;
export const getEntityLinksByDownstreamContextUrl = () => `${getApiBaseUrl()}/api/contentstore/v2/downstreams/`; export const getEntityLinksByDownstreamContextUrl = (downstreamContextKey: string) => `${getApiBaseUrl()}/api/contentstore/v2/upstreams/${downstreamContextKey}`;
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 interface PublishableEntityLink { export interface PublishableEntityLink {
id: number;
upstreamUsageKey: string; upstreamUsageKey: string;
upstreamContextKey: string; upstreamContextKey: string;
upstreamContextTitle: string; upstreamContextTitle: string;
upstreamVersion: number; upstreamVersion: string;
downstreamUsageKey: string; downstreamUsageKey: string;
downstreamContextTitle: string;
downstreamContextKey: string; downstreamContextKey: string;
versionSynced: number; versionSynced: string;
versionDeclined: number | null; versionDeclined: string;
created: string; created: string;
updated: string; updated: string;
readyToSync: boolean; readyToSync: boolean;
} }
export interface PublishableEntityLinkSummary { export const getEntityLinksByDownstreamContext = async (
upstreamContextKey: string; downstreamContextKey: string,
upstreamContextTitle: string;
readyToSyncCount: number;
totalCount: number;
lastPublishedAt: string;
}
export const getEntityLinks = async (
downstreamContextKey?: string,
readyToSync?: boolean,
upstreamUsageKey?: string,
): Promise<PublishableEntityLink[]> => { ): Promise<PublishableEntityLink[]> => {
const { data } = await getAuthenticatedHttpClient() const { data } = await getAuthenticatedHttpClient()
.get(getEntityLinksByDownstreamContextUrl(), { .get(getEntityLinksByDownstreamContextUrl(downstreamContextKey));
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));
return camelCaseObject(data); return camelCaseObject(data);
}; };

View File

@@ -1,10 +1,11 @@
import { initializeMockApp } from '@edx/frontend-platform'; import { initializeMockApp } from '@edx/frontend-platform';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import { renderHook } from '@testing-library/react-hooks';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import MockAdapter from 'axios-mock-adapter'; import MockAdapter from 'axios-mock-adapter';
import { renderHook, waitFor } from '@testing-library/react'; import { waitFor } from '@testing-library/react';
import { getEntityLinksByDownstreamContextUrl } from './api'; import { getEntityLinksByDownstreamContextUrl } from './api';
import { useEntityLinks } from './apiHooks'; import { useEntityLinksByDownstreamContext } from './apiHooks';
let axiosMock: MockAdapter; let axiosMock: MockAdapter;
@@ -35,24 +36,15 @@ describe('course libraries api hooks', () => {
axiosMock = new MockAdapter(getAuthenticatedHttpClient()); axiosMock = new MockAdapter(getAuthenticatedHttpClient());
}); });
afterEach(() => { it('should create library block', async () => {
axiosMock.reset(); const courseKey = 'course-v1:some+key';
}); const url = getEntityLinksByDownstreamContextUrl(courseKey);
it('should return links for course', async () => {
const courseId = 'course-v1:some+key';
const url = getEntityLinksByDownstreamContextUrl();
axiosMock.onGet(url).reply(200, []); axiosMock.onGet(url).reply(200, []);
const { result } = renderHook(() => useEntityLinks({ courseId }), { wrapper }); const { result } = renderHook(() => useEntityLinksByDownstreamContext(courseKey), { wrapper });
await waitFor(() => { await waitFor(() => {
expect(result.current.isLoading).toBeFalsy(); expect(result.current.isLoading).toBeFalsy();
}); });
expect(result.current.data).toEqual([]);
expect(axiosMock.history.get[0].url).toEqual(url); 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,65 +1,20 @@
import { import {
useQuery, useQuery,
} from '@tanstack/react-query'; } from '@tanstack/react-query';
import { getEntityLinks, getEntityLinksSummaryByDownstreamContext } from './api'; import { getEntityLinksByDownstreamContext } from './api';
export const courseLibrariesQueryKeys = { export const courseLibrariesQueryKeys = {
all: ['courseLibraries'], all: ['courseLibraries'],
courseLibraries: (courseId?: string) => [...courseLibrariesQueryKeys.all, courseId], courseLibraries: (courseKey?: string) => [...courseLibrariesQueryKeys.all, courseKey],
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'],
}; };
/** /**
* Hook to fetch list of publishable entity links by course key. * Hook to fetch a content library by its ID.
* (That is, get a list of the library components used in the given course.)
*/ */
export const useEntityLinks = ({ export const useEntityLinksByDownstreamContext = (courseKey: string | undefined) => (
courseId, readyToSync, upstreamUsageKey,
}: {
courseId?: string,
readyToSync?: boolean,
upstreamUsageKey?: string,
}) => (
useQuery({ useQuery({
queryKey: courseLibrariesQueryKeys.courseReadyToSyncLibraries({ queryKey: courseLibrariesQueryKeys.courseLibraries(courseKey),
courseId, queryFn: () => getEntityLinksByDownstreamContext(courseKey!),
readyToSync, enabled: courseKey !== undefined,
upstreamUsageKey,
}),
queryFn: () => getEntityLinks(
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,
}) })
); );

View File

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

View File

@@ -18,7 +18,7 @@ const messages = defineMessages({
}, },
homeTabDescription: { homeTabDescription: {
id: 'course-authoring.course-libraries.tab.home.description', 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', description: 'Description text for home tab',
}, },
homeTabDescriptionEmpty: { homeTabDescriptionEmpty: {
@@ -28,18 +28,18 @@ const messages = defineMessages({
}, },
reviewTabTitle: { reviewTabTitle: {
id: 'course-authoring.course-libraries.tab.review.title', id: 'course-authoring.course-libraries.tab.review.title',
defaultMessage: 'Review Content Updates', defaultMessage: 'Review Content Updates ({count})',
description: 'Tab title for review tab', description: 'Tab title for review tab',
}, },
reviewTabDescriptionEmpty: { reviewTabTitleEmpty: {
id: 'course-authoring.course-libraries.tab.home.description-no-links', id: 'course-authoring.course-libraries.tab.review.title-no-updates',
defaultMessage: 'All components are up to date', defaultMessage: 'Review Content Updates',
description: 'Description text for home tab', description: 'Tab title for review tab when no updates are available',
}, },
breadcrumbLabel: { breadcrumbAriaLabel: {
id: 'course-authoring.course-libraries.downstream-block.breadcrumb.label', id: 'course-authoring.course-libraries.downstream-block.breadcrumb.aria-label',
defaultMessage: 'Location:', defaultMessage: 'Component breadcrumb',
description: 'label for breadcrumb in component cards in course libraries page.', description: 'Aria label for breadcrumb in component cards in course libraries page.',
}, },
totalComponentLabel: { totalComponentLabel: {
id: 'course-authoring.course-libraries.libcard.total-component.label', id: 'course-authoring.course-libraries.libcard.total-component.label',
@@ -58,7 +58,7 @@ const messages = defineMessages({
}, },
outOfSyncCountAlertTitle: { outOfSyncCountAlertTitle: {
id: 'course-authoring.course-libraries.libcard.out-of-sync.alert.title', 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', description: 'Alert message shown when library components are out of sync',
}, },
reviewUpdatesBtn: { reviewUpdatesBtn: {
@@ -76,46 +76,6 @@ const messages = defineMessages({
defaultMessage: 'This page cannot be shown: Libraries v2 are disabled.', 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.', 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.',
},
}); });
export default messages; export default messages;

View File

@@ -22,7 +22,6 @@ import {
verticalListSortingStrategy, verticalListSortingStrategy,
} from '@dnd-kit/sortable'; } from '@dnd-kit/sortable';
import { useLocation } from 'react-router-dom'; import { useLocation } from 'react-router-dom';
import { CourseAuthoringOutlineSidebarSlot } from '../plugin-slots/CourseAuthoringOutlineSidebarSlot';
import { LoadingSpinner } from '../generic/Loading'; import { LoadingSpinner } from '../generic/Loading';
import { getProcessingNotification } from '../generic/processing-notification/data/selectors'; import { getProcessingNotification } from '../generic/processing-notification/data/selectors';
@@ -36,6 +35,8 @@ import AlertMessage from '../generic/alert-message';
import getPageHeadTitle from '../generic/utils'; import getPageHeadTitle from '../generic/utils';
import { getCurrentItem, getProctoredExamsFlag } from './data/selectors'; import { getCurrentItem, getProctoredExamsFlag } from './data/selectors';
import { COURSE_BLOCK_NAMES } from './constants'; 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 StatusBar from './status-bar/StatusBar';
import EnableHighlightsModal from './enable-highlights-modal/EnableHighlightsModal'; import EnableHighlightsModal from './enable-highlights-modal/EnableHighlightsModal';
import SectionCard from './section-card/SectionCard'; import SectionCard from './section-card/SectionCard';
@@ -45,16 +46,15 @@ import HighlightsModal from './highlights-modal/HighlightsModal';
import EmptyPlaceholder from './empty-placeholder/EmptyPlaceholder'; import EmptyPlaceholder from './empty-placeholder/EmptyPlaceholder';
import PublishModal from './publish-modal/PublishModal'; import PublishModal from './publish-modal/PublishModal';
import PageAlerts from './page-alerts/PageAlerts'; import PageAlerts from './page-alerts/PageAlerts';
import DraggableList from './drag-helper/DraggableList'; import DraggableList from '../generic/drag-helper/DraggableList';
import { import {
canMoveSection, canMoveSection,
possibleUnitMoves, possibleUnitMoves,
possibleSubsectionMoves, possibleSubsectionMoves,
} from './drag-helper/utils'; } from '../generic/drag-helper/utils';
import { useCourseOutline } from './hooks'; import { useCourseOutline } from './hooks';
import messages from './messages'; import messages from './messages';
import { getTagsExportFile } from './data/api'; import { getTagsExportFile } from './data/api';
import CourseOutlineHeaderActionsSlot from '../plugin-slots/CourseOutlineHeaderActionsSlot';
const CourseOutline = ({ courseId }) => { const CourseOutline = ({ courseId }) => {
const intl = useIntl(); const intl = useIntl();
@@ -103,12 +103,13 @@ const CourseOutline = ({ courseId }) => {
handleNewSectionSubmit, handleNewSectionSubmit,
handleNewSubsectionSubmit, handleNewSubsectionSubmit,
handleNewUnitSubmit, handleNewUnitSubmit,
handleAddUnitFromLibrary,
getUnitUrl, getUnitUrl,
handleVideoSharingOptionChange, handleVideoSharingOptionChange,
handleCopyToClipboardClick,
handlePasteClipboardClick, handlePasteClipboardClick,
notificationDismissUrl, notificationDismissUrl,
discussionsSettings, discussionsSettings,
discussionsIncontextFeedbackUrl,
discussionsIncontextLearnmoreUrl, discussionsIncontextLearnmoreUrl,
deprecatedBlocksInfo, deprecatedBlocksInfo,
proctoringErrors, proctoringErrors,
@@ -241,6 +242,7 @@ const CourseOutline = ({ courseId }) => {
notificationDismissUrl={notificationDismissUrl} notificationDismissUrl={notificationDismissUrl}
handleDismissNotification={handleDismissNotification} handleDismissNotification={handleDismissNotification}
discussionsSettings={discussionsSettings} discussionsSettings={discussionsSettings}
discussionsIncontextFeedbackUrl={discussionsIncontextFeedbackUrl}
discussionsIncontextLearnmoreUrl={discussionsIncontextLearnmoreUrl} discussionsIncontextLearnmoreUrl={discussionsIncontextLearnmoreUrl}
deprecatedBlocksInfo={deprecatedBlocksInfo} deprecatedBlocksInfo={deprecatedBlocksInfo}
proctoringErrors={proctoringErrors} proctoringErrors={proctoringErrors}
@@ -265,6 +267,7 @@ const CourseOutline = ({ courseId }) => {
notificationDismissUrl={notificationDismissUrl} notificationDismissUrl={notificationDismissUrl}
handleDismissNotification={handleDismissNotification} handleDismissNotification={handleDismissNotification}
discussionsSettings={discussionsSettings} discussionsSettings={discussionsSettings}
discussionsIncontextFeedbackUrl={discussionsIncontextFeedbackUrl}
discussionsIncontextLearnmoreUrl={discussionsIncontextLearnmoreUrl} discussionsIncontextLearnmoreUrl={discussionsIncontextLearnmoreUrl}
deprecatedBlocksInfo={deprecatedBlocksInfo} deprecatedBlocksInfo={deprecatedBlocksInfo}
proctoringErrors={proctoringErrors} proctoringErrors={proctoringErrors}
@@ -292,7 +295,7 @@ const CourseOutline = ({ courseId }) => {
title={intl.formatMessage(messages.headingTitle)} title={intl.formatMessage(messages.headingTitle)}
subtitle={intl.formatMessage(messages.headingSubtitle)} subtitle={intl.formatMessage(messages.headingSubtitle)}
headerActions={( headerActions={(
<CourseOutlineHeaderActionsSlot <HeaderNavigations
isReIndexShow={isReIndexShow} isReIndexShow={isReIndexShow}
isSectionsExpanded={isSectionsExpanded} isSectionsExpanded={isSectionsExpanded}
headerNavigationsActions={headerNavigationsActions} headerNavigationsActions={headerNavigationsActions}
@@ -300,7 +303,6 @@ const CourseOutline = ({ courseId }) => {
hasSections={Boolean(sectionsList.length)} hasSections={Boolean(sectionsList.length)}
courseActions={courseActions} courseActions={courseActions}
errors={errors} errors={errors}
sections={sections}
/> />
)} )}
/> />
@@ -375,7 +377,6 @@ const CourseOutline = ({ courseId }) => {
section, section,
section.childInfo.children, section.childInfo.children,
)} )}
isSectionsExpanded={isSectionsExpanded}
isSelfPaced={statusBarData.isSelfPaced} isSelfPaced={statusBarData.isSelfPaced}
isCustomRelativeDatesActive={isCustomRelativeDatesActive} isCustomRelativeDatesActive={isCustomRelativeDatesActive}
savingStatus={savingStatus} savingStatus={savingStatus}
@@ -385,7 +386,6 @@ const CourseOutline = ({ courseId }) => {
onDuplicateSubmit={handleDuplicateSubsectionSubmit} onDuplicateSubmit={handleDuplicateSubsectionSubmit}
onOpenConfigureModal={openConfigureModal} onOpenConfigureModal={openConfigureModal}
onNewUnitSubmit={handleNewUnitSubmit} onNewUnitSubmit={handleNewUnitSubmit}
onAddUnitFromLibrary={handleAddUnitFromLibrary}
onOrderChange={updateSubsectionOrderByIndex} onOrderChange={updateSubsectionOrderByIndex}
onPasteClick={handlePasteClipboardClick} onPasteClick={handlePasteClipboardClick}
> >
@@ -419,6 +419,7 @@ const CourseOutline = ({ courseId }) => {
onDuplicateSubmit={handleDuplicateUnitSubmit} onDuplicateSubmit={handleDuplicateUnitSubmit}
getTitleLink={getUnitUrl} getTitleLink={getUnitUrl}
onOrderChange={updateUnitOrderByIndex} onOrderChange={updateUnitOrderByIndex}
onCopyToClipboardClick={handleCopyToClipboardClick}
discussionsSettings={discussionsSettings} discussionsSettings={discussionsSettings}
/> />
))} ))}
@@ -456,11 +457,7 @@ const CourseOutline = ({ courseId }) => {
</article> </article>
</Layout.Element> </Layout.Element>
<Layout.Element> <Layout.Element>
<CourseAuthoringOutlineSidebarSlot <OutlineSideBar courseId={courseId} />
courseId={courseId}
courseName={courseName}
sections={sections}
/>
</Layout.Element> </Layout.Element>
</Layout> </Layout>
<EnableHighlightsModal <EnableHighlightsModal

View File

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

View File

@@ -58,18 +58,22 @@ import {
moveUnitOver, moveUnitOver,
moveSubsection, moveSubsection,
moveUnit, moveUnit,
} from './drag-helper/utils'; } from '../generic/drag-helper/utils';
import { postXBlockBaseApiUrl } from '../course-unit/data/api';
import { COMPONENT_TYPES } from '../generic/block-type-utils/constants';
let axiosMock; let axiosMock;
let store; let store;
const mockPathname = '/foo-bar'; const mockPathname = '/foo-bar';
const courseId = '123'; const courseId = '123';
const containerKey = 'lct:org:lib:unit:1';
window.HTMLElement.prototype.scrollIntoView = jest.fn(); 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.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'), ...jest.requireActual('react-router-dom'),
useLocation: jest.fn(), useLocation: jest.fn(),
@@ -96,24 +100,6 @@ jest.mock('./data/api', () => ({
getTagsCount: () => jest.fn().mockResolvedValue({}), getTagsCount: () => jest.fn().mockResolvedValue({}),
})); }));
// Mock ComponentPicker to call onComponentSelected on click
jest.mock('../library-authoring/component-picker', () => ({
ComponentPicker: (props) => {
const onClick = () => {
// eslint-disable-next-line react/prop-types
props.onComponentSelected({
usageKey: containerKey,
blockType: 'unti',
});
};
return (
<button type="submit" onClick={onClick}>
Dummy button
</button>
);
},
}));
const queryClient = new QueryClient(); const queryClient = new QueryClient();
jest.mock('@dnd-kit/core', () => ({ jest.mock('@dnd-kit/core', () => ({
@@ -153,9 +139,7 @@ describe('<CourseOutline />', () => {
pathname: mockPathname, pathname: mockPathname,
}); });
store = initializeStore({ store = initializeStore();
studioHome: { studioHomeData: { librariesV2Enabled: true } },
});
axiosMock = new MockAdapter(getAuthenticatedHttpClient()); axiosMock = new MockAdapter(getAuthenticatedHttpClient());
axiosMock axiosMock
.onGet(getCourseOutlineIndexApiUrl(courseId)) .onGet(getCourseOutlineIndexApiUrl(courseId))
@@ -174,10 +158,6 @@ describe('<CourseOutline />', () => {
await executeThunk(fetchCourseOutlineIndexQuery(courseId), store.dispatch); await executeThunk(fetchCourseOutlineIndexQuery(courseId), store.dispatch);
}); });
afterEach(() => {
jest.restoreAllMocks();
});
it('render CourseOutline component correctly', async () => { it('render CourseOutline component correctly', async () => {
const { getByText } = render(<RootWrapper />); const { getByText } = render(<RootWrapper />);
@@ -288,15 +268,13 @@ describe('<CourseOutline />', () => {
}); });
it('check that new section list is saved when dragged', async () => { it('check that new section list is saved when dragged', async () => {
const { findAllByRole, findByTestId } = render(<RootWrapper />); const { findAllByRole } = render(<RootWrapper />);
const expandAllButton = await findByTestId('expand-collapse-all-button'); const courseBlockId = courseOutlineIndexMock.courseStructure.id;
fireEvent.click(expandAllButton);
const [section] = store.getState().courseOutline.sectionsList;
const sectionsDraggers = await findAllByRole('button', { name: 'Drag to reorder' }); const sectionsDraggers = await findAllByRole('button', { name: 'Drag to reorder' });
const draggableButton = sectionsDraggers[1]; const draggableButton = sectionsDraggers[6];
axiosMock axiosMock
.onPut(getCourseBlockApiUrl(section.id)) .onPut(getCourseBlockApiUrl(courseBlockId))
.reply(200, { dummy: 'value' }); .reply(200, { dummy: 'value' });
const section1 = store.getState().courseOutline.sectionsList[0].id; const section1 = store.getState().courseOutline.sectionsList[0].id;
@@ -315,15 +293,13 @@ describe('<CourseOutline />', () => {
}); });
it('check section list is restored to original order when API call fails', async () => { it('check section list is restored to original order when API call fails', async () => {
const { findAllByRole, findByTestId } = render(<RootWrapper />); const { findAllByRole } = render(<RootWrapper />);
const expandAllButton = await findByTestId('expand-collapse-all-button'); const courseBlockId = courseOutlineIndexMock.courseStructure.id;
fireEvent.click(expandAllButton);
const [section] = store.getState().courseOutline.sectionsList;
const sectionsDraggers = await findAllByRole('button', { name: 'Drag to reorder' }); const sectionsDraggers = await findAllByRole('button', { name: 'Drag to reorder' });
const draggableButton = sectionsDraggers[1]; const draggableButton = sectionsDraggers[6];
axiosMock axiosMock
.onPut(getCourseBlockApiUrl(section.id)) .onPut(getCourseBlockApiUrl(courseBlockId))
.reply(500); .reply(500);
const section1 = store.getState().courseOutline.sectionsList[0].id; const section1 = store.getState().courseOutline.sectionsList[0].id;
@@ -398,6 +374,8 @@ describe('<CourseOutline />', () => {
const { findAllByTestId } = render(<RootWrapper />); const { findAllByTestId } = render(<RootWrapper />);
const [sectionElement] = await findAllByTestId('section-card'); const [sectionElement] = await findAllByTestId('section-card');
const [subsectionElement] = await within(sectionElement).findAllByTestId('subsection-card'); const [subsectionElement] = await within(sectionElement).findAllByTestId('subsection-card');
const expandBtn = await within(subsectionElement).findByTestId('subsection-card-header__expanded-btn');
fireEvent.click(expandBtn);
const units = await within(subsectionElement).findAllByTestId('unit-card'); const units = await within(subsectionElement).findAllByTestId('unit-card');
expect(units.length).toBe(1); expect(units.length).toBe(1);
@@ -418,40 +396,6 @@ describe('<CourseOutline />', () => {
})); }));
}); });
it('adds a unit from library correctly', async () => {
render(<RootWrapper />);
const [sectionElement] = await screen.findAllByTestId('section-card');
const [subsectionElement] = await within(sectionElement).findAllByTestId('subsection-card');
const units = await within(subsectionElement).findAllByTestId('unit-card');
expect(units.length).toBe(1);
axiosMock
.onPost(postXBlockBaseApiUrl())
.reply(200, {
locator: 'some',
});
const addUnitFromLibraryButton = within(subsectionElement).getByRole('button', {
name: /use unit from library/i,
});
fireEvent.click(addUnitFromLibraryButton);
// click dummy button to execute onComponentSelected prop.
const dummyBtn = await screen.findByRole('button', { name: 'Dummy button' });
fireEvent.click(dummyBtn);
waitFor(() => expect(axiosMock.history.post.length).toBe(1));
const [section] = courseOutlineIndexMock.courseStructure.childInfo.children;
const [subsection] = section.childInfo.children;
expect(axiosMock.history.post[0].data).toBe(JSON.stringify({
type: COMPONENT_TYPES.libraryV2,
category: 'vertical',
parent_locator: subsection.id,
library_content_key: containerKey,
}));
});
it('render checklist value correctly', async () => { it('render checklist value correctly', async () => {
const { getByText } = render(<RootWrapper />); const { getByText } = render(<RootWrapper />);
@@ -645,6 +589,8 @@ describe('<CourseOutline />', () => {
await checkEditTitle(section, subsectionElement, subsection, 'New subsection name', 'subsection'); await checkEditTitle(section, subsectionElement, subsection, 'New subsection name', 'subsection');
// check unit // check unit
const expandBtn = await within(subsectionElement).findByTestId('subsection-card-header__expanded-btn');
fireEvent.click(expandBtn);
const [unit] = subsection.childInfo.children; const [unit] = subsection.childInfo.children;
const [unitElement] = await within(subsectionElement).findAllByTestId('unit-card'); const [unitElement] = await within(subsectionElement).findAllByTestId('unit-card');
await checkEditTitle(section, unitElement, unit, 'New unit name', 'unit'); await checkEditTitle(section, unitElement, unit, 'New unit name', 'unit');
@@ -657,6 +603,8 @@ describe('<CourseOutline />', () => {
const [sectionElement] = await screen.findAllByTestId('section-card'); const [sectionElement] = await screen.findAllByTestId('section-card');
const [subsection] = section.childInfo.children; const [subsection] = section.childInfo.children;
const [subsectionElement] = await within(sectionElement).findAllByTestId('subsection-card'); const [subsectionElement] = await within(sectionElement).findAllByTestId('subsection-card');
const expandBtn = await within(subsectionElement).findByTestId('subsection-card-header__expanded-btn');
fireEvent.click(expandBtn);
const [unit] = subsection.childInfo.children; const [unit] = subsection.childInfo.children;
const [unitElement] = await within(subsectionElement).findAllByTestId('unit-card'); const [unitElement] = await within(subsectionElement).findAllByTestId('unit-card');
@@ -695,6 +643,8 @@ describe('<CourseOutline />', () => {
const [sectionElement] = await findAllByTestId('section-card'); const [sectionElement] = await findAllByTestId('section-card');
const [subsection] = section.childInfo.children; const [subsection] = section.childInfo.children;
const [subsectionElement] = await within(sectionElement).findAllByTestId('subsection-card'); const [subsectionElement] = await within(sectionElement).findAllByTestId('subsection-card');
const expandBtn = await within(subsectionElement).findByTestId('subsection-card-header__expanded-btn');
fireEvent.click(expandBtn);
const [unit] = subsection.childInfo.children; const [unit] = subsection.childInfo.children;
const [unitElement] = await within(subsectionElement).findAllByTestId('unit-card'); const [unitElement] = await within(subsectionElement).findAllByTestId('unit-card');
@@ -764,6 +714,8 @@ describe('<CourseOutline />', () => {
const [sectionElement] = await findAllByTestId('section-card'); const [sectionElement] = await findAllByTestId('section-card');
const [subsection] = section.childInfo.children; const [subsection] = section.childInfo.children;
const [subsectionElement] = await within(sectionElement).findAllByTestId('subsection-card'); const [subsectionElement] = await within(sectionElement).findAllByTestId('subsection-card');
const expandBtn = await within(subsectionElement).findByTestId('subsection-card-header__expanded-btn');
fireEvent.click(expandBtn);
const [unit] = subsection.childInfo.children; const [unit] = subsection.childInfo.children;
const [unitElement] = await within(subsectionElement).findAllByTestId('unit-card'); const [unitElement] = await within(subsectionElement).findAllByTestId('unit-card');
@@ -1472,6 +1424,8 @@ describe('<CourseOutline />', () => {
const [firstSection] = await findAllByTestId('section-card'); const [firstSection] = await findAllByTestId('section-card');
const [firstSubsection] = await within(firstSection).findAllByTestId('subsection-card'); const [firstSubsection] = await within(firstSection).findAllByTestId('subsection-card');
const subsectionExpandButton = await within(firstSubsection).getByTestId('subsection-card-header__expanded-btn');
fireEvent.click(subsectionExpandButton);
const [firstUnit] = await within(firstSubsection).findAllByTestId('unit-card'); const [firstUnit] = await within(firstSubsection).findAllByTestId('unit-card');
const unitDropdownButton = await within(firstUnit).findByTestId('unit-card-header__menu-button'); const unitDropdownButton = await within(firstUnit).findByTestId('unit-card-header__menu-button');
@@ -1831,6 +1785,8 @@ describe('<CourseOutline />', () => {
const [, sectionElement] = await findAllByTestId('section-card'); const [, sectionElement] = await findAllByTestId('section-card');
const [, subsection] = section.childInfo.children; const [, subsection] = section.childInfo.children;
const [, subsectionElement] = await within(sectionElement).findAllByTestId('subsection-card'); const [, subsectionElement] = await within(sectionElement).findAllByTestId('subsection-card');
const expandBtn = await within(subsectionElement).findByTestId('subsection-card-header__expanded-btn');
await act(async () => fireEvent.click(expandBtn));
const [, secondUnit] = subsection.childInfo.children; const [, secondUnit] = subsection.childInfo.children;
const [, unitElement] = await within(subsectionElement).findAllByTestId('unit-card'); const [, unitElement] = await within(subsectionElement).findAllByTestId('unit-card');
@@ -1870,6 +1826,8 @@ describe('<CourseOutline />', () => {
const [, sectionElement] = await findAllByTestId('section-card'); const [, sectionElement] = await findAllByTestId('section-card');
const [firstSubsection, subsection] = section.childInfo.children; const [firstSubsection, subsection] = section.childInfo.children;
const [, subsectionElement] = await within(sectionElement).findAllByTestId('subsection-card'); const [, subsectionElement] = await within(sectionElement).findAllByTestId('subsection-card');
const expandBtn = await within(subsectionElement).findByTestId('subsection-card-header__expanded-btn');
await act(async () => fireEvent.click(expandBtn));
const [unit] = subsection.childInfo.children; const [unit] = subsection.childInfo.children;
const [unitElement] = await within(subsectionElement).findAllByTestId('unit-card'); const [unitElement] = await within(subsectionElement).findAllByTestId('unit-card');
@@ -1905,6 +1863,8 @@ describe('<CourseOutline />', () => {
const [subsection] = secondSection.childInfo.children; const [subsection] = secondSection.childInfo.children;
const firstSectionLastSubsection = firstSection.childInfo.children[firstSection.childInfo.children.length - 1]; const firstSectionLastSubsection = firstSection.childInfo.children[firstSection.childInfo.children.length - 1];
const [subsectionElement] = await within(sectionElement).findAllByTestId('subsection-card'); const [subsectionElement] = await within(sectionElement).findAllByTestId('subsection-card');
const expandBtn = await within(subsectionElement).findByTestId('subsection-card-header__expanded-btn');
await act(async () => fireEvent.click(expandBtn));
const [unit] = subsection.childInfo.children; const [unit] = subsection.childInfo.children;
const [unitElement] = await within(subsectionElement).findAllByTestId('unit-card'); const [unitElement] = await within(subsectionElement).findAllByTestId('unit-card');
@@ -1949,6 +1909,8 @@ describe('<CourseOutline />', () => {
const [, sectionElement] = await findAllByTestId('section-card'); const [, sectionElement] = await findAllByTestId('section-card');
const [firstSubsection, subsection] = section.childInfo.children; const [firstSubsection, subsection] = section.childInfo.children;
const [subsectionElement] = await within(sectionElement).findAllByTestId('subsection-card'); const [subsectionElement] = await within(sectionElement).findAllByTestId('subsection-card');
const expandBtn = await within(subsectionElement).findByTestId('subsection-card-header__expanded-btn');
await act(async () => fireEvent.click(expandBtn));
const lastUnitIdx = firstSubsection.childInfo.children.length - 1; const lastUnitIdx = firstSubsection.childInfo.children.length - 1;
const unit = firstSubsection.childInfo.children[lastUnitIdx]; const unit = firstSubsection.childInfo.children[lastUnitIdx];
const unitElement = (await within(subsectionElement).findAllByTestId('unit-card'))[lastUnitIdx]; const unitElement = (await within(subsectionElement).findAllByTestId('unit-card'))[lastUnitIdx];
@@ -1986,6 +1948,8 @@ describe('<CourseOutline />', () => {
const secondSectionLastSubsection = secondSection.childInfo.children[lastSubIndex]; const secondSectionLastSubsection = secondSection.childInfo.children[lastSubIndex];
const thirdSectionFirstSubsection = thirdSection.childInfo.children[0]; const thirdSectionFirstSubsection = thirdSection.childInfo.children[0];
const subsectionElement = (await within(sectionElement).findAllByTestId('subsection-card'))[lastSubIndex]; const subsectionElement = (await within(sectionElement).findAllByTestId('subsection-card'))[lastSubIndex];
const expandBtn = await within(subsectionElement).findByTestId('subsection-card-header__expanded-btn');
await act(async () => fireEvent.click(expandBtn));
const lastUnitIdx = secondSectionLastSubsection.childInfo.children.length - 1; const lastUnitIdx = secondSectionLastSubsection.childInfo.children.length - 1;
const unit = secondSectionLastSubsection.childInfo.children[lastUnitIdx]; const unit = secondSectionLastSubsection.childInfo.children[lastUnitIdx];
const unitElement = (await within(subsectionElement).findAllByTestId('unit-card'))[lastUnitIdx]; const unitElement = (await within(subsectionElement).findAllByTestId('unit-card'))[lastUnitIdx];
@@ -2030,6 +1994,8 @@ describe('<CourseOutline />', () => {
const sections = await findAllByTestId('section-card'); const sections = await findAllByTestId('section-card');
const [sectionElement] = sections; const [sectionElement] = sections;
const [subsectionElement] = await within(sectionElement).findAllByTestId('subsection-card'); const [subsectionElement] = await within(sectionElement).findAllByTestId('subsection-card');
const expandBtn = await within(subsectionElement).findByTestId('subsection-card-header__expanded-btn');
await act(async () => fireEvent.click(expandBtn));
// get first and only unit in the subsection // get first and only unit in the subsection
const [firstUnit] = await within(subsectionElement).findAllByTestId('unit-card'); const [firstUnit] = await within(subsectionElement).findAllByTestId('unit-card');
@@ -2049,6 +2015,8 @@ describe('<CourseOutline />', () => {
const lastSection = sections[sections.length - 1]; const lastSection = sections[sections.length - 1];
// it has only one subsection // it has only one subsection
const [lastSubsectionElement] = await within(lastSection).findAllByTestId('subsection-card'); const [lastSubsectionElement] = await within(lastSection).findAllByTestId('subsection-card');
const lastExpandBtn = await within(lastSubsectionElement).findByTestId('subsection-card-header__expanded-btn');
await act(async () => fireEvent.click(lastExpandBtn));
// get last and the only unit in the subsection // get last and the only unit in the subsection
const [lastUnit] = await within(lastSubsectionElement).findAllByTestId('unit-card'); const [lastUnit] = await within(lastSubsectionElement).findAllByTestId('unit-card');
@@ -2069,9 +2037,6 @@ describe('<CourseOutline />', () => {
const { findAllByTestId } = render(<RootWrapper />); const { findAllByTestId } = render(<RootWrapper />);
const [sectionElement] = await findAllByTestId('section-card'); const [sectionElement] = await findAllByTestId('section-card');
const [subsectionElement] = await within(sectionElement).findAllByTestId('subsection-card');
const expandBtn = await within(subsectionElement).findByTestId('subsection-card-header__expanded-btn');
fireEvent.click(expandBtn);
const [section] = store.getState().courseOutline.sectionsList; const [section] = store.getState().courseOutline.sectionsList;
const subsectionsDraggers = within(sectionElement).getAllByRole('button', { name: 'Drag to reorder' }); const subsectionsDraggers = within(sectionElement).getAllByRole('button', { name: 'Drag to reorder' });
const draggableButton = subsectionsDraggers[1]; const draggableButton = subsectionsDraggers[1];
@@ -2103,9 +2068,6 @@ describe('<CourseOutline />', () => {
const { findAllByTestId } = render(<RootWrapper />); const { findAllByTestId } = render(<RootWrapper />);
const [sectionElement] = await findAllByTestId('section-card'); const [sectionElement] = await findAllByTestId('section-card');
const [subsectionElement] = await within(sectionElement).findAllByTestId('subsection-card');
const expandBtn = await within(subsectionElement).findByTestId('subsection-card-header__expanded-btn');
fireEvent.click(expandBtn);
const [section] = store.getState().courseOutline.sectionsList; const [section] = store.getState().courseOutline.sectionsList;
const subsectionsDraggers = within(sectionElement).getAllByRole('button', { name: 'Drag to reorder' }); const subsectionsDraggers = within(sectionElement).getAllByRole('button', { name: 'Drag to reorder' });
const draggableButton = subsectionsDraggers[1]; const draggableButton = subsectionsDraggers[1];
@@ -2135,6 +2097,8 @@ describe('<CourseOutline />', () => {
const [subsectionElement] = await within(sectionElement).findAllByTestId('subsection-card'); const [subsectionElement] = await within(sectionElement).findAllByTestId('subsection-card');
const section = store.getState().courseOutline.sectionsList[2]; const section = store.getState().courseOutline.sectionsList[2];
const [subsection] = section.childInfo.children; const [subsection] = section.childInfo.children;
const expandBtn = within(subsectionElement).getByTestId('subsection-card-header__expanded-btn');
fireEvent.click(expandBtn);
const unitDraggers = await within(subsectionElement).findAllByRole('button', { name: 'Drag to reorder' }); const unitDraggers = await within(subsectionElement).findAllByRole('button', { name: 'Drag to reorder' });
const draggableButton = unitDraggers[1]; const draggableButton = unitDraggers[1];
const sections = courseOutlineIndexMock.courseStructure.childInfo.children; const sections = courseOutlineIndexMock.courseStructure.childInfo.children;
@@ -2169,6 +2133,8 @@ describe('<CourseOutline />', () => {
const [subsectionElement] = await within(sectionElement).findAllByTestId('subsection-card'); const [subsectionElement] = await within(sectionElement).findAllByTestId('subsection-card');
const section = store.getState().courseOutline.sectionsList[2]; const section = store.getState().courseOutline.sectionsList[2];
const [subsection] = section.childInfo.children; const [subsection] = section.childInfo.children;
const expandBtn = within(subsectionElement).getByTestId('subsection-card-header__expanded-btn');
fireEvent.click(expandBtn);
const unitDraggers = await within(subsectionElement).findAllByRole('button', { name: 'Drag to reorder' }); const unitDraggers = await within(subsectionElement).findAllByRole('button', { name: 'Drag to reorder' });
const draggableButton = unitDraggers[1]; const draggableButton = unitDraggers[1];
const sections = courseOutlineIndexMock.courseStructure.childInfo.children; const sections = courseOutlineIndexMock.courseStructure.childInfo.children;
@@ -2206,6 +2172,8 @@ describe('<CourseOutline />', () => {
.onGet(getXBlockApiUrl(section.id)) .onGet(getXBlockApiUrl(section.id))
.reply(200, courseSectionMock); .reply(200, courseSectionMock);
let [subsectionElement] = await within(sectionElement).findAllByTestId('subsection-card'); let [subsectionElement] = await within(sectionElement).findAllByTestId('subsection-card');
const expandBtn = await within(subsectionElement).findByTestId('subsection-card-header__expanded-btn');
await act(async () => fireEvent.click(expandBtn));
const [unit] = subsection.childInfo.children; const [unit] = subsection.childInfo.children;
const [unitElement] = await within(subsectionElement).findAllByTestId('unit-card'); const [unitElement] = await within(subsectionElement).findAllByTestId('unit-card');
@@ -2214,6 +2182,9 @@ describe('<CourseOutline />', () => {
.onPost(getClipboardUrl(), { .onPost(getClipboardUrl(), {
usage_key: unit.id, usage_key: unit.id,
}).reply(200, clipboardUnit); }).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 // find menu button and click on it to open menu
const menu = await within(unitElement).findByTestId('unit-card-header__menu-button'); const menu = await within(unitElement).findByTestId('unit-card-header__menu-button');
@@ -2223,6 +2194,9 @@ describe('<CourseOutline />', () => {
const copyButton = await within(unitElement).findByText(cardHeaderMessages.menuCopy.defaultMessage); const copyButton = await within(unitElement).findByText(cardHeaderMessages.menuCopy.defaultMessage);
await act(async () => fireEvent.click(copyButton)); 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'); [subsectionElement] = await within(sectionElement).findAllByTestId('subsection-card');
// find clipboard content label // find clipboard content label
const clipboardLabel = await within(subsectionElement).findByText( const clipboardLabel = await within(subsectionElement).findByText(
@@ -2286,14 +2260,9 @@ describe('<CourseOutline />', () => {
it('should show toats on export tags', async () => { it('should show toats on export tags', async () => {
const expectedResponse = 'this is a test'; const expectedResponse = 'this is a test';
axiosMock
// Delay to ensure we see "Please wait."
// Without the delay the success message renders too quickly
const delayedResponse = axiosMock
.onGet(exportTags(courseId)) .onGet(exportTags(courseId))
.withDelayInMs(500); .reply(200, expectedResponse);
delayedResponse(200, expectedResponse);
useLocation.mockReturnValue({ useLocation.mockReturnValue({
pathname: '/foo-bar', pathname: '/foo-bar',
hash: '#export-tags', hash: '#export-tags',
@@ -2301,41 +2270,37 @@ describe('<CourseOutline />', () => {
window.URL.createObjectURL = jest.fn().mockReturnValue('http://example.com/archivo'); window.URL.createObjectURL = jest.fn().mockReturnValue('http://example.com/archivo');
window.URL.revokeObjectURL = jest.fn(); window.URL.revokeObjectURL = jest.fn();
render(<RootWrapper />); 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)); const expectedRequest = axiosMock.history.get.filter(request => request.url === exportTags(courseId));
expect(expectedRequest.length).toBe(1); 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 () => { it('should show toast on export tags error', async () => {
// Delay to ensure we see "Please wait." axiosMock
// Without the delay the error renders too quickly
const delayedResponse = axiosMock
.onGet(exportTags(courseId)) .onGet(exportTags(courseId))
.withDelayInMs(500); .reply(404);
delayedResponse(404);
useLocation.mockReturnValue({ useLocation.mockReturnValue({
pathname: '/foo-bar', pathname: '/foo-bar',
hash: '#export-tags', hash: '#export-tags',
}); });
render(<RootWrapper />); 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();
await screen.findByText('An error has occurred creating the file'); 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 axiosMock
.onGet(getCourseOutlineIndexApiUrl(courseId)) .onGet(getCourseOutlineIndexApiUrl(courseId))
.reply(403); .reply(403);
const { getByTestId } = render(<RootWrapper />); const { getByRole } = render(<RootWrapper />);
await waitFor(() => { await waitFor(() => {
expect(getByTestId('redux-provider')).toBeInTheDocument(); expect(getByRole('alert')).toBeInTheDocument();
const { outlineIndexLoadingStatus } = store.getState().courseOutline.loadingStatus; const { outlineIndexLoadingStatus } = store.getState().courseOutline.loadingStatus;
expect(outlineIndexLoadingStatus).toEqual(RequestStatus.DENIED); expect(outlineIndexLoadingStatus).toEqual(RequestStatus.DENIED);
}); });

View File

@@ -60,7 +60,7 @@ module.exports = {
highlightsEnabledForMessaging: false, highlightsEnabledForMessaging: false,
highlightsEnabled: true, highlightsEnabled: true,
highlightsPreviewOnly: false, 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, enableProctoredExams: true,
createZendeskTickets: true, createZendeskTickets: true,
enableTimedExams: true, enableTimedExams: true,
@@ -128,7 +128,7 @@ module.exports = {
], ],
highlightsEnabled: true, highlightsEnabled: true,
highlightsPreviewOnly: false, 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: { childInfo: {
category: 'sequential', category: 'sequential',
displayName: 'Subsection', displayName: 'Subsection',
@@ -292,11 +292,6 @@ module.exports = {
selectedPartitionIndex: -1, selectedPartitionIndex: -1,
selectedGroupsLabel: '', selectedGroupsLabel: '',
}, },
upstreamInfo: {
readyToSync: false,
upstreamRef: undefined,
versionSynced: undefined,
},
}, },
], ],
}, },
@@ -522,7 +517,7 @@ module.exports = {
], ],
highlightsEnabled: true, highlightsEnabled: true,
highlightsPreviewOnly: false, 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: { childInfo: {
category: 'sequential', category: 'sequential',
displayName: 'Subsection', displayName: 'Subsection',
@@ -680,11 +675,6 @@ module.exports = {
selectedPartitionIndex: -1, selectedPartitionIndex: -1,
selectedGroupsLabel: '', selectedGroupsLabel: '',
}, },
upstreamInfo: {
readyToSync: false,
upstreamRef: undefined,
versionSynced: undefined,
},
}, },
{ {
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@vertical_2dbb0072785e', id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@vertical_2dbb0072785e',
@@ -769,11 +759,6 @@ module.exports = {
selectedPartitionIndex: -1, selectedPartitionIndex: -1,
selectedGroupsLabel: '', selectedGroupsLabel: '',
}, },
upstreamInfo: {
readyToSync: false,
upstreamRef: undefined,
versionSynced: undefined,
},
}, },
{ {
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@vertical_98cf62510471', id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@vertical_98cf62510471',
@@ -858,11 +843,6 @@ module.exports = {
selectedPartitionIndex: -1, selectedPartitionIndex: -1,
selectedGroupsLabel: '', selectedGroupsLabel: '',
}, },
upstreamInfo: {
readyToSync: false,
upstreamRef: undefined,
versionSynced: undefined,
},
}, },
{ {
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@vertical_d32bf9b2242c', id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@vertical_d32bf9b2242c',
@@ -947,11 +927,6 @@ module.exports = {
selectedPartitionIndex: -1, selectedPartitionIndex: -1,
selectedGroupsLabel: '', selectedGroupsLabel: '',
}, },
upstreamInfo: {
readyToSync: false,
upstreamRef: undefined,
versionSynced: undefined,
},
}, },
{ {
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@4e592689563243c484af947465eaef0d', id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@4e592689563243c484af947465eaef0d',
@@ -1036,11 +1011,6 @@ module.exports = {
selectedPartitionIndex: -1, selectedPartitionIndex: -1,
selectedGroupsLabel: '', selectedGroupsLabel: '',
}, },
upstreamInfo: {
readyToSync: false,
upstreamRef: undefined,
versionSynced: undefined,
},
}, },
], ],
}, },
@@ -1226,11 +1196,6 @@ module.exports = {
selectedPartitionIndex: -1, selectedPartitionIndex: -1,
selectedGroupsLabel: '', selectedGroupsLabel: '',
}, },
upstreamInfo: {
readyToSync: false,
upstreamRef: undefined,
versionSynced: undefined,
},
}, },
{ {
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@vertical_aae927868e55', id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@vertical_aae927868e55',
@@ -1315,11 +1280,6 @@ module.exports = {
selectedPartitionIndex: -1, selectedPartitionIndex: -1,
selectedGroupsLabel: '', selectedGroupsLabel: '',
}, },
upstreamInfo: {
readyToSync: false,
upstreamRef: undefined,
versionSynced: undefined,
},
}, },
{ {
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@vertical_c037f3757df1', id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@vertical_c037f3757df1',
@@ -1404,11 +1364,6 @@ module.exports = {
selectedPartitionIndex: -1, selectedPartitionIndex: -1,
selectedGroupsLabel: '', selectedGroupsLabel: '',
}, },
upstreamInfo: {
readyToSync: false,
upstreamRef: undefined,
versionSynced: undefined,
},
}, },
{ {
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@vertical_bc69a47c6fae', id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@vertical_bc69a47c6fae',
@@ -1493,11 +1448,6 @@ module.exports = {
selectedPartitionIndex: -1, selectedPartitionIndex: -1,
selectedGroupsLabel: '', selectedGroupsLabel: '',
}, },
upstreamInfo: {
readyToSync: false,
upstreamRef: undefined,
versionSynced: undefined,
},
}, },
{ {
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@8f89194410954e768bde1764985454a7', id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@8f89194410954e768bde1764985454a7',
@@ -1582,11 +1532,6 @@ module.exports = {
selectedPartitionIndex: -1, selectedPartitionIndex: -1,
selectedGroupsLabel: '', selectedGroupsLabel: '',
}, },
upstreamInfo: {
readyToSync: false,
upstreamRef: undefined,
versionSynced: undefined,
},
}, },
], ],
}, },
@@ -1772,11 +1717,6 @@ module.exports = {
selectedPartitionIndex: -1, selectedPartitionIndex: -1,
selectedGroupsLabel: '', selectedGroupsLabel: '',
}, },
upstreamInfo: {
readyToSync: false,
upstreamRef: undefined,
versionSynced: undefined,
},
}, },
], ],
}, },
@@ -1897,7 +1837,7 @@ module.exports = {
highlights: [], highlights: [],
highlightsEnabled: true, highlightsEnabled: true,
highlightsPreviewOnly: false, 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: { childInfo: {
category: 'sequential', category: 'sequential',
displayName: 'Subsection', displayName: 'Subsection',
@@ -2055,11 +1995,6 @@ module.exports = {
selectedPartitionIndex: -1, selectedPartitionIndex: -1,
selectedGroupsLabel: '', selectedGroupsLabel: '',
}, },
upstreamInfo: {
readyToSync: false,
upstreamRef: undefined,
versionSynced: undefined,
},
}, },
{ {
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@vertical_f04afeac0131', id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@vertical_f04afeac0131',
@@ -2144,11 +2079,6 @@ module.exports = {
selectedPartitionIndex: -1, selectedPartitionIndex: -1,
selectedGroupsLabel: '', selectedGroupsLabel: '',
}, },
upstreamInfo: {
readyToSync: false,
upstreamRef: undefined,
versionSynced: undefined,
},
}, },
{ {
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@b6662b497c094bcc9b870d8270c90c93', id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@b6662b497c094bcc9b870d8270c90c93',
@@ -2233,11 +2163,6 @@ module.exports = {
selectedPartitionIndex: -1, selectedPartitionIndex: -1,
selectedGroupsLabel: '', selectedGroupsLabel: '',
}, },
upstreamInfo: {
readyToSync: false,
upstreamRef: undefined,
versionSynced: undefined,
},
}, },
{ {
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@f91d8d31f7cf48ce990f8d8745ae4cfa', id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@f91d8d31f7cf48ce990f8d8745ae4cfa',
@@ -2322,11 +2247,6 @@ module.exports = {
selectedPartitionIndex: -1, selectedPartitionIndex: -1,
selectedGroupsLabel: '', selectedGroupsLabel: '',
}, },
upstreamInfo: {
readyToSync: false,
upstreamRef: undefined,
versionSynced: undefined,
},
}, },
{ {
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@vertical_ac391cde8a91', id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@vertical_ac391cde8a91',
@@ -2411,11 +2331,6 @@ module.exports = {
selectedPartitionIndex: -1, selectedPartitionIndex: -1,
selectedGroupsLabel: '', selectedGroupsLabel: '',
}, },
upstreamInfo: {
readyToSync: false,
upstreamRef: undefined,
versionSynced: undefined,
},
}, },
{ {
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@vertical_36e0beb03f0a', id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@vertical_36e0beb03f0a',
@@ -2500,11 +2415,6 @@ module.exports = {
selectedPartitionIndex: -1, selectedPartitionIndex: -1,
selectedGroupsLabel: '', selectedGroupsLabel: '',
}, },
upstreamInfo: {
readyToSync: false,
upstreamRef: undefined,
versionSynced: undefined,
},
}, },
{ {
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@1b0e2c2c84884b95b1c99fb678cc964c', id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@1b0e2c2c84884b95b1c99fb678cc964c',
@@ -2589,11 +2499,6 @@ module.exports = {
selectedPartitionIndex: -1, selectedPartitionIndex: -1,
selectedGroupsLabel: '', selectedGroupsLabel: '',
}, },
upstreamInfo: {
readyToSync: false,
upstreamRef: undefined,
versionSynced: undefined,
},
}, },
{ {
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@c7e98fd39a6944edb6b286c32e1150ff', id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@c7e98fd39a6944edb6b286c32e1150ff',
@@ -2678,11 +2583,6 @@ module.exports = {
selectedPartitionIndex: -1, selectedPartitionIndex: -1,
selectedGroupsLabel: '', selectedGroupsLabel: '',
}, },
upstreamInfo: {
readyToSync: false,
upstreamRef: undefined,
versionSynced: undefined,
},
}, },
{ {
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@d6eaa391d2be41dea20b8b1bfbcb1c45', id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@d6eaa391d2be41dea20b8b1bfbcb1c45',
@@ -2767,11 +2667,6 @@ module.exports = {
selectedPartitionIndex: -1, selectedPartitionIndex: -1,
selectedGroupsLabel: '', selectedGroupsLabel: '',
}, },
upstreamInfo: {
readyToSync: false,
upstreamRef: undefined,
versionSynced: undefined,
},
}, },
], ],
}, },
@@ -2892,7 +2787,7 @@ module.exports = {
highlights: [], highlights: [],
highlightsEnabled: true, highlightsEnabled: true,
highlightsPreviewOnly: false, 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: { childInfo: {
category: 'sequential', category: 'sequential',
displayName: 'Subsection', displayName: 'Subsection',
@@ -3050,11 +2945,6 @@ module.exports = {
selectedPartitionIndex: -1, selectedPartitionIndex: -1,
selectedGroupsLabel: '', selectedGroupsLabel: '',
}, },
upstreamInfo: {
readyToSync: false,
upstreamRef: undefined,
versionSynced: undefined,
},
}, },
], ],
}, },
@@ -3154,6 +3044,7 @@ module.exports = {
blocks: [], blocks: [],
advanceSettingsUrl: '/settings/advanced/course-v1:edx+101+y76', advanceSettingsUrl: '/settings/advanced/course-v1:edx+101+y76',
}, },
discussionsIncontextFeedbackUrl: '',
discussionsIncontextLearnmoreUrl: '', discussionsIncontextLearnmoreUrl: '',
initialState: { initialState: {
expandedLocators: [ expandedLocators: [

View File

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

View File

@@ -55,7 +55,7 @@ module.exports = {
highlights: [], highlights: [],
highlights_enabled: true, highlights_enabled: true,
highlights_preview_only: false, 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: { child_info: {
category: 'sequential', category: 'sequential',
display_name: 'Subsection', display_name: 'Subsection',

View File

@@ -15,7 +15,6 @@ import {
import { import {
MoreVert as MoveVertIcon, MoreVert as MoveVertIcon,
EditOutline as EditIcon, EditOutline as EditIcon,
Sync as SyncIcon,
} from '@openedx/paragon/icons'; } from '@openedx/paragon/icons';
import { useContentTagsCount } from '../../generic/data/apiHooks'; import { useContentTagsCount } from '../../generic/data/apiHooks';
@@ -55,9 +54,6 @@ const CardHeader = ({
discussionEnabled, discussionEnabled,
discussionsSettings, discussionsSettings,
parentInfo, parentInfo,
extraActionsComponent,
onClickSync,
readyToSync,
}) => { }) => {
const intl = useIntl(); const intl = useIntl();
const [searchParams] = useSearchParams(); const [searchParams] = useSearchParams();
@@ -133,23 +129,12 @@ const CardHeader = ({
) : ( ) : (
<> <>
{titleComponent} {titleComponent}
{readyToSync && (
<IconButton
className="item-card-button-icon"
data-testid={`${namePrefix}-sync-button`}
alt={intl.formatMessage(messages.readyToSyncButtonAlt)}
iconAs={SyncIcon}
onClick={onClickSync}
/>
)}
<IconButton <IconButton
className="item-card-button-icon" className="item-card-edit-icon"
data-testid={`${namePrefix}-edit-button`} data-testid={`${namePrefix}-edit-button`}
alt={intl.formatMessage(messages.altButtonEdit)} alt={intl.formatMessage(messages.altButtonEdit)}
iconAs={EditIcon} iconAs={EditIcon}
onClick={onClickEdit} onClick={onClickEdit}
// @ts-ignore
disabled={isDisabledEditField}
/> />
</> </>
)} )}
@@ -160,7 +145,6 @@ const CardHeader = ({
{ getConfig().ENABLE_TAGGING_TAXONOMY_PAGES === 'true' && !!contentTagCount && ( { getConfig().ENABLE_TAGGING_TAXONOMY_PAGES === 'true' && !!contentTagCount && (
<TagCount count={contentTagCount} onClick={openManageTagsDrawer} /> <TagCount count={contentTagCount} onClick={openManageTagsDrawer} />
)} )}
{extraActionsComponent}
<Dropdown data-testid={`${namePrefix}-card-header__menu`} onClick={onClickMenuButton}> <Dropdown data-testid={`${namePrefix}-card-header__menu`} onClick={onClickMenuButton}>
<Dropdown.Toggle <Dropdown.Toggle
className="item-card-header__menu" className="item-card-header__menu"
@@ -192,7 +176,6 @@ const CardHeader = ({
</Dropdown.Item> </Dropdown.Item>
<Dropdown.Item <Dropdown.Item
data-testid={`${namePrefix}-card-header__menu-configure-button`} data-testid={`${namePrefix}-card-header__menu-configure-button`}
disabled={isDisabledEditField}
onClick={onClickConfigure} onClick={onClickConfigure}
> >
{intl.formatMessage(messages.menuConfigure)} {intl.formatMessage(messages.menuConfigure)}
@@ -200,7 +183,6 @@ const CardHeader = ({
{getConfig().ENABLE_TAGGING_TAXONOMY_PAGES === 'true' && ( {getConfig().ENABLE_TAGGING_TAXONOMY_PAGES === 'true' && (
<Dropdown.Item <Dropdown.Item
data-testid={`${namePrefix}-card-header__menu-manage-tags-button`} data-testid={`${namePrefix}-card-header__menu-manage-tags-button`}
disabled={isDisabledEditField}
onClick={openManageTagsDrawer} onClick={openManageTagsDrawer}
> >
{intl.formatMessage(messages.menuManageTags)} {intl.formatMessage(messages.menuManageTags)}
@@ -270,9 +252,6 @@ CardHeader.defaultProps = {
discussionsSettings: {}, discussionsSettings: {},
parentInfo: {}, parentInfo: {},
cardId: '', cardId: '',
extraActionsComponent: null,
readyToSync: false,
onClickSync: null,
}; };
CardHeader.propTypes = { CardHeader.propTypes = {
@@ -316,11 +295,6 @@ CardHeader.propTypes = {
isTimeLimited: PropTypes.bool, isTimeLimited: PropTypes.bool,
graded: 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,
onClickSync: PropTypes.func,
readyToSync: PropTypes.bool,
}; };
export default CardHeader; export default CardHeader;

View File

@@ -12,7 +12,7 @@
color: $black; color: $black;
} }
.item-card-button-icon { .item-card-edit-icon {
opacity: 0; opacity: 0;
transition: opacity .3s linear; transition: opacity .3s linear;
margin-right: .5rem; margin-right: .5rem;
@@ -23,7 +23,7 @@
} }
&:hover { &:hover {
.item-card-button-icon { .item-card-edit-icon {
opacity: 1; opacity: 1;
} }
} }

View File

@@ -240,35 +240,6 @@ describe('<CardHeader />', () => {
expect(await findByTestId('subsection-edit-field')).toBeDisabled(); expect(await findByTestId('subsection-edit-field')).toBeDisabled();
}); });
it('check editing is enabled when isDisabledEditField is false', async () => {
const { getByTestId } = renderComponent({
...cardHeaderProps,
});
expect(getByTestId('subsection-edit-button')).toBeEnabled();
// Ensure menu items related to editing are enabled
const menuButton = getByTestId('subsection-card-header__menu-button');
await act(async () => fireEvent.click(menuButton));
expect(await getByTestId('subsection-card-header__menu-configure-button')).not.toHaveAttribute('aria-disabled');
expect(await getByTestId('subsection-card-header__menu-manage-tags-button')).not.toHaveAttribute('aria-disabled');
});
it('check editing is disabled when isDisabledEditField is true', async () => {
const { getByTestId } = renderComponent({
...cardHeaderProps,
isDisabledEditField: true,
});
expect(await getByTestId('subsection-edit-button')).toBeDisabled();
// Ensure menu items related to editing are disabled
const menuButton = getByTestId('subsection-card-header__menu-button');
await act(async () => fireEvent.click(menuButton));
expect(await getByTestId('subsection-card-header__menu-configure-button')).toHaveAttribute('aria-disabled', 'true');
expect(await getByTestId('subsection-card-header__menu-manage-tags-button')).toHaveAttribute('aria-disabled', 'true');
});
it('calls onClickDelete when item is clicked', async () => { it('calls onClickDelete when item is clicked', async () => {
const { findByText, findByTestId } = renderComponent(); const { findByText, findByTestId } = renderComponent();
@@ -368,19 +339,4 @@ describe('<CardHeader />', () => {
renderComponent(); renderComponent();
expect(screen.queryByText('0')).not.toBeInTheDocument(); expect(screen.queryByText('0')).not.toBeInTheDocument();
}); });
it('should render sync button when is ready to sync', () => {
const mockClickSync = jest.fn();
renderComponent({
readyToSync: true,
onClickSync: mockClickSync,
});
const syncButton = screen.getByRole('button', { name: /update available - click to sync/i });
expect(syncButton).toBeInTheDocument();
fireEvent.click(syncButton);
expect(mockClickSync).toHaveBeenCalled();
});
}); });

View File

@@ -77,11 +77,6 @@ const messages = defineMessages({
id: 'course-authoring.course-outline.card.menu.manageTags', id: 'course-authoring.course-outline.card.menu.manageTags',
defaultMessage: 'Manage tags', defaultMessage: 'Manage tags',
}, },
readyToSyncButtonAlt: {
id: 'course-authoring.course-outline.card.button.sync.alt',
defaultMessage: 'Update available - click to sync',
description: 'Alt text for the sync icon button.',
},
}); });
export default messages; export default messages;

View File

@@ -28,6 +28,7 @@ export const getCourseReindexApiUrl = (reindexLink) => `${getApiBaseUrl()}${rein
export const getXBlockBaseApiUrl = () => `${getApiBaseUrl()}/xblock/`; export const getXBlockBaseApiUrl = () => `${getApiBaseUrl()}/xblock/`;
export const getCourseItemApiUrl = (itemId) => `${getXBlockBaseApiUrl()}${itemId}`; export const getCourseItemApiUrl = (itemId) => `${getXBlockBaseApiUrl()}${itemId}`;
export const getXBlockApiUrl = (blockId) => `${getXBlockBaseApiUrl()}outline/${blockId}`; 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/`; 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 {string} courseReleaseDate
* @property {Object} courseStructure * @property {Object} courseStructure
* @property {Object} deprecatedBlocksInfo * @property {Object} deprecatedBlocksInfo
* @property {string} discussionsIncontextFeedbackUrl
* @property {string} discussionsIncontextLearnmoreUrl * @property {string} discussionsIncontextLearnmoreUrl
* @property {Object} initialState * @property {Object} initialState
* @property {Object} initialUserClipboard * @property {Object} initialUserClipboard

View File

@@ -1,4 +1,5 @@
import { RequestStatus } from '../../data/constants'; import { RequestStatus } from '../../data/constants';
import { updateClipboardData } from '../../generic/data/slice';
import { NOTIFICATION_MESSAGES } from '../../constants'; import { NOTIFICATION_MESSAGES } from '../../constants';
import { COURSE_BLOCK_NAMES } from '../constants'; import { COURSE_BLOCK_NAMES } from '../constants';
import { import {
@@ -53,7 +54,6 @@ import {
setPasteFileNotices, setPasteFileNotices,
updateCourseLaunchQueryStatus, updateCourseLaunchQueryStatus,
} from './slice'; } from './slice';
import { createCourseXblock } from '../../course-unit/data/api';
export function fetchCourseOutlineIndexQuery(courseId) { export function fetchCourseOutlineIndexQuery(courseId) {
return async (dispatch) => { return async (dispatch) => {
@@ -71,6 +71,7 @@ export function fetchCourseOutlineIndexQuery(courseId) {
}, },
} = outlineIndex; } = outlineIndex;
dispatch(fetchOutlineIndexSuccess(outlineIndex)); dispatch(fetchOutlineIndexSuccess(outlineIndex));
dispatch(updateClipboardData(outlineIndex.initialUserClipboard));
dispatch(updateStatusBar({ dispatch(updateStatusBar({
courseReleaseDate, courseReleaseDate,
highlightsEnabledForMessaging, highlightsEnabledForMessaging,
@@ -541,26 +542,6 @@ export function addNewUnitQuery(parentLocator, callback) {
}; };
} }
export function addUnitFromLibrary(body, callback) {
return async (dispatch) => {
dispatch(updateSavingStatus({ status: RequestStatus.PENDING }));
dispatch(showProcessingNotification(NOTIFICATION_MESSAGES.saving));
try {
await createCourseXblock(body).then(async (result) => {
if (result) {
dispatch(updateSavingStatus({ status: RequestStatus.SUCCESSFUL }));
dispatch(hideProcessingNotification());
callback(result.locator);
}
});
} catch (error) /* istanbul ignore next */ {
dispatch(hideProcessingNotification());
dispatch(updateSavingStatus({ status: RequestStatus.FAILED }));
}
};
}
function setBlockOrderListQuery( function setBlockOrderListQuery(
parentId, parentId,
blockIds, blockIds,

View File

@@ -25,7 +25,7 @@ const HeaderNavigations = ({
} = headerNavigationsActions; } = headerNavigationsActions;
return ( return (
<> <nav className="header-navigations ml-auto">
{courseActions.childAddable && ( {courseActions.childAddable && (
<OverlayTrigger <OverlayTrigger
placement="bottom" placement="bottom"
@@ -66,8 +66,6 @@ const HeaderNavigations = ({
{hasSections && ( {hasSections && (
<Button <Button
variant="outline-primary" variant="outline-primary"
id="expand-collapse-all-button"
data-testid="expand-collapse-all-button"
iconBefore={isSectionsExpanded ? ArrowUpIcon : ArrowDownIcon} iconBefore={isSectionsExpanded ? ArrowUpIcon : ArrowDownIcon}
onClick={handleExpandAll} onClick={handleExpandAll}
> >
@@ -92,7 +90,7 @@ const HeaderNavigations = ({
{intl.formatMessage(messages.viewLiveButton)} {intl.formatMessage(messages.viewLiveButton)}
</Button> </Button>
</OverlayTrigger> </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 { useToggle } from '@openedx/paragon';
import { getConfig } from '@edx/frontend-platform'; import { getConfig } from '@edx/frontend-platform';
import { copyToClipboard } from '../generic/data/thunks';
import { getSavingStatus as getGenericSavingStatus } from '../generic/data/selectors'; import { getSavingStatus as getGenericSavingStatus } from '../generic/data/selectors';
import { getWaffleFlags } from '../data/selectors'; import { getWaffleFlags } from '../data/selectors';
import { RequestStatus } from '../data/constants'; import { RequestStatus } from '../data/constants';
@@ -53,7 +54,6 @@ import {
setUnitOrderListQuery, setUnitOrderListQuery,
pasteClipboardContent, pasteClipboardContent,
dismissNotificationQuery, dismissNotificationQuery,
addUnitFromLibrary,
} from './data/thunk'; } from './data/thunk';
const useCourseOutline = ({ courseId }) => { const useCourseOutline = ({ courseId }) => {
@@ -67,13 +67,13 @@ const useCourseOutline = ({ courseId }) => {
lmsLink, lmsLink,
notificationDismissUrl, notificationDismissUrl,
discussionsSettings, discussionsSettings,
discussionsIncontextFeedbackUrl,
discussionsIncontextLearnmoreUrl, discussionsIncontextLearnmoreUrl,
deprecatedBlocksInfo, deprecatedBlocksInfo,
proctoringErrors, proctoringErrors,
mfeProctoredExamSettingsUrl, mfeProctoredExamSettingsUrl,
advanceSettingsUrl, advanceSettingsUrl,
} = useSelector(getOutlineIndexData); } = useSelector(getOutlineIndexData);
const { outlineIndexLoadingStatus, reIndexLoadingStatus } = useSelector(getLoadingStatus); const { outlineIndexLoadingStatus, reIndexLoadingStatus } = useSelector(getLoadingStatus);
const statusBarData = useSelector(getStatusBarData); const statusBarData = useSelector(getStatusBarData);
const savingStatus = useSelector(getSavingStatus); const savingStatus = useSelector(getSavingStatus);
@@ -97,6 +97,10 @@ const useCourseOutline = ({ courseId }) => {
const isSavingStatusFailed = savingStatus === RequestStatus.FAILED || genericSavingStatus === RequestStatus.FAILED; const isSavingStatusFailed = savingStatus === RequestStatus.FAILED || genericSavingStatus === RequestStatus.FAILED;
const handleCopyToClipboardClick = (usageKey) => {
dispatch(copyToClipboard(usageKey));
};
const handlePasteClipboardClick = (parentLocator, sectionId) => { const handlePasteClipboardClick = (parentLocator, sectionId) => {
dispatch(pasteClipboardContent(parentLocator, sectionId)); dispatch(pasteClipboardContent(parentLocator, sectionId));
}; };
@@ -129,10 +133,6 @@ const useCourseOutline = ({ courseId }) => {
dispatch(addNewUnitQuery(subsectionId, openUnitPage)); dispatch(addNewUnitQuery(subsectionId, openUnitPage));
}; };
const handleAddUnitFromLibrary = (body) => {
dispatch(addUnitFromLibrary(body, openUnitPage));
};
const headerNavigationsActions = { const headerNavigationsActions = {
handleNewSection: handleNewSectionSubmit, handleNewSection: handleNewSectionSubmit,
handleReIndex: () => { handleReIndex: () => {
@@ -341,11 +341,12 @@ const useCourseOutline = ({ courseId }) => {
getUnitUrl, getUnitUrl,
openUnitPage, openUnitPage,
handleNewUnitSubmit, handleNewUnitSubmit,
handleAddUnitFromLibrary,
handleVideoSharingOptionChange, handleVideoSharingOptionChange,
handleCopyToClipboardClick,
handlePasteClipboardClick, handlePasteClipboardClick,
notificationDismissUrl, notificationDismissUrl,
discussionsSettings, discussionsSettings,
discussionsIncontextFeedbackUrl,
discussionsIncontextLearnmoreUrl, discussionsIncontextLearnmoreUrl,
deprecatedBlocksInfo, deprecatedBlocksInfo,
proctoringErrors, proctoringErrors,

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,37 +1,30 @@
// @ts-check // @ts-check
import React, { import React, {
useContext, useEffect, useState, useRef, useCallback, useContext, useEffect, useState, useRef,
} from 'react'; } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { useDispatch, useSelector } from 'react-redux'; import { useDispatch } from 'react-redux';
import { useSearchParams } from 'react-router-dom'; import { useSearchParams } from 'react-router-dom';
import { useIntl } from '@edx/frontend-platform/i18n'; import { useIntl } from '@edx/frontend-platform/i18n';
import { Button, StandardModal, useToggle } from '@openedx/paragon'; import { Button, useToggle } from '@openedx/paragon';
import { Add as IconAdd } from '@openedx/paragon/icons'; import { Add as IconAdd } from '@openedx/paragon/icons';
import classNames from 'classnames'; import classNames from 'classnames';
import { isEmpty } from 'lodash'; import { isEmpty } from 'lodash';
import CourseOutlineSubsectionCardExtraActionsSlot from '../../plugin-slots/CourseOutlineSubsectionCardExtraActionsSlot';
import { setCurrentItem, setCurrentSection, setCurrentSubsection } from '../data/slice'; import { setCurrentItem, setCurrentSection, setCurrentSubsection } from '../data/slice';
import { RequestStatus } from '../../data/constants'; import { RequestStatus } from '../../data/constants';
import CardHeader from '../card-header/CardHeader'; import CardHeader from '../card-header/CardHeader';
import SortableItem from '../drag-helper/SortableItem'; import SortableItem from '../../generic/drag-helper/SortableItem';
import { DragContext } from '../drag-helper/DragContextProvider'; import { DragContext } from '../../generic/drag-helper/DragContextProvider';
import { useClipboard, PasteComponent } from '../../generic/clipboard'; import { useCopyToClipboard, PasteComponent } from '../../generic/clipboard';
import TitleButton from '../card-header/TitleButton'; import TitleButton from '../card-header/TitleButton';
import XBlockStatus from '../xblock-status/XBlockStatus'; import XBlockStatus from '../xblock-status/XBlockStatus';
import { getItemStatus, getItemStatusBorder, scrollToElement } from '../utils'; import { getItemStatus, getItemStatusBorder, scrollToElement } from '../utils';
import messages from './messages'; import messages from './messages';
import { ComponentPicker } from '../../library-authoring';
import { COMPONENT_TYPES } from '../../generic/block-type-utils/constants';
import { ContainerType } from '../../generic/key-utils';
import { ContentType } from '../../library-authoring/routes';
import { getStudioHomeData } from '../../studio-home/data/selectors';
const SubsectionCard = ({ const SubsectionCard = ({
section, section,
subsection, subsection,
isSectionsExpanded,
isSelfPaced, isSelfPaced,
isCustomRelativeDatesActive, isCustomRelativeDatesActive,
children, children,
@@ -43,7 +36,6 @@ const SubsectionCard = ({
onOpenDeleteModal, onOpenDeleteModal,
onDuplicateSubmit, onDuplicateSubmit,
onNewUnitSubmit, onNewUnitSubmit,
onAddUnitFromLibrary,
onOrderChange, onOrderChange,
onOpenConfigureModal, onOpenConfigureModal,
onPasteClick, onPasteClick,
@@ -57,18 +49,7 @@ const SubsectionCard = ({
const isScrolledToElement = locatorId === subsection.id; const isScrolledToElement = locatorId === subsection.id;
const [isFormOpen, openForm, closeForm] = useToggle(false); const [isFormOpen, openForm, closeForm] = useToggle(false);
const namePrefix = 'subsection'; const namePrefix = 'subsection';
const { sharedClipboardData, showPasteUnit } = useClipboard(); const { sharedClipboardData, showPasteUnit } = useCopyToClipboard();
// WARNING: Do not use "useStudioHome" to get "librariesV2Enabled" flag below,
// as it has a useEffect that fetches course waffle flags whenever
// location.search is updated. Course search updates location.search when
// user types, which will then trigger the useEffect and reload the page.
// See https://github.com/openedx/frontend-app-authoring/pull/1938.
const { librariesV2Enabled } = useSelector(getStudioHomeData);
const [
isAddLibraryUnitModalOpen,
openAddLibraryUnitModal,
closeAddLibraryUnitModal,
] = useToggle(false);
const { const {
id, id,
@@ -99,7 +80,7 @@ const SubsectionCard = ({
return false; return false;
}; };
const [isExpanded, setIsExpanded] = useState(containsSearchResult() || !isHeaderVisible || isSectionsExpanded); const [isExpanded, setIsExpanded] = useState(containsSearchResult() || !isHeaderVisible);
const subsectionStatus = getItemStatus({ const subsectionStatus = getItemStatus({
published, published,
visibilityState, visibilityState,
@@ -107,10 +88,6 @@ const SubsectionCard = ({
}); });
const borderStyle = getItemStatusBorder(subsectionStatus); const borderStyle = getItemStatusBorder(subsectionStatus);
useEffect(() => {
setIsExpanded(isSectionsExpanded);
}, [isSectionsExpanded]);
const handleExpandContent = () => { const handleExpandContent = () => {
setIsExpanded((prevState) => !prevState); setIsExpanded((prevState) => !prevState);
}; };
@@ -150,13 +127,6 @@ const SubsectionCard = ({
/> />
); );
const extraActionsComponent = (
<CourseOutlineSubsectionCardExtraActionsSlot
subsection={subsection}
section={section}
/>
);
useEffect(() => { useEffect(() => {
if (activeId === id && isExpanded) { if (activeId === id && isExpanded) {
setIsExpanded(false); setIsExpanded(false);
@@ -194,129 +164,89 @@ const SubsectionCard = ({
&& !(isHeaderVisible === false) && !(isHeaderVisible === false)
); );
const handleSelectLibraryUnit = useCallback((selectedUnit) => {
onAddUnitFromLibrary({
type: COMPONENT_TYPES.libraryV2,
category: ContainerType.Vertical,
parentLocator: id,
libraryContentKey: selectedUnit.usageKey,
});
closeAddLibraryUnitModal();
}, []);
return ( return (
<> <SortableItem
<SortableItem id={id}
id={id} category={category}
category={category} key={id}
key={id} isDraggable={isDraggable}
isDraggable={isDraggable} isDroppable={actions.childAddable}
isDroppable={actions.childAddable} componentStyle={{
componentStyle={{ background: '#f8f7f6',
background: '#f8f7f6', ...borderStyle,
...borderStyle, }}
}} >
<div
className={`subsection-card ${isScrolledToElement ? 'highlight' : ''}`}
data-testid="subsection-card"
ref={currentRef}
> >
<div {isHeaderVisible && (
className={`subsection-card ${isScrolledToElement ? 'highlight' : ''}`} <>
data-testid="subsection-card" <CardHeader
ref={currentRef} title={displayName}
> status={subsectionStatus}
{isHeaderVisible && ( cardId={id}
<> hasChanges={hasChanges}
<CardHeader onClickMenuButton={handleClickMenuButton}
title={displayName} onClickPublish={onOpenPublishModal}
status={subsectionStatus} onClickEdit={openForm}
cardId={id} onClickDelete={onOpenDeleteModal}
hasChanges={hasChanges} onClickMoveUp={handleSubsectionMoveUp}
onClickMenuButton={handleClickMenuButton} onClickMoveDown={handleSubsectionMoveDown}
onClickPublish={onOpenPublishModal} onClickConfigure={onOpenConfigureModal}
onClickEdit={openForm} isFormOpen={isFormOpen}
onClickDelete={onOpenDeleteModal} closeForm={closeForm}
onClickMoveUp={handleSubsectionMoveUp} onEditSubmit={handleEditSubmit}
onClickMoveDown={handleSubsectionMoveDown} isDisabledEditField={savingStatus === RequestStatus.IN_PROGRESS}
onClickConfigure={onOpenConfigureModal} onClickDuplicate={onDuplicateSubmit}
isFormOpen={isFormOpen} titleComponent={titleComponent}
closeForm={closeForm} namePrefix={namePrefix}
onEditSubmit={handleEditSubmit} actions={actions}
isDisabledEditField={savingStatus === RequestStatus.IN_PROGRESS} proctoringExamConfigurationLink={proctoringExamConfigurationLink}
onClickDuplicate={onDuplicateSubmit} isSequential
titleComponent={titleComponent} />
namePrefix={namePrefix} <div className="subsection-card__content item-children" data-testid="subsection-card__content">
actions={actions} <XBlockStatus
proctoringExamConfigurationLink={proctoringExamConfigurationLink} isSelfPaced={isSelfPaced}
isSequential isCustomRelativeDatesActive={isCustomRelativeDatesActive}
extraActionsComponent={extraActionsComponent} blockData={subsection}
/> />
<div className="subsection-card__content item-children" data-testid="subsection-card__content">
<XBlockStatus
isSelfPaced={isSelfPaced}
isCustomRelativeDatesActive={isCustomRelativeDatesActive}
blockData={subsection}
/>
</div>
</>
)}
{(isExpanded) && (
<div
data-testid="subsection-card__units"
className={classNames('subsection-card__units', { 'item-children': isDraggable })}
>
{children}
{actions.childAddable && (
<>
<Button
data-testid="new-unit-button"
className="mt-4"
variant="outline-primary"
iconBefore={IconAdd}
block
onClick={handleNewButtonClick}
>
{intl.formatMessage(messages.newUnitButton)}
</Button>
{enableCopyPasteUnits && showPasteUnit && sharedClipboardData && (
<PasteComponent
className="mt-4"
text={intl.formatMessage(messages.pasteButton)}
clipboardData={sharedClipboardData}
onClick={handlePasteButtonClick}
/>
)}
{librariesV2Enabled && (
<Button
data-testid="use-unit-from-library"
className="mt-4"
variant="outline-primary"
iconBefore={IconAdd}
block
onClick={openAddLibraryUnitModal}
>
{intl.formatMessage(messages.useUnitFromLibraryButton)}
</Button>
)}
</>
)}
</div> </div>
)} </>
</div> )}
</SortableItem> {isExpanded && (
<StandardModal <div
title={intl.formatMessage(messages.unitPickerModalTitle)} data-testid="subsection-card__units"
isOpen={isAddLibraryUnitModalOpen} className={classNames('subsection-card__units', { 'item-children': isDraggable })}
onClose={closeAddLibraryUnitModal} >
isOverflowVisible={false} {children}
size="xl" {actions.childAddable && (
> <>
<ComponentPicker <Button
showOnlyPublished data-testid="new-unit-button"
extraFilter={['block_type = "unit"']} className="mt-4"
componentPickerMode="single" variant="outline-primary"
onComponentSelected={handleSelectLibraryUnit} iconBefore={IconAdd}
visibleTabs={[ContentType.units]} block
/> onClick={handleNewButtonClick}
</StandardModal> >
</> {intl.formatMessage(messages.newUnitButton)}
</Button>
{enableCopyPasteUnits && showPasteUnit && (
<PasteComponent
className="mt-4"
text={intl.formatMessage(messages.pasteButton)}
clipboardData={sharedClipboardData}
onClick={handlePasteButtonClick}
/>
)}
</>
)}
</div>
)}
</div>
</SortableItem>
); );
}; };
@@ -359,7 +289,6 @@ SubsectionCard.propTypes = {
}).isRequired, }).isRequired,
}).isRequired, }).isRequired,
children: PropTypes.node, children: PropTypes.node,
isSectionsExpanded: PropTypes.bool.isRequired,
isSelfPaced: PropTypes.bool.isRequired, isSelfPaced: PropTypes.bool.isRequired,
isCustomRelativeDatesActive: PropTypes.bool.isRequired, isCustomRelativeDatesActive: PropTypes.bool.isRequired,
onOpenPublishModal: PropTypes.func.isRequired, onOpenPublishModal: PropTypes.func.isRequired,
@@ -368,7 +297,6 @@ SubsectionCard.propTypes = {
onOpenDeleteModal: PropTypes.func.isRequired, onOpenDeleteModal: PropTypes.func.isRequired,
onDuplicateSubmit: PropTypes.func.isRequired, onDuplicateSubmit: PropTypes.func.isRequired,
onNewUnitSubmit: PropTypes.func.isRequired, onNewUnitSubmit: PropTypes.func.isRequired,
onAddUnitFromLibrary: PropTypes.func.isRequired,
index: PropTypes.number.isRequired, index: PropTypes.number.isRequired,
getPossibleMoves: PropTypes.func.isRequired, getPossibleMoves: PropTypes.func.isRequired,
onOrderChange: PropTypes.func.isRequired, onOrderChange: PropTypes.func.isRequired,

View File

@@ -1,6 +1,6 @@
import { MemoryRouter } from 'react-router-dom'; import { MemoryRouter } from 'react-router-dom';
import { import {
act, render, fireEvent, within, screen, act, render, fireEvent, within,
} from '@testing-library/react'; } from '@testing-library/react';
import { IntlProvider } from '@edx/frontend-platform/i18n'; import { IntlProvider } from '@edx/frontend-platform/i18n';
import { AppProvider } from '@edx/frontend-platform/react'; import { AppProvider } from '@edx/frontend-platform/react';
@@ -10,12 +10,9 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import initializeStore from '../../store'; import initializeStore from '../../store';
import SubsectionCard from './SubsectionCard'; import SubsectionCard from './SubsectionCard';
import cardHeaderMessages from '../card-header/messages'; import cardHeaderMessages from '../card-header/messages';
import { COMPONENT_TYPES } from '../../generic/block-type-utils/constants';
let store; let store;
const mockPathname = '/foo-bar'; const mockPathname = '/foo-bar';
const containerKey = 'lct:org:lib:unit:1';
const handleOnAddUnitFromLibrary = jest.fn();
jest.mock('react-router-dom', () => ({ jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'), ...jest.requireActual('react-router-dom'),
@@ -24,30 +21,12 @@ jest.mock('react-router-dom', () => ({
}), }),
})); }));
jest.mock('react-redux', () => ({ const clipboardBroadcastChannelMock = {
...jest.requireActual('react-redux'), postMessage: jest.fn(),
useSelector: () => ({ close: jest.fn(),
librariesV2Enabled: true, };
}),
}));
// Mock ComponentPicker to call onComponentSelected on click global.BroadcastChannel = jest.fn(() => clipboardBroadcastChannelMock);
jest.mock('../../library-authoring/component-picker', () => ({
ComponentPicker: (props) => {
const onClick = () => {
// eslint-disable-next-line react/prop-types
props.onComponentSelected({
usageKey: containerKey,
blockType: 'unti',
});
};
return (
<button type="submit" onClick={onClick}>
Dummy button
</button>
);
},
}));
const unit = { const unit = {
id: 'unit-1', id: 'unit-1',
@@ -108,7 +87,6 @@ const renderComponent = (props, entry = '/') => render(
onOpenHighlightsModal={jest.fn()} onOpenHighlightsModal={jest.fn()}
onOpenDeleteModal={jest.fn()} onOpenDeleteModal={jest.fn()}
onNewUnitSubmit={jest.fn()} onNewUnitSubmit={jest.fn()}
onAddUnitFromLibrary={handleOnAddUnitFromLibrary}
isCustomRelativeDatesActive={false} isCustomRelativeDatesActive={false}
onEditClick={jest.fn()} onEditClick={jest.fn()}
savingStatus="" savingStatus=""
@@ -276,31 +254,4 @@ describe('<SubsectionCard />', () => {
expect(cardUnits).toBeNull(); expect(cardUnits).toBeNull();
expect(newUnitButton).toBeNull(); expect(newUnitButton).toBeNull();
}); });
it('should add unit from library', async () => {
renderComponent();
const expandButton = await screen.findByTestId('subsection-card-header__expanded-btn');
fireEvent.click(expandButton);
const useUnitFromLibraryButton = screen.getByRole('button', {
name: /use unit from library/i,
});
expect(useUnitFromLibraryButton).toBeInTheDocument();
fireEvent.click(useUnitFromLibraryButton);
expect(await screen.findByText('Select unit'));
// click dummy button to execute onComponentSelected prop.
const dummyBtn = await screen.findByRole('button', { name: 'Dummy button' });
fireEvent.click(dummyBtn);
expect(handleOnAddUnitFromLibrary).toHaveBeenCalled();
expect(handleOnAddUnitFromLibrary).toHaveBeenCalledWith({
type: COMPONENT_TYPES.libraryV2,
parentLocator: '123',
category: 'vertical',
libraryContentKey: containerKey,
});
});
}); });

View File

@@ -4,22 +4,10 @@ const messages = defineMessages({
newUnitButton: { newUnitButton: {
id: 'course-authoring.course-outline.subsection.button.new-unit', id: 'course-authoring.course-outline.subsection.button.new-unit',
defaultMessage: 'New unit', defaultMessage: 'New unit',
description: 'Message of the button to create a new unit in a subsection.',
}, },
pasteButton: { pasteButton: {
id: 'course-authoring.course-outline.subsection.button.paste-unit', id: 'course-authoring.course-outline.subsection.button.paste-unit',
defaultMessage: 'Paste unit', defaultMessage: 'Paste unit',
description: 'Message of the button to paste a new unit in a subsection.',
},
useUnitFromLibraryButton: {
id: 'course-authoring.course-outline.subsection.button.use-unit-from-library',
defaultMessage: 'Use unit from library',
description: 'Message of the button to add a new unit from a library in a subsection.',
},
unitPickerModalTitle: {
id: 'course-authoring.course-outline.subsection.unit.modal.single-title.text',
defaultMessage: 'Select unit',
description: 'Library unit picker modal title.',
}, },
}); });

View File

@@ -1,28 +1,18 @@
// @ts-check // @ts-check
import React, { import React, { useEffect, useRef } from 'react';
useCallback,
useEffect,
useMemo,
useRef,
} from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { useDispatch } from 'react-redux'; import { useDispatch } from 'react-redux';
import { useToggle } from '@openedx/paragon'; import { useToggle } from '@openedx/paragon';
import { isEmpty } from 'lodash'; import { isEmpty } from 'lodash';
import { useSearchParams } from 'react-router-dom'; import { useSearchParams } from 'react-router-dom';
import CourseOutlineUnitCardExtraActionsSlot from '../../plugin-slots/CourseOutlineUnitCardExtraActionsSlot';
import { setCurrentItem, setCurrentSection, setCurrentSubsection } from '../data/slice'; import { setCurrentItem, setCurrentSection, setCurrentSubsection } from '../data/slice';
import { fetchCourseSectionQuery } from '../data/thunk';
import { RequestStatus } from '../../data/constants'; import { RequestStatus } from '../../data/constants';
import { isUnitReadOnly } from '../../course-unit/data/utils';
import CardHeader from '../card-header/CardHeader'; 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 TitleLink from '../card-header/TitleLink';
import XBlockStatus from '../xblock-status/XBlockStatus'; import XBlockStatus from '../xblock-status/XBlockStatus';
import { getItemStatus, getItemStatusBorder, scrollToElement } from '../utils'; import { getItemStatus, getItemStatusBorder, scrollToElement } from '../utils';
import { useClipboard } from '../../generic/clipboard';
import { PreviewLibraryXBlockChanges } from '../../course-unit/preview-changes';
const UnitCard = ({ const UnitCard = ({
unit, unit,
@@ -40,6 +30,7 @@ const UnitCard = ({
onDuplicateSubmit, onDuplicateSubmit,
getTitleLink, getTitleLink,
onOrderChange, onOrderChange,
onCopyToClipboardClick,
discussionsSettings, discussionsSettings,
}) => { }) => {
const currentRef = useRef(null); const currentRef = useRef(null);
@@ -48,11 +39,8 @@ const UnitCard = ({
const locatorId = searchParams.get('show'); const locatorId = searchParams.get('show');
const isScrolledToElement = locatorId === unit.id; const isScrolledToElement = locatorId === unit.id;
const [isFormOpen, openForm, closeForm] = useToggle(false); const [isFormOpen, openForm, closeForm] = useToggle(false);
const [isSyncModalOpen, openSyncModal, closeSyncModal] = useToggle(false);
const namePrefix = 'unit'; const namePrefix = 'unit';
const { copyToClipboard } = useClipboard();
const { const {
id, id,
category, category,
@@ -64,24 +52,8 @@ const UnitCard = ({
isHeaderVisible = true, isHeaderVisible = true,
enableCopyPasteUnits = false, enableCopyPasteUnits = false,
discussionEnabled, discussionEnabled,
upstreamInfo,
} = unit; } = unit;
const blockSyncData = useMemo(() => {
if (!upstreamInfo.readyToSync) {
return undefined;
}
return {
displayName,
downstreamBlockId: id,
upstreamBlockId: upstreamInfo.upstreamRef,
upstreamBlockVersionSynced: upstreamInfo.versionSynced,
isVertical: true,
};
}, [upstreamInfo]);
const readOnly = isUnitReadOnly(unit);
// re-create actions object for customizations // re-create actions object for customizations
const actions = { ...unitActions }; const actions = { ...unitActions };
// add actions to control display of move up & down menu buton. // add actions to control display of move up & down menu buton.
@@ -126,13 +98,9 @@ const UnitCard = ({
}; };
const handleCopyClick = () => { const handleCopyClick = () => {
copyToClipboard(id); onCopyToClipboardClick(unit.id);
}; };
const handleOnPostChangeSync = useCallback(async () => {
await dispatch(fetchCourseSectionQuery([section.id]));
}, [dispatch, section]);
const titleComponent = ( const titleComponent = (
<TitleLink <TitleLink
title={displayName} title={displayName}
@@ -141,14 +109,6 @@ const UnitCard = ({
/> />
); );
const extraActionsComponent = (
<CourseOutlineUnitCardExtraActionsSlot
unit={unit}
subsection={subsection}
section={section}
/>
);
useEffect(() => { useEffect(() => {
// if this items has been newly added, scroll to it. // if this items has been newly added, scroll to it.
// we need to check section.shouldScroll as whole section is fetched when a // we need to check section.shouldScroll as whole section is fetched when a
@@ -173,71 +133,58 @@ const UnitCard = ({
const isDraggable = actions.draggable && (actions.allowMoveUp || actions.allowMoveDown); const isDraggable = actions.draggable && (actions.allowMoveUp || actions.allowMoveDown);
return ( return (
<> <SortableItem
<SortableItem id={id}
id={id} category={category}
category={category} key={id}
key={id} isDraggable={isDraggable}
isDraggable={isDraggable} isDroppable={actions.childAddable}
isDroppable={actions.childAddable} componentStyle={{
componentStyle={{ background: '#fdfdfd',
background: '#fdfdfd', ...borderStyle,
...borderStyle, }}
}} >
<div
className={`unit-card ${isScrolledToElement ? 'highlight' : ''}`}
data-testid="unit-card"
ref={currentRef}
> >
<div <CardHeader
className={`unit-card ${isScrolledToElement ? 'highlight' : ''}`} title={displayName}
data-testid="unit-card" status={unitStatus}
ref={currentRef} hasChanges={hasChanges}
> cardId={id}
<CardHeader onClickMenuButton={handleClickMenuButton}
title={displayName} onClickPublish={onOpenPublishModal}
status={unitStatus} onClickConfigure={onOpenConfigureModal}
hasChanges={hasChanges} onClickEdit={openForm}
cardId={id} onClickDelete={onOpenDeleteModal}
onClickMenuButton={handleClickMenuButton} onClickMoveUp={handleUnitMoveUp}
onClickPublish={onOpenPublishModal} onClickMoveDown={handleUnitMoveDown}
onClickConfigure={onOpenConfigureModal} isFormOpen={isFormOpen}
onClickEdit={openForm} closeForm={closeForm}
onClickDelete={onOpenDeleteModal} onEditSubmit={handleEditSubmit}
onClickMoveUp={handleUnitMoveUp} isDisabledEditField={savingStatus === RequestStatus.IN_PROGRESS}
onClickMoveDown={handleUnitMoveDown} onClickDuplicate={onDuplicateSubmit}
onClickSync={openSyncModal} titleComponent={titleComponent}
isFormOpen={isFormOpen} namePrefix={namePrefix}
closeForm={closeForm} actions={actions}
onEditSubmit={handleEditSubmit} isVertical
isDisabledEditField={readOnly || savingStatus === RequestStatus.IN_PROGRESS} enableCopyPasteUnits={enableCopyPasteUnits}
onClickDuplicate={onDuplicateSubmit} onClickCopy={handleCopyClick}
titleComponent={titleComponent} discussionEnabled={discussionEnabled}
namePrefix={namePrefix} discussionsSettings={discussionsSettings}
actions={actions} parentInfo={parentInfo}
isVertical
enableCopyPasteUnits={enableCopyPasteUnits}
onClickCopy={handleCopyClick}
discussionEnabled={discussionEnabled}
discussionsSettings={discussionsSettings}
parentInfo={parentInfo}
extraActionsComponent={extraActionsComponent}
readyToSync={upstreamInfo.readyToSync}
/>
<div className="unit-card__content item-children" data-testid="unit-card__content">
<XBlockStatus
isSelfPaced={isSelfPaced}
isCustomRelativeDatesActive={isCustomRelativeDatesActive}
blockData={unit}
/>
</div>
</div>
</SortableItem>
{blockSyncData && (
<PreviewLibraryXBlockChanges
blockData={blockSyncData}
isModalOpen={isSyncModalOpen}
closeModal={closeSyncModal}
postChange={handleOnPostChangeSync}
/> />
)} <div className="unit-card__content item-children" data-testid="unit-card__content">
</> <XBlockStatus
isSelfPaced={isSelfPaced}
isCustomRelativeDatesActive={isCustomRelativeDatesActive}
blockData={unit}
/>
</div>
</div>
</SortableItem>
); );
}; };
@@ -263,11 +210,6 @@ UnitCard.propTypes = {
isHeaderVisible: PropTypes.bool, isHeaderVisible: PropTypes.bool,
enableCopyPasteUnits: PropTypes.bool, enableCopyPasteUnits: PropTypes.bool,
discussionEnabled: PropTypes.bool, discussionEnabled: PropTypes.bool,
upstreamInfo: PropTypes.shape({
readyToSync: PropTypes.bool.isRequired,
upstreamRef: PropTypes.string.isRequired,
versionSynced: PropTypes.number.isRequired,
}).isRequired,
}).isRequired, }).isRequired,
subsection: PropTypes.shape({ subsection: PropTypes.shape({
id: PropTypes.string.isRequired, id: PropTypes.string.isRequired,
@@ -299,6 +241,7 @@ UnitCard.propTypes = {
onOrderChange: PropTypes.func.isRequired, onOrderChange: PropTypes.func.isRequired,
isSelfPaced: PropTypes.bool.isRequired, isSelfPaced: PropTypes.bool.isRequired,
isCustomRelativeDatesActive: PropTypes.bool.isRequired, isCustomRelativeDatesActive: PropTypes.bool.isRequired,
onCopyToClipboardClick: PropTypes.func.isRequired,
discussionsSettings: PropTypes.shape({ discussionsSettings: PropTypes.shape({
providerType: PropTypes.string, providerType: PropTypes.string,
enableGradedUnits: PropTypes.bool, enableGradedUnits: PropTypes.bool,

View File

@@ -1,6 +1,6 @@
import React from 'react';
import { import {
act, render, fireEvent, within, screen, act, render, fireEvent, within,
waitFor,
} from '@testing-library/react'; } from '@testing-library/react';
import { IntlProvider } from '@edx/frontend-platform/i18n'; import { IntlProvider } from '@edx/frontend-platform/i18n';
import { AppProvider } from '@edx/frontend-platform/react'; import { AppProvider } from '@edx/frontend-platform/react';
@@ -12,17 +12,6 @@ import UnitCard from './UnitCard';
import cardMessages from '../card-header/messages'; import cardMessages from '../card-header/messages';
let store; let store;
const mockUseAcceptLibraryBlockChanges = jest.fn();
const mockUseIgnoreLibraryBlockChanges = jest.fn();
jest.mock('../../course-unit/data/apiHooks', () => ({
useAcceptLibraryBlockChanges: () => ({
mutateAsync: mockUseAcceptLibraryBlockChanges,
}),
useIgnoreLibraryBlockChanges: () => ({
mutateAsync: mockUseIgnoreLibraryBlockChanges,
}),
}));
const section = { const section = {
id: '1', id: '1',
@@ -55,11 +44,6 @@ const unit = {
duplicable: true, duplicable: true,
}, },
isHeaderVisible: true, isHeaderVisible: true,
upstreamInfo: {
readyToSync: true,
upstreamRef: 'lct:org1:lib1:unit:1',
versionSynced: 1,
},
}; };
const queryClient = new QueryClient(); const queryClient = new QueryClient();
@@ -78,6 +62,7 @@ const renderComponent = (props) => render(
onOpenPublishModal={jest.fn()} onOpenPublishModal={jest.fn()}
onOpenDeleteModal={jest.fn()} onOpenDeleteModal={jest.fn()}
onOpenConfigureModal={jest.fn()} onOpenConfigureModal={jest.fn()}
onCopyToClipboardClick={jest.fn()}
savingStatus="" savingStatus=""
onEditSubmit={jest.fn()} onEditSubmit={jest.fn()}
onDuplicateSubmit={jest.fn()} onDuplicateSubmit={jest.fn()}
@@ -164,51 +149,4 @@ describe('<UnitCard />', () => {
}); });
expect(queryByRole('status')).not.toBeInTheDocument(); expect(queryByRole('status')).not.toBeInTheDocument();
}); });
it('should sync unit changes from upstream', async () => {
renderComponent();
expect(await screen.findByTestId('unit-card-header')).toBeInTheDocument();
// Click on sync button
const syncButton = screen.getByRole('button', { name: /update available - click to sync/i });
fireEvent.click(syncButton);
// Should open compare preview modal
expect(screen.getByRole('heading', { name: /preview changes: unit name/i })).toBeInTheDocument();
expect(screen.getByText('Preview not available')).toBeInTheDocument();
// Click on accept changes
const acceptChangesButton = screen.getByText(/accept changes/i);
fireEvent.click(acceptChangesButton);
await waitFor(() => expect(mockUseAcceptLibraryBlockChanges).toHaveBeenCalled());
});
it('should decline sync unit changes from upstream', async () => {
renderComponent();
expect(await screen.findByTestId('unit-card-header')).toBeInTheDocument();
// Click on sync button
const syncButton = screen.getByRole('button', { name: /update available - click to sync/i });
fireEvent.click(syncButton);
// Should open compare preview modal
expect(screen.getByRole('heading', { name: /preview changes: unit name/i })).toBeInTheDocument();
expect(screen.getByText('Preview not available')).toBeInTheDocument();
// Click on ignore changes
const ignoreChangesButton = screen.getByRole('button', { name: /ignore changes/i });
fireEvent.click(ignoreChangesButton);
// Should open the confirmation modal
expect(screen.getByRole('heading', { name: /ignore these changes\?/i })).toBeInTheDocument();
// Click on ignore button
const ignoreButton = screen.getByRole('button', { name: /ignore/i });
fireEvent.click(ignoreButton);
await waitFor(() => expect(mockUseIgnoreLibraryBlockChanges).toHaveBeenCalled());
});
}); });

View File

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

View File

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

View File

@@ -3,14 +3,14 @@ import PropTypes from 'prop-types';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import { useParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
import { import {
Alert, Container, Layout, Button, TransitionReplace, Container, Layout, Stack, Button, TransitionReplace,
} from '@openedx/paragon'; } 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 { import {
Warning as WarningIcon, Warning as WarningIcon,
CheckCircle as CheckCircleIcon, CheckCircle as CheckCircleIcon,
} from '@openedx/paragon/icons'; } from '@openedx/paragon/icons';
import { CourseAuthoringUnitSidebarSlot } from '../plugin-slots/CourseAuthoringUnitSidebarSlot';
import { getProcessingNotification } from '../generic/processing-notification/data/selectors'; import { getProcessingNotification } from '../generic/processing-notification/data/selectors';
import SubHeader from '../generic/sub-header/SubHeader'; import SubHeader from '../generic/sub-header/SubHeader';
@@ -25,23 +25,25 @@ import Loading from '../generic/Loading';
import AddComponent from './add-component/AddComponent'; import AddComponent from './add-component/AddComponent';
import HeaderTitle from './header-title/HeaderTitle'; import HeaderTitle from './header-title/HeaderTitle';
import Breadcrumbs from './breadcrumbs/Breadcrumbs'; import Breadcrumbs from './breadcrumbs/Breadcrumbs';
import HeaderNavigations from './header-navigations/HeaderNavigations';
import Sequence from './course-sequence'; import Sequence from './course-sequence';
import Sidebar from './sidebar';
import { useCourseUnit, useLayoutGrid, useScrollToLastPosition } from './hooks'; import { useCourseUnit, useLayoutGrid, useScrollToLastPosition } from './hooks';
import messages from './messages'; 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 { PasteNotificationAlert } from './clipboard';
import XBlockContainerIframe from './xblock-container-iframe'; import XBlockContainerIframe from './xblock-container-iframe';
import MoveModal from './move-modal'; import MoveModal from './move-modal';
import IframePreviewLibraryXBlockChanges from './preview-changes'; import PreviewLibraryXBlockChanges from './preview-changes';
import CourseUnitHeaderActionsSlot from '../plugin-slots/CourseUnitHeaderActionsSlot';
const CourseUnit = ({ courseId }) => { const CourseUnit = ({ courseId }) => {
const { blockId } = useParams(); const { blockId } = useParams();
const intl = useIntl(); const intl = useIntl();
const { const {
courseUnit,
isLoading, isLoading,
sequenceId, sequenceId,
courseUnitLoadingStatus,
unitTitle, unitTitle,
unitCategory, unitCategory,
errorMessage, errorMessage,
@@ -50,7 +52,6 @@ const CourseUnit = ({ courseId }) => {
isTitleEditFormOpen, isTitleEditFormOpen,
isUnitVerticalType, isUnitVerticalType,
isUnitLibraryType, isUnitLibraryType,
isSplitTestType,
staticFileNotices, staticFileNotices,
currentlyVisibleToStudents, currentlyVisibleToStudents,
unitXBlockActions, unitXBlockActions,
@@ -71,12 +72,9 @@ const CourseUnit = ({ courseId }) => {
handleRollbackMovedXBlock, handleRollbackMovedXBlock,
handleCloseXBlockMovedAlert, handleCloseXBlockMovedAlert,
handleNavigateToTargetUnit, handleNavigateToTargetUnit,
addComponentTemplateData,
} = useCourseUnit({ courseId, blockId }); } = useCourseUnit({ courseId, blockId });
const layoutGrid = useLayoutGrid(unitCategory, isUnitLibraryType); const layoutGrid = useLayoutGrid(unitCategory, isUnitLibraryType);
const readOnly = !!courseUnit.readOnly;
useEffect(() => { useEffect(() => {
document.title = getPageHeadTitle('', unitTitle); document.title = getPageHeadTitle('', unitTitle);
}, [unitTitle]); }, [unitTitle]);
@@ -138,24 +136,6 @@ const CourseUnit = ({ courseId }) => {
/> />
) : null} ) : null}
</TransitionReplace> </TransitionReplace>
{courseUnit.upstreamInfo?.upstreamLink && (
<AlertMessage
title={intl.formatMessage(
messages.alertLibraryUnitReadOnlyText,
{
link: (
<Alert.Link
className="ml-1"
href={courseUnit.upstreamInfo.upstreamLink}
>
{intl.formatMessage(messages.alertLibraryUnitReadOnlyLinkText)}
</Alert.Link>
),
},
)}
variant="info"
/>
)}
<SubHeader <SubHeader
hideBorder hideBorder
title={( title={(
@@ -174,11 +154,9 @@ const CourseUnit = ({ courseId }) => {
/> />
)} )}
headerActions={( headerActions={(
<CourseUnitHeaderActionsSlot <HeaderNavigations
category={unitCategory} unitCategory={unitCategory}
headerNavigationsActions={headerNavigationsActions} headerNavigationsActions={headerNavigationsActions}
unitTitle={unitTitle}
verticalBlocks={courseVerticalChildren.children}
/> />
)} )}
/> />
@@ -210,27 +188,20 @@ const CourseUnit = ({ courseId }) => {
<XBlockContainerIframe <XBlockContainerIframe
courseId={courseId} courseId={courseId}
blockId={blockId} blockId={blockId}
isUnitVerticalType={isUnitVerticalType}
courseUnitLoadingStatus={courseUnitLoadingStatus}
unitXBlockActions={unitXBlockActions} unitXBlockActions={unitXBlockActions}
courseVerticalChildren={courseVerticalChildren.children} courseVerticalChildren={courseVerticalChildren.children}
handleConfigureSubmit={handleConfigureSubmit} handleConfigureSubmit={handleConfigureSubmit}
/> />
{!readOnly && ( {isUnitVerticalType && (
<AddComponent <AddComponent
parentLocator={blockId} blockId={blockId}
isSplitTestType={isSplitTestType}
isUnitVerticalType={isUnitVerticalType}
handleCreateNewCourseXBlock={handleCreateNewCourseXBlock} handleCreateNewCourseXBlock={handleCreateNewCourseXBlock}
addComponentTemplateData={addComponentTemplateData}
/> />
)} )}
{!readOnly && showPasteXBlock && canPasteComponent && isUnitVerticalType && ( {showPasteXBlock && canPasteComponent && isUnitVerticalType && (
<PasteComponent <PasteComponent
clipboardData={sharedClipboardData} clipboardData={sharedClipboardData}
onClick={ onClick={handleCreateNewCourseXBlock}
() => handleCreateNewCourseXBlock({ stagedContent: 'clipboard', parentLocator: blockId })
}
text={intl.formatMessage(messages.pasteButtonText)} text={intl.formatMessage(messages.pasteButtonText)}
/> />
)} )}
@@ -240,18 +211,26 @@ const CourseUnit = ({ courseId }) => {
closeModal={closeMoveModal} closeModal={closeMoveModal}
courseId={courseId} courseId={courseId}
/> />
<IframePreviewLibraryXBlockChanges /> <PreviewLibraryXBlockChanges />
</Layout.Element> </Layout.Element>
<Layout.Element> <Layout.Element>
<CourseAuthoringUnitSidebarSlot <Stack gap={3}>
courseId={courseId} {isUnitVerticalType && (
blockId={blockId} <>
unitTitle={unitTitle} <Sidebar data-testid="course-unit-sidebar">
xBlocks={courseVerticalChildren.children} <PublishControls blockId={blockId} />
readOnly={readOnly} </Sidebar>
isUnitVerticalType={isUnitVerticalType} {getConfig().ENABLE_TAGGING_TAXONOMY_PAGES === 'true' && (
isSplitTestType={isSplitTestType} <Sidebar className="tags-sidebar">
/> <TagsSidebarControls />
</Sidebar>
)}
<Sidebar data-testid="course-unit-location-sidebar">
<LocationInfo />
</Sidebar>
</>
)}
</Stack>
</Layout.Element> </Layout.Element>
</Layout> </Layout>
</section> </section>
@@ -274,4 +253,4 @@ CourseUnit.propTypes = {
courseId: PropTypes.string.isRequired, courseId: PropTypes.string.isRequired,
}; };
export default CourseUnit; export default injectIntl(CourseUnit);

View File

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

View File

@@ -1,5 +1,4 @@
import MockAdapter from 'axios-mock-adapter'; import MockAdapter from 'axios-mock-adapter';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { import {
act, render, waitFor, within, screen, act, render, waitFor, within, screen,
} from '@testing-library/react'; } from '@testing-library/react';
@@ -48,8 +47,10 @@ import { executeThunk } from '../utils';
import { IFRAME_FEATURE_POLICY } from '../constants'; import { IFRAME_FEATURE_POLICY } from '../constants';
import pasteComponentMessages from '../generic/clipboard/paste-component/messages'; import pasteComponentMessages from '../generic/clipboard/paste-component/messages';
import pasteNotificationsMessages from './clipboard/paste-notification/messages'; import pasteNotificationsMessages from './clipboard/paste-notification/messages';
import headerNavigationsMessages from './header-navigations/messages';
import headerTitleMessages from './header-title/messages'; import headerTitleMessages from './header-title/messages';
import courseSequenceMessages from './course-sequence/messages'; import courseSequenceMessages from './course-sequence/messages';
import sidebarMessages from './sidebar/messages';
import { extractCourseUnitId } from './sidebar/utils'; import { extractCourseUnitId } from './sidebar/utils';
import CourseUnit from './CourseUnit'; import CourseUnit from './CourseUnit';
@@ -59,17 +60,13 @@ import configureModalMessages from '../generic/configure-modal/messages';
import { getContentTaxonomyTagsApiUrl, getContentTaxonomyTagsCountApiUrl } from '../content-tags-drawer/data/api'; import { getContentTaxonomyTagsApiUrl, getContentTaxonomyTagsCountApiUrl } from '../content-tags-drawer/data/api';
import addComponentMessages from './add-component/messages'; import addComponentMessages from './add-component/messages';
import { messageTypes, PUBLISH_TYPES, UNIT_VISIBILITY_STATES } from './constants'; 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 moveModalMessages from './move-modal/messages';
import xblockContainerIframeMessages from './xblock-container-iframe/messages'; import xblockContainerIframeMessages from './xblock-container-iframe/messages';
import headerNavigationsMessages from './header-navigations/messages';
import sidebarMessages from './sidebar/messages';
import messages from './messages'; import messages from './messages';
import * as selectors from '../data/selectors';
let axiosMock; let axiosMock;
let store; let store;
let queryClient;
const courseId = '123'; const courseId = '123';
const blockId = '567890'; const blockId = '567890';
const unitDisplayName = courseUnitIndexMock.metadata.display_name; const unitDisplayName = courseUnitIndexMock.metadata.display_name;
@@ -94,6 +91,52 @@ jest.mock('react-router-dom', () => ({
useNavigate: () => mockedUsedNavigate, 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. * Simulates receiving a post message event for testing purposes.
* This can be used to mimic events like deletion or other actions * This can be used to mimic events like deletion or other actions
@@ -114,9 +157,7 @@ const RootWrapper = () => (
<AppProvider store={store}> <AppProvider store={store}>
<IntlProvider locale="en"> <IntlProvider locale="en">
<IframeProvider> <IframeProvider>
<QueryClientProvider client={queryClient}> <CourseUnit courseId={courseId} />
<CourseUnit courseId={courseId} />
</QueryClientProvider>
</IframeProvider> </IframeProvider>
</IntlProvider> </IntlProvider>
</AppProvider> </AppProvider>
@@ -135,13 +176,6 @@ describe('<CourseUnit />', () => {
window.scrollTo = jest.fn(); window.scrollTo = jest.fn();
global.localStorage.clear(); global.localStorage.clear();
store = initializeStore(); store = initializeStore();
queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: false,
},
},
});
axiosMock = new MockAdapter(getAuthenticatedHttpClient()); axiosMock = new MockAdapter(getAuthenticatedHttpClient());
axiosMock axiosMock
.onGet(getClipboardUrl()) .onGet(getClipboardUrl())
@@ -160,37 +194,37 @@ describe('<CourseUnit />', () => {
await executeThunk(fetchCourseVerticalChildrenData(blockId), store.dispatch); await executeThunk(fetchCourseVerticalChildrenData(blockId), store.dispatch);
axiosMock axiosMock
.onGet(getContentTaxonomyTagsApiUrl(blockId)) .onGet(getContentTaxonomyTagsApiUrl(blockId))
.reply(200, { taxonomies: [] }); .reply(200, {});
axiosMock axiosMock
.onGet(getContentTaxonomyTagsCountApiUrl(blockId)) .onGet(getContentTaxonomyTagsCountApiUrl(blockId))
.reply(200, 17); .reply(200, 17);
}); });
it('render CourseUnit component correctly', async () => { it('render CourseUnit component correctly', async () => {
render(<RootWrapper />); const { getByText, getByRole, getByTestId } = render(<RootWrapper />);
const currentSectionName = courseUnitIndexMock.ancestor_info.ancestors[1].display_name; const currentSectionName = courseUnitIndexMock.ancestor_info.ancestors[1].display_name;
const currentSubSectionName = courseUnitIndexMock.ancestor_info.ancestors[1].display_name; const currentSubSectionName = courseUnitIndexMock.ancestor_info.ancestors[1].display_name;
await waitFor(() => { await waitFor(() => {
const unitHeaderTitle = screen.getByTestId('unit-header-title'); const unitHeaderTitle = getByTestId('unit-header-title');
expect(screen.getByText(unitDisplayName)).toBeInTheDocument(); expect(getByText(unitDisplayName)).toBeInTheDocument();
expect(within(unitHeaderTitle).getByRole('button', { name: headerTitleMessages.altButtonEdit.defaultMessage })).toBeInTheDocument(); expect(within(unitHeaderTitle).getByRole('button', { name: headerTitleMessages.altButtonEdit.defaultMessage })).toBeInTheDocument();
expect(within(unitHeaderTitle).getByRole('button', { name: headerTitleMessages.altButtonSettings.defaultMessage })).toBeInTheDocument(); expect(within(unitHeaderTitle).getByRole('button', { name: headerTitleMessages.altButtonSettings.defaultMessage })).toBeInTheDocument();
expect(screen.getByRole('button', { name: headerNavigationsMessages.viewLiveButton.defaultMessage })).toBeInTheDocument(); expect(getByRole('button', { name: headerNavigationsMessages.viewLiveButton.defaultMessage })).toBeInTheDocument();
expect(screen.getByRole('button', { name: headerNavigationsMessages.previewButton.defaultMessage })).toBeInTheDocument(); expect(getByRole('button', { name: headerNavigationsMessages.previewButton.defaultMessage })).toBeInTheDocument();
expect(screen.getByRole('button', { name: currentSectionName })).toBeInTheDocument(); expect(getByRole('button', { name: currentSectionName })).toBeInTheDocument();
expect(screen.getByRole('button', { name: currentSubSectionName })).toBeInTheDocument(); expect(getByRole('button', { name: currentSubSectionName })).toBeInTheDocument();
}); });
}); });
it('renders the course unit iframe with correct attributes', async () => { it('renders the course unit iframe with correct attributes', async () => {
render(<RootWrapper />); const { getByTitle } = render(<RootWrapper />);
await waitFor(() => { await waitFor(() => {
const iframe = screen.getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage); const iframe = getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
expect(iframe).toHaveAttribute('src', `${getConfig().STUDIO_BASE_URL}/container_embed/${blockId}`); expect(iframe).toHaveAttribute('src', `${getConfig().STUDIO_BASE_URL}/container_embed/${blockId}`);
expect(iframe).toHaveAttribute('allow', IFRAME_FEATURE_POLICY); 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('scrolling', 'no');
expect(iframe).toHaveAttribute('referrerpolicy', 'origin'); expect(iframe).toHaveAttribute('referrerpolicy', 'origin');
expect(iframe).toHaveAttribute('loading', 'lazy'); expect(iframe).toHaveAttribute('loading', 'lazy');
@@ -199,39 +233,40 @@ describe('<CourseUnit />', () => {
}); });
it('adjusts iframe height dynamically based on courseXBlockDropdownHeight postMessage event', async () => { 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); await waitFor(() => {
expect(iframe).toHaveAttribute('style', 'height: 0px;'); const iframe = getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
simulatePostMessageEvent(messageTypes.toggleCourseXBlockDropdown, { expect(iframe).toHaveAttribute('style', 'width: 100%; height: 0px;');
courseXBlockDropdownHeight: 200, 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 () => { it('displays an error alert when a studioAjaxError message is received', async () => {
render(<RootWrapper />); const { getByTitle, getByTestId } = render(<RootWrapper />);
await waitFor(() => { await waitFor(() => {
const xblocksIframe = screen.getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage); const xblocksIframe = getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
expect(xblocksIframe).toBeInTheDocument(); expect(xblocksIframe).toBeInTheDocument();
simulatePostMessageEvent(messageTypes.studioAjaxError, { simulatePostMessageEvent(messageTypes.studioAjaxError, {
error: 'Some error text...', error: 'Some error text...',
}); });
}); });
expect(screen.getByTestId('saving-error-alert')).toBeInTheDocument(); expect(getByTestId('saving-error-alert')).toBeInTheDocument();
}); });
it('renders XBlock iframe and opens legacy edit modal on editXBlock message', async () => { it('renders XBlock iframe and opens legacy edit modal on editXBlock message', async () => {
render(<RootWrapper />); const { getByTitle } = render(<RootWrapper />);
await waitFor(() => { await waitFor(() => {
const xblocksIframe = screen.getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage); const xblocksIframe = getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
expect(xblocksIframe).toBeInTheDocument(); expect(xblocksIframe).toBeInTheDocument();
simulatePostMessageEvent(messageTypes.editXBlock, { id: blockId }); simulatePostMessageEvent(messageTypes.editXBlock, { id: blockId });
const legacyXBlockEditModalIframe = screen.getByTitle( const legacyXBlockEditModalIframe = getByTitle(
xblockContainerIframeMessages.legacyEditModalIframeTitle.defaultMessage, xblockContainerIframeMessages.legacyEditModalIframeTitle.defaultMessage,
); );
expect(legacyXBlockEditModalIframe).toBeInTheDocument(); expect(legacyXBlockEditModalIframe).toBeInTheDocument();
@@ -239,24 +274,27 @@ describe('<CourseUnit />', () => {
}); });
it('renders the xBlocks iframe and opens the tags drawer on postMessage event', async () => { 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 }); 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 () => { it('closes the legacy edit modal when closeXBlockEditorModal message is received', async () => {
render(<RootWrapper />); const { getByTitle, queryByTitle } = render(<RootWrapper />);
await waitFor(() => { await waitFor(() => {
const xblocksIframe = screen.getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage); const xblocksIframe = getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
expect(xblocksIframe).toBeInTheDocument(); expect(xblocksIframe).toBeInTheDocument();
simulatePostMessageEvent(messageTypes.closeXBlockEditorModal, { id: blockId }); simulatePostMessageEvent(messageTypes.closeXBlockEditorModal, { id: blockId });
const legacyXBlockEditModalIframe = screen.queryByTitle( const legacyXBlockEditModalIframe = queryByTitle(
xblockContainerIframeMessages.legacyEditModalIframeTitle.defaultMessage, xblockContainerIframeMessages.legacyEditModalIframeTitle.defaultMessage,
); );
expect(legacyXBlockEditModalIframe).not.toBeInTheDocument(); expect(legacyXBlockEditModalIframe).not.toBeInTheDocument();
@@ -264,14 +302,14 @@ describe('<CourseUnit />', () => {
}); });
it('closes legacy edit modal and updates course unit sidebar after saveEditedXBlockData message', async () => { it('closes legacy edit modal and updates course unit sidebar after saveEditedXBlockData message', async () => {
render(<RootWrapper />); const { getByTitle, queryByTitle, getByTestId } = render(<RootWrapper />);
await waitFor(() => { await waitFor(() => {
const xblocksIframe = screen.getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage); const xblocksIframe = getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
expect(xblocksIframe).toBeInTheDocument(); expect(xblocksIframe).toBeInTheDocument();
simulatePostMessageEvent(messageTypes.saveEditedXBlockData); simulatePostMessageEvent(messageTypes.saveEditedXBlockData);
const legacyXBlockEditModalIframe = screen.queryByTitle( const legacyXBlockEditModalIframe = queryByTitle(
xblockContainerIframeMessages.legacyEditModalIframeTitle.defaultMessage, xblockContainerIframeMessages.legacyEditModalIframeTitle.defaultMessage,
); );
expect(legacyXBlockEditModalIframe).not.toBeInTheDocument(); expect(legacyXBlockEditModalIframe).not.toBeInTheDocument();
@@ -286,7 +324,7 @@ describe('<CourseUnit />', () => {
}); });
await waitFor(() => { await waitFor(() => {
const courseUnitSidebar = screen.getByTestId('course-unit-sidebar'); const courseUnitSidebar = getByTestId('course-unit-sidebar');
expect( expect(
within(courseUnitSidebar).getByText(sidebarMessages.sidebarTitleDraftUnpublishedChanges.defaultMessage), within(courseUnitSidebar).getByText(sidebarMessages.sidebarTitleDraftUnpublishedChanges.defaultMessage),
).toBeInTheDocument(); ).toBeInTheDocument();
@@ -305,10 +343,10 @@ describe('<CourseUnit />', () => {
}); });
it('updates course unit sidebar after receiving refreshPositions message', async () => { it('updates course unit sidebar after receiving refreshPositions message', async () => {
render(<RootWrapper />); const { getByTitle, getByTestId } = render(<RootWrapper />);
await waitFor(() => { await waitFor(() => {
const xblocksIframe = screen.getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage); const xblocksIframe = getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
expect(xblocksIframe).toBeInTheDocument(); expect(xblocksIframe).toBeInTheDocument();
simulatePostMessageEvent(messageTypes.refreshPositions); simulatePostMessageEvent(messageTypes.refreshPositions);
}); });
@@ -322,7 +360,7 @@ describe('<CourseUnit />', () => {
}); });
await waitFor(() => { await waitFor(() => {
const courseUnitSidebar = screen.getByTestId('course-unit-sidebar'); const courseUnitSidebar = getByTestId('course-unit-sidebar');
expect( expect(
within(courseUnitSidebar).getByText(sidebarMessages.sidebarTitleDraftUnpublishedChanges.defaultMessage), within(courseUnitSidebar).getByText(sidebarMessages.sidebarTitleDraftUnpublishedChanges.defaultMessage),
).toBeInTheDocument(); ).toBeInTheDocument();
@@ -341,10 +379,12 @@ describe('<CourseUnit />', () => {
}); });
it('checks whether xblock is removed when the corresponding delete button is clicked and the sidebar is the updated', async () => { it('checks whether xblock is removed when the corresponding delete button is clicked and the sidebar is the updated', async () => {
render(<RootWrapper />); const {
getByTitle, getByText, queryByRole, getAllByRole, getByRole,
} = render(<RootWrapper />);
await waitFor(async () => { await waitFor(() => {
const iframe = screen.getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage); const iframe = getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
expect(iframe).toHaveAttribute( expect(iframe).toHaveAttribute(
'aria-label', 'aria-label',
xblockContainerIframeMessages.xblockIframeLabel.defaultMessage xblockContainerIframeMessages.xblockIframeLabel.defaultMessage
@@ -355,15 +395,16 @@ describe('<CourseUnit />', () => {
usageId: courseVerticalChildrenMock.children[0].block_id, usageId: courseVerticalChildrenMock.children[0].block_id,
}); });
expect(screen.getByText(/Delete this component?/i)).toBeInTheDocument(); expect(getByText(/Delete this component?/i)).toBeInTheDocument();
expect(screen.getByText(/Deleting this component is permanent and cannot be undone./i)).toBeInTheDocument(); expect(getByText(/Deleting this component is permanent and cannot be undone./i)).toBeInTheDocument();
const dialog = screen.getByRole('dialog'); expect(getByRole('dialog')).toBeInTheDocument();
expect(dialog).toBeInTheDocument();
// Find the Cancel and Delete buttons within the iframe by their specific classes // Find the Cancel and Delete buttons within the iframe by their specific classes
const cancelButton = await within(dialog).findByRole('button', { name: /Cancel/i }); const cancelButton = getAllByRole('button', { name: /Cancel/i })
const deleteButton = await within(dialog).findByRole('button', { name: /Delete/i }); .find(({ classList }) => classList.contains('btn-tertiary'));
const deleteButton = getAllByRole('button', { name: /Delete/i })
.find(({ classList }) => classList.contains('btn-primary'));
expect(cancelButton).toBeInTheDocument(); expect(cancelButton).toBeInTheDocument();
@@ -371,7 +412,7 @@ describe('<CourseUnit />', () => {
usageId: courseVerticalChildrenMock.children[0].block_id, usageId: courseVerticalChildrenMock.children[0].block_id,
}); });
expect(screen.getByRole('dialog')).toBeInTheDocument(); expect(getByRole('dialog')).toBeInTheDocument();
userEvent.click(deleteButton); userEvent.click(deleteButton);
}); });
@@ -392,14 +433,14 @@ describe('<CourseUnit />', () => {
await waitFor(() => { await waitFor(() => {
// check if the sidebar status is Published and Live // check if the sidebar status is Published and Live
expect(screen.getByText(sidebarMessages.sidebarTitlePublishedAndLive.defaultMessage)).toBeInTheDocument(); expect(getByText(sidebarMessages.sidebarTitlePublishedAndLive.defaultMessage)).toBeInTheDocument();
expect(screen.getByText( expect(getByText(
sidebarMessages.publishLastPublished.defaultMessage sidebarMessages.publishLastPublished.defaultMessage
.replace('{publishedOn}', courseUnitIndexMock.published_on) .replace('{publishedOn}', courseUnitIndexMock.published_on)
.replace('{publishedBy}', userName), .replace('{publishedBy}', userName),
)).toBeInTheDocument(); )).toBeInTheDocument();
expect(screen.queryByRole('button', { name: sidebarMessages.actionButtonPublishTitle.defaultMessage })).not.toBeInTheDocument(); expect(queryByRole('button', { name: sidebarMessages.actionButtonPublishTitle.defaultMessage })).not.toBeInTheDocument();
expect(screen.getByText(unitDisplayName)).toBeInTheDocument(); expect(getByText(unitDisplayName)).toBeInTheDocument();
}); });
axiosMock axiosMock
@@ -430,28 +471,28 @@ describe('<CourseUnit />', () => {
await executeThunk(editCourseUnitVisibilityAndData(blockId, PUBLISH_TYPES.makePublic, true), store.dispatch); await executeThunk(editCourseUnitVisibilityAndData(blockId, PUBLISH_TYPES.makePublic, true), store.dispatch);
await waitFor(() => { await waitFor(() => {
const iframe = screen.getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage); const iframe = getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
expect(iframe).toHaveAttribute( expect(iframe).toHaveAttribute(
'aria-label', 'aria-label',
xblockContainerIframeMessages.xblockIframeLabel.defaultMessage xblockContainerIframeMessages.xblockIframeLabel.defaultMessage
.replace('{xblockCount}', updatedCourseVerticalChildren.length), .replace('{xblockCount}', updatedCourseVerticalChildren.length),
); );
// after removing the xblock, the sidebar status changes to Draft (unpublished changes) // after removing the xblock, the sidebar status changes to Draft (unpublished changes)
expect(screen.getByText(sidebarMessages.sidebarTitleDraftUnpublishedChanges.defaultMessage)).toBeInTheDocument(); expect(getByText(sidebarMessages.sidebarTitleDraftUnpublishedChanges.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.visibilityStaffAndLearnersTitle.defaultMessage)).toBeInTheDocument(); expect(getByText(sidebarMessages.visibilityStaffAndLearnersTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.releaseStatusTitle.defaultMessage)).toBeInTheDocument(); expect(getByText(sidebarMessages.releaseStatusTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.sidebarBodyNote.defaultMessage)).toBeInTheDocument(); expect(getByText(sidebarMessages.sidebarBodyNote.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.visibilityWillBeVisibleToTitle.defaultMessage)).toBeInTheDocument(); expect(getByText(sidebarMessages.visibilityWillBeVisibleToTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.visibilityCheckboxTitle.defaultMessage)).toBeInTheDocument(); expect(getByText(sidebarMessages.visibilityCheckboxTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.actionButtonPublishTitle.defaultMessage)).toBeInTheDocument(); expect(getByText(sidebarMessages.actionButtonPublishTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.actionButtonDiscardChangesTitle.defaultMessage)).toBeInTheDocument(); expect(getByText(sidebarMessages.actionButtonDiscardChangesTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(courseUnitIndexMock.release_date)).toBeInTheDocument(); expect(getByText(courseUnitIndexMock.release_date)).toBeInTheDocument();
expect(screen.getByText( expect(getByText(
sidebarMessages.publishInfoDraftSaved.defaultMessage sidebarMessages.publishInfoDraftSaved.defaultMessage
.replace('{editedOn}', courseUnitIndexMock.edited_on) .replace('{editedOn}', courseUnitIndexMock.edited_on)
.replace('{editedBy}', courseUnitIndexMock.edited_by), .replace('{editedBy}', courseUnitIndexMock.edited_by),
)).toBeInTheDocument(); )).toBeInTheDocument();
expect(screen.getByText( expect(getByText(
sidebarMessages.releaseInfoWithSection.defaultMessage sidebarMessages.releaseInfoWithSection.defaultMessage
.replace('{sectionName}', courseUnitIndexMock.release_date_from), .replace('{sectionName}', courseUnitIndexMock.release_date_from),
)).toBeInTheDocument(); )).toBeInTheDocument();
@@ -459,16 +500,14 @@ describe('<CourseUnit />', () => {
}); });
it('checks if xblock is a duplicate when the corresponding duplicate button is clicked and if the sidebar status is updated', async () => { it('checks if xblock is a duplicate when the corresponding duplicate button is clicked and if the sidebar status is updated', async () => {
render(<RootWrapper />); const {
getByTitle, getByRole, getByText, queryByRole,
} = render(<RootWrapper />);
simulatePostMessageEvent(messageTypes.duplicateXBlock, { simulatePostMessageEvent(messageTypes.duplicateXBlock, {
id: courseVerticalChildrenMock.children[0].block_id, id: courseVerticalChildrenMock.children[0].block_id,
}); });
axiosMock
.onGet(getCourseUnitApiUrl(blockId))
.reply(200, courseUnitIndexMock);
axiosMock axiosMock
.onPost(postXBlockBaseApiUrl({ .onPost(postXBlockBaseApiUrl({
parent_locator: blockId, parent_locator: blockId,
@@ -479,14 +518,8 @@ describe('<CourseUnit />', () => {
const updatedCourseVerticalChildren = [ const updatedCourseVerticalChildren = [
...courseVerticalChildrenMock.children, ...courseVerticalChildrenMock.children,
{ {
...courseVerticalChildrenMock.children[0],
name: 'New Cloned XBlock', name: 'New Cloned XBlock',
block_id: '1234567890',
block_type: 'drag-and-drop-v2',
user_partition_info: {
selectable_partitions: [],
selected_partition_index: -1,
selected_groups_label: '',
},
}, },
]; ];
@@ -498,9 +531,9 @@ describe('<CourseUnit />', () => {
}); });
await waitFor(() => { await waitFor(() => {
userEvent.click(screen.getByRole('button', { name: sidebarMessages.actionButtonPublishTitle.defaultMessage })); userEvent.click(getByRole('button', { name: sidebarMessages.actionButtonPublishTitle.defaultMessage }));
const iframe = screen.getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage); const iframe = getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
expect(iframe).toHaveAttribute( expect(iframe).toHaveAttribute(
'aria-label', 'aria-label',
xblockContainerIframeMessages.xblockIframeLabel.defaultMessage xblockContainerIframeMessages.xblockIframeLabel.defaultMessage
@@ -529,14 +562,14 @@ describe('<CourseUnit />', () => {
await waitFor(() => { await waitFor(() => {
// check if the sidebar status is Published and Live // check if the sidebar status is Published and Live
expect(screen.getByText(sidebarMessages.sidebarTitlePublishedAndLive.defaultMessage)).toBeInTheDocument(); expect(getByText(sidebarMessages.sidebarTitlePublishedAndLive.defaultMessage)).toBeInTheDocument();
expect(screen.getByText( expect(getByText(
sidebarMessages.publishLastPublished.defaultMessage sidebarMessages.publishLastPublished.defaultMessage
.replace('{publishedOn}', courseUnitIndexMock.published_on) .replace('{publishedOn}', courseUnitIndexMock.published_on)
.replace('{publishedBy}', userName), .replace('{publishedBy}', userName),
)).toBeInTheDocument(); )).toBeInTheDocument();
expect(screen.queryByRole('button', { name: sidebarMessages.actionButtonPublishTitle.defaultMessage })).not.toBeInTheDocument(); expect(queryByRole('button', { name: sidebarMessages.actionButtonPublishTitle.defaultMessage })).not.toBeInTheDocument();
expect(screen.getByText(unitDisplayName)).toBeInTheDocument(); expect(getByText(unitDisplayName)).toBeInTheDocument();
}); });
axiosMock axiosMock
@@ -545,7 +578,7 @@ describe('<CourseUnit />', () => {
await executeThunk(editCourseUnitVisibilityAndData(blockId, PUBLISH_TYPES.makePublic, true), store.dispatch); await executeThunk(editCourseUnitVisibilityAndData(blockId, PUBLISH_TYPES.makePublic, true), store.dispatch);
await waitFor(() => { await waitFor(() => {
const iframe = screen.getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage); const iframe = getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
expect(iframe).toHaveAttribute( expect(iframe).toHaveAttribute(
'aria-label', 'aria-label',
xblockContainerIframeMessages.xblockIframeLabel.defaultMessage xblockContainerIframeMessages.xblockIframeLabel.defaultMessage
@@ -553,21 +586,21 @@ describe('<CourseUnit />', () => {
); );
// after duplicate the xblock, the sidebar status changes to Draft (unpublished changes) // after duplicate the xblock, the sidebar status changes to Draft (unpublished changes)
expect(screen.getByText(sidebarMessages.sidebarTitleDraftUnpublishedChanges.defaultMessage)).toBeInTheDocument(); expect(getByText(sidebarMessages.sidebarTitleDraftUnpublishedChanges.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.visibilityStaffAndLearnersTitle.defaultMessage)).toBeInTheDocument(); expect(getByText(sidebarMessages.visibilityStaffAndLearnersTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.releaseStatusTitle.defaultMessage)).toBeInTheDocument(); expect(getByText(sidebarMessages.releaseStatusTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.sidebarBodyNote.defaultMessage)).toBeInTheDocument(); expect(getByText(sidebarMessages.sidebarBodyNote.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.visibilityWillBeVisibleToTitle.defaultMessage)).toBeInTheDocument(); expect(getByText(sidebarMessages.visibilityWillBeVisibleToTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.visibilityCheckboxTitle.defaultMessage)).toBeInTheDocument(); expect(getByText(sidebarMessages.visibilityCheckboxTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.actionButtonPublishTitle.defaultMessage)).toBeInTheDocument(); expect(getByText(sidebarMessages.actionButtonPublishTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.actionButtonDiscardChangesTitle.defaultMessage)).toBeInTheDocument(); expect(getByText(sidebarMessages.actionButtonDiscardChangesTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(courseUnitIndexMock.release_date)).toBeInTheDocument(); expect(getByText(courseUnitIndexMock.release_date)).toBeInTheDocument();
expect(screen.getByText( expect(getByText(
sidebarMessages.publishInfoDraftSaved.defaultMessage sidebarMessages.publishInfoDraftSaved.defaultMessage
.replace('{editedOn}', courseUnitIndexMock.edited_on) .replace('{editedOn}', courseUnitIndexMock.edited_on)
.replace('{editedBy}', courseUnitIndexMock.edited_by), .replace('{editedBy}', courseUnitIndexMock.edited_by),
)).toBeInTheDocument(); )).toBeInTheDocument();
expect(screen.getByText( expect(getByText(
sidebarMessages.releaseInfoWithSection.defaultMessage sidebarMessages.releaseInfoWithSection.defaultMessage
.replace('{sectionName}', courseUnitIndexMock.release_date_from), .replace('{sectionName}', courseUnitIndexMock.release_date_from),
)).toBeInTheDocument(); )).toBeInTheDocument();
@@ -577,19 +610,19 @@ describe('<CourseUnit />', () => {
it('handles CourseUnit header action buttons', async () => { it('handles CourseUnit header action buttons', async () => {
const { open } = window; const { open } = window;
window.open = jest.fn(); window.open = jest.fn();
render(<RootWrapper />); const { getByRole } = render(<RootWrapper />);
const { const {
draft_preview_link: draftPreviewLink, draft_preview_link: draftPreviewLink,
published_preview_link: publishedPreviewLink, published_preview_link: publishedPreviewLink,
} = courseSectionVerticalMock; } = courseSectionVerticalMock;
await waitFor(() => { await waitFor(() => {
const viewLiveButton = screen.getByRole('button', { name: headerNavigationsMessages.viewLiveButton.defaultMessage }); const viewLiveButton = getByRole('button', { name: headerNavigationsMessages.viewLiveButton.defaultMessage });
userEvent.click(viewLiveButton); userEvent.click(viewLiveButton);
expect(window.open).toHaveBeenCalled(); expect(window.open).toHaveBeenCalled();
expect(window.open).toHaveBeenCalledWith(publishedPreviewLink, '_blank'); expect(window.open).toHaveBeenCalledWith(publishedPreviewLink, '_blank');
const previewButton = screen.getByRole('button', { name: headerNavigationsMessages.previewButton.defaultMessage }); const previewButton = getByRole('button', { name: headerNavigationsMessages.previewButton.defaultMessage });
userEvent.click(previewButton); userEvent.click(previewButton);
expect(window.open).toHaveBeenCalled(); expect(window.open).toHaveBeenCalled();
expect(window.open).toHaveBeenCalledWith(draftPreviewLink, '_blank'); expect(window.open).toHaveBeenCalledWith(draftPreviewLink, '_blank');
@@ -599,7 +632,12 @@ describe('<CourseUnit />', () => {
}); });
it('checks courseUnit title changing when edit query is successfully', async () => { it('checks courseUnit title changing when edit query is successfully', async () => {
render(<RootWrapper />); const {
findByText,
queryByRole,
getByRole,
getByTestId,
} = render(<RootWrapper />);
let editTitleButton = null; let editTitleButton = null;
let titleEditField = null; let titleEditField = null;
const newDisplayName = `${unitDisplayName} new`; const newDisplayName = `${unitDisplayName} new`;
@@ -635,7 +673,7 @@ describe('<CourseUnit />', () => {
}); });
await waitFor(() => { await waitFor(() => {
const unitHeaderTitle = screen.getByTestId('unit-header-title'); const unitHeaderTitle = getByTestId('unit-header-title');
editTitleButton = within(unitHeaderTitle) editTitleButton = within(unitHeaderTitle)
.getByRole('button', { name: headerTitleMessages.altButtonEdit.defaultMessage }); .getByRole('button', { name: headerTitleMessages.altButtonEdit.defaultMessage });
titleEditField = within(unitHeaderTitle) titleEditField = within(unitHeaderTitle)
@@ -643,7 +681,7 @@ describe('<CourseUnit />', () => {
}); });
expect(titleEditField).not.toBeInTheDocument(); expect(titleEditField).not.toBeInTheDocument();
userEvent.click(editTitleButton); userEvent.click(editTitleButton);
titleEditField = screen.getByRole('textbox', { name: headerTitleMessages.ariaLabelButtonEdit.defaultMessage }); titleEditField = getByRole('textbox', { name: headerTitleMessages.ariaLabelButtonEdit.defaultMessage });
await userEvent.clear(titleEditField); await userEvent.clear(titleEditField);
await userEvent.type(titleEditField, newDisplayName); await userEvent.type(titleEditField, newDisplayName);
@@ -651,10 +689,9 @@ describe('<CourseUnit />', () => {
expect(titleEditField).toHaveValue(newDisplayName); expect(titleEditField).toHaveValue(newDisplayName);
titleEditField = screen.queryByRole('textbox', { name: headerTitleMessages.ariaLabelButtonEdit.defaultMessage }); titleEditField = queryByRole('textbox', { name: headerTitleMessages.ariaLabelButtonEdit.defaultMessage });
titleEditField = screen.queryByRole('textbox', { name: headerTitleMessages.ariaLabelButtonEdit.defaultMessage });
expect(titleEditField).not.toBeInTheDocument(); expect(titleEditField).not.toBeInTheDocument();
expect(await screen.findByText(newDisplayName)).toBeInTheDocument(); expect(await findByText(newDisplayName)).toBeInTheDocument();
}); });
it('doesn\'t handle creating xblock and displays an error message', async () => { it('doesn\'t handle creating xblock and displays an error message', async () => {
@@ -674,14 +711,15 @@ describe('<CourseUnit />', () => {
}); });
}); });
it('handle creating Problem xblock and showing editor modal', async () => { it('handle creating Problem xblock and navigate to editor page', async () => {
const { courseKey, locator } = courseCreateXblockMock;
axiosMock axiosMock
.onPost(postXBlockBaseApiUrl({ type: 'problem', category: 'problem', parentLocator: blockId })) .onPost(postXBlockBaseApiUrl({ type: 'problem', category: 'problem', parentLocator: blockId }))
.reply(200, courseCreateXblockMock); .reply(200, courseCreateXblockMock);
render(<RootWrapper />); const { getByText, getByRole } = render(<RootWrapper />);
await waitFor(() => { await waitFor(() => {
userEvent.click(screen.getByRole('button', { name: sidebarMessages.actionButtonPublishTitle.defaultMessage })); userEvent.click(getByRole('button', { name: sidebarMessages.actionButtonPublishTitle.defaultMessage }));
}); });
axiosMock axiosMock
@@ -701,12 +739,13 @@ describe('<CourseUnit />', () => {
await executeThunk(editCourseUnitVisibilityAndData(blockId, PUBLISH_TYPES.makePublic, true), store.dispatch); await executeThunk(editCourseUnitVisibilityAndData(blockId, PUBLISH_TYPES.makePublic, true), store.dispatch);
await waitFor(() => { await waitFor(() => {
const problemButton = screen.getByRole('button', { const problemButton = getByRole('button', {
name: new RegExp(`problem ${addComponentMessages.buttonText.defaultMessage} Problem`, 'i'), name: new RegExp(`problem ${addComponentMessages.buttonText.defaultMessage} Problem`, 'i'),
hidden: true,
}); });
userEvent.click(problemButton); userEvent.click(problemButton);
expect(mockedUsedNavigate).toHaveBeenCalled();
expect(mockedUsedNavigate).toHaveBeenCalledWith(`/course/${courseKey}/editor/problem/${locator}`);
}); });
axiosMock axiosMock
@@ -716,28 +755,66 @@ describe('<CourseUnit />', () => {
await executeThunk(editCourseUnitVisibilityAndData(blockId, PUBLISH_TYPES.makePublic, true), store.dispatch); await executeThunk(editCourseUnitVisibilityAndData(blockId, PUBLISH_TYPES.makePublic, true), store.dispatch);
// after creating problem xblock, the sidebar status changes to Draft (unpublished changes) // after creating problem xblock, the sidebar status changes to Draft (unpublished changes)
expect(screen.getByText(sidebarMessages.sidebarTitleDraftUnpublishedChanges.defaultMessage)).toBeInTheDocument(); expect(getByText(sidebarMessages.sidebarTitleDraftUnpublishedChanges.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.visibilityStaffAndLearnersTitle.defaultMessage)).toBeInTheDocument(); expect(getByText(sidebarMessages.visibilityStaffAndLearnersTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.releaseStatusTitle.defaultMessage)).toBeInTheDocument(); expect(getByText(sidebarMessages.releaseStatusTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.sidebarBodyNote.defaultMessage)).toBeInTheDocument(); expect(getByText(sidebarMessages.sidebarBodyNote.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.visibilityWillBeVisibleToTitle.defaultMessage)).toBeInTheDocument(); expect(getByText(sidebarMessages.visibilityWillBeVisibleToTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.visibilityCheckboxTitle.defaultMessage)).toBeInTheDocument(); expect(getByText(sidebarMessages.visibilityCheckboxTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.actionButtonPublishTitle.defaultMessage)).toBeInTheDocument(); expect(getByText(sidebarMessages.actionButtonPublishTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.actionButtonDiscardChangesTitle.defaultMessage)).toBeInTheDocument(); expect(getByText(sidebarMessages.actionButtonDiscardChangesTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(courseUnitIndexMock.release_date)).toBeInTheDocument(); expect(getByText(courseUnitIndexMock.release_date)).toBeInTheDocument();
expect(screen.getByText( expect(getByText(
sidebarMessages.publishInfoDraftSaved.defaultMessage sidebarMessages.publishInfoDraftSaved.defaultMessage
.replace('{editedOn}', courseUnitIndexMock.edited_on) .replace('{editedOn}', courseUnitIndexMock.edited_on)
.replace('{editedBy}', courseUnitIndexMock.edited_by), .replace('{editedBy}', courseUnitIndexMock.edited_by),
)).toBeInTheDocument(); )).toBeInTheDocument();
expect(screen.getByText( expect(getByText(
sidebarMessages.releaseInfoWithSection.defaultMessage sidebarMessages.releaseInfoWithSection.defaultMessage
.replace('{sectionName}', courseUnitIndexMock.release_date_from), .replace('{sectionName}', courseUnitIndexMock.release_date_from),
)).toBeInTheDocument(); )).toBeInTheDocument();
}); });
it('handle creating Text xblock and saves scroll position in localStorage', async () => {
const { getByText, getByRole } = render(<RootWrapper />);
const xblockType = 'text';
axiosMock
.onPost(postXBlockBaseApiUrl({ type: xblockType, category: 'html', parentLocator: blockId }))
.reply(200, courseCreateXblockMock);
window.scrollTo(0, 250);
Object.defineProperty(window, 'scrollY', { value: 250, configurable: true });
await waitFor(() => {
const textButton = screen.getByRole('button', { name: /Text/i });
expect(getByText(addComponentMessages.title.defaultMessage)).toBeInTheDocument();
userEvent.click(textButton);
const addXBlockDialog = getByRole('dialog');
expect(addXBlockDialog).toBeInTheDocument();
expect(getByText(
addComponentMessages.modalContainerTitle.defaultMessage.replace('{componentTitle}', xblockType),
)).toBeInTheDocument();
const textRadio = screen.getByRole('radio', { name: /Text/i });
userEvent.click(textRadio);
expect(textRadio).toBeChecked();
const selectBtn = getByRole('button', { name: addComponentMessages.modalBtnText.defaultMessage });
expect(selectBtn).toBeInTheDocument();
userEvent.click(selectBtn);
});
expect(localStorage.getItem('createXBlockLastYPosition')).toBe('250');
});
it('correct addition of a new course unit after click on the "Add new unit" button', async () => { it('correct addition of a new course unit after click on the "Add new unit" button', async () => {
render(<RootWrapper />); const { getByRole, getAllByTestId } = render(<RootWrapper />);
let units = null; let units = null;
const updatedCourseSectionVerticalData = cloneDeep(courseSectionVerticalMock); const updatedCourseSectionVerticalData = cloneDeep(courseSectionVerticalMock);
const updatedAncestorsChild = updatedCourseSectionVerticalData.xblock_info.ancestor_info.ancestors[0]; const updatedAncestorsChild = updatedCourseSectionVerticalData.xblock_info.ancestor_info.ancestors[0];
@@ -747,7 +824,7 @@ describe('<CourseUnit />', () => {
]); ]);
await waitFor(async () => { await waitFor(async () => {
units = screen.getAllByTestId('course-unit-btn'); units = getAllByTestId('course-unit-btn');
const courseUnits = courseSectionVerticalMock.xblock_info.ancestor_info.ancestors[0].child_info.children; const courseUnits = courseSectionVerticalMock.xblock_info.ancestor_info.ancestors[0].child_info.children;
expect(units).toHaveLength(courseUnits.length); expect(units).toHaveLength(courseUnits.length);
}); });
@@ -764,8 +841,8 @@ describe('<CourseUnit />', () => {
await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch); await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch);
const addNewUnitBtn = screen.getByRole('button', { name: courseSequenceMessages.newUnitBtnText.defaultMessage }); const addNewUnitBtn = getByRole('button', { name: courseSequenceMessages.newUnitBtnText.defaultMessage });
units = screen.getAllByTestId('course-unit-btn'); units = getAllByTestId('course-unit-btn');
const updatedCourseUnits = updatedCourseSectionVerticalData const updatedCourseUnits = updatedCourseSectionVerticalData
.xblock_info.ancestor_info.ancestors[0].child_info.children; .xblock_info.ancestor_info.ancestors[0].child_info.children;
@@ -777,7 +854,7 @@ describe('<CourseUnit />', () => {
}); });
it('the sequence unit is updated after changing the unit header', async () => { it('the sequence unit is updated after changing the unit header', async () => {
render(<RootWrapper />); const { getAllByTestId, getByTestId } = render(<RootWrapper />);
const updatedCourseSectionVerticalData = cloneDeep(courseSectionVerticalMock); const updatedCourseSectionVerticalData = cloneDeep(courseSectionVerticalMock);
const updatedAncestorsChild = updatedCourseSectionVerticalData.xblock_info.ancestor_info.ancestors[0]; const updatedAncestorsChild = updatedCourseSectionVerticalData.xblock_info.ancestor_info.ancestors[0];
set(updatedCourseSectionVerticalData, 'xblock_info.ancestor_info.ancestors[0].child_info.children', [ set(updatedCourseSectionVerticalData, 'xblock_info.ancestor_info.ancestors[0].child_info.children', [
@@ -809,7 +886,7 @@ describe('<CourseUnit />', () => {
await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch); await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch);
const unitHeaderTitle = screen.getByTestId('unit-header-title'); const unitHeaderTitle = getByTestId('unit-header-title');
const editTitleButton = within(unitHeaderTitle).getByRole('button', { name: headerTitleMessages.altButtonEdit.defaultMessage }); const editTitleButton = within(unitHeaderTitle).getByRole('button', { name: headerTitleMessages.altButtonEdit.defaultMessage });
userEvent.click(editTitleButton); userEvent.click(editTitleButton);
@@ -821,21 +898,20 @@ describe('<CourseUnit />', () => {
await userEvent.tab(); await userEvent.tab();
await waitFor(async () => { await waitFor(async () => {
const units = screen.getAllByTestId('course-unit-btn'); const units = getAllByTestId('course-unit-btn');
expect(units.some(unit => unit.title === newDisplayName)).toBe(true); expect(units.some(unit => unit.title === newDisplayName)).toBe(true);
}); });
}); });
it('handles creating Video xblock and showing editor modal using videogalleryflow', async () => { it('handles creating Video xblock and navigates to editor page', async () => {
const waffleSpy = jest.spyOn(selectors, 'getWaffleFlags').mockReturnValue({ useVideoGalleryFlow: true }); const { courseKey, locator } = courseCreateXblockMock;
axiosMock axiosMock
.onPost(postXBlockBaseApiUrl({ type: 'video', category: 'video', parentLocator: blockId })) .onPost(postXBlockBaseApiUrl({ type: 'video', category: 'video', parentLocator: blockId }))
.reply(200, courseCreateXblockMock); .reply(200, courseCreateXblockMock);
render(<RootWrapper />); const { getByText, queryByRole, getByRole } = render(<RootWrapper />);
await waitFor(() => { await waitFor(() => {
userEvent.click(screen.getByRole('button', { name: sidebarMessages.actionButtonPublishTitle.defaultMessage })); userEvent.click(getByRole('button', { name: sidebarMessages.actionButtonPublishTitle.defaultMessage }));
}); });
axiosMock axiosMock
@@ -856,102 +932,23 @@ describe('<CourseUnit />', () => {
await waitFor(() => { await waitFor(() => {
// check if the sidebar status is Published and Live // check if the sidebar status is Published and Live
expect(screen.getByText(sidebarMessages.sidebarTitlePublishedAndLive.defaultMessage)).toBeInTheDocument(); expect(getByText(sidebarMessages.sidebarTitlePublishedAndLive.defaultMessage)).toBeInTheDocument();
}); expect(getByText(
expect(screen.getByText(
sidebarMessages.publishLastPublished.defaultMessage
.replace('{publishedOn}', courseUnitIndexMock.published_on)
.replace('{publishedBy}', userName),
)).toBeInTheDocument();
expect(screen.queryByRole('button', { name: sidebarMessages.actionButtonPublishTitle.defaultMessage })).not.toBeInTheDocument();
const videoButton = screen.getByRole('button', {
name: new RegExp(`${addComponentMessages.buttonText.defaultMessage} Video`, 'i'),
hidden: true,
});
userEvent.click(videoButton);
axiosMock
.onGet(getCourseUnitApiUrl(blockId))
.reply(200, courseUnitIndexMock);
await executeThunk(editCourseUnitVisibilityAndData(blockId, PUBLISH_TYPES.makePublic, true), store.dispatch);
// after creating video xblock, the sidebar status changes to Draft (unpublished changes)
expect(screen.getByText(sidebarMessages.sidebarTitleDraftUnpublishedChanges.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.visibilityStaffAndLearnersTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.releaseStatusTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.sidebarBodyNote.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.visibilityWillBeVisibleToTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.visibilityCheckboxTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.actionButtonPublishTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.actionButtonDiscardChangesTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(courseUnitIndexMock.release_date)).toBeInTheDocument();
expect(screen.getByText(
sidebarMessages.publishInfoDraftSaved.defaultMessage
.replace('{editedOn}', courseUnitIndexMock.edited_on)
.replace('{editedBy}', courseUnitIndexMock.edited_by),
)).toBeInTheDocument();
expect(screen.getByText(
sidebarMessages.releaseInfoWithSection.defaultMessage
.replace('{sectionName}', courseUnitIndexMock.release_date_from),
)).toBeInTheDocument();
expect(screen.getByRole('heading', { name: /add video to your course/i, hidden: true })).toBeInTheDocument();
waffleSpy.mockRestore();
});
it('handles creating Video xblock and showing editor modal', async () => {
axiosMock
.onPost(postXBlockBaseApiUrl({ type: 'video', category: 'video', parentLocator: blockId }))
.reply(200, courseCreateXblockMock);
render(<RootWrapper />);
await waitFor(() => {
userEvent.click(screen.getByRole('button', { name: sidebarMessages.actionButtonPublishTitle.defaultMessage }));
});
axiosMock
.onPost(getXBlockBaseApiUrl(blockId), {
publish: PUBLISH_TYPES.makePublic,
})
.reply(200, { dummy: 'value' });
axiosMock
.onGet(getCourseUnitApiUrl(blockId))
.reply(200, {
...courseUnitIndexMock,
visibility_state: UNIT_VISIBILITY_STATES.live,
has_changes: false,
published_by: userName,
});
await executeThunk(editCourseUnitVisibilityAndData(blockId, PUBLISH_TYPES.makePublic, true), store.dispatch);
await waitFor(() => {
// check if the sidebar status is Published and Live
expect(screen.getByText(sidebarMessages.sidebarTitlePublishedAndLive.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(
sidebarMessages.publishLastPublished.defaultMessage sidebarMessages.publishLastPublished.defaultMessage
.replace('{publishedOn}', courseUnitIndexMock.published_on) .replace('{publishedOn}', courseUnitIndexMock.published_on)
.replace('{publishedBy}', userName), .replace('{publishedBy}', userName),
)).toBeInTheDocument(); )).toBeInTheDocument();
expect(screen.queryByRole('button', { name: sidebarMessages.actionButtonPublishTitle.defaultMessage })).not.toBeInTheDocument(); expect(queryByRole('button', { name: sidebarMessages.actionButtonPublishTitle.defaultMessage })).not.toBeInTheDocument();
const videoButton = screen.getByRole('button', { const videoButton = getByRole('button', {
name: new RegExp(`${addComponentMessages.buttonText.defaultMessage} Video`, 'i'), name: new RegExp(`${addComponentMessages.buttonText.defaultMessage} Video`, 'i'),
hidden: true,
}); });
userEvent.click(videoButton); userEvent.click(videoButton);
expect(mockedUsedNavigate).toHaveBeenCalled();
expect(mockedUsedNavigate).toHaveBeenCalledWith(`/course/${courseKey}/editor/video/${locator}`);
}); });
/** TODO -- fix this test.
await waitFor(() => {
expect(getByRole('textbox', { name: /paste your video id or url/i })).toBeInTheDocument();
});
*/
axiosMock axiosMock
.onGet(getCourseUnitApiUrl(blockId)) .onGet(getCourseUnitApiUrl(blockId))
.reply(200, courseUnitIndexMock); .reply(200, courseUnitIndexMock);
@@ -959,45 +956,45 @@ describe('<CourseUnit />', () => {
await executeThunk(editCourseUnitVisibilityAndData(blockId, PUBLISH_TYPES.makePublic, true), store.dispatch); await executeThunk(editCourseUnitVisibilityAndData(blockId, PUBLISH_TYPES.makePublic, true), store.dispatch);
// after creating video xblock, the sidebar status changes to Draft (unpublished changes) // after creating video xblock, the sidebar status changes to Draft (unpublished changes)
expect(screen.getByText(sidebarMessages.sidebarTitleDraftUnpublishedChanges.defaultMessage)).toBeInTheDocument(); expect(getByText(sidebarMessages.sidebarTitleDraftUnpublishedChanges.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.visibilityStaffAndLearnersTitle.defaultMessage)).toBeInTheDocument(); expect(getByText(sidebarMessages.visibilityStaffAndLearnersTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.releaseStatusTitle.defaultMessage)).toBeInTheDocument(); expect(getByText(sidebarMessages.releaseStatusTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.sidebarBodyNote.defaultMessage)).toBeInTheDocument(); expect(getByText(sidebarMessages.sidebarBodyNote.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.visibilityWillBeVisibleToTitle.defaultMessage)).toBeInTheDocument(); expect(getByText(sidebarMessages.visibilityWillBeVisibleToTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.visibilityCheckboxTitle.defaultMessage)).toBeInTheDocument(); expect(getByText(sidebarMessages.visibilityCheckboxTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.actionButtonPublishTitle.defaultMessage)).toBeInTheDocument(); expect(getByText(sidebarMessages.actionButtonPublishTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.actionButtonDiscardChangesTitle.defaultMessage)).toBeInTheDocument(); expect(getByText(sidebarMessages.actionButtonDiscardChangesTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(courseUnitIndexMock.release_date)).toBeInTheDocument(); expect(getByText(courseUnitIndexMock.release_date)).toBeInTheDocument();
expect(screen.getByText( expect(getByText(
sidebarMessages.publishInfoDraftSaved.defaultMessage sidebarMessages.publishInfoDraftSaved.defaultMessage
.replace('{editedOn}', courseUnitIndexMock.edited_on) .replace('{editedOn}', courseUnitIndexMock.edited_on)
.replace('{editedBy}', courseUnitIndexMock.edited_by), .replace('{editedBy}', courseUnitIndexMock.edited_by),
)).toBeInTheDocument(); )).toBeInTheDocument();
expect(screen.getByText( expect(getByText(
sidebarMessages.releaseInfoWithSection.defaultMessage sidebarMessages.releaseInfoWithSection.defaultMessage
.replace('{sectionName}', courseUnitIndexMock.release_date_from), .replace('{sectionName}', courseUnitIndexMock.release_date_from),
)).toBeInTheDocument(); )).toBeInTheDocument();
}); });
it('renders course unit details for a draft with unpublished changes', async () => { it('renders course unit details for a draft with unpublished changes', async () => {
render(<RootWrapper />); const { getByText } = render(<RootWrapper />);
await waitFor(() => { await waitFor(() => {
expect(screen.getByText(sidebarMessages.sidebarTitleDraftUnpublishedChanges.defaultMessage)).toBeInTheDocument(); expect(getByText(sidebarMessages.sidebarTitleDraftUnpublishedChanges.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.visibilityStaffAndLearnersTitle.defaultMessage)).toBeInTheDocument(); expect(getByText(sidebarMessages.visibilityStaffAndLearnersTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.releaseStatusTitle.defaultMessage)).toBeInTheDocument(); expect(getByText(sidebarMessages.releaseStatusTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.sidebarBodyNote.defaultMessage)).toBeInTheDocument(); expect(getByText(sidebarMessages.sidebarBodyNote.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.visibilityWillBeVisibleToTitle.defaultMessage)).toBeInTheDocument(); expect(getByText(sidebarMessages.visibilityWillBeVisibleToTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.visibilityCheckboxTitle.defaultMessage)).toBeInTheDocument(); expect(getByText(sidebarMessages.visibilityCheckboxTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.actionButtonPublishTitle.defaultMessage)).toBeInTheDocument(); expect(getByText(sidebarMessages.actionButtonPublishTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.actionButtonDiscardChangesTitle.defaultMessage)).toBeInTheDocument(); expect(getByText(sidebarMessages.actionButtonDiscardChangesTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(courseUnitIndexMock.release_date)).toBeInTheDocument(); expect(getByText(courseUnitIndexMock.release_date)).toBeInTheDocument();
expect(screen.getByText( expect(getByText(
sidebarMessages.publishInfoDraftSaved.defaultMessage sidebarMessages.publishInfoDraftSaved.defaultMessage
.replace('{editedOn}', courseUnitIndexMock.edited_on) .replace('{editedOn}', courseUnitIndexMock.edited_on)
.replace('{editedBy}', courseUnitIndexMock.edited_by), .replace('{editedBy}', courseUnitIndexMock.edited_by),
)).toBeInTheDocument(); )).toBeInTheDocument();
expect(screen.getByText( expect(getByText(
sidebarMessages.releaseInfoWithSection.defaultMessage sidebarMessages.releaseInfoWithSection.defaultMessage
.replace('{sectionName}', courseUnitIndexMock.release_date_from), .replace('{sectionName}', courseUnitIndexMock.release_date_from),
)).toBeInTheDocument(); )).toBeInTheDocument();
@@ -1005,14 +1002,14 @@ describe('<CourseUnit />', () => {
}); });
it('renders course unit details in the sidebar', async () => { it('renders course unit details in the sidebar', async () => {
render(<RootWrapper />); const { getByText } = render(<RootWrapper />);
const courseUnitLocationId = extractCourseUnitId(courseUnitIndexMock.id); const courseUnitLocationId = extractCourseUnitId(courseUnitIndexMock.id);
await waitFor(() => { await waitFor(() => {
expect(screen.getByText(sidebarMessages.sidebarHeaderUnitLocationTitle.defaultMessage)).toBeInTheDocument(); expect(getByText(sidebarMessages.sidebarHeaderUnitLocationTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.unitLocationTitle.defaultMessage)).toBeInTheDocument(); expect(getByText(sidebarMessages.unitLocationTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(courseUnitLocationId)).toBeInTheDocument(); expect(getByText(courseUnitLocationId)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.unitLocationDescription.defaultMessage expect(getByText(sidebarMessages.unitLocationDescription.defaultMessage
.replace('{id}', courseUnitLocationId))).toBeInTheDocument(); .replace('{id}', courseUnitLocationId))).toBeInTheDocument();
}); });
}); });
@@ -1050,13 +1047,13 @@ describe('<CourseUnit />', () => {
}); });
it('should toggle visibility from sidebar and update course unit state accordingly', async () => { it('should toggle visibility from sidebar and update course unit state accordingly', async () => {
render(<RootWrapper />); const { getByRole, getByTestId } = render(<RootWrapper />);
let courseUnitSidebar; let courseUnitSidebar;
let draftUnpublishedChangesHeading; let draftUnpublishedChangesHeading;
let visibilityCheckbox; let visibilityCheckbox;
await waitFor(() => { await waitFor(() => {
courseUnitSidebar = screen.getByTestId('course-unit-sidebar'); courseUnitSidebar = getByTestId('course-unit-sidebar');
draftUnpublishedChangesHeading = within(courseUnitSidebar) draftUnpublishedChangesHeading = within(courseUnitSidebar)
.getByText(sidebarMessages.sidebarTitleDraftUnpublishedChanges.defaultMessage); .getByText(sidebarMessages.sidebarTitleDraftUnpublishedChanges.defaultMessage);
@@ -1093,7 +1090,7 @@ describe('<CourseUnit />', () => {
userEvent.click(visibilityCheckbox); userEvent.click(visibilityCheckbox);
const modalNotification = screen.getByRole('dialog'); const modalNotification = getByRole('dialog');
const makeVisibilityBtn = within(modalNotification).getByRole('button', { name: sidebarMessages.modalMakeVisibilityActionButtonText.defaultMessage }); const makeVisibilityBtn = within(modalNotification).getByRole('button', { name: sidebarMessages.modalMakeVisibilityActionButtonText.defaultMessage });
const cancelBtn = within(modalNotification).getByRole('button', { name: sidebarMessages.modalMakeVisibilityCancelButtonText.defaultMessage }); const cancelBtn = within(modalNotification).getByRole('button', { name: sidebarMessages.modalMakeVisibilityCancelButtonText.defaultMessage });
const headingElement = within(modalNotification).getByRole('heading', { name: sidebarMessages.modalMakeVisibilityTitle.defaultMessage, class: 'pgn__modal-title' }); const headingElement = within(modalNotification).getByRole('heading', { name: sidebarMessages.modalMakeVisibilityTitle.defaultMessage, class: 'pgn__modal-title' });
@@ -1125,12 +1122,12 @@ describe('<CourseUnit />', () => {
}); });
it('should publish course unit after click on the "Publish" button', async () => { it('should publish course unit after click on the "Publish" button', async () => {
render(<RootWrapper />); const { getByTestId } = render(<RootWrapper />);
let courseUnitSidebar; let courseUnitSidebar;
let publishBtn; let publishBtn;
await waitFor(() => { await waitFor(() => {
courseUnitSidebar = screen.getByTestId('course-unit-sidebar'); courseUnitSidebar = getByTestId('course-unit-sidebar');
publishBtn = within(courseUnitSidebar).queryByRole('button', { name: sidebarMessages.actionButtonPublishTitle.defaultMessage }); publishBtn = within(courseUnitSidebar).queryByRole('button', { name: sidebarMessages.actionButtonPublishTitle.defaultMessage });
expect(publishBtn).toBeInTheDocument(); expect(publishBtn).toBeInTheDocument();
@@ -1164,12 +1161,12 @@ describe('<CourseUnit />', () => {
}); });
it('should discard changes after click on the "Discard changes" button', async () => { it('should discard changes after click on the "Discard changes" button', async () => {
render(<RootWrapper />); const { getByTestId, getByRole } = render(<RootWrapper />);
let courseUnitSidebar; let courseUnitSidebar;
let discardChangesBtn; let discardChangesBtn;
await waitFor(() => { await waitFor(() => {
courseUnitSidebar = screen.getByTestId('course-unit-sidebar'); courseUnitSidebar = getByTestId('course-unit-sidebar');
const draftUnpublishedChangesHeading = within(courseUnitSidebar) const draftUnpublishedChangesHeading = within(courseUnitSidebar)
.getByText(sidebarMessages.sidebarTitleDraftUnpublishedChanges.defaultMessage); .getByText(sidebarMessages.sidebarTitleDraftUnpublishedChanges.defaultMessage);
@@ -1179,7 +1176,7 @@ describe('<CourseUnit />', () => {
userEvent.click(discardChangesBtn); userEvent.click(discardChangesBtn);
const modalNotification = screen.getByRole('dialog'); const modalNotification = getByRole('dialog');
expect(modalNotification).toBeInTheDocument(); expect(modalNotification).toBeInTheDocument();
expect(within(modalNotification) expect(within(modalNotification)
.getByText(sidebarMessages.modalDiscardUnitChangesDescription.defaultMessage)).toBeInTheDocument(); .getByText(sidebarMessages.modalDiscardUnitChangesDescription.defaultMessage)).toBeInTheDocument();
@@ -1216,7 +1213,7 @@ describe('<CourseUnit />', () => {
}); });
it('should toggle visibility from header configure modal and update course unit state accordingly', async () => { it('should toggle visibility from header configure modal and update course unit state accordingly', async () => {
render(<RootWrapper />); const { getByRole, getByTestId } = render(<RootWrapper />);
let courseUnitSidebar; let courseUnitSidebar;
let sidebarVisibilityCheckbox; let sidebarVisibilityCheckbox;
let modalVisibilityCheckbox; let modalVisibilityCheckbox;
@@ -1224,16 +1221,16 @@ describe('<CourseUnit />', () => {
let restrictAccessSelect; let restrictAccessSelect;
await waitFor(() => { await waitFor(() => {
courseUnitSidebar = screen.getByTestId('course-unit-sidebar'); courseUnitSidebar = getByTestId('course-unit-sidebar');
sidebarVisibilityCheckbox = within(courseUnitSidebar) sidebarVisibilityCheckbox = within(courseUnitSidebar)
.getByLabelText(sidebarMessages.visibilityCheckboxTitle.defaultMessage); .getByLabelText(sidebarMessages.visibilityCheckboxTitle.defaultMessage);
expect(sidebarVisibilityCheckbox).not.toBeChecked(); expect(sidebarVisibilityCheckbox).not.toBeChecked();
const headerConfigureBtn = screen.getByRole('button', { name: /settings/i }); const headerConfigureBtn = getByRole('button', { name: /settings/i });
expect(headerConfigureBtn).toBeInTheDocument(); expect(headerConfigureBtn).toBeInTheDocument();
userEvent.click(headerConfigureBtn); userEvent.click(headerConfigureBtn);
configureModal = screen.getByTestId('configure-modal'); configureModal = getByTestId('configure-modal');
restrictAccessSelect = within(configureModal) restrictAccessSelect = within(configureModal)
.getByRole('combobox', { name: configureModalMessages.restrictAccessTo.defaultMessage }); .getByRole('combobox', { name: configureModalMessages.restrictAccessTo.defaultMessage });
expect(within(configureModal) expect(within(configureModal)
@@ -1289,8 +1286,8 @@ describe('<CourseUnit />', () => {
...getConfig(), ...getConfig(),
ENABLE_TAGGING_TAXONOMY_PAGES: 'true', ENABLE_TAGGING_TAXONOMY_PAGES: 'true',
}); });
render(<RootWrapper />); const { getByText } = render(<RootWrapper />);
await waitFor(() => { expect(screen.getByText('Unit tags')).toBeInTheDocument(); }); await waitFor(() => { expect(getByText('Unit tags')).toBeInTheDocument(); });
}); });
it('hides the Tags sidebar when not enabled', async () => { it('hides the Tags sidebar when not enabled', async () => {
@@ -1298,13 +1295,15 @@ describe('<CourseUnit />', () => {
...getConfig(), ...getConfig(),
ENABLE_TAGGING_TAXONOMY_PAGES: 'false', ENABLE_TAGGING_TAXONOMY_PAGES: 'false',
}); });
render(<RootWrapper />); const { queryByText } = render(<RootWrapper />);
await waitFor(() => { expect(screen.queryByText('Unit tags')).not.toBeInTheDocument(); }); await waitFor(() => { expect(queryByText('Unit tags')).not.toBeInTheDocument(); });
}); });
describe('Copy paste functionality', () => { describe('Copy paste functionality', () => {
it('should copy a unit, paste it as a new unit, and update the course section vertical data', async () => { it('should copy a unit, paste it as a new unit, and update the course section vertical data', async () => {
render(<RootWrapper />); const {
getAllByTestId, getByRole,
} = render(<RootWrapper />);
axiosMock axiosMock
.onGet(getCourseUnitApiUrl(courseId)) .onGet(getCourseUnitApiUrl(courseId))
@@ -1313,11 +1312,18 @@ describe('<CourseUnit />', () => {
enable_copy_paste_units: true, enable_copy_paste_units: true,
}); });
axiosMock
.onGet(getCourseSectionVerticalApiUrl(blockId))
.reply(200, {
...courseSectionVerticalMock,
user_clipboard: clipboardUnit,
});
await executeThunk(fetchCourseUnitQuery(courseId), store.dispatch); await executeThunk(fetchCourseUnitQuery(courseId), store.dispatch);
await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch); await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch);
userEvent.click(screen.getByRole('button', { name: sidebarMessages.actionButtonCopyUnitTitle.defaultMessage })); userEvent.click(getByRole('button', { name: sidebarMessages.actionButtonCopyUnitTitle.defaultMessage }));
userEvent.click(screen.getByRole('button', { name: courseSequenceMessages.pasteAsNewUnitLink.defaultMessage })); userEvent.click(getByRole('button', { name: courseSequenceMessages.pasteAsNewUnitLink.defaultMessage }));
let units = null; let units = null;
const updatedCourseSectionVerticalData = cloneDeep(courseSectionVerticalMock); const updatedCourseSectionVerticalData = cloneDeep(courseSectionVerticalMock);
@@ -1328,7 +1334,7 @@ describe('<CourseUnit />', () => {
]); ]);
await waitFor(() => { await waitFor(() => {
units = screen.getAllByTestId('course-unit-btn'); units = getAllByTestId('course-unit-btn');
const courseUnits = courseSectionVerticalMock.xblock_info.ancestor_info.ancestors[0].child_info.children; const courseUnits = courseSectionVerticalMock.xblock_info.ancestor_info.ancestors[0].child_info.children;
expect(units).toHaveLength(courseUnits.length); expect(units).toHaveLength(courseUnits.length);
}); });
@@ -1344,7 +1350,7 @@ describe('<CourseUnit />', () => {
await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch); await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch);
units = screen.getAllByTestId('course-unit-btn'); units = getAllByTestId('course-unit-btn');
const updatedCourseUnits = updatedCourseSectionVerticalData const updatedCourseUnits = updatedCourseSectionVerticalData
.xblock_info.ancestor_info.ancestors[0].child_info.children; .xblock_info.ancestor_info.ancestors[0].child_info.children;
@@ -1355,16 +1361,18 @@ describe('<CourseUnit />', () => {
}); });
it('should increase the number of course XBlocks after copying and pasting a block', async () => { it('should increase the number of course XBlocks after copying and pasting a block', async () => {
render(<RootWrapper />); const { getByRole, getByTitle } = render(<RootWrapper />);
simulatePostMessageEvent(messageTypes.copyXBlock, { simulatePostMessageEvent(messageTypes.copyXBlock, {
id: courseVerticalChildrenMock.children[0].block_id, id: courseVerticalChildrenMock.children[0].block_id,
}); });
axiosMock axiosMock
.onGet(getClipboardUrl()) .onGet(getCourseSectionVerticalApiUrl(blockId))
.reply(200, clipboardXBlock); .reply(200, {
...courseSectionVerticalMock,
user_clipboard: clipboardXBlock,
});
axiosMock axiosMock
.onGet(getCourseUnitApiUrl(courseId)) .onGet(getCourseUnitApiUrl(courseId))
.reply(200, { .reply(200, {
@@ -1374,11 +1382,11 @@ describe('<CourseUnit />', () => {
await executeThunk(fetchCourseUnitQuery(courseId), store.dispatch); await executeThunk(fetchCourseUnitQuery(courseId), store.dispatch);
await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch); await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch);
userEvent.click(screen.getByRole('button', { name: sidebarMessages.actionButtonCopyUnitTitle.defaultMessage })); userEvent.click(getByRole('button', { name: sidebarMessages.actionButtonCopyUnitTitle.defaultMessage }));
userEvent.click(screen.getByRole('button', { name: messages.pasteButtonText.defaultMessage })); userEvent.click(getByRole('button', { name: messages.pasteButtonText.defaultMessage }));
await waitFor(() => { await waitFor(() => {
const iframe = screen.getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage); const iframe = getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
expect(iframe).toHaveAttribute( expect(iframe).toHaveAttribute(
'aria-label', 'aria-label',
xblockContainerIframeMessages.xblockIframeLabel.defaultMessage xblockContainerIframeMessages.xblockIframeLabel.defaultMessage
@@ -1414,7 +1422,7 @@ describe('<CourseUnit />', () => {
await executeThunk(fetchCourseVerticalChildrenData(blockId), store.dispatch); await executeThunk(fetchCourseVerticalChildrenData(blockId), store.dispatch);
await waitFor(() => { await waitFor(() => {
const iframe = screen.getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage); const iframe = getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
expect(iframe).toHaveAttribute( expect(iframe).toHaveAttribute(
'aria-label', 'aria-label',
xblockContainerIframeMessages.xblockIframeLabel.defaultMessage xblockContainerIframeMessages.xblockIframeLabel.defaultMessage
@@ -1424,7 +1432,9 @@ describe('<CourseUnit />', () => {
}); });
it('displays a notification about new files after pasting a component', async () => { it('displays a notification about new files after pasting a component', async () => {
render(<RootWrapper />); const {
queryByTestId, getByTestId, getByRole,
} = render(<RootWrapper />);
axiosMock axiosMock
.onGet(getCourseUnitApiUrl(courseId)) .onGet(getCourseUnitApiUrl(courseId))
@@ -1433,11 +1443,18 @@ describe('<CourseUnit />', () => {
enable_copy_paste_units: true, enable_copy_paste_units: true,
}); });
axiosMock
.onGet(getCourseSectionVerticalApiUrl(blockId))
.reply(200, {
...courseSectionVerticalMock,
user_clipboard: clipboardUnit,
});
await executeThunk(fetchCourseUnitQuery(courseId), store.dispatch); await executeThunk(fetchCourseUnitQuery(courseId), store.dispatch);
await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch); await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch);
userEvent.click(screen.getByRole('button', { name: sidebarMessages.actionButtonCopyUnitTitle.defaultMessage })); userEvent.click(getByRole('button', { name: sidebarMessages.actionButtonCopyUnitTitle.defaultMessage }));
userEvent.click(screen.getByRole('button', { name: courseSequenceMessages.pasteAsNewUnitLink.defaultMessage })); userEvent.click(getByRole('button', { name: courseSequenceMessages.pasteAsNewUnitLink.defaultMessage }));
const updatedCourseSectionVerticalData = cloneDeep(courseSectionVerticalMock); const updatedCourseSectionVerticalData = cloneDeep(courseSectionVerticalMock);
const updatedAncestorsChild = updatedCourseSectionVerticalData.xblock_info.ancestor_info.ancestors[0]; const updatedAncestorsChild = updatedCourseSectionVerticalData.xblock_info.ancestor_info.ancestors[0];
@@ -1456,7 +1473,7 @@ describe('<CourseUnit />', () => {
global.localStorage.setItem('staticFileNotices', JSON.stringify(clipboardMockResponse.staticFileNotices)); global.localStorage.setItem('staticFileNotices', JSON.stringify(clipboardMockResponse.staticFileNotices));
await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch); await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch);
await executeThunk(createNewCourseXBlock(camelCaseObject(postXBlockBody), null, blockId), store.dispatch); await executeThunk(createNewCourseXBlock(camelCaseObject(postXBlockBody), null, blockId), store.dispatch);
const newFilesAlert = screen.getByTestId('has-new-files-alert'); const newFilesAlert = getByTestId('has-new-files-alert');
expect(within(newFilesAlert) expect(within(newFilesAlert)
.getByText(pasteNotificationsMessages.hasNewFilesTitle.defaultMessage)).toBeInTheDocument(); .getByText(pasteNotificationsMessages.hasNewFilesTitle.defaultMessage)).toBeInTheDocument();
@@ -1470,11 +1487,13 @@ describe('<CourseUnit />', () => {
userEvent.click(within(newFilesAlert).getByText(/Dismiss/i)); userEvent.click(within(newFilesAlert).getByText(/Dismiss/i));
expect(screen.queryByTestId('has-new-files-alert')).toBeNull(); expect(queryByTestId('has-new-files-alert')).toBeNull();
}); });
it('displays a notification about conflicting errors after pasting a component', async () => { it('displays a notification about conflicting errors after pasting a component', async () => {
render(<RootWrapper />); const {
queryByTestId, getByTestId, getByRole,
} = render(<RootWrapper />);
axiosMock axiosMock
.onGet(getCourseUnitApiUrl(courseId)) .onGet(getCourseUnitApiUrl(courseId))
@@ -1483,11 +1502,18 @@ describe('<CourseUnit />', () => {
enable_copy_paste_units: true, enable_copy_paste_units: true,
}); });
axiosMock
.onGet(getCourseSectionVerticalApiUrl(blockId))
.reply(200, {
...courseSectionVerticalMock,
user_clipboard: clipboardUnit,
});
await executeThunk(fetchCourseUnitQuery(courseId), store.dispatch); await executeThunk(fetchCourseUnitQuery(courseId), store.dispatch);
await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch); await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch);
userEvent.click(screen.getByRole('button', { name: sidebarMessages.actionButtonCopyUnitTitle.defaultMessage })); userEvent.click(getByRole('button', { name: sidebarMessages.actionButtonCopyUnitTitle.defaultMessage }));
userEvent.click(screen.getByRole('button', { name: courseSequenceMessages.pasteAsNewUnitLink.defaultMessage })); userEvent.click(getByRole('button', { name: courseSequenceMessages.pasteAsNewUnitLink.defaultMessage }));
const updatedCourseSectionVerticalData = cloneDeep(courseSectionVerticalMock); const updatedCourseSectionVerticalData = cloneDeep(courseSectionVerticalMock);
const updatedAncestorsChild = updatedCourseSectionVerticalData.xblock_info.ancestor_info.ancestors[0]; const updatedAncestorsChild = updatedCourseSectionVerticalData.xblock_info.ancestor_info.ancestors[0];
@@ -1508,7 +1534,7 @@ describe('<CourseUnit />', () => {
global.localStorage.setItem('staticFileNotices', JSON.stringify(clipboardMockResponse.staticFileNotices)); global.localStorage.setItem('staticFileNotices', JSON.stringify(clipboardMockResponse.staticFileNotices));
await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch); await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch);
await executeThunk(createNewCourseXBlock(camelCaseObject(postXBlockBody), null, blockId), store.dispatch); await executeThunk(createNewCourseXBlock(camelCaseObject(postXBlockBody), null, blockId), store.dispatch);
const conflictingErrorsAlert = screen.getByTestId('has-conflicting-errors-alert'); const conflictingErrorsAlert = getByTestId('has-conflicting-errors-alert');
expect(within(conflictingErrorsAlert) expect(within(conflictingErrorsAlert)
.getByText(pasteNotificationsMessages.hasConflictingErrorsTitle.defaultMessage)).toBeInTheDocument(); .getByText(pasteNotificationsMessages.hasConflictingErrorsTitle.defaultMessage)).toBeInTheDocument();
@@ -1522,11 +1548,13 @@ describe('<CourseUnit />', () => {
userEvent.click(within(conflictingErrorsAlert).getByText(/Dismiss/i)); userEvent.click(within(conflictingErrorsAlert).getByText(/Dismiss/i));
expect(screen.queryByTestId('has-conflicting-errors-alert')).toBeNull(); expect(queryByTestId('has-conflicting-errors-alert')).toBeNull();
}); });
it('displays a notification about error files after pasting a component', async () => { it('displays a notification about error files after pasting a component', async () => {
render(<RootWrapper />); const {
queryByTestId, getByTestId, getByRole,
} = render(<RootWrapper />);
axiosMock axiosMock
.onGet(getCourseUnitApiUrl(courseId)) .onGet(getCourseUnitApiUrl(courseId))
@@ -1535,11 +1563,18 @@ describe('<CourseUnit />', () => {
enable_copy_paste_units: true, enable_copy_paste_units: true,
}); });
axiosMock
.onGet(getCourseSectionVerticalApiUrl(blockId))
.reply(200, {
...courseSectionVerticalMock,
user_clipboard: clipboardUnit,
});
await executeThunk(fetchCourseUnitQuery(courseId), store.dispatch); await executeThunk(fetchCourseUnitQuery(courseId), store.dispatch);
await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch); await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch);
userEvent.click(screen.getByRole('button', { name: sidebarMessages.actionButtonCopyUnitTitle.defaultMessage })); userEvent.click(getByRole('button', { name: sidebarMessages.actionButtonCopyUnitTitle.defaultMessage }));
userEvent.click(screen.getByRole('button', { name: courseSequenceMessages.pasteAsNewUnitLink.defaultMessage })); userEvent.click(getByRole('button', { name: courseSequenceMessages.pasteAsNewUnitLink.defaultMessage }));
const updatedCourseSectionVerticalData = cloneDeep(courseSectionVerticalMock); const updatedCourseSectionVerticalData = cloneDeep(courseSectionVerticalMock);
const updatedAncestorsChild = updatedCourseSectionVerticalData.xblock_info.ancestor_info.ancestors[0]; const updatedAncestorsChild = updatedCourseSectionVerticalData.xblock_info.ancestor_info.ancestors[0];
@@ -1560,7 +1595,7 @@ describe('<CourseUnit />', () => {
global.localStorage.setItem('staticFileNotices', JSON.stringify(clipboardMockResponse.staticFileNotices)); global.localStorage.setItem('staticFileNotices', JSON.stringify(clipboardMockResponse.staticFileNotices));
await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch); await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch);
await executeThunk(createNewCourseXBlock(camelCaseObject(postXBlockBody), null, blockId), store.dispatch); await executeThunk(createNewCourseXBlock(camelCaseObject(postXBlockBody), null, blockId), store.dispatch);
const errorFilesAlert = screen.getByTestId('has-error-files-alert'); const errorFilesAlert = getByTestId('has-error-files-alert');
expect(within(errorFilesAlert) expect(within(errorFilesAlert)
.getByText(pasteNotificationsMessages.hasErrorsTitle.defaultMessage)).toBeInTheDocument(); .getByText(pasteNotificationsMessages.hasErrorsTitle.defaultMessage)).toBeInTheDocument();
@@ -1569,11 +1604,11 @@ describe('<CourseUnit />', () => {
userEvent.click(within(errorFilesAlert).getByText(/Dismiss/i)); userEvent.click(within(errorFilesAlert).getByText(/Dismiss/i));
expect(screen.queryByTestId('has-error-files')).toBeNull(); expect(queryByTestId('has-error-files')).toBeNull();
}); });
it('should hide the "Paste component" block if canPasteComponent is false', async () => { it('should hide the "Paste component" block if canPasteComponent is false', async () => {
render(<RootWrapper />); const { queryByText, queryByRole } = render(<RootWrapper />);
axiosMock axiosMock
.onGet(getCourseVerticalChildrenApiUrl(blockId)) .onGet(getCourseVerticalChildrenApiUrl(blockId))
@@ -1584,10 +1619,10 @@ describe('<CourseUnit />', () => {
await executeThunk(fetchCourseVerticalChildrenData(blockId), store.dispatch); await executeThunk(fetchCourseVerticalChildrenData(blockId), store.dispatch);
expect(screen.queryByRole('button', { expect(queryByRole('button', {
name: messages.pasteButtonText.defaultMessage, name: messages.pasteButtonText.defaultMessage,
})).not.toBeInTheDocument(); })).not.toBeInTheDocument();
expect(screen.queryByText( expect(queryByText(
pasteComponentMessages.pasteButtonWhatsInClipboardText.defaultMessage, pasteComponentMessages.pasteButtonWhatsInClipboardText.defaultMessage,
)).not.toBeInTheDocument(); )).not.toBeInTheDocument();
}); });
@@ -1621,42 +1656,58 @@ describe('<CourseUnit />', () => {
}); });
it('should display "Move Modal" on receive trigger message', async () => { it('should display "Move Modal" on receive trigger message', async () => {
render(<RootWrapper />); const {
getByText,
getByRole,
} = render(<RootWrapper />);
await screen.findByText(unitDisplayName); await act(async () => {
await waitFor(() => {
expect(getByText(unitDisplayName))
.toBeInTheDocument();
});
axiosMock axiosMock
.onGet(getCourseOutlineInfoUrl(courseId)) .onGet(getCourseOutlineInfoUrl(courseId))
.reply(200, courseOutlineInfoMock); .reply(200, courseOutlineInfoMock);
await executeThunk(getCourseOutlineInfoQuery(courseId), store.dispatch); await executeThunk(getCourseOutlineInfoQuery(courseId), store.dispatch);
window.dispatchEvent(messageEvent); window.dispatchEvent(messageEvent);
});
await screen.findByText( expect(getByText(
moveModalMessages.moveModalTitle.defaultMessage.replace('{displayName}', requestData.title), moveModalMessages.moveModalTitle.defaultMessage.replace('{displayName}', requestData.title),
); )).toBeInTheDocument();
expect(screen.getByRole('button', { name: moveModalMessages.moveModalSubmitButton.defaultMessage })).toBeInTheDocument(); expect(getByRole('button', { name: moveModalMessages.moveModalSubmitButton.defaultMessage })).toBeInTheDocument();
expect(screen.getByRole('button', { name: moveModalMessages.moveModalCancelButton.defaultMessage })).toBeInTheDocument(); expect(getByRole('button', { name: moveModalMessages.moveModalCancelButton.defaultMessage })).toBeInTheDocument();
}); });
it('should navigates to xBlock current unit', async () => { it('should navigates to xBlock current unit', async () => {
render(<RootWrapper />); const {
getByText,
getByRole,
} = render(<RootWrapper />);
await screen.findByText(unitDisplayName); await act(async () => {
await waitFor(() => {
expect(getByText(unitDisplayName))
.toBeInTheDocument();
});
axiosMock axiosMock
.onGet(getCourseOutlineInfoUrl(courseId)) .onGet(getCourseOutlineInfoUrl(courseId))
.reply(200, courseOutlineInfoMock); .reply(200, courseOutlineInfoMock);
await executeThunk(getCourseOutlineInfoQuery(courseId), store.dispatch); await executeThunk(getCourseOutlineInfoQuery(courseId), store.dispatch);
window.dispatchEvent(messageEvent); window.dispatchEvent(messageEvent);
});
await screen.findByText( expect(getByText(
moveModalMessages.moveModalTitle.defaultMessage.replace('{displayName}', requestData.title), moveModalMessages.moveModalTitle.defaultMessage.replace('{displayName}', requestData.title),
); )).toBeInTheDocument();
const currentSection = courseOutlineInfoMock.child_info.children[1]; const currentSection = courseOutlineInfoMock.child_info.children[1];
const currentSectionItemBtn = screen.getByRole('button', { const currentSectionItemBtn = getByRole('button', {
name: `${currentSection.display_name} ${moveModalMessages.moveModalOutlineItemCurrentLocationText.defaultMessage} ${moveModalMessages.moveModalOutlineItemViewText.defaultMessage}`, name: `${currentSection.display_name} ${moveModalMessages.moveModalOutlineItemCurrentLocationText.defaultMessage} ${moveModalMessages.moveModalOutlineItemViewText.defaultMessage}`,
}); });
expect(currentSectionItemBtn).toBeInTheDocument(); expect(currentSectionItemBtn).toBeInTheDocument();
@@ -1664,7 +1715,7 @@ describe('<CourseUnit />', () => {
await waitFor(() => { await waitFor(() => {
const currentSubsection = currentSection.child_info.children[0]; const currentSubsection = currentSection.child_info.children[0];
const currentSubsectionItemBtn = screen.getByRole('button', { const currentSubsectionItemBtn = getByRole('button', {
name: `${currentSubsection.display_name} ${moveModalMessages.moveModalOutlineItemCurrentLocationText.defaultMessage} ${moveModalMessages.moveModalOutlineItemViewText.defaultMessage}`, name: `${currentSubsection.display_name} ${moveModalMessages.moveModalOutlineItemCurrentLocationText.defaultMessage} ${moveModalMessages.moveModalOutlineItemViewText.defaultMessage}`,
}); });
expect(currentSubsectionItemBtn).toBeInTheDocument(); expect(currentSubsectionItemBtn).toBeInTheDocument();
@@ -1672,7 +1723,7 @@ describe('<CourseUnit />', () => {
}); });
await waitFor(() => { await waitFor(() => {
const currentComponentLocationText = screen.getByText( const currentComponentLocationText = getByText(
moveModalMessages.moveModalOutlineItemCurrentComponentLocationText.defaultMessage, moveModalMessages.moveModalOutlineItemCurrentComponentLocationText.defaultMessage,
); );
expect(currentComponentLocationText).toBeInTheDocument(); expect(currentComponentLocationText).toBeInTheDocument();
@@ -1680,7 +1731,10 @@ describe('<CourseUnit />', () => {
}); });
it('should allow move operation and handles it successfully', async () => { it('should allow move operation and handles it successfully', async () => {
render(<RootWrapper />); const {
getByText,
getByRole,
} = render(<RootWrapper />);
axiosMock axiosMock
.onPatch(postXBlockBaseApiUrl()) .onPatch(postXBlockBaseApiUrl())
@@ -1690,21 +1744,26 @@ describe('<CourseUnit />', () => {
.onGet(getCourseUnitApiUrl(blockId)) .onGet(getCourseUnitApiUrl(blockId))
.reply(200, courseUnitIndexMock); .reply(200, courseUnitIndexMock);
await screen.findByText(unitDisplayName); await act(async () => {
await waitFor(() => {
expect(getByText(unitDisplayName))
.toBeInTheDocument();
});
axiosMock axiosMock
.onGet(getCourseOutlineInfoUrl(courseId)) .onGet(getCourseOutlineInfoUrl(courseId))
.reply(200, courseOutlineInfoMock); .reply(200, courseOutlineInfoMock);
await executeThunk(getCourseOutlineInfoQuery(courseId), store.dispatch); await executeThunk(getCourseOutlineInfoQuery(courseId), store.dispatch);
window.dispatchEvent(messageEvent); window.dispatchEvent(messageEvent);
});
await screen.findByText( expect(getByText(
moveModalMessages.moveModalTitle.defaultMessage.replace('{displayName}', requestData.title), moveModalMessages.moveModalTitle.defaultMessage.replace('{displayName}', requestData.title),
); )).toBeInTheDocument();
const currentSection = courseOutlineInfoMock.child_info.children[1]; const currentSection = courseOutlineInfoMock.child_info.children[1];
const currentSectionItemBtn = screen.getByRole('button', { const currentSectionItemBtn = getByRole('button', {
name: `${currentSection.display_name} ${moveModalMessages.moveModalOutlineItemCurrentLocationText.defaultMessage} ${moveModalMessages.moveModalOutlineItemViewText.defaultMessage}`, name: `${currentSection.display_name} ${moveModalMessages.moveModalOutlineItemCurrentLocationText.defaultMessage} ${moveModalMessages.moveModalOutlineItemViewText.defaultMessage}`,
}); });
expect(currentSectionItemBtn).toBeInTheDocument(); expect(currentSectionItemBtn).toBeInTheDocument();
@@ -1712,7 +1771,7 @@ describe('<CourseUnit />', () => {
const currentSubsection = currentSection.child_info.children[1]; const currentSubsection = currentSection.child_info.children[1];
await waitFor(() => { await waitFor(() => {
const currentSubsectionItemBtn = screen.getByRole('button', { const currentSubsectionItemBtn = getByRole('button', {
name: `${currentSubsection.display_name} ${moveModalMessages.moveModalOutlineItemViewText.defaultMessage}`, name: `${currentSubsection.display_name} ${moveModalMessages.moveModalOutlineItemViewText.defaultMessage}`,
}); });
expect(currentSubsectionItemBtn).toBeInTheDocument(); expect(currentSubsectionItemBtn).toBeInTheDocument();
@@ -1721,14 +1780,14 @@ describe('<CourseUnit />', () => {
await waitFor(() => { await waitFor(() => {
const currentUnit = currentSubsection.child_info.children[0]; const currentUnit = currentSubsection.child_info.children[0];
const currentUnitItemBtn = screen.getByRole('button', { const currentUnitItemBtn = getByRole('button', {
name: `${currentUnit.display_name} ${moveModalMessages.moveModalOutlineItemViewText.defaultMessage}`, name: `${currentUnit.display_name} ${moveModalMessages.moveModalOutlineItemViewText.defaultMessage}`,
}); });
expect(currentUnitItemBtn).toBeInTheDocument(); expect(currentUnitItemBtn).toBeInTheDocument();
userEvent.click(currentUnitItemBtn); userEvent.click(currentUnitItemBtn);
}); });
const moveModalBtn = screen.getByRole('button', { const moveModalBtn = getByRole('button', {
name: moveModalMessages.moveModalSubmitButton.defaultMessage, name: moveModalMessages.moveModalSubmitButton.defaultMessage,
}); });
expect(moveModalBtn).toBeInTheDocument(); expect(moveModalBtn).toBeInTheDocument();
@@ -1742,7 +1801,10 @@ describe('<CourseUnit />', () => {
}); });
it('should display "Move Confirmation" alert after moving and undo operations', async () => { it('should display "Move Confirmation" alert after moving and undo operations', async () => {
render(<RootWrapper />); const {
queryByRole,
getByText,
} = render(<RootWrapper />);
axiosMock axiosMock
.onPatch(postXBlockBaseApiUrl()) .onPatch(postXBlockBaseApiUrl())
@@ -1759,18 +1821,18 @@ describe('<CourseUnit />', () => {
simulatePostMessageEvent(messageTypes.rollbackMovedXBlock, { locator: requestData.sourceLocator }); simulatePostMessageEvent(messageTypes.rollbackMovedXBlock, { locator: requestData.sourceLocator });
const dismissButton = screen.queryByRole('button', { const dismissButton = queryByRole('button', {
name: /dismiss/i, hidden: true, name: /dismiss/i, hidden: true,
}); });
const undoButton = screen.queryByRole('button', { const undoButton = queryByRole('button', {
name: messages.undoMoveButton.defaultMessage, hidden: true, name: messages.undoMoveButton.defaultMessage, hidden: true,
}); });
const newLocationButton = screen.queryByRole('button', { const newLocationButton = queryByRole('button', {
name: messages.newLocationButton.defaultMessage, hidden: true, name: messages.newLocationButton.defaultMessage, hidden: true,
}); });
expect(screen.getByText(messages.alertMoveSuccessTitle.defaultMessage)).toBeInTheDocument(); expect(getByText(messages.alertMoveSuccessTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(`${requestData.title} has been moved`)).toBeInTheDocument(); expect(getByText(`${requestData.title} has been moved`)).toBeInTheDocument();
expect(dismissButton).toBeInTheDocument(); expect(dismissButton).toBeInTheDocument();
expect(undoButton).toBeInTheDocument(); expect(undoButton).toBeInTheDocument();
expect(newLocationButton).toBeInTheDocument(); expect(newLocationButton).toBeInTheDocument();
@@ -1778,9 +1840,9 @@ describe('<CourseUnit />', () => {
userEvent.click(undoButton); userEvent.click(undoButton);
await waitFor(() => { await waitFor(() => {
expect(screen.getByText(messages.alertMoveCancelTitle.defaultMessage)).toBeInTheDocument(); expect(getByText(messages.alertMoveCancelTitle.defaultMessage)).toBeInTheDocument();
}); });
expect(screen.getByText( expect(getByText(
messages.alertMoveCancelDescription.defaultMessage.replace('{title}', requestData.title), messages.alertMoveCancelDescription.defaultMessage.replace('{title}', requestData.title),
)).toBeInTheDocument(); )).toBeInTheDocument();
expect(dismissButton).toBeInTheDocument(); expect(dismissButton).toBeInTheDocument();
@@ -1789,7 +1851,9 @@ describe('<CourseUnit />', () => {
}); });
it('should navigate to new location by button click', async () => { it('should navigate to new location by button click', async () => {
render(<RootWrapper />); const {
queryByRole,
} = render(<RootWrapper />);
axiosMock axiosMock
.onPatch(postXBlockBaseApiUrl()) .onPatch(postXBlockBaseApiUrl())
@@ -1804,7 +1868,7 @@ describe('<CourseUnit />', () => {
callbackFn: requestData.callbackFn, callbackFn: requestData.callbackFn,
}), store.dispatch); }), store.dispatch);
const newLocationButton = screen.queryByRole('button', { const newLocationButton = queryByRole('button', {
name: messages.newLocationButton.defaultMessage, hidden: true, name: messages.newLocationButton.defaultMessage, hidden: true,
}); });
userEvent.click(newLocationButton); userEvent.click(newLocationButton);
@@ -1816,15 +1880,17 @@ describe('<CourseUnit />', () => {
}); });
describe('XBlock restrict access', () => { describe('XBlock restrict access', () => {
it('opens xblock restrict access modal successfully', async () => { it('opens xblock restrict access modal successfully', () => {
render(<RootWrapper />); const {
getByTitle, getByTestId,
} = render(<RootWrapper />);
const modalSubtitleText = configureModalMessages.restrictAccessTo.defaultMessage; const modalSubtitleText = configureModalMessages.restrictAccessTo.defaultMessage;
const modalCancelBtnText = configureModalMessages.cancelButton.defaultMessage; const modalCancelBtnText = configureModalMessages.cancelButton.defaultMessage;
const modalSaveBtnText = configureModalMessages.saveButton.defaultMessage; const modalSaveBtnText = configureModalMessages.saveButton.defaultMessage;
await waitFor(() => { waitFor(() => {
const iframe = screen.getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage); const iframe = getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
const usageId = courseVerticalChildrenMock.children[0].block_id; const usageId = courseVerticalChildrenMock.children[0].block_id;
expect(iframe).toBeInTheDocument(); expect(iframe).toBeInTheDocument();
@@ -1833,8 +1899,8 @@ describe('<CourseUnit />', () => {
}); });
}); });
await waitFor(() => { waitFor(() => {
const configureModal = screen.getByTestId('configure-modal'); const configureModal = getByTestId('configure-modal');
expect(within(configureModal).getByText(modalSubtitleText)).toBeInTheDocument(); expect(within(configureModal).getByText(modalSubtitleText)).toBeInTheDocument();
expect(within(configureModal).getByRole('button', { name: modalCancelBtnText })).toBeInTheDocument(); expect(within(configureModal).getByRole('button', { name: modalCancelBtnText })).toBeInTheDocument();
@@ -1843,18 +1909,20 @@ describe('<CourseUnit />', () => {
}); });
it('closes xblock restrict access modal when cancel button is clicked', async () => { it('closes xblock restrict access modal when cancel button is clicked', async () => {
render(<RootWrapper />); const {
getByTitle, queryByTestId, getByTestId,
} = render(<RootWrapper />);
await waitFor(() => { waitFor(() => {
const iframe = screen.getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage); const iframe = getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
expect(iframe).toBeInTheDocument(); expect(iframe).toBeInTheDocument();
simulatePostMessageEvent(messageTypes.manageXBlockAccess, { simulatePostMessageEvent(messageTypes.manageXBlockAccess, {
usageId: courseVerticalChildrenMock.children[0].block_id, usageId: courseVerticalChildrenMock.children[0].block_id,
}); });
}); });
await waitFor(() => { waitFor(() => {
const configureModal = screen.getByTestId('configure-modal'); const configureModal = getByTestId('configure-modal');
expect(configureModal).toBeInTheDocument(); expect(configureModal).toBeInTheDocument();
userEvent.click(within(configureModal).getByRole('button', { userEvent.click(within(configureModal).getByRole('button', {
name: configureModalMessages.cancelButton.defaultMessage, name: configureModalMessages.cancelButton.defaultMessage,
@@ -1862,7 +1930,7 @@ describe('<CourseUnit />', () => {
expect(handleConfigureSubmitMock).not.toHaveBeenCalled(); expect(handleConfigureSubmitMock).not.toHaveBeenCalled();
}); });
expect(screen.queryByTestId('configure-modal')).not.toBeInTheDocument(); expect(queryByTestId('configure-modal')).not.toBeInTheDocument();
}); });
it('handles submit xblock restrict access data when save button is clicked', async () => { it('handles submit xblock restrict access data when save button is clicked', async () => {
@@ -1873,100 +1941,86 @@ describe('<CourseUnit />', () => {
}) })
.reply(200, { dummy: 'value' }); .reply(200, { dummy: 'value' });
render(<RootWrapper />); const {
getByTitle, getByRole, getByTestId,
} = render(<RootWrapper />);
const accessGroupName1 = userPartitionInfoFormatted.selectablePartitions[0].groups[0].name; const accessGroupName1 = userPartitionInfoFormatted.selectablePartitions[0].groups[0].name;
const accessGroupName2 = userPartitionInfoFormatted.selectablePartitions[0].groups[1].name; const accessGroupName2 = userPartitionInfoFormatted.selectablePartitions[0].groups[1].name;
await waitFor(() => { waitFor(() => {
const iframe = screen.getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage); const iframe = getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
expect(iframe).toBeInTheDocument(); expect(iframe).toBeInTheDocument();
});
await act(async () => {
simulatePostMessageEvent(messageTypes.manageXBlockAccess, { simulatePostMessageEvent(messageTypes.manageXBlockAccess, {
usageId: courseVerticalChildrenMock.children[0].block_id, usageId: courseVerticalChildrenMock.children[0].block_id,
}); });
}); });
const configureModal = await waitFor(() => screen.getByTestId('configure-modal')); waitFor(() => {
expect(configureModal).toBeInTheDocument(); const configureModal = getByTestId('configure-modal');
expect(configureModal).toBeInTheDocument();
expect(within(configureModal).queryByText(accessGroupName1)).not.toBeInTheDocument(); expect(within(configureModal).queryByText(accessGroupName1)).not.toBeInTheDocument();
expect(within(configureModal).queryByText(accessGroupName2)).not.toBeInTheDocument(); expect(within(configureModal).queryByText(accessGroupName2)).not.toBeInTheDocument();
const restrictAccessSelect = screen.getByRole('combobox', { const restrictAccessSelect = getByRole('combobox', {
name: configureModalMessages.restrictAccessTo.defaultMessage, 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();
}); });
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);
}); });
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(screen.queryByTestId('configure-modal')).not.toBeInTheDocument();
}); });
}); });
const checkLegacyEditModalOnEditMessage = async () => { it('renders and navigates to the new HTML XBlock editor after xblock duplicating', async () => {
render(<RootWrapper />); const { getByTitle } = render(<RootWrapper />);
const updatedCourseVerticalChildrenMock = JSON.parse(JSON.stringify(courseVerticalChildrenMock));
const targetBlockId = updatedCourseVerticalChildrenMock.children[1].block_id;
updatedCourseVerticalChildrenMock.children = updatedCourseVerticalChildrenMock.children
.map((child) => (child.block_id === targetBlockId
? { ...child, block_type: 'html' }
: child));
axiosMock
.onGet(getCourseVerticalChildrenApiUrl(blockId))
.reply(200, updatedCourseVerticalChildrenMock);
await executeThunk(fetchCourseVerticalChildrenData(blockId), store.dispatch);
await waitFor(() => { await waitFor(() => {
const editButton = screen.getByTestId('header-edit-button'); const iframe = getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
expect(editButton).toBeInTheDocument(); expect(iframe).toBeInTheDocument();
const xblocksIframe = screen.getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage); simulatePostMessageEvent(messageTypes.currentXBlockId, {
expect(xblocksIframe).toBeInTheDocument(); id: targetBlockId,
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) waitFor(() => {
.getByRole('button', { name: configureModalMessages.saveButton.defaultMessage }); simulatePostMessageEvent(messageTypes.duplicateXBlock, {});
userEvent.click(modalSaveBtn); simulatePostMessageEvent(messageTypes.newXBlockEditor, {});
}; expect(mockedUsedNavigate)
.toHaveBeenCalledWith(`/course/${courseId}/editor/html/${targetBlockId}`, { replace: true });
});
});
describe('Library Content page', () => { describe('Library Content page', () => {
const newUnitId = '12345'; const newUnitId = '12345';
@@ -1987,45 +2041,29 @@ describe('<CourseUnit />', () => {
}, },
}); });
await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch); await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch);
axiosMock
.onGet(getCourseUnitApiUrl(courseId))
.reply(200, {
...courseUnitIndexMock,
category: 'library_content',
ancestor_info: {
...courseUnitIndexMock.ancestor_info,
child_info: {
...courseUnitIndexMock.ancestor_info.child_info,
category: 'library_content',
},
},
});
await executeThunk(fetchCourseUnitQuery(courseId), store.dispatch);
}); });
it('navigates to library content page on receive window event', async () => { it('navigates to library content page on receive window event', () => {
render(<RootWrapper />); render(<RootWrapper />);
await waitFor(() => { simulatePostMessageEvent(messageTypes.handleViewXBlockContent, { usageId: newUnitId });
simulatePostMessageEvent(messageTypes.handleViewXBlockContent, { usageId: newUnitId }); expect(mockedUsedNavigate).toHaveBeenCalledWith(`/course/${courseId}/container/${newUnitId}/${sequenceId}`);
expect(mockedUsedNavigate).toHaveBeenCalledWith(`/course/${courseId}/container/${newUnitId}/${sequenceId}`);
});
}); });
it('should render library content page correctly', async () => { it('should render library content page correctly', async () => {
const { const {
findByText, getByText,
getByRole, getByRole,
queryByRole, queryByRole,
findByTestId, getByTestId,
} = render(<RootWrapper />); } = render(<RootWrapper />);
const currentSectionName = courseUnitIndexMock.ancestor_info.ancestors[1].display_name; const currentSectionName = courseUnitIndexMock.ancestor_info.ancestors[1].display_name;
const currentSubSectionName = 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(() => { 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.altButtonEdit.defaultMessage })).toBeInTheDocument();
expect(within(unitHeaderTitle).getByRole('button', { name: headerTitleMessages.altButtonSettings.defaultMessage })).toBeInTheDocument(); expect(within(unitHeaderTitle).getByRole('button', { name: headerTitleMessages.altButtonSettings.defaultMessage })).toBeInTheDocument();
expect(getByRole('button', { name: currentSectionName })).toBeInTheDocument(); expect(getByRole('button', { name: currentSectionName })).toBeInTheDocument();
@@ -2039,257 +2077,5 @@ describe('<CourseUnit />', () => {
expect(queryByRole('heading', { name: /unit location/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 () => {
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 = screen.getByTestId('unit-header-title');
expect(screen.getByText(unitDisplayName)).toBeInTheDocument();
expect(within(unitHeaderTitle).getByRole('button', { name: headerTitleMessages.altButtonEdit.defaultMessage })).toBeInTheDocument();
expect(within(unitHeaderTitle).getByRole('button', { name: headerTitleMessages.altButtonSettings.defaultMessage })).toBeInTheDocument();
expect(screen.getByRole('button', { name: currentSectionName })).toBeInTheDocument();
expect(screen.getByRole('button', { name: currentSubSectionName })).toBeInTheDocument();
expect(screen.queryByRole('heading', { name: addComponentMessages.title.defaultMessage })).not.toBeInTheDocument();
expect(screen.queryByRole('button', { name: headerNavigationsMessages.viewLiveButton.defaultMessage })).not.toBeInTheDocument();
expect(screen.queryByRole('button', { name: headerNavigationsMessages.previewButton.defaultMessage })).not.toBeInTheDocument();
expect(screen.queryByRole('heading', { name: /unit tags/i })).not.toBeInTheDocument();
expect(screen.queryByRole('heading', { name: /unit location/i })).not.toBeInTheDocument();
// Sidebar
const sidebarContent = [
{ query: screen.queryByRole, type: 'heading', name: sidebarMessages.sidebarSplitTestAddComponentTitle.defaultMessage },
{ query: screen.queryByText, name: sidebarMessages.sidebarSplitTestSelectComponentType.defaultMessage.replaceAll('{bold_tag}', '') },
{ query: screen.queryByText, name: sidebarMessages.sidebarSplitTestComponentAdded.defaultMessage },
{ query: screen.queryByRole, type: 'heading', name: sidebarMessages.sidebarSplitTestEditComponentTitle.defaultMessage },
{
query: screen.queryByText,
name: sidebarMessages.sidebarSplitTestEditComponentInstruction.defaultMessage
.replaceAll('{bold_tag}', ''),
},
{
query: screen.queryByRole,
type: 'heading',
name: sidebarMessages.sidebarSplitTestReorganizeComponentTitle.defaultMessage,
},
{
query: screen.queryByText,
name: sidebarMessages.sidebarSplitTestReorganizeComponentInstruction.defaultMessage,
},
{
query: screen.queryByText,
name: sidebarMessages.sidebarSplitTestReorganizeGroupsInstruction.defaultMessage,
},
{
query: screen.queryByRole,
type: 'heading',
name: sidebarMessages.sidebarSplitTestExperimentComponentTitle.defaultMessage,
},
{
query: screen.queryByText,
name: sidebarMessages.sidebarSplitTestExperimentComponentInstruction.defaultMessage,
},
{
query: screen.queryByRole,
type: 'link',
name: sidebarMessages.sidebarSplitTestLearnMoreLinkLabel.defaultMessage,
},
];
sidebarContent.forEach(({ query, type, name }) => {
expect(type ? query(type, { name }) : query(name)).toBeInTheDocument();
});
expect(
screen.queryByRole('link', { name: sidebarMessages.sidebarSplitTestLearnMoreLinkLabel.defaultMessage }),
).toHaveAttribute('href', helpLinkUrl);
});
});
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 () => {
render(<RootWrapper />);
const updatedCourseVerticalChildrenMock = JSON.parse(JSON.stringify(courseVerticalChildrenMock));
const targetBlockId = updatedCourseVerticalChildrenMock.children[1].block_id;
updatedCourseVerticalChildrenMock.children = updatedCourseVerticalChildrenMock.children
.map((child) => (child.block_id === targetBlockId
? { ...child, block_type: 'html' }
: child));
axiosMock
.onGet(getCourseUnitApiUrl(blockId))
.reply(200, courseUnitIndexMock);
axiosMock
.onPost(postXBlockBaseApiUrl({
parent_locator: blockId,
duplicate_source_locator: courseVerticalChildrenMock.children[0].block_id,
}))
.replyOnce(200, { locator: '1234567890' });
axiosMock
.onGet(getCourseVerticalChildrenApiUrl(blockId))
.reply(200, updatedCourseVerticalChildrenMock);
await executeThunk(fetchCourseVerticalChildrenData(blockId), store.dispatch);
await waitFor(() => {
const iframe = screen.getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
expect(iframe).toBeInTheDocument();
simulatePostMessageEvent(messageTypes.currentXBlockId, {
id: targetBlockId,
});
});
waitFor(() => {
simulatePostMessageEvent(messageTypes.duplicateXBlock, {});
simulatePostMessageEvent(messageTypes.newXBlockEditor, {});
expect(mockedUsedNavigate)
.toHaveBeenCalledWith(`/course/${courseId}/editor/html/${targetBlockId}`, { replace: true });
});
});
it('renders units from libraries with some components read-only', async () => {
setConfig({
...getConfig(),
ENABLE_TAGGING_TAXONOMY_PAGES: 'true',
});
render(<RootWrapper />);
axiosMock
.onGet(getCourseUnitApiUrl(courseId))
.reply(200, {
...courseUnitIndexMock,
upstreamInfo: {
upstreamRef: 'lct:org:lib:unit:unit-1',
upstreamLink: 'some-link',
},
});
await executeThunk(fetchCourseUnitQuery(courseId), store.dispatch);
expect(screen.getByText(/this unit can only be edited from the \./i)).toBeInTheDocument();
// Disable the "Edit" button
const unitHeaderTitle = screen.getByTestId('unit-header-title');
const editButton = within(unitHeaderTitle).getByRole(
'button',
{ name: 'Edit' },
);
expect(editButton).toBeInTheDocument();
expect(editButton).toBeDisabled();
// The "Publish" button should still be enabled
const courseUnitSidebar = screen.getByTestId('course-unit-sidebar');
const publishButton = within(courseUnitSidebar).getByRole(
'button',
{ name: sidebarMessages.actionButtonPublishTitle.defaultMessage },
);
expect(publishButton).toBeInTheDocument();
expect(publishButton).toBeEnabled();
// Disable the "Manage Tags" button
const manageTagsButton = screen.getByRole(
'button',
{ name: tagsDrawerMessages.manageTagsButton.defaultMessage },
);
expect(manageTagsButton).toBeInTheDocument();
expect(manageTagsButton).toBeDisabled();
// Does not render the "Add Components" section
expect(screen.queryByText(addComponentMessages.title.defaultMessage)).not.toBeInTheDocument();
}); });
}); });

View File

@@ -5,7 +5,7 @@ module.exports = {
has_children: true, has_children: true,
video_sharing_enabled: true, video_sharing_enabled: true,
video_sharing_options: 'per-video', 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, unit_level_discussions: false,
child_info: { child_info: {
category: 'chapter', category: 'chapter',
@@ -18,7 +18,7 @@ module.exports = {
has_children: true, has_children: true,
video_sharing_enabled: true, video_sharing_enabled: true,
video_sharing_options: 'per-video', 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: { child_info: {
category: 'sequential', category: 'sequential',
display_name: 'Subsection', display_name: 'Subsection',
@@ -30,7 +30,7 @@ module.exports = {
has_children: true, has_children: true,
video_sharing_enabled: true, video_sharing_enabled: true,
video_sharing_options: 'per-video', 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: { child_info: {
category: 'vertical', category: 'vertical',
display_name: 'Unit', display_name: 'Unit',
@@ -42,7 +42,7 @@ module.exports = {
has_children: true, has_children: true,
video_sharing_enabled: true, video_sharing_enabled: true,
video_sharing_options: 'per-video', 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: { child_info: {
children: [ children: [
{ {
@@ -52,7 +52,7 @@ module.exports = {
has_children: false, has_children: false,
video_sharing_enabled: true, video_sharing_enabled: true,
video_sharing_options: 'per-video', 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', id: 'block-v1:edX+DemoX+Demo_Course+type@html+block@f7cc083ff66d442eafafd48152881276',
@@ -61,7 +61,7 @@ module.exports = {
has_children: false, has_children: false,
video_sharing_enabled: true, video_sharing_enabled: true,
video_sharing_options: 'per-video', 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', id: 'block-v1:edX+DemoX+Demo_Course+type@video+block@0b9e39477cf34507a7a48f74be381fdd',
@@ -70,7 +70,7 @@ module.exports = {
has_children: false, has_children: false,
video_sharing_enabled: true, video_sharing_enabled: true,
video_sharing_options: 'per-video', 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', id: 'block-v1:edX+DemoX+Demo_Course+type@video+block@6e72ebc448694e42ac56553af74304e7',
@@ -79,7 +79,7 @@ module.exports = {
has_children: false, has_children: false,
video_sharing_enabled: true, video_sharing_enabled: true,
video_sharing_options: 'per-video', 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, has_children: true,
video_sharing_enabled: true, video_sharing_enabled: true,
video_sharing_options: 'per-video', 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, has_children: true,
video_sharing_enabled: true, video_sharing_enabled: true,
video_sharing_options: 'per-video', 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: { child_info: {
category: 'sequential', category: 'sequential',
display_name: 'Subsection', display_name: 'Subsection',
@@ -118,7 +118,7 @@ module.exports = {
has_children: true, has_children: true,
video_sharing_enabled: true, video_sharing_enabled: true,
video_sharing_options: 'per-video', 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: { child_info: {
category: 'vertical', category: 'vertical',
display_name: 'Unit', display_name: 'Unit',
@@ -130,7 +130,7 @@ module.exports = {
has_children: true, has_children: true,
video_sharing_enabled: true, video_sharing_enabled: true,
video_sharing_options: 'per-video', 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: { child_info: {
children: [ children: [
{ {
@@ -140,7 +140,7 @@ module.exports = {
has_children: false, has_children: false,
video_sharing_enabled: true, video_sharing_enabled: true,
video_sharing_options: 'per-video', 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, has_children: true,
video_sharing_enabled: true, video_sharing_enabled: true,
video_sharing_options: 'per-video', 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: { child_info: {
children: [ children: [
{ {
@@ -162,7 +162,7 @@ module.exports = {
has_children: false, has_children: false,
video_sharing_enabled: true, video_sharing_enabled: true,
video_sharing_options: 'per-video', 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', id: 'block-v1:edX+DemoX+Demo_Course+type@video+block@7e9b434e6de3435ab99bd3fb25bde807',
@@ -171,7 +171,7 @@ module.exports = {
has_children: false, has_children: false,
video_sharing_enabled: true, video_sharing_enabled: true,
video_sharing_options: 'per-video', 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', id: 'block-v1:edX+DemoX+Demo_Course+type@discussion+block@412dc8dbb6674014862237b23c1f643f',
@@ -180,7 +180,7 @@ module.exports = {
has_children: false, has_children: false,
video_sharing_enabled: true, video_sharing_enabled: true,
video_sharing_options: 'per-video', 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, has_children: true,
video_sharing_enabled: true, video_sharing_enabled: true,
video_sharing_options: 'per-video', 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: { child_info: {
children: [ children: [
{ {
@@ -202,7 +202,7 @@ module.exports = {
has_children: false, has_children: false,
video_sharing_enabled: true, video_sharing_enabled: true,
video_sharing_options: 'per-video', 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', id: 'block-v1:edX+DemoX+Demo_Course+type@video+block@5c90cffecd9b48b188cbfea176bf7fe9',
@@ -211,7 +211,7 @@ module.exports = {
has_children: false, has_children: false,
video_sharing_enabled: true, video_sharing_enabled: true,
video_sharing_options: 'per-video', 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', id: 'block-v1:edX+DemoX+Demo_Course+type@discussion+block@722085be27c84ac693cfebc8ac5da700',
@@ -220,7 +220,7 @@ module.exports = {
has_children: false, has_children: false,
video_sharing_enabled: true, video_sharing_enabled: true,
video_sharing_options: 'per-video', 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, has_children: true,
video_sharing_enabled: true, video_sharing_enabled: true,
video_sharing_options: 'per-video', 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: { child_info: {
children: [ children: [
{ {
@@ -242,7 +242,7 @@ module.exports = {
has_children: false, has_children: false,
video_sharing_enabled: true, video_sharing_enabled: true,
video_sharing_options: 'per-video', 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', id: 'block-v1:edX+DemoX+Demo_Course+type@discussion+block@9f9e1373cc8243b985c8750cc8acec7d',
@@ -251,7 +251,7 @@ module.exports = {
has_children: false, has_children: false,
video_sharing_enabled: true, video_sharing_enabled: true,
video_sharing_options: 'per-video', 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, has_children: true,
video_sharing_enabled: true, video_sharing_enabled: true,
video_sharing_options: 'per-video', 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: { child_info: {
children: [ children: [
{ {
@@ -273,7 +273,7 @@ module.exports = {
has_children: false, has_children: false,
video_sharing_enabled: true, video_sharing_enabled: true,
video_sharing_options: 'per-video', 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', id: 'block-v1:edX+DemoX+Demo_Course+type@video+block@636541acbae448d98ab484b028c9a7f6',
@@ -282,7 +282,7 @@ module.exports = {
has_children: false, has_children: false,
video_sharing_enabled: true, video_sharing_enabled: true,
video_sharing_options: 'per-video', 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', id: 'block-v1:edX+DemoX+Demo_Course+type@discussion+block@e2cb0e0994f84b0abfa5f4ae42ed9d44',
@@ -291,7 +291,7 @@ module.exports = {
has_children: false, has_children: false,
video_sharing_enabled: true, video_sharing_enabled: true,
video_sharing_options: 'per-video', 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, has_children: true,
video_sharing_enabled: true, video_sharing_enabled: true,
video_sharing_options: 'per-video', 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: { child_info: {
children: [ children: [
{ {
@@ -313,7 +313,7 @@ module.exports = {
has_children: false, has_children: false,
video_sharing_enabled: true, video_sharing_enabled: true,
video_sharing_options: 'per-video', 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', id: 'block-v1:edX+DemoX+Demo_Course+type@discussion+block@3169f89efde2452993f2f2d9bc74f5b2',
@@ -322,7 +322,7 @@ module.exports = {
has_children: false, has_children: false,
video_sharing_enabled: true, video_sharing_enabled: true,
video_sharing_options: 'per-video', 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, has_children: true,
video_sharing_enabled: true, video_sharing_enabled: true,
video_sharing_options: 'per-video', 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: { child_info: {
children: [ children: [
{ {
@@ -344,7 +344,7 @@ module.exports = {
has_children: false, has_children: false,
video_sharing_enabled: true, video_sharing_enabled: true,
video_sharing_options: 'per-video', 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', id: 'block-v1:edX+DemoX+Demo_Course+type@discussion+block@1c8d47c425724346a7968fa1bc745dcd',
@@ -353,7 +353,7 @@ module.exports = {
has_children: false, has_children: false,
video_sharing_enabled: true, video_sharing_enabled: true,
video_sharing_options: 'per-video', 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, has_children: true,
video_sharing_enabled: true, video_sharing_enabled: true,
video_sharing_options: 'per-video', 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: { child_info: {
children: [ children: [
{ {
@@ -375,7 +375,7 @@ module.exports = {
has_children: false, has_children: false,
video_sharing_enabled: true, video_sharing_enabled: true,
video_sharing_options: 'per-video', 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', id: 'block-v1:edX+DemoX+Demo_Course+type@html+block@2574c523e97b477a9d72fbb37bfb995f',
@@ -384,7 +384,7 @@ module.exports = {
has_children: false, has_children: false,
video_sharing_enabled: true, video_sharing_enabled: true,
video_sharing_options: 'per-video', 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', id: 'block-v1:edX+DemoX+Demo_Course+type@problem+block@932e6f2ce8274072a355a94560216d1a',
@@ -393,7 +393,7 @@ module.exports = {
has_children: false, has_children: false,
video_sharing_enabled: true, video_sharing_enabled: true,
video_sharing_options: 'per-video', 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', id: 'block-v1:edX+DemoX+Demo_Course+type@problem+block@303034da25524878a2e66fb57c91cf85',
@@ -402,7 +402,7 @@ module.exports = {
has_children: false, has_children: false,
video_sharing_enabled: true, video_sharing_enabled: true,
video_sharing_options: 'per-video', 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', id: 'block-v1:edX+DemoX+Demo_Course+type@discussion+block@ffa5817d49e14fec83ad6187cbe16358',
@@ -411,7 +411,7 @@ module.exports = {
has_children: false, has_children: false,
video_sharing_enabled: true, video_sharing_enabled: true,
video_sharing_options: 'per-video', 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, has_children: true,
video_sharing_enabled: true, video_sharing_enabled: true,
video_sharing_options: 'per-video', 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: { child_info: {
children: [ children: [
{ {
@@ -433,7 +433,7 @@ module.exports = {
has_children: false, has_children: false,
video_sharing_enabled: true, video_sharing_enabled: true,
video_sharing_options: 'per-video', 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, has_children: true,
video_sharing_enabled: true, video_sharing_enabled: true,
video_sharing_options: 'per-video', 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: { child_info: {
category: 'vertical', category: 'vertical',
display_name: 'Unit', display_name: 'Unit',
@@ -460,7 +460,7 @@ module.exports = {
has_children: true, has_children: true,
video_sharing_enabled: true, video_sharing_enabled: true,
video_sharing_options: 'per-video', 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: { child_info: {
children: [ children: [
{ {
@@ -470,7 +470,7 @@ module.exports = {
has_children: false, has_children: false,
video_sharing_enabled: true, video_sharing_enabled: true,
video_sharing_options: 'per-video', 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', id: 'block-v1:edX+DemoX+Demo_Course+type@discussion+block@e5eac7e1a5a24f5fa7ed77bb6d136591',
@@ -479,7 +479,7 @@ module.exports = {
has_children: false, has_children: false,
video_sharing_enabled: true, video_sharing_enabled: true,
video_sharing_options: 'per-video', 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, has_children: true,
video_sharing_enabled: true, video_sharing_enabled: true,
video_sharing_options: 'per-video', 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: { child_info: {
children: [ children: [
{ {
@@ -501,7 +501,7 @@ module.exports = {
has_children: false, has_children: false,
video_sharing_enabled: true, video_sharing_enabled: true,
video_sharing_options: 'per-video', 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', id: 'block-v1:edX+DemoX+Demo_Course+type@discussion+block@5ab88e67d46049b9aa694cb240c39cef',
@@ -510,7 +510,7 @@ module.exports = {
has_children: false, has_children: false,
video_sharing_enabled: true, video_sharing_enabled: true,
video_sharing_options: 'per-video', 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, has_children: true,
video_sharing_enabled: true, video_sharing_enabled: true,
video_sharing_options: 'per-video', 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: { child_info: {
children: [ children: [
{ {
@@ -532,7 +532,7 @@ module.exports = {
has_children: false, has_children: false,
video_sharing_enabled: true, video_sharing_enabled: true,
video_sharing_options: 'per-video', 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', id: 'block-v1:edX+DemoX+Demo_Course+type@discussion+block@67c26b1e826e47aaa29757f62bcd1ad0',
@@ -541,7 +541,7 @@ module.exports = {
has_children: false, has_children: false,
video_sharing_enabled: true, video_sharing_enabled: true,
video_sharing_options: 'per-video', 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, has_children: true,
video_sharing_enabled: true, video_sharing_enabled: true,
video_sharing_options: 'per-video', 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: { child_info: {
children: [ children: [
{ {
@@ -563,7 +563,7 @@ module.exports = {
has_children: false, has_children: false,
video_sharing_enabled: true, video_sharing_enabled: true,
video_sharing_options: 'per-video', 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', id: 'block-v1:edX+DemoX+Demo_Course+type@discussion+block@870371212ba04dcf9536d7c7b8f3109e',
@@ -572,7 +572,7 @@ module.exports = {
has_children: false, has_children: false,
video_sharing_enabled: true, video_sharing_enabled: true,
video_sharing_options: 'per-video', 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, has_children: true,
video_sharing_enabled: true, video_sharing_enabled: true,
video_sharing_options: 'per-video', 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: { child_info: {
children: [ children: [
{ {
@@ -594,7 +594,7 @@ module.exports = {
has_children: false, has_children: false,
video_sharing_enabled: true, video_sharing_enabled: true,
video_sharing_options: 'per-video', 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', id: 'block-v1:edX+DemoX+Demo_Course+type@discussion+block@4d672c5893cb4f1dad0de67d2008522e',
@@ -603,7 +603,7 @@ module.exports = {
has_children: false, has_children: false,
video_sharing_enabled: true, video_sharing_enabled: true,
video_sharing_options: 'per-video', 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, has_children: true,
video_sharing_enabled: true, video_sharing_enabled: true,
video_sharing_options: 'per-video', 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: { child_info: {
children: [ children: [
{ {
@@ -625,7 +625,7 @@ module.exports = {
has_children: false, has_children: false,
video_sharing_enabled: true, video_sharing_enabled: true,
video_sharing_options: 'per-video', 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', id: 'block-v1:edX+DemoX+Demo_Course+type@discussion+block@501aed9d902349eeb2191fa505548de2',
@@ -634,7 +634,7 @@ module.exports = {
has_children: false, has_children: false,
video_sharing_enabled: true, video_sharing_enabled: true,
video_sharing_options: 'per-video', 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, has_children: true,
video_sharing_enabled: true, video_sharing_enabled: true,
video_sharing_options: 'per-video', 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: { child_info: {
children: [ children: [
{ {
@@ -656,7 +656,7 @@ module.exports = {
has_children: false, has_children: false,
video_sharing_enabled: true, video_sharing_enabled: true,
video_sharing_options: 'per-video', 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', id: 'block-v1:edX+DemoX+Demo_Course+type@discussion+block@6244918637ed4ff4b5f94a840a7e4b43',
@@ -665,7 +665,7 @@ module.exports = {
has_children: false, has_children: false,
video_sharing_enabled: true, video_sharing_enabled: true,
video_sharing_options: 'per-video', 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, has_children: true,
video_sharing_enabled: true, video_sharing_enabled: true,
video_sharing_options: 'per-video', 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: { child_info: {
children: [], children: [],
}, },
@@ -695,7 +695,7 @@ module.exports = {
has_children: true, has_children: true,
video_sharing_enabled: true, video_sharing_enabled: true,
video_sharing_options: 'per-video', 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: { child_info: {
category: 'sequential', category: 'sequential',
display_name: 'Subsection', display_name: 'Subsection',
@@ -707,7 +707,7 @@ module.exports = {
has_children: true, has_children: true,
video_sharing_enabled: true, video_sharing_enabled: true,
video_sharing_options: 'per-video', 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: { child_info: {
category: 'vertical', category: 'vertical',
display_name: 'Unit', display_name: 'Unit',
@@ -719,7 +719,7 @@ module.exports = {
has_children: true, has_children: true,
video_sharing_enabled: true, video_sharing_enabled: true,
video_sharing_options: 'per-video', 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: { child_info: {
children: [ children: [
{ {
@@ -729,7 +729,7 @@ module.exports = {
has_children: false, has_children: false,
video_sharing_enabled: true, video_sharing_enabled: true,
video_sharing_options: 'per-video', 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, has_children: true,
video_sharing_enabled: true, video_sharing_enabled: true,
video_sharing_options: 'per-video', 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: { child_info: {
children: [ children: [
{ {
@@ -751,7 +751,7 @@ module.exports = {
has_children: false, has_children: false,
video_sharing_enabled: true, video_sharing_enabled: true,
video_sharing_options: 'per-video', 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', id: 'block-v1:edX+DemoX+Demo_Course+type@discussion+block@6f7a6670f87147149caeff6afa07a526',
@@ -760,7 +760,7 @@ module.exports = {
has_children: false, has_children: false,
video_sharing_enabled: true, video_sharing_enabled: true,
video_sharing_options: 'per-video', 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, has_children: true,
video_sharing_enabled: true, video_sharing_enabled: true,
video_sharing_options: 'per-video', 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: { child_info: {
children: [ children: [
{ {
@@ -782,7 +782,7 @@ module.exports = {
has_children: false, has_children: false,
video_sharing_enabled: true, video_sharing_enabled: true,
video_sharing_options: 'per-video', 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', id: 'block-v1:edX+DemoX+Demo_Course+type@discussion+block@e0d7423118ab432582d03e8e8dad8e36',
@@ -791,7 +791,7 @@ module.exports = {
has_children: false, has_children: false,
video_sharing_enabled: true, video_sharing_enabled: true,
video_sharing_options: 'per-video', 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, has_children: true,
video_sharing_enabled: true, video_sharing_enabled: true,
video_sharing_options: 'per-video', 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: { child_info: {
children: [ children: [
{ {
@@ -813,7 +813,7 @@ module.exports = {
has_children: false, has_children: false,
video_sharing_enabled: true, video_sharing_enabled: true,
video_sharing_options: 'per-video', 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', id: 'block-v1:edX+DemoX+Demo_Course+type@discussion+block@03f051f9a8814881a3783d2511613aa6',
@@ -822,7 +822,7 @@ module.exports = {
has_children: false, has_children: false,
video_sharing_enabled: true, video_sharing_enabled: true,
video_sharing_options: 'per-video', 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, has_children: true,
video_sharing_enabled: true, video_sharing_enabled: true,
video_sharing_options: 'per-video', 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: { child_info: {
children: [ children: [
{ {
@@ -844,7 +844,7 @@ module.exports = {
has_children: false, has_children: false,
video_sharing_enabled: true, video_sharing_enabled: true,
video_sharing_options: 'per-video', 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, has_children: true,
video_sharing_enabled: true, video_sharing_enabled: true,
video_sharing_options: 'per-video', 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: { child_info: {
category: 'vertical', category: 'vertical',
display_name: 'Unit', display_name: 'Unit',
@@ -871,7 +871,7 @@ module.exports = {
has_children: true, has_children: true,
video_sharing_enabled: true, video_sharing_enabled: true,
video_sharing_options: 'per-video', 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: { child_info: {
children: [ children: [
{ {
@@ -881,7 +881,7 @@ module.exports = {
has_children: false, has_children: false,
video_sharing_enabled: true, video_sharing_enabled: true,
video_sharing_options: 'per-video', 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, has_children: true,
video_sharing_enabled: true, video_sharing_enabled: true,
video_sharing_options: 'per-video', 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: { child_info: {
children: [ children: [
{ {
@@ -903,7 +903,7 @@ module.exports = {
has_children: false, has_children: false,
video_sharing_enabled: true, video_sharing_enabled: true,
video_sharing_options: 'per-video', 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', id: 'block-v1:edX+DemoX+Demo_Course+type@problem+block@python_grader',
@@ -912,7 +912,7 @@ module.exports = {
has_children: false, has_children: false,
video_sharing_enabled: true, video_sharing_enabled: true,
video_sharing_options: 'per-video', 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', id: 'block-v1:edX+DemoX+Demo_Course+type@discussion+block@c6cd4bea43454aaea60ad01beb0cf213',
@@ -921,7 +921,7 @@ module.exports = {
has_children: false, has_children: false,
video_sharing_enabled: true, video_sharing_enabled: true,
video_sharing_options: 'per-video', 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, has_children: true,
video_sharing_enabled: true, video_sharing_enabled: true,
video_sharing_options: 'per-video', 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: { child_info: {
children: [ children: [
{ {
@@ -943,7 +943,7 @@ module.exports = {
has_children: false, has_children: false,
video_sharing_enabled: true, video_sharing_enabled: true,
video_sharing_options: 'per-video', 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', id: 'block-v1:edX+DemoX+Demo_Course+type@problem+block@free_form_simulation',
@@ -952,7 +952,7 @@ module.exports = {
has_children: false, has_children: false,
video_sharing_enabled: true, video_sharing_enabled: true,
video_sharing_options: 'per-video', 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', id: 'block-v1:edX+DemoX+Demo_Course+type@problem+block@logic_gate_problem',
@@ -961,7 +961,7 @@ module.exports = {
has_children: false, has_children: false,
video_sharing_enabled: true, video_sharing_enabled: true,
video_sharing_options: 'per-video', 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', id: 'block-v1:edX+DemoX+Demo_Course+type@discussion+block@4f06b358a96f4d1dae57d6d81acd06f2',
@@ -970,7 +970,7 @@ module.exports = {
has_children: false, has_children: false,
video_sharing_enabled: true, video_sharing_enabled: true,
video_sharing_options: 'per-video', 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, has_children: true,
video_sharing_enabled: true, video_sharing_enabled: true,
video_sharing_options: 'per-video', 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: { child_info: {
children: [ children: [
{ {
@@ -992,7 +992,7 @@ module.exports = {
has_children: false, has_children: false,
video_sharing_enabled: true, video_sharing_enabled: true,
video_sharing_options: 'per-video', 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', id: 'block-v1:edX+DemoX+Demo_Course+type@problem+block@700x_proteinmake',
@@ -1001,7 +1001,7 @@ module.exports = {
has_children: false, has_children: false,
video_sharing_enabled: true, video_sharing_enabled: true,
video_sharing_options: 'per-video', 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', id: 'block-v1:edX+DemoX+Demo_Course+type@discussion+block@ed01bcd164e64038a78964a16eac3edc',
@@ -1010,7 +1010,7 @@ module.exports = {
has_children: false, has_children: false,
video_sharing_enabled: true, video_sharing_enabled: true,
video_sharing_options: 'per-video', 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, has_children: true,
video_sharing_enabled: true, video_sharing_enabled: true,
video_sharing_options: 'per-video', 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: { child_info: {
children: [ children: [
{ {
@@ -1032,7 +1032,7 @@ module.exports = {
has_children: false, has_children: false,
video_sharing_enabled: true, video_sharing_enabled: true,
video_sharing_options: 'per-video', 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, has_children: true,
video_sharing_enabled: true, video_sharing_enabled: true,
video_sharing_options: 'per-video', 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: { child_info: {
category: 'vertical', category: 'vertical',
display_name: 'Unit', display_name: 'Unit',
@@ -1059,7 +1059,7 @@ module.exports = {
has_children: true, has_children: true,
video_sharing_enabled: true, video_sharing_enabled: true,
video_sharing_options: 'per-video', 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: { child_info: {
children: [ children: [
{ {
@@ -1069,7 +1069,7 @@ module.exports = {
has_children: false, has_children: false,
video_sharing_enabled: true, video_sharing_enabled: true,
video_sharing_options: 'per-video', 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', id: 'block-v1:edX+DemoX+Demo_Course+type@discussion+block@12ad4f3ff4c14114a6e629b00e000976',
@@ -1078,7 +1078,7 @@ module.exports = {
has_children: false, has_children: false,
video_sharing_enabled: true, video_sharing_enabled: true,
video_sharing_options: 'per-video', 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, has_children: true,
video_sharing_enabled: true, video_sharing_enabled: true,
video_sharing_options: 'per-video', 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: { child_info: {
category: 'sequential', category: 'sequential',
display_name: 'Subsection', display_name: 'Subsection',
@@ -1108,7 +1108,7 @@ module.exports = {
has_children: true, has_children: true,
video_sharing_enabled: true, video_sharing_enabled: true,
video_sharing_options: 'per-video', 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: { child_info: {
category: 'vertical', category: 'vertical',
display_name: 'Unit', display_name: 'Unit',
@@ -1120,7 +1120,7 @@ module.exports = {
has_children: true, has_children: true,
video_sharing_enabled: true, video_sharing_enabled: true,
video_sharing_options: 'per-video', 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: { child_info: {
children: [ children: [
{ {
@@ -1130,7 +1130,7 @@ module.exports = {
has_children: false, has_children: false,
video_sharing_enabled: true, video_sharing_enabled: true,
video_sharing_options: 'per-video', 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, has_children: true,
video_sharing_enabled: true, video_sharing_enabled: true,
video_sharing_options: 'per-video', 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: { child_info: {
children: [ children: [
{ {
@@ -1152,7 +1152,7 @@ module.exports = {
has_children: false, has_children: false,
video_sharing_enabled: true, video_sharing_enabled: true,
video_sharing_options: 'per-video', 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', id: 'block-v1:edX+DemoX+Demo_Course+type@discussion+block@discussion_5deb6081620d',
@@ -1161,7 +1161,7 @@ module.exports = {
has_children: false, has_children: false,
video_sharing_enabled: true, video_sharing_enabled: true,
video_sharing_options: 'per-video', 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, has_children: true,
video_sharing_enabled: true, video_sharing_enabled: true,
video_sharing_options: 'per-video', 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: { child_info: {
children: [ children: [
{ {
@@ -1183,7 +1183,7 @@ module.exports = {
has_children: false, has_children: false,
video_sharing_enabled: true, video_sharing_enabled: true,
video_sharing_options: 'per-video', 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, has_children: true,
video_sharing_enabled: true, video_sharing_enabled: true,
video_sharing_options: 'per-video', 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: { child_info: {
children: [ children: [
{ {
@@ -1205,7 +1205,7 @@ module.exports = {
has_children: false, has_children: false,
video_sharing_enabled: true, video_sharing_enabled: true,
video_sharing_options: 'per-video', 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, has_children: true,
video_sharing_enabled: true, video_sharing_enabled: true,
video_sharing_options: 'per-video', 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: { child_info: {
category: 'vertical', category: 'vertical',
display_name: 'Unit', display_name: 'Unit',
@@ -1232,7 +1232,7 @@ module.exports = {
has_children: true, has_children: true,
video_sharing_enabled: true, video_sharing_enabled: true,
video_sharing_options: 'per-video', 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: { child_info: {
children: [ children: [
{ {
@@ -1242,7 +1242,7 @@ module.exports = {
has_children: false, has_children: false,
video_sharing_enabled: true, video_sharing_enabled: true,
video_sharing_options: 'per-video', 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, has_children: true,
video_sharing_enabled: true, video_sharing_enabled: true,
video_sharing_options: 'per-video', 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: { child_info: {
category: 'vertical', category: 'vertical',
display_name: 'Unit', display_name: 'Unit',
@@ -1269,7 +1269,7 @@ module.exports = {
has_children: true, has_children: true,
video_sharing_enabled: true, video_sharing_enabled: true,
video_sharing_options: 'per-video', 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: { child_info: {
children: [ children: [
{ {
@@ -1279,7 +1279,7 @@ module.exports = {
has_children: false, has_children: false,
video_sharing_enabled: true, video_sharing_enabled: true,
video_sharing_options: 'per-video', 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', id: 'block-v1:edX+DemoX+Demo_Course+type@html+block@55cbc99f262443d886a25cf84594eafb',
@@ -1288,7 +1288,7 @@ module.exports = {
has_children: false, has_children: false,
video_sharing_enabled: true, video_sharing_enabled: true,
video_sharing_options: 'per-video', 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', id: 'block-v1:edX+DemoX+Demo_Course+type@discussion+block@ade92343df3d4953a40ab3adc8805390',
@@ -1297,7 +1297,7 @@ module.exports = {
has_children: false, has_children: false,
video_sharing_enabled: true, video_sharing_enabled: true,
video_sharing_options: 'per-video', 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, has_children: true,
video_sharing_enabled: true, video_sharing_enabled: true,
video_sharing_options: 'per-video', 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: { child_info: {
category: 'sequential', category: 'sequential',
display_name: 'Subsection', display_name: 'Subsection',
@@ -1327,7 +1327,7 @@ module.exports = {
has_children: true, has_children: true,
video_sharing_enabled: true, video_sharing_enabled: true,
video_sharing_options: 'per-video', 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: { child_info: {
category: 'vertical', category: 'vertical',
display_name: 'Unit', display_name: 'Unit',
@@ -1339,7 +1339,7 @@ module.exports = {
has_children: true, has_children: true,
video_sharing_enabled: true, video_sharing_enabled: true,
video_sharing_options: 'per-video', 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: { child_info: {
children: [ children: [
{ {
@@ -1349,7 +1349,7 @@ module.exports = {
has_children: false, has_children: false,
video_sharing_enabled: true, video_sharing_enabled: true,
video_sharing_options: 'per-video', 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, has_children: true,
video_sharing_enabled: true, video_sharing_enabled: true,
video_sharing_options: 'per-video', 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: { child_info: {
children: [ children: [
{ {
@@ -1371,7 +1371,7 @@ module.exports = {
has_children: false, has_children: false,
video_sharing_enabled: true, video_sharing_enabled: true,
video_sharing_options: 'per-video', 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', id: 'block-v1:edX+DemoX+Demo_Course+type@discussion+block@4aba537a78774bd5a862485a8563c345',
@@ -1380,7 +1380,7 @@ module.exports = {
has_children: false, has_children: false,
video_sharing_enabled: true, video_sharing_enabled: true,
video_sharing_options: 'per-video', 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, has_children: true,
video_sharing_enabled: true, video_sharing_enabled: true,
video_sharing_options: 'per-video', 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: { child_info: {
children: [ children: [
{ {
@@ -1402,7 +1402,7 @@ module.exports = {
has_children: false, has_children: false,
video_sharing_enabled: true, video_sharing_enabled: true,
video_sharing_options: 'per-video', 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', id: 'block-v1:edX+DemoX+Demo_Course+type@discussion+block@f480df4ce91347c5ae4301ddf6146238',
@@ -1411,7 +1411,7 @@ module.exports = {
has_children: false, has_children: false,
video_sharing_enabled: true, video_sharing_enabled: true,
video_sharing_options: 'per-video', 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, has_children: true,
video_sharing_enabled: true, video_sharing_enabled: true,
video_sharing_options: 'per-video', 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: { child_info: {
children: [ children: [
{ {
@@ -1433,7 +1433,7 @@ module.exports = {
has_children: false, has_children: false,
video_sharing_enabled: true, video_sharing_enabled: true,
video_sharing_options: 'per-video', 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', id: 'block-v1:edX+DemoX+Demo_Course+type@discussion+block@b8cec2a19ebf463f90cd3544c7927b0e',
@@ -1442,7 +1442,7 @@ module.exports = {
has_children: false, has_children: false,
video_sharing_enabled: true, video_sharing_enabled: true,
video_sharing_options: 'per-video', 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, has_children: true,
video_sharing_enabled: true, video_sharing_enabled: true,
video_sharing_options: 'per-video', 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: { child_info: {
children: [ children: [
{ {
@@ -1464,7 +1464,7 @@ module.exports = {
has_children: false, has_children: false,
video_sharing_enabled: true, video_sharing_enabled: true,
video_sharing_options: 'per-video', 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', id: 'block-v1:edX+DemoX+Demo_Course+type@problem+block@d1b84dcd39b0423d9e288f27f0f7f242',
@@ -1473,7 +1473,7 @@ module.exports = {
has_children: false, has_children: false,
video_sharing_enabled: true, video_sharing_enabled: true,
video_sharing_options: 'per-video', 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', id: 'block-v1:edX+DemoX+Demo_Course+type@discussion+block@cd177caa62444fbca48aa8f843f09eac',
@@ -1482,7 +1482,7 @@ module.exports = {
has_children: false, has_children: false,
video_sharing_enabled: true, video_sharing_enabled: true,
video_sharing_options: 'per-video', 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, has_children: true,
video_sharing_enabled: true, video_sharing_enabled: true,
video_sharing_options: 'per-video', 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: { child_info: {
children: [ children: [
{ {
@@ -1504,7 +1504,7 @@ module.exports = {
has_children: false, has_children: false,
video_sharing_enabled: true, video_sharing_enabled: true,
video_sharing_options: 'per-video', 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', id: 'block-v1:edX+DemoX+Demo_Course+type@discussion+block@ddede76df71045ffa16de9d1481d2119',
@@ -1513,7 +1513,7 @@ module.exports = {
has_children: false, has_children: false,
video_sharing_enabled: true, video_sharing_enabled: true,
video_sharing_options: 'per-video', 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, has_children: true,
video_sharing_enabled: true, video_sharing_enabled: true,
video_sharing_options: 'per-video', 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: { child_info: {
children: [ children: [
{ {
@@ -1535,7 +1535,7 @@ module.exports = {
has_children: false, has_children: false,
video_sharing_enabled: true, video_sharing_enabled: true,
video_sharing_options: 'per-video', 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', id: 'block-v1:edX+DemoX+Demo_Course+type@discussion+block@1a810b1a3b2447b998f0917d0e5a802b',
@@ -1544,7 +1544,7 @@ module.exports = {
has_children: false, has_children: false,
video_sharing_enabled: true, video_sharing_enabled: true,
video_sharing_options: 'per-video', 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, has_children: true,
video_sharing_enabled: true, video_sharing_enabled: true,
video_sharing_options: 'per-video', 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: { child_info: {
children: [ children: [
{ {
@@ -1566,7 +1566,7 @@ module.exports = {
has_children: false, has_children: false,
video_sharing_enabled: true, video_sharing_enabled: true,
video_sharing_options: 'per-video', 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', id: 'block-v1:edX+DemoX+Demo_Course+type@discussion+block@23e6eda482c04335af2bb265beacaf59',
@@ -1575,7 +1575,7 @@ module.exports = {
has_children: false, has_children: false,
video_sharing_enabled: true, video_sharing_enabled: true,
video_sharing_options: 'per-video', 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, has_children: true,
video_sharing_enabled: true, video_sharing_enabled: true,
video_sharing_options: 'per-video', 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: { child_info: {
children: [ children: [
{ {
@@ -1597,7 +1597,7 @@ module.exports = {
has_children: false, has_children: false,
video_sharing_enabled: true, video_sharing_enabled: true,
video_sharing_options: 'per-video', 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', id: 'block-v1:edX+DemoX+Demo_Course+type@html+block@6b6bee43c7c641509da71c9299cc9f5a',
@@ -1606,7 +1606,7 @@ module.exports = {
has_children: false, has_children: false,
video_sharing_enabled: true, video_sharing_enabled: true,
video_sharing_options: 'per-video', 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, has_children: true,
video_sharing_enabled: true, video_sharing_enabled: true,
video_sharing_options: 'per-video', 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: { child_info: {
category: 'sequential', category: 'sequential',
display_name: 'Subsection', display_name: 'Subsection',
@@ -1636,7 +1636,7 @@ module.exports = {
has_children: true, has_children: true,
video_sharing_enabled: true, video_sharing_enabled: true,
video_sharing_options: 'per-video', 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: { child_info: {
category: 'vertical', category: 'vertical',
display_name: 'Unit', display_name: 'Unit',
@@ -1648,7 +1648,7 @@ module.exports = {
has_children: true, has_children: true,
video_sharing_enabled: true, video_sharing_enabled: true,
video_sharing_options: 'per-video', 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: { child_info: {
children: [ children: [
{ {
@@ -1658,7 +1658,7 @@ module.exports = {
has_children: false, has_children: false,
video_sharing_enabled: true, video_sharing_enabled: true,
video_sharing_options: 'per-video', 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, has_children: true,
video_sharing_enabled: true, video_sharing_enabled: true,
video_sharing_options: 'per-video', 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: [],
highlights_enabled: true, highlights_enabled: true,
highlights_preview_only: false, 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, ancestor_has_staff_lock: false,
user_partition_info: { user_partition_info: {
selectable_partitions: [ selectable_partitions: [
@@ -1396,7 +1396,7 @@ module.exports = {
highlights_enabled_for_messaging: false, highlights_enabled_for_messaging: false,
highlights_enabled: true, highlights_enabled: true,
highlights_preview_only: false, 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, enable_proctored_exams: false,
create_zendesk_tickets: true, create_zendesk_tickets: true,
enable_timed_exams: true, enable_timed_exams: true,

View File

@@ -968,7 +968,7 @@ module.exports = {
highlights: [], highlights: [],
highlights_enabled: true, highlights_enabled: true,
highlights_preview_only: false, 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, ancestor_has_staff_lock: false,
user_partition_info: { user_partition_info: {
selectable_partitions: [ selectable_partitions: [
@@ -1054,7 +1054,7 @@ module.exports = {
highlights_enabled_for_messaging: false, highlights_enabled_for_messaging: false,
highlights_enabled: true, highlights_enabled: true,
highlights_preview_only: false, 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, enable_proctored_exams: false,
create_zendesk_tickets: true, create_zendesk_tickets: true,
enable_timed_exams: true, enable_timed_exams: true,
@@ -1120,7 +1120,4 @@ module.exports = {
has_partition_group_components: false, has_partition_group_components: false,
release_date_from: 'Section "Example Week 1: Getting Started"', release_date_from: 'Section "Example Week 1: Getting Started"',
staff_lock_from: null, staff_lock_from: null,
upstreamInfo: {
upstreamLink: undefined,
},
}; };

View File

@@ -1,60 +1,39 @@
import { useCallback, useState } from 'react'; import { useCallback, useState } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import { getConfig } from '@edx/frontend-platform'; import { useNavigate } from 'react-router-dom';
import { useIntl, FormattedMessage } from '@edx/frontend-platform/i18n'; import { useIntl, FormattedMessage } from '@edx/frontend-platform/i18n';
import { import {
ActionRow, Button, StandardModal, useToggle, ActionRow, Button, StandardModal, useToggle,
} from '@openedx/paragon'; } from '@openedx/paragon';
import { getCourseSectionVertical } from '../data/selectors'; import { getCourseSectionVertical } from '../data/selectors';
import { getWaffleFlags } from '../../data/selectors';
import { COMPONENT_TYPES } from '../../generic/block-type-utils/constants'; import { COMPONENT_TYPES } from '../../generic/block-type-utils/constants';
import ComponentModalView from './add-component-modals/ComponentModalView'; import ComponentModalView from './add-component-modals/ComponentModalView';
import AddComponentButton from './add-component-btn'; import AddComponentButton from './add-component-btn';
import messages from './messages'; import messages from './messages';
import { ComponentPicker } from '../../library-authoring/component-picker'; import { ComponentPicker } from '../../library-authoring/component-picker';
import { messageTypes } from '../constants'; import { messageTypes } from '../constants';
import { useIframe } from '../../generic/hooks/context/hooks'; import { useIframe } from '../context/hooks';
import { useEventListener } from '../../generic/hooks'; import { useEventListener } from '../../generic/hooks';
import VideoSelectorPage from '../../editors/VideoSelectorPage';
import EditorPage from '../../editors/EditorPage';
const AddComponent = ({ const AddComponent = ({ blockId, handleCreateNewCourseXBlock }) => {
parentLocator, const navigate = useNavigate();
isSplitTestType,
isUnitVerticalType,
addComponentTemplateData,
handleCreateNewCourseXBlock,
}) => {
const intl = useIntl(); const intl = useIntl();
const [isOpenAdvanced, openAdvanced, closeAdvanced] = useToggle(false); const [isOpenAdvanced, openAdvanced, closeAdvanced] = useToggle(false);
const [isOpenHtml, openHtml, closeHtml] = useToggle(false); const [isOpenHtml, openHtml, closeHtml] = useToggle(false);
const [isOpenOpenAssessment, openOpenAssessment, closeOpenAssessment] = useToggle(false); const [isOpenOpenAssessment, openOpenAssessment, closeOpenAssessment] = useToggle(false);
const { componentTemplates = {} } = useSelector(getCourseSectionVertical); const { componentTemplates = {} } = useSelector(getCourseSectionVertical);
const blockId = addComponentTemplateData.parentLocator || parentLocator;
const [isAddLibraryContentModalOpen, showAddLibraryContentModal, closeAddLibraryContentModal] = useToggle(); const [isAddLibraryContentModalOpen, showAddLibraryContentModal, closeAddLibraryContentModal] = useToggle();
const [isVideoSelectorModalOpen, showVideoSelectorModal, closeVideoSelectorModal] = useToggle();
const [isXBlockEditorModalOpen, showXBlockEditorModal, closeXBlockEditorModal] = useToggle();
const [blockType, setBlockType] = useState(null);
const [courseId, setCourseId] = useState(null);
const [newBlockId, setNewBlockId] = useState(null);
const [isSelectLibraryContentModalOpen, showSelectLibraryContentModal, closeSelectLibraryContentModal] = useToggle(); const [isSelectLibraryContentModalOpen, showSelectLibraryContentModal, closeSelectLibraryContentModal] = useToggle();
const [selectedComponents, setSelectedComponents] = useState([]); const [selectedComponents, setSelectedComponents] = useState([]);
const [usageId, setUsageId] = useState(null);
const { sendMessageToIframe } = useIframe(); const { sendMessageToIframe } = useIframe();
const { useVideoGalleryFlow, useReactMarkdownEditor } = useSelector(getWaffleFlags);
const receiveMessage = useCallback(({ data: { type, payload } }) => { const receiveMessage = useCallback(({ data: { type } }) => {
if (type === messageTypes.showMultipleComponentPicker) { if (type === messageTypes.showMultipleComponentPicker) {
showSelectLibraryContentModal(); showSelectLibraryContentModal();
} }
if (type === messageTypes.showSingleComponentPicker) { }, [showSelectLibraryContentModal]);
setUsageId(payload.usageId);
showAddLibraryContentModal();
}
}, [showSelectLibraryContentModal, showAddLibraryContentModal, setUsageId]);
useEventListener('message', receiveMessage); useEventListener('message', receiveMessage);
@@ -63,21 +42,15 @@ const AddComponent = ({
closeSelectLibraryContentModal(); closeSelectLibraryContentModal();
}, [selectedComponents]); }, [selectedComponents]);
const onXBlockSave = useCallback(/* istanbul ignore next */ () => {
closeXBlockEditorModal();
closeVideoSelectorModal();
sendMessageToIframe(messageTypes.refreshXBlock, null);
}, [closeXBlockEditorModal, closeVideoSelectorModal, sendMessageToIframe]);
const handleLibraryV2Selection = useCallback((selection) => { const handleLibraryV2Selection = useCallback((selection) => {
handleCreateNewCourseXBlock({ handleCreateNewCourseXBlock({
type: COMPONENT_TYPES.libraryV2, type: COMPONENT_TYPES.libraryV2,
category: selection.blockType, category: selection.blockType,
parentLocator: usageId || blockId, parentLocator: blockId,
libraryContentKey: selection.usageKey, libraryContentKey: selection.usageKey,
}); });
closeAddLibraryContentModal(); closeAddLibraryContentModal();
}, [usageId]); }, []);
const handleCreateNewXBlock = (type, moduleName) => { const handleCreateNewXBlock = (type, moduleName) => {
switch (type) { switch (type) {
@@ -86,27 +59,11 @@ const AddComponent = ({
handleCreateNewCourseXBlock({ type, parentLocator: blockId }); handleCreateNewCourseXBlock({ type, parentLocator: blockId });
break; break;
case COMPONENT_TYPES.problem: case COMPONENT_TYPES.problem:
handleCreateNewCourseXBlock({ type, parentLocator: blockId }, ({ courseKey, locator }) => {
setCourseId(courseKey);
setBlockType(type);
setNewBlockId(locator);
showXBlockEditorModal();
});
break;
case COMPONENT_TYPES.video: case COMPONENT_TYPES.video:
handleCreateNewCourseXBlock( handleCreateNewCourseXBlock({ type, parentLocator: blockId }, ({ courseKey, locator }) => {
{ type, parentLocator: blockId }, localStorage.setItem('createXBlockLastYPosition', window.scrollY);
/* istanbul ignore next */ ({ courseKey, locator }) => { navigate(`/course/${courseKey}/editor/${type}/${locator}`);
setCourseId(courseKey); });
setBlockType(type);
setNewBlockId(locator);
if (useVideoGalleryFlow) {
showVideoSelectorModal();
} else {
showXBlockEditorModal();
}
},
);
break; break;
// TODO: The library functional will be a bit different of current legacy (CMS) // TODO: The library functional will be a bit different of current legacy (CMS)
// behaviour and this ticket is on hold (blocked by other development team). // behaviour and this ticket is on hold (blocked by other development team).
@@ -120,185 +77,127 @@ const AddComponent = ({
showAddLibraryContentModal(); showAddLibraryContentModal();
break; break;
case COMPONENT_TYPES.advanced: case COMPONENT_TYPES.advanced:
handleCreateNewCourseXBlock({ type: moduleName, category: moduleName, parentLocator: blockId }); handleCreateNewCourseXBlock({
type: moduleName, category: moduleName, parentLocator: blockId,
});
break; break;
case COMPONENT_TYPES.openassessment: case COMPONENT_TYPES.openassessment:
handleCreateNewCourseXBlock({ boilerplate: moduleName, category: type, parentLocator: blockId }); handleCreateNewCourseXBlock({
boilerplate: moduleName, category: type, parentLocator: blockId,
});
break; break;
case COMPONENT_TYPES.html: case COMPONENT_TYPES.html:
handleCreateNewCourseXBlock({ handleCreateNewCourseXBlock({
type, type,
boilerplate: moduleName, boilerplate: moduleName,
parentLocator: blockId, parentLocator: blockId,
}, /* istanbul ignore next */ ({ courseKey, locator }) => { }, ({ courseKey, locator }) => {
setCourseId(courseKey); localStorage.setItem('createXBlockLastYPosition', window.scrollY);
setBlockType(type); navigate(`/course/${courseKey}/editor/html/${locator}`);
setNewBlockId(locator);
showXBlockEditorModal();
}); });
break; break;
default: default:
} }
}; };
if (isUnitVerticalType || isSplitTestType) { if (!Object.keys(componentTemplates).length) {
return ( return null;
<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>
<StandardModal
title={intl.formatMessage(messages.videoPickerModalTitle)}
isOpen={isVideoSelectorModalOpen}
onClose={closeVideoSelectorModal}
isOverflowVisible={false}
size="xl"
>
<div className="selector-page">
<VideoSelectorPage
blockId={newBlockId}
courseId={courseId}
studioEndpointUrl={getConfig().STUDIO_BASE_URL}
lmsEndpointUrl={getConfig().LMS_BASE_URL}
onCancel={closeVideoSelectorModal}
returnFunction={/* istanbul ignore next */ () => onXBlockSave}
/>
</div>
</StandardModal>
{isXBlockEditorModalOpen && (
<div className="editor-page">
<EditorPage
courseId={courseId}
blockType={blockType}
blockId={newBlockId}
isMarkdownEditorEnabledForCourse={useReactMarkdownEditor}
studioEndpointUrl={getConfig().STUDIO_BASE_URL}
lmsEndpointUrl={getConfig().LMS_BASE_URL}
onClose={closeXBlockEditorModal}
returnFunction={/* istanbul ignore next */ () => onXBlockSave}
/>
</div>
)}
</div>
);
} }
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;
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 = { AddComponent.propTypes = {
isSplitTestType: PropTypes.bool.isRequired, blockId: PropTypes.string.isRequired,
isUnitVerticalType: PropTypes.bool.isRequired,
parentLocator: PropTypes.string.isRequired,
handleCreateNewCourseXBlock: PropTypes.func.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; export default AddComponent;

View File

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

View File

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

View File

@@ -31,11 +31,6 @@ const messages = defineMessages({
defaultMessage: 'Add selected components', defaultMessage: 'Add selected components',
description: 'Problem bank component add button text.', description: 'Problem bank component add button text.',
}, },
videoPickerModalTitle: {
id: 'course-authoring.course-unit.modal.video-title.text',
defaultMessage: 'Select video',
description: 'Video picker modal title.',
},
modalContainerTitle: { modalContainerTitle: {
id: 'course-authoring.course-unit.modal.container.title', id: 'course-authoring.course-unit.modal.container.title',
defaultMessage: 'Add {componentTitle} component', defaultMessage: 'Add {componentTitle} component',
@@ -63,27 +58,27 @@ const messages = defineMessages({
}, },
modalComponentSupportTooltipFullySupported: { modalComponentSupportTooltipFullySupported: {
id: 'course-authoring.course-unit.modal.component.support.tooltip.fully-supported', id: 'course-authoring.course-unit.modal.component.support.tooltip.fully-supported',
defaultMessage: 'Fully supported tools and features are available for Open edX installations, ' defaultMessage: 'Fully supported tools and features are available on edX, are '
+ 'are fully tested, have user interfaces where applicable, and are documented in the ' + 'fully tested, have user interfaces where applicable, and are documented in the '
+ 'official Open edX guides that are available on docs.openedx.org.', + 'official edX guides that are available on docs.edx.org.',
description: 'Message for support status tooltip for modules with full platform support', description: 'Message for support status tooltip for modules with full platform support',
}, },
modalComponentSupportTooltipNotSupported: { modalComponentSupportTooltipNotSupported: {
id: 'course-authoring.course-unit.modal.component.support.tooltip.not-supported', id: 'course-authoring.course-unit.modal.component.support.tooltip.not-supported',
defaultMessage: 'Tools with no support are not maintained by the Open edX community, ' defaultMessage: 'Tools with no support are not maintained by edX, and might be '
+ 'and might be deprecated in the future. They are not recommended for use in ' + 'deprecated in the future. They are not recommended for use in courses due to '
+ 'courses due to non-compliance with one or more of the base requirements, such as ' + 'non-compliance with one or more of the base requirements, such as testing, '
+ 'testing, accessibility, internationalization, and documentation.', + 'accessibility, internationalization, and documentation.',
description: 'Message for support status tooltip for modules which is not supported', description: 'Message for support status tooltip for modules which is not supported',
}, },
modalComponentSupportTooltipProvisionallySupported: { modalComponentSupportTooltipProvisionallySupported: {
id: 'course-authoring.course-unit.modal.component.support.tooltip.provisionally-support', id: 'course-authoring.course-unit.modal.component.support.tooltip.provisionally-support',
defaultMessage: 'Provisionally supported tools might lack the robustness of functionality ' 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 ' + '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 ' + 'before using them in your course, especially in graded sections. Complete documentation '
+ 'might not be available for provisionally supported tools, or documentation might be ' + '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', description: 'Message for support status tooltip for modules with provisional platform support',
}, },
}); });

View File

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

View File

@@ -26,55 +26,43 @@ const Breadcrumbs = ({ courseId, parentUnitId }: { courseId: string, parentUnitI
isOutlinePage ? getPathToCourseOutlinePage(url) : getPathToCourseUnitPage(url) isOutlinePage ? getPathToCourseOutlinePage(url) : getPathToCourseUnitPage(url)
); );
const hasChildWithUrl = (children = []) => (
!!children.filter((child : any) => child?.url).length
);
return ( return (
<nav className="d-flex align-center mb-2.5"> <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) => ( {ancestorXblocks.map(({ children, title, isLast }, index) => (
<li <li
className="d-flex mb-2.5" className="d-flex"
// eslint-disable-next-line react/no-array-index-key // eslint-disable-next-line react/no-array-index-key
key={`${title}-${index}`} key={`${title}-${index}`}
> >
{hasChildWithUrl(children) ? ( <Dropdown>
<Dropdown> <Dropdown.Toggle
<Dropdown.Toggle id="breadcrumbs-dropdown-section"
id="breadcrumbs-dropdown-section" variant="link"
variant="link" className="p-0 text-primary small"
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">
<span className="small text-gray-700"> <span className="small text-gray-700">
{title} {title}
</span> </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 && ( {!isLast && (
<Icon <Icon
src={ChevronRightIcon} 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 = { export const messageTypes = {
modal: 'plugin.modal',
resize: 'plugin.resize',
videoFullScreen: 'plugin.videoFullScreen',
refreshXBlock: 'refreshXBlock', refreshXBlock: 'refreshXBlock',
showMoveXBlockModal: 'showMoveXBlockModal', showMoveXBlockModal: 'showMoveXBlockModal',
completeXBlockMoving: 'completeXBlockMoving', completeXBlockMoving: 'completeXBlockMoving',
rollbackMovedXBlock: 'rollbackMovedXBlock', rollbackMovedXBlock: 'rollbackMovedXBlock',
showMultipleComponentPicker: 'showMultipleComponentPicker', showMultipleComponentPicker: 'showMultipleComponentPicker',
showSingleComponentPicker: 'showSingleComponentPicker',
addSelectedComponentsToBank: 'addSelectedComponentsToBank', addSelectedComponentsToBank: 'addSelectedComponentsToBank',
showXBlockLibraryChangesPreview: 'showXBlockLibraryChangesPreview', showXBlockLibraryChangesPreview: 'showXBlockLibraryChangesPreview',
copyXBlock: 'copyXBlock', copyXBlock: 'copyXBlock',
@@ -60,7 +69,6 @@ export const messageTypes = {
addXBlock: 'addXBlock', addXBlock: 'addXBlock',
scrollToXBlock: 'scrollToXBlock', scrollToXBlock: 'scrollToXBlock',
handleViewXBlockContent: 'handleViewXBlockContent', handleViewXBlockContent: 'handleViewXBlockContent',
handleViewGroupConfigurations: 'handleViewGroupConfigurations',
editXBlock: 'editXBlock', editXBlock: 'editXBlock',
closeXBlockEditorModal: 'closeXBlockEditorModal', closeXBlockEditorModal: 'closeXBlockEditorModal',
saveEditedXBlockData: 'saveEditedXBlockData', saveEditedXBlockData: 'saveEditedXBlockData',
@@ -68,10 +76,4 @@ export const messageTypes = {
studioAjaxError: 'studioAjaxError', studioAjaxError: 'studioAjaxError',
refreshPositions: 'refreshPositions', refreshPositions: 'refreshPositions',
openManageTags: 'openManageTags', 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 { ReactNode } from 'react';
import { renderHook } from '@testing-library/react'; import { renderHook } from '@testing-library/react-hooks';
import { useIframe } from './hooks'; import { useIframe } from './hooks';
import { IframeProvider } from './iFrameContext'; import { IframeProvider } from './iFrameContext';
describe('useIframe hook', () => { describe('useIframe hook', () => {
it('throws an error when used outside of IframeProvider', () => { 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', () => { it('returns context value when used inside IframeProvider', () => {

View File

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

View File

@@ -12,19 +12,16 @@ export function useSequenceNavigationMetadata(courseId, currentSequenceId, curre
const isLastUnit = !nextUrl; const isLastUnit = !nextUrl;
const sequenceIds = useSelector(getSequenceIds); const sequenceIds = useSelector(getSequenceIds);
const sequenceIndex = sequenceIds.indexOf(currentSequenceId); const sequenceIndex = sequenceIds.indexOf(currentSequenceId);
let unitIndex = sequence?.unitIds.indexOf(currentUnitId); const unitIndex = sequence.unitIds.indexOf(currentUnitId);
const nextSequenceId = sequenceIndex < sequenceIds.length - 1 ? sequenceIds[sequenceIndex + 1] : null; const nextSequenceId = sequenceIndex < sequenceIds.length - 1 ? sequenceIds[sequenceIndex + 1] : null;
const previousSequenceId = sequenceIndex > 0 ? sequenceIds[sequenceIndex - 1] : null; const previousSequenceId = sequenceIndex > 0 ? sequenceIds[sequenceIndex - 1] : null;
if (!unitIndex) {
// Handle case where unitIndex is not found
unitIndex = 0;
}
let nextLink; let nextLink;
const nextIndex = unitIndex + 1; const nextIndex = unitIndex + 1;
if (nextIndex < sequence?.unitIds.length) { if (nextIndex < sequence.unitIds.length) {
const nextUnitId = sequence?.unitIds[nextIndex]; const nextUnitId = sequence.unitIds[nextIndex];
nextLink = `/course/${courseId}/container/${nextUnitId}/${currentSequenceId}`; nextLink = `/course/${courseId}/container/${nextUnitId}/${currentSequenceId}`;
} else if (nextSequenceId) { } else if (nextSequenceId) {
const pathToNextUnit = decodeURIComponent(nextUrl); const pathToNextUnit = decodeURIComponent(nextUrl);
@@ -35,7 +32,7 @@ export function useSequenceNavigationMetadata(courseId, currentSequenceId, curre
const previousIndex = unitIndex - 1; const previousIndex = unitIndex - 1;
if (previousIndex >= 0) { if (previousIndex >= 0) {
const previousUnitId = sequence?.unitIds[previousIndex]; const previousUnitId = sequence.unitIds[previousIndex];
previousLink = `/course/${courseId}/container/${previousUnitId}/${currentSequenceId}`; previousLink = `/course/${courseId}/container/${previousUnitId}/${currentSequenceId}`;
} else if (previousSequenceId) { } else if (previousSequenceId) {
const pathToPreviousUnit = decodeURIComponent(prevUrl); const pathToPreviousUnit = decodeURIComponent(prevUrl);

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 { useSelector } from 'react-redux';
import { useSequenceNavigationMetadata } from './hooks'; import { useSequenceNavigationMetadata } from './hooks';
import { getCourseSectionVertical, getSequenceIds } from '../data/selectors'; import { getCourseSectionVertical, getSequenceIds } from '../data/selectors';

View File

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

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