Compare commits
128 Commits
saad/remov
...
release/te
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f459f53343 | ||
|
|
a5a7d03d12 | ||
|
|
41fc478efe | ||
|
|
06497bf85c | ||
|
|
7e0b7f94e8 | ||
|
|
4bc34c268b | ||
|
|
2973614e3b | ||
|
|
bdc99fddc3 | ||
|
|
92c59cbf0c | ||
|
|
b6bd94c114 | ||
|
|
c9896a8fe5 | ||
|
|
4ba8cde587 | ||
|
|
86d0a7e7db | ||
|
|
1968d146cd | ||
|
|
3e737b5b0d | ||
|
|
fcdf1fdecb | ||
|
|
efb1a28b4d | ||
|
|
1ff5e5bdae | ||
|
|
19ef80553a | ||
|
|
2beb91c63b | ||
|
|
d325a92204 | ||
|
|
7dfd93d4f1 | ||
|
|
e34df7f270 | ||
|
|
317bc757cf | ||
|
|
212a54f76e | ||
|
|
944d1316ad | ||
|
|
dd731a0d19 | ||
|
|
976dfcaab7 | ||
|
|
403dfa1e6b | ||
|
|
1919eb4845 | ||
|
|
3d6e221f99 | ||
|
|
fab786a6c6 | ||
|
|
a162929fd7 | ||
|
|
6c4634ebbe | ||
|
|
79f865b328 | ||
|
|
d5e36cf2b8 | ||
|
|
8ffafc094f | ||
|
|
b375806fd2 | ||
|
|
ab0e0d71c1 | ||
|
|
b30a1c8c5e | ||
|
|
855b44f745 | ||
|
|
2d55ba4ccc | ||
|
|
d62c4cf4f8 | ||
|
|
9824502278 | ||
|
|
d6b51ecf0c | ||
|
|
1fe1f93314 | ||
|
|
fbc1273955 | ||
|
|
7edb3528ba | ||
|
|
e7c22b1cbf | ||
|
|
380f3be164 | ||
|
|
74d7d66c59 | ||
|
|
e2189f2fdd | ||
|
|
293b7941dd | ||
|
|
eaa075464c | ||
|
|
03d732846e | ||
|
|
c1302f1089 | ||
|
|
ea26981393 | ||
|
|
55e505eb36 | ||
|
|
9002f7acfe | ||
|
|
febf5cf5d0 | ||
|
|
ac127e2b15 | ||
|
|
06bdff1796 | ||
|
|
ea0a031d7b | ||
|
|
ea8a8e5285 | ||
|
|
9adfa58d65 | ||
|
|
4ddb8c3168 | ||
|
|
3b2adc2fc1 | ||
|
|
4bd2c3b29a | ||
|
|
f531d5471d | ||
|
|
f24b89c847 | ||
|
|
d9dcdfe1e3 | ||
|
|
990073cb38 | ||
|
|
afecd8ba83 | ||
|
|
aa8a5bfba4 | ||
|
|
87695ae636 | ||
|
|
681854209a | ||
|
|
a522c48045 | ||
|
|
f46e4ce4e8 | ||
|
|
a43027b328 | ||
|
|
01365d080e | ||
|
|
e00a4c4d03 | ||
|
|
341a03c50b | ||
|
|
5df7adffec | ||
|
|
04faf54ad8 | ||
|
|
d688cf57b7 | ||
|
|
fe36e65d2d | ||
|
|
8e99ebf072 | ||
|
|
024537c80e | ||
|
|
0ddcbbb7a5 | ||
|
|
7ceeb32820 | ||
|
|
451b821c3b | ||
|
|
68d62cd62f | ||
|
|
2a31434a55 | ||
|
|
fdd8928f36 | ||
|
|
552ff395df | ||
|
|
c324446722 | ||
|
|
15fcb55075 | ||
|
|
d1a6af51a4 | ||
|
|
55344bc55d | ||
|
|
a23f6a6fa7 | ||
|
|
5cedaacc3e | ||
|
|
0ce0b7526e | ||
|
|
3685dbd6a1 | ||
|
|
272e30f1b1 | ||
|
|
98ae74e78c | ||
|
|
df7405ec39 | ||
|
|
d497bf2ccc | ||
|
|
94f34074ce | ||
|
|
92a8b42e36 | ||
|
|
08368582e3 | ||
|
|
a52f6d9b94 | ||
|
|
bac63583ac | ||
|
|
545bb4a8a6 | ||
|
|
9e65424ca6 | ||
|
|
27c4eec746 | ||
|
|
cc20dfd8ca | ||
|
|
a26e3f9e92 | ||
|
|
e66da2cb49 | ||
|
|
77a55d9ad3 | ||
|
|
3aa409d065 | ||
|
|
732fd28eb9 | ||
|
|
091e120224 | ||
|
|
1174b09ac4 | ||
|
|
b2472cfc0a | ||
|
|
17ebb90cd1 | ||
|
|
49fbe766b0 | ||
|
|
dbba4dd296 | ||
|
|
0eda5aec23 |
3
.env
3
.env
@@ -44,4 +44,5 @@ INVITE_STUDENTS_EMAIL_TO=''
|
||||
ENABLE_HOME_PAGE_COURSE_API_V2=true
|
||||
ENABLE_CHECKLIST_QUALITY=''
|
||||
ENABLE_GRADING_METHOD_IN_PROBLEMS=false
|
||||
LIBRARY_SUPPORTED_BLOCKS="problem,video,html,drag-and-drop-v2"
|
||||
# "Multi-level" blocks are unsupported in libraries
|
||||
LIBRARY_UNSUPPORTED_BLOCKS="conditional,step-builder,problem-builder"
|
||||
|
||||
@@ -47,4 +47,5 @@ INVITE_STUDENTS_EMAIL_TO="someone@domain.com"
|
||||
ENABLE_HOME_PAGE_COURSE_API_V2=true
|
||||
ENABLE_CHECKLIST_QUALITY=true
|
||||
ENABLE_GRADING_METHOD_IN_PROBLEMS=false
|
||||
LIBRARY_SUPPORTED_BLOCKS="problem,video,html,drag-and-drop-v2"
|
||||
# "Multi-level" blocks are unsupported in libraries
|
||||
LIBRARY_UNSUPPORTED_BLOCKS="conditional,step-builder,problem-builder"
|
||||
|
||||
@@ -39,5 +39,5 @@ INVITE_STUDENTS_EMAIL_TO="someone@domain.com"
|
||||
ENABLE_HOME_PAGE_COURSE_API_V2=true
|
||||
ENABLE_CHECKLIST_QUALITY=true
|
||||
ENABLE_GRADING_METHOD_IN_PROBLEMS=false
|
||||
# "other" is used to test the workflow for creating blocks that aren't supported by the built-in editors
|
||||
LIBRARY_SUPPORTED_BLOCKS="problem,video,html,drag-and-drop-v2,other"
|
||||
# "Multi-level" blocks are unsupported in libraries
|
||||
LIBRARY_UNSUPPORTED_BLOCKS="conditional,step-builder,problem-builder"
|
||||
|
||||
@@ -38,7 +38,7 @@ Cloning and Setup
|
||||
|
||||
git clone https://github.com/openedx/frontend-app-authoring.git
|
||||
|
||||
2. Use node v20.x.
|
||||
2. Use the version of Node specified in the ``.nvmrc`` file.
|
||||
|
||||
The current version of the micro-frontend build scripts supports node 20.
|
||||
Using other major versions of node *may* work, but this is unsupported. For
|
||||
@@ -315,7 +315,7 @@ In additional to the standard settings, the following local configurations can b
|
||||
Developing
|
||||
**********
|
||||
|
||||
`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.
|
||||
`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.
|
||||
|
||||
|
||||
If your devstack includes the default Demo course, you can visit the following URLs to see content:
|
||||
|
||||
@@ -10,4 +10,5 @@ coverage:
|
||||
threshold: 0%
|
||||
ignore:
|
||||
- "src/grading-settings/grading-scale/react-ranger.js"
|
||||
- "src/generic/DraggableList/verticalSortableList.ts"
|
||||
- "src/index.js"
|
||||
|
||||
9331
package-lock.json
generated
9331
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
34
package.json
34
package.json
@@ -23,11 +23,6 @@
|
||||
"test:ci": "TZ=UTC fedx-scripts jest --silent --coverage --passWithNoTests",
|
||||
"types": "tsc --noEmit"
|
||||
},
|
||||
"husky": {
|
||||
"hooks": {
|
||||
"pre-commit": "npm run lint"
|
||||
}
|
||||
},
|
||||
"author": "edX",
|
||||
"license": "AGPL-3.0",
|
||||
"homepage": "https://github.com/openedx/frontend-app-authoring#readme",
|
||||
@@ -40,6 +35,7 @@
|
||||
"dependencies": {
|
||||
"@codemirror/lang-html": "^6.0.0",
|
||||
"@codemirror/lang-xml": "^6.0.0",
|
||||
"@codemirror/lang-markdown": "^6.0.0",
|
||||
"@codemirror/lint": "^6.2.1",
|
||||
"@codemirror/state": "^6.0.0",
|
||||
"@codemirror/view": "^6.0.0",
|
||||
@@ -49,10 +45,10 @@
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
"@edx/brand": "npm:@openedx/brand-openedx@^1.2.3",
|
||||
"@edx/browserslist-config": "1.2.0",
|
||||
"@edx/frontend-component-footer": "^14.1.0",
|
||||
"@edx/frontend-component-header": "^5.8.3",
|
||||
"@edx/frontend-enterprise-hotjar": "^2.0.0",
|
||||
"@edx/frontend-platform": "^8.0.3",
|
||||
"@edx/frontend-component-footer": "^14.6.0",
|
||||
"@edx/frontend-component-header": "^6.2.0",
|
||||
"@edx/frontend-enterprise-hotjar": "^7.2.0",
|
||||
"@edx/frontend-platform": "^8.3.1",
|
||||
"@edx/openedx-atlas": "^0.6.0",
|
||||
"@openedx-plugins/course-app-calculator": "file:plugins/course-apps/calculator",
|
||||
"@openedx-plugins/course-app-edxnotes": "file:plugins/course-apps/edxnotes",
|
||||
@@ -64,9 +60,9 @@
|
||||
"@openedx-plugins/course-app-teams": "file:plugins/course-apps/teams",
|
||||
"@openedx-plugins/course-app-wiki": "file:plugins/course-apps/wiki",
|
||||
"@openedx-plugins/course-app-xpert_unit_summary": "file:plugins/course-apps/xpert_unit_summary",
|
||||
"@openedx/frontend-build": "^14.2.0",
|
||||
"@openedx/frontend-plugin-framework": "^1.2.1",
|
||||
"@openedx/paragon": "^22.8.1",
|
||||
"@openedx/frontend-build": "^14.3.3",
|
||||
"@openedx/frontend-plugin-framework": "^1.7.0",
|
||||
"@openedx/paragon": "^22.16.0",
|
||||
"@redux-devtools/extension": "^3.3.0",
|
||||
"@reduxjs/toolkit": "1.9.7",
|
||||
"@tanstack/react-query": "4.36.1",
|
||||
@@ -85,9 +81,9 @@
|
||||
"moment-shortformat": "^2.1.0",
|
||||
"npm": "^10.8.1",
|
||||
"prop-types": "^15.8.1",
|
||||
"react": "17.0.2",
|
||||
"react": "^18.3.1",
|
||||
"react-datepicker": "^4.13.0",
|
||||
"react-dom": "17.0.2",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-error-boundary": "^4.0.13",
|
||||
"react-helmet": "^6.1.0",
|
||||
"react-onclickoutside": "^6.13.0",
|
||||
@@ -110,21 +106,19 @@
|
||||
"yup": "0.31.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@edx/react-unit-test-utils": "3.0.0",
|
||||
"@edx/react-unit-test-utils": "^4.0.0",
|
||||
"@edx/stylelint-config-edx": "2.3.3",
|
||||
"@edx/typescript-config": "^1.0.1",
|
||||
"@testing-library/jest-dom": "5.17.0",
|
||||
"@testing-library/react": "12.1.5",
|
||||
"@testing-library/react-hooks": "^8.0.1",
|
||||
"@testing-library/jest-dom": "^6.6.3",
|
||||
"@testing-library/react": "^16.2.0",
|
||||
"@testing-library/user-event": "^13.2.1",
|
||||
"@types/lodash": "^4.17.7",
|
||||
"axios-mock-adapter": "1.22.0",
|
||||
"eslint-import-resolver-webpack": "^0.13.8",
|
||||
"fetch-mock-jest": "^1.5.1",
|
||||
"husky": "7.0.4",
|
||||
"jest-canvas-mock": "^2.5.2",
|
||||
"jest-expect-message": "^1.1.3",
|
||||
"react-test-renderer": "17.0.2",
|
||||
"react-test-renderer": "^18.3.1",
|
||||
"redux-mock-store": "^1.5.4"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Form, Hyperlink } from '@openedx/paragon';
|
||||
import PropTypes from 'prop-types';
|
||||
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';
|
||||
|
||||
const BbbSettings = ({
|
||||
intl,
|
||||
values,
|
||||
setFieldValue,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const [bbbPlan, setBbbPlan] = useState(values.tierType);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -107,12 +107,10 @@ const BbbSettings = ({
|
||||
)}
|
||||
</>
|
||||
</>
|
||||
|
||||
);
|
||||
};
|
||||
|
||||
BbbSettings.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
values: PropTypes.shape({
|
||||
consumerKey: PropTypes.string,
|
||||
consumerSecret: PropTypes.string,
|
||||
@@ -127,4 +125,4 @@ BbbSettings.propTypes = {
|
||||
setFieldValue: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(BbbSettings);
|
||||
export default BbbSettings;
|
||||
|
||||
@@ -1,42 +1,43 @@
|
||||
import React from 'react';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import PropTypes from 'prop-types';
|
||||
import FormikControl from 'CourseAuthoring/generic/FormikControl';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
const LiveCommonFields = ({
|
||||
intl,
|
||||
values,
|
||||
}) => (
|
||||
<>
|
||||
<p className="pb-2">{intl.formatMessage(messages.formInstructions)}</p>
|
||||
<FormikControl
|
||||
name="consumerKey"
|
||||
value={values.consumerKey}
|
||||
floatingLabel={intl.formatMessage(messages.consumerKey)}
|
||||
className="pb-1"
|
||||
type="input"
|
||||
/>
|
||||
<FormikControl
|
||||
name="consumerSecret"
|
||||
value={values.consumerSecret}
|
||||
floatingLabel={intl.formatMessage(messages.consumerSecret)}
|
||||
className="pb-1"
|
||||
type="password"
|
||||
/>
|
||||
<FormikControl
|
||||
name="launchUrl"
|
||||
value={values.launchUrl}
|
||||
floatingLabel={intl.formatMessage(messages.launchUrl)}
|
||||
className="pb-1"
|
||||
type="input"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
return (
|
||||
<>
|
||||
<p className="pb-2">{intl.formatMessage(messages.formInstructions)}</p>
|
||||
<FormikControl
|
||||
name="consumerKey"
|
||||
value={values.consumerKey}
|
||||
floatingLabel={intl.formatMessage(messages.consumerKey)}
|
||||
className="pb-1"
|
||||
type="input"
|
||||
/>
|
||||
<FormikControl
|
||||
name="consumerSecret"
|
||||
value={values.consumerSecret}
|
||||
floatingLabel={intl.formatMessage(messages.consumerSecret)}
|
||||
className="pb-1"
|
||||
type="password"
|
||||
/>
|
||||
<FormikControl
|
||||
name="launchUrl"
|
||||
value={values.launchUrl}
|
||||
floatingLabel={intl.formatMessage(messages.launchUrl)}
|
||||
className="pb-1"
|
||||
type="input"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
LiveCommonFields.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
values: PropTypes.shape({
|
||||
consumerKey: PropTypes.string,
|
||||
consumerSecret: PropTypes.string,
|
||||
@@ -45,4 +46,4 @@ LiveCommonFields.propTypes = {
|
||||
}).isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(LiveCommonFields);
|
||||
export default LiveCommonFields;
|
||||
|
||||
@@ -2,7 +2,7 @@ import React, { useEffect } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { camelCase } from 'lodash';
|
||||
import { Icon } from '@openedx/paragon';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import PropTypes from 'prop-types';
|
||||
import * as Yup from 'yup';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
@@ -20,9 +20,9 @@ import ZoomSettings from './ZoomSettings';
|
||||
import BBBSettings from './BBBSettings';
|
||||
|
||||
const LiveSettings = ({
|
||||
intl,
|
||||
onClose,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const navigate = useNavigate();
|
||||
const dispatch = useDispatch();
|
||||
const courseId = useSelector(state => state.courseDetail.courseId);
|
||||
@@ -130,8 +130,7 @@ const LiveSettings = ({
|
||||
};
|
||||
|
||||
LiveSettings.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(LiveSettings);
|
||||
export default LiveSettings;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import PropTypes from 'prop-types';
|
||||
import FormikControl from 'CourseAuthoring/generic/FormikControl';
|
||||
|
||||
@@ -8,37 +8,38 @@ import { providerNames } from './constants';
|
||||
import LiveCommonFields from './LiveCommonFields';
|
||||
|
||||
const ZoomSettings = ({
|
||||
intl,
|
||||
values,
|
||||
}) => (
|
||||
// eslint-disable-next-line react/jsx-no-useless-fragment
|
||||
<>
|
||||
{!values.piiSharingEnable ? (
|
||||
<p data-testid="request-pii-sharing">
|
||||
{intl.formatMessage(messages.requestPiiSharingEnable, { provider: providerNames[values.provider] })}
|
||||
</p>
|
||||
) : (
|
||||
<>
|
||||
{(values.piiSharingEmail || values.piiSharingUsername)
|
||||
&& (
|
||||
<p data-testid="helper-text">
|
||||
{intl.formatMessage(messages.providerHelperText, { providerName: providerNames[values.provider] })}
|
||||
</p>
|
||||
)}
|
||||
<LiveCommonFields values={values} />
|
||||
<FormikControl
|
||||
name="launchEmail"
|
||||
value={values.launchEmail}
|
||||
floatingLabel={intl.formatMessage(messages.launchEmail)}
|
||||
type="input"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
return (
|
||||
// eslint-disable-next-line react/jsx-no-useless-fragment
|
||||
<>
|
||||
{!values.piiSharingEnable ? (
|
||||
<p data-testid="request-pii-sharing">
|
||||
{intl.formatMessage(messages.requestPiiSharingEnable, { provider: providerNames[values.provider] })}
|
||||
</p>
|
||||
) : (
|
||||
<>
|
||||
{(values.piiSharingEmail || values.piiSharingUsername)
|
||||
&& (
|
||||
<p data-testid="helper-text">
|
||||
{intl.formatMessage(messages.providerHelperText, { providerName: providerNames[values.provider] })}
|
||||
</p>
|
||||
)}
|
||||
<LiveCommonFields values={values} />
|
||||
<FormikControl
|
||||
name="launchEmail"
|
||||
value={values.launchEmail}
|
||||
floatingLabel={intl.formatMessage(messages.launchEmail)}
|
||||
type="input"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
ZoomSettings.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
values: PropTypes.shape({
|
||||
consumerKey: PropTypes.string,
|
||||
consumerSecret: PropTypes.string,
|
||||
@@ -51,4 +52,4 @@ ZoomSettings.propTypes = {
|
||||
}).isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(ZoomSettings);
|
||||
export default ZoomSettings;
|
||||
|
||||
@@ -142,7 +142,7 @@ describe('ORASettings', () => {
|
||||
renderComponent();
|
||||
await mockStore({ apiStatus: 200, enabled: false });
|
||||
|
||||
const label = screen.getByText(messages.enableFlexPeerGradeLabel.defaultMessage);
|
||||
const label = await screen.findByText(messages.enableFlexPeerGradeLabel.defaultMessage);
|
||||
const enableBadge = screen.queryByTestId('enable-badge');
|
||||
|
||||
expect(label).toBeVisible();
|
||||
|
||||
@@ -8,7 +8,7 @@ import PropTypes from 'prop-types';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
|
||||
import { injectIntl, intlShape, FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
import { useIntl, FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
ActionRow, Alert, Badge, Form, Hyperlink, ModalDialog, StatefulButton,
|
||||
} from '@openedx/paragon';
|
||||
@@ -25,7 +25,8 @@ import { PagesAndResourcesContext } from 'CourseAuthoring/pages-and-resources/Pa
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
const ProctoringSettings = ({ intl, onClose }) => {
|
||||
const ProctoringSettings = ({ onClose }) => {
|
||||
const intl = useIntl();
|
||||
const initialFormValues = {
|
||||
enableProctoredExams: false,
|
||||
proctoringProvider: false,
|
||||
@@ -652,10 +653,9 @@ const ProctoringSettings = ({ intl, onClose }) => {
|
||||
};
|
||||
|
||||
ProctoringSettings.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
ProctoringSettings.defaultProps = {};
|
||||
|
||||
export default injectIntl(ProctoringSettings);
|
||||
export default ProctoringSettings;
|
||||
|
||||
@@ -544,12 +544,9 @@ describe('ProctoredExamSettings', () => {
|
||||
|
||||
describe('Connection states', () => {
|
||||
it('Shows the spinner before the connection is complete', async () => {
|
||||
await act(async () => {
|
||||
render(intlWrapper(<IntlProctoredExamSettings {...defaultProps} />));
|
||||
// This expectation is _inside_ the `act` intentionally, so that it executes immediately.
|
||||
const spinner = screen.getByRole('status');
|
||||
expect(spinner.textContent).toEqual('Loading...');
|
||||
});
|
||||
render(intlWrapper(<IntlProctoredExamSettings {...defaultProps} />));
|
||||
const spinner = await screen.findByRole('status');
|
||||
expect(spinner.textContent).toEqual('Loading...');
|
||||
});
|
||||
|
||||
it('Show connection error message when we suffer studio server side error', async () => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import * as Yup from 'yup';
|
||||
@@ -8,7 +8,8 @@ import { useAppSetting } from 'CourseAuthoring/utils';
|
||||
import AppSettingsModal from 'CourseAuthoring/pages-and-resources/app-settings-modal/AppSettingsModal';
|
||||
import messages from './messages';
|
||||
|
||||
const ProgressSettings = ({ intl, onClose }) => {
|
||||
const ProgressSettings = ({ onClose }) => {
|
||||
const intl = useIntl();
|
||||
const [disableProgressGraph, saveSetting] = useAppSetting('disableProgressGraph');
|
||||
const showProgressGraphSetting = getConfig().ENABLE_PROGRESS_GRAPH_SETTINGS.toString().toLowerCase() === 'true';
|
||||
|
||||
@@ -48,8 +49,7 @@ const ProgressSettings = ({ intl, onClose }) => {
|
||||
};
|
||||
|
||||
ProgressSettings.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(ProgressSettings);
|
||||
export default ProgressSettings;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Button, Form, TransitionReplace } from '@openedx/paragon';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { useState } from 'react';
|
||||
@@ -30,8 +30,9 @@ const TeamTypeNameMessage = {
|
||||
};
|
||||
|
||||
const GroupEditor = ({
|
||||
intl, group, onDelete, onChange, onBlur, fieldNameCommonBase, errors,
|
||||
group, onDelete, onChange, onBlur, fieldNameCommonBase, errors,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const [isDeleting, setDeleting] = useState(false);
|
||||
const [isOpen, setOpen] = useState(group.id === null);
|
||||
const initiateDeletion = () => setDeleting(true);
|
||||
@@ -149,7 +150,6 @@ export const groupShape = PropTypes.shape({
|
||||
});
|
||||
|
||||
GroupEditor.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
fieldNameCommonBase: PropTypes.string.isRequired,
|
||||
errors: PropTypes.shape({
|
||||
name: PropTypes.string,
|
||||
@@ -170,4 +170,4 @@ GroupEditor.defaultProps = {
|
||||
},
|
||||
};
|
||||
|
||||
export default injectIntl(GroupEditor);
|
||||
export default GroupEditor;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Button, Form } from '@openedx/paragon';
|
||||
import { Add } from '@openedx/paragon/icons';
|
||||
|
||||
@@ -17,15 +17,16 @@ import messages from './messages';
|
||||
setupYupExtensions();
|
||||
|
||||
const TeamSettings = ({
|
||||
intl,
|
||||
onClose,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const [teamsConfiguration, saveSettings] = useAppSetting('teamsConfiguration');
|
||||
const blankNewGroup = {
|
||||
name: '',
|
||||
description: '',
|
||||
type: GroupTypes.OPEN,
|
||||
maxTeamSize: null,
|
||||
userPartitionId: null,
|
||||
id: null,
|
||||
key: uuid(),
|
||||
};
|
||||
@@ -38,6 +39,7 @@ const TeamSettings = ({
|
||||
type: group.type,
|
||||
description: group.description,
|
||||
max_team_size: group.maxTeamSize,
|
||||
user_partition_id: group.userPartitionId,
|
||||
}));
|
||||
return saveSettings({
|
||||
team_sets: groups,
|
||||
@@ -164,8 +166,7 @@ const TeamSettings = ({
|
||||
};
|
||||
|
||||
TeamSettings.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(TeamSettings);
|
||||
export default TeamSettings;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import * as Yup from 'yup';
|
||||
@@ -8,7 +8,8 @@ import { useAppSetting } from 'CourseAuthoring/utils';
|
||||
import AppSettingsModal from 'CourseAuthoring/pages-and-resources/app-settings-modal/AppSettingsModal';
|
||||
import messages from './messages';
|
||||
|
||||
const WikiSettings = ({ intl, onClose }) => {
|
||||
const WikiSettings = ({ onClose }) => {
|
||||
const intl = useIntl();
|
||||
const [enablePublicWiki, saveSetting] = useAppSetting('allowPublicWikiAccess');
|
||||
const handleSettingsSave = (values) => saveSetting(values.enablePublicWiki);
|
||||
|
||||
@@ -32,7 +33,7 @@ const WikiSettings = ({ intl, onClose }) => {
|
||||
label={intl.formatMessage(messages.enablePublicWikiLabel)}
|
||||
helpText={intl.formatMessage(messages.enablePublicWikiHelp)}
|
||||
onChange={handleChange}
|
||||
onBlue={handleBlur}
|
||||
onBlur={handleBlur}
|
||||
checked={values.enablePublicWiki}
|
||||
/>
|
||||
)
|
||||
@@ -42,8 +43,7 @@ const WikiSettings = ({ intl, onClose }) => {
|
||||
};
|
||||
|
||||
WikiSettings.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(WikiSettings);
|
||||
export default WikiSettings;
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import React, { useCallback, useContext, useEffect } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { PagesAndResourcesContext } from 'CourseAuthoring/pages-and-resources/PagesAndResourcesProvider';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
@@ -10,7 +9,8 @@ import messages from './messages';
|
||||
|
||||
import { fetchXpertSettings } from './data/thunks';
|
||||
|
||||
const XpertUnitSummarySettings = ({ intl }) => {
|
||||
const XpertUnitSummarySettings = () => {
|
||||
const intl = useIntl();
|
||||
const { path: pagesAndResourcesPath, courseId } = useContext(PagesAndResourcesContext);
|
||||
const dispatch = useDispatch();
|
||||
const navigate = useNavigate();
|
||||
@@ -38,8 +38,4 @@ const XpertUnitSummarySettings = ({ intl }) => {
|
||||
);
|
||||
};
|
||||
|
||||
XpertUnitSummarySettings.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(XpertUnitSummarySettings);
|
||||
export default XpertUnitSummarySettings;
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
import { AppProvider, PageWrap } from '@edx/frontend-platform/react';
|
||||
import {
|
||||
queryByTestId, render, waitFor, getByText, fireEvent,
|
||||
findByTestId, queryByTestId, render, waitFor, getByText, fireEvent,
|
||||
} from '@testing-library/react';
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
import PagesAndResourcesProvider from 'CourseAuthoring/pages-and-resources/PagesAndResourcesProvider';
|
||||
@@ -106,8 +106,9 @@ describe('XpertUnitSummarySettings', () => {
|
||||
});
|
||||
|
||||
test('Shows switch on if enabled from backend', async () => {
|
||||
const enableBadge = await findByTestId(container, 'enable-badge');
|
||||
expect(container.querySelector('#enable-xpert-unit-summary-toggle').checked).toBeTruthy();
|
||||
expect(queryByTestId(container, 'enable-badge')).toBeTruthy();
|
||||
expect(enableBadge).toBeTruthy();
|
||||
});
|
||||
|
||||
test('Shows switch on if disabled from backend', async () => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
ActionRow,
|
||||
Alert,
|
||||
@@ -70,38 +70,40 @@ AppSettingsForm.defaultProps = {
|
||||
};
|
||||
|
||||
const SettingsModalBase = ({
|
||||
intl, title, onClose, variant, isMobile, children, footer,
|
||||
}) => (
|
||||
<ModalDialog
|
||||
title={title}
|
||||
isOpen
|
||||
onClose={onClose}
|
||||
size="lg"
|
||||
variant={variant}
|
||||
hasCloseButton={isMobile}
|
||||
isFullscreenOnMobile
|
||||
>
|
||||
<ModalDialog.Header>
|
||||
<ModalDialog.Title data-testid="modal-title">
|
||||
{title}
|
||||
</ModalDialog.Title>
|
||||
</ModalDialog.Header>
|
||||
<ModalDialog.Body>
|
||||
{children}
|
||||
</ModalDialog.Body>
|
||||
<ModalDialog.Footer className="p-4">
|
||||
<ActionRow>
|
||||
<ModalDialog.CloseButton variant="tertiary">
|
||||
{intl.formatMessage(messages.cancel)}
|
||||
</ModalDialog.CloseButton>
|
||||
{footer}
|
||||
</ActionRow>
|
||||
</ModalDialog.Footer>
|
||||
</ModalDialog>
|
||||
);
|
||||
title, onClose, variant, isMobile, children, footer,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
return (
|
||||
<ModalDialog
|
||||
title={title}
|
||||
isOpen
|
||||
onClose={onClose}
|
||||
size="lg"
|
||||
variant={variant}
|
||||
hasCloseButton={isMobile}
|
||||
isFullscreenOnMobile
|
||||
>
|
||||
<ModalDialog.Header>
|
||||
<ModalDialog.Title data-testid="modal-title">
|
||||
{title}
|
||||
</ModalDialog.Title>
|
||||
</ModalDialog.Header>
|
||||
<ModalDialog.Body>
|
||||
{children}
|
||||
</ModalDialog.Body>
|
||||
<ModalDialog.Footer className="p-4">
|
||||
<ActionRow>
|
||||
<ModalDialog.CloseButton variant="tertiary">
|
||||
{intl.formatMessage(messages.cancel)}
|
||||
</ModalDialog.CloseButton>
|
||||
{footer}
|
||||
</ActionRow>
|
||||
</ModalDialog.Footer>
|
||||
</ModalDialog>
|
||||
);
|
||||
};
|
||||
|
||||
SettingsModalBase.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
title: PropTypes.string.isRequired,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
variant: PropTypes.oneOf(['default', 'dark']).isRequired,
|
||||
@@ -115,11 +117,11 @@ SettingsModalBase.defaultProps = {
|
||||
};
|
||||
|
||||
const ResetUnitsButton = ({
|
||||
intl,
|
||||
courseId,
|
||||
checked,
|
||||
visible,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const resetStatusRequestStatus = useSelector(getResetStatus);
|
||||
const dispatch = useDispatch();
|
||||
|
||||
@@ -185,7 +187,6 @@ const ResetUnitsButton = ({
|
||||
};
|
||||
|
||||
ResetUnitsButton.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
courseId: PropTypes.string.isRequired,
|
||||
checked: PropTypes.oneOf(['true', 'false']).isRequired,
|
||||
visible: PropTypes.bool,
|
||||
@@ -196,7 +197,6 @@ ResetUnitsButton.defaultProps = {
|
||||
};
|
||||
|
||||
const SettingsModal = ({
|
||||
intl,
|
||||
appId,
|
||||
title,
|
||||
children,
|
||||
@@ -213,6 +213,7 @@ const SettingsModal = ({
|
||||
allUnitsEnabledText,
|
||||
noUnitsEnabledText,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const { courseId } = useContext(PagesAndResourcesContext);
|
||||
const loadingStatus = useSelector(getLoadingStatus);
|
||||
const updateSettingsRequestStatus = useSelector(getSavingStatus);
|
||||
@@ -372,7 +373,6 @@ const SettingsModal = ({
|
||||
>
|
||||
{allUnitsEnabledText}
|
||||
<ResetUnitsButton
|
||||
intl={intl}
|
||||
courseId={courseId}
|
||||
checked={formikProps.values.checked}
|
||||
visible={formikProps.values.checked === 'true'}
|
||||
@@ -385,7 +385,6 @@ const SettingsModal = ({
|
||||
>
|
||||
{noUnitsEnabledText}
|
||||
<ResetUnitsButton
|
||||
intl={intl}
|
||||
courseId={courseId}
|
||||
checked={formikProps.values.checked}
|
||||
visible={formikProps.values.checked === 'false'}
|
||||
@@ -423,7 +422,6 @@ const SettingsModal = ({
|
||||
};
|
||||
|
||||
SettingsModal.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
title: PropTypes.string.isRequired,
|
||||
appId: PropTypes.string.isRequired,
|
||||
children: PropTypes.func,
|
||||
@@ -450,4 +448,4 @@ SettingsModal.defaultProps = {
|
||||
enableReinitialize: false,
|
||||
};
|
||||
|
||||
export default injectIntl(SettingsModal);
|
||||
export default SettingsModal;
|
||||
|
||||
@@ -5,13 +5,13 @@ import { useDispatch, useSelector } from 'react-redux';
|
||||
import {
|
||||
useLocation,
|
||||
} from 'react-router-dom';
|
||||
import { StudioFooter } from '@edx/frontend-component-footer';
|
||||
import { StudioFooterSlot } from '@edx/frontend-component-footer';
|
||||
import Header from './header';
|
||||
import { fetchCourseDetail, fetchWaffleFlags } from './data/thunks';
|
||||
import { useModel } from './generic/model-store';
|
||||
import NotFoundAlert from './generic/NotFoundAlert';
|
||||
import PermissionDeniedAlert from './generic/PermissionDeniedAlert';
|
||||
import { fetchStudioHomeData } from './studio-home/data/thunks';
|
||||
import { fetchOnlyStudioHomeData } from './studio-home/data/thunks';
|
||||
import { getCourseAppsApiStatus } from './pages-and-resources/data/selectors';
|
||||
import { RequestStatus } from './data/constants';
|
||||
import Loading from './generic/Loading';
|
||||
@@ -25,7 +25,7 @@ const CourseAuthoringPage = ({ courseId, children }) => {
|
||||
}, [courseId]);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(fetchStudioHomeData());
|
||||
dispatch(fetchOnlyStudioHomeData());
|
||||
}, []);
|
||||
|
||||
const courseDetail = useModel('courseDetails', courseId);
|
||||
@@ -66,7 +66,7 @@ const CourseAuthoringPage = ({ courseId, children }) => {
|
||||
)
|
||||
)}
|
||||
{children}
|
||||
{!inProgress && !isEditor && <StudioFooter />}
|
||||
{!inProgress && !isEditor && <StudioFooterSlot />}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
} from 'react-router-dom';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { PageWrap } from '@edx/frontend-platform/react';
|
||||
import { Textbooks } from 'CourseAuthoring/textbooks';
|
||||
import { Textbooks } from './textbooks';
|
||||
import CourseAuthoringPage from './CourseAuthoringPage';
|
||||
import { PagesAndResources } from './pages-and-resources';
|
||||
import EditorContainer from './editors/EditorContainer';
|
||||
@@ -17,7 +17,7 @@ import ScheduleAndDetails from './schedule-and-details';
|
||||
import { GradingSettings } from './grading-settings';
|
||||
import CourseTeam from './course-team/CourseTeam';
|
||||
import { CourseUpdates } from './course-updates';
|
||||
import { CourseUnit, IframeProvider } from './course-unit';
|
||||
import { CourseUnit } from './course-unit';
|
||||
import { Certificates } from './certificates';
|
||||
import CourseExportPage from './export-page/CourseExportPage';
|
||||
import CourseOptimizerPage from './optimizer-page/CourseOptimizerPage';
|
||||
@@ -25,7 +25,8 @@ import CourseImportPage from './import-page/CourseImportPage';
|
||||
import { DECODED_ROUTES } from './constants';
|
||||
import CourseChecklist from './course-checklist';
|
||||
import GroupConfigurations from './group-configurations';
|
||||
import CourseLibraries from './course-libraries';
|
||||
import { 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:
|
||||
|
||||
16
src/__mocks__/clipboardSubsection.js
Normal file
16
src/__mocks__/clipboardSubsection.js
Normal file
@@ -0,0 +1,16 @@
|
||||
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',
|
||||
};
|
||||
@@ -1,2 +1,3 @@
|
||||
export { default as clipboardUnit } from './clipboardUnit';
|
||||
export { default as clipboardSubsection } from './clipboardSubsection';
|
||||
export { default as clipboardXBlock } from './clipboardXBlock';
|
||||
|
||||
@@ -2,7 +2,7 @@ import React from 'react';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import { Container } from '@openedx/paragon';
|
||||
import { StudioFooter } from '@edx/frontend-component-footer';
|
||||
import { StudioFooterSlot } from '@edx/frontend-component-footer';
|
||||
|
||||
import Header from '../header';
|
||||
import messages from './messages';
|
||||
@@ -29,7 +29,7 @@ const AccessibilityPage = ({
|
||||
<AccessibilityBody {...{ email, communityAccessibilityLink }} />
|
||||
<AccessibilityForm accessibilityEmail={email} />
|
||||
</Container>
|
||||
<StudioFooter />
|
||||
<StudioFooterSlot />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import { camelCaseObject, getConfig } from '@edx/frontend-platform';
|
||||
/* eslint-disable import/prefer-default-export */
|
||||
import {
|
||||
camelCaseObject,
|
||||
getConfig,
|
||||
} from '@edx/frontend-platform';
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
import { camelCase } from 'lodash';
|
||||
import { convertObjectToSnakeCase } from '../../utils';
|
||||
|
||||
const getApiBaseUrl = () => getConfig().STUDIO_BASE_URL;
|
||||
@@ -14,7 +19,19 @@ const getProctoringErrorsApiUrl = () => `${getApiBaseUrl()}/api/contentstore/v1/
|
||||
export async function getCourseAdvancedSettings(courseId) {
|
||||
const { data } = await getAuthenticatedHttpClient()
|
||||
.get(`${getCourseAdvancedSettingsApiUrl(courseId)}?fetch_all=0`);
|
||||
return camelCaseObject(data);
|
||||
const keepValues = {};
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -26,7 +43,19 @@ export async function getCourseAdvancedSettings(courseId) {
|
||||
export async function updateCourseAdvancedSettings(courseId, settings) {
|
||||
const { data } = await getAuthenticatedHttpClient()
|
||||
.patch(`${getCourseAdvancedSettingsApiUrl(courseId)}`, convertObjectToSnakeCase(settings));
|
||||
return camelCaseObject(data);
|
||||
const keepValues = {};
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -36,5 +65,17 @@ export async function updateCourseAdvancedSettings(courseId, settings) {
|
||||
*/
|
||||
export async function getProctoringExamErrors(courseId) {
|
||||
const { data } = await getAuthenticatedHttpClient().get(`${getProctoringErrorsApiUrl()}${courseId}`);
|
||||
return camelCaseObject(data);
|
||||
const keepValues = {};
|
||||
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;
|
||||
}
|
||||
|
||||
236
src/advanced-settings/data/api.test.js
Normal file
236
src/advanced-settings/data/api.test.js
Normal file
@@ -0,0 +1,236 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -59,6 +59,7 @@ export const COURSE_BLOCK_NAMES = ({
|
||||
sequential: { id: 'sequential', name: 'Subsection' },
|
||||
vertical: { id: 'vertical', name: 'Unit' },
|
||||
libraryContent: { id: 'library_content', name: 'Library content' },
|
||||
splitTest: { id: 'split_test', name: 'Split Test' },
|
||||
component: { id: 'component', name: 'Component' },
|
||||
});
|
||||
|
||||
@@ -91,3 +92,17 @@ export const REGEX_RULES = {
|
||||
export const IFRAME_FEATURE_POLICY = (
|
||||
'microphone *; camera *; midi *; geolocation *; encrypted-media *; clipboard-write *'
|
||||
);
|
||||
|
||||
export const iframeStateKeys = {
|
||||
iframeHeight: 'iframeHeight',
|
||||
hasLoaded: 'hasLoaded',
|
||||
showError: 'showError',
|
||||
windowTopOffset: 'windowTopOffset',
|
||||
};
|
||||
|
||||
export const iframeMessageTypes = {
|
||||
modal: 'plugin.modal',
|
||||
resize: 'plugin.resize',
|
||||
videoFullScreen: 'plugin.videoFullScreen',
|
||||
xblockEvent: 'xblock-event',
|
||||
};
|
||||
|
||||
@@ -699,7 +699,7 @@ describe('<ContentTagsCollapsible />', () => {
|
||||
const xButtonAppliedTag = within(appliedTag).getByRole('button', {
|
||||
name: /delete/i,
|
||||
});
|
||||
xButtonAppliedTag.click();
|
||||
await userEvent.click(xButtonAppliedTag);
|
||||
|
||||
// Check that the applied tag has been removed
|
||||
expect(appliedTag).not.toBeInTheDocument();
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import {
|
||||
act,
|
||||
fireEvent,
|
||||
initializeMocks,
|
||||
render,
|
||||
@@ -22,6 +21,7 @@ const path = '/content/:contentId?/*';
|
||||
const mockOnClose = jest.fn();
|
||||
const mockSetBlockingSheet = jest.fn();
|
||||
const mockNavigate = jest.fn();
|
||||
const mockSidebarAction = jest.fn();
|
||||
mockContentTaxonomyTagsData.applyMock();
|
||||
mockTaxonomyListData.applyMock();
|
||||
mockTaxonomyTagsData.applyMock();
|
||||
@@ -41,6 +41,11 @@ jest.mock('react-router-dom', () => ({
|
||||
useNavigate: () => mockNavigate,
|
||||
}));
|
||||
|
||||
jest.mock('../library-authoring/common/context/SidebarContext', () => ({
|
||||
...jest.requireActual('../library-authoring/common/context/SidebarContext'),
|
||||
useSidebarContext: () => ({ sidebarAction: mockSidebarAction() }),
|
||||
}));
|
||||
|
||||
const renderDrawer = (contentId, drawerParams = {}) => (
|
||||
render(
|
||||
<ContentTagsDrawerSheetContext.Provider value={drawerParams}>
|
||||
@@ -61,19 +66,15 @@ describe('<ContentTagsDrawer />', () => {
|
||||
});
|
||||
|
||||
it('shows spinner before the content data query is complete', async () => {
|
||||
await act(async () => {
|
||||
renderDrawer(stagedTagsId);
|
||||
const spinner = screen.getAllByRole('status')[0];
|
||||
expect(spinner.textContent).toEqual('Loading'); // Uses <Spinner />
|
||||
});
|
||||
renderDrawer(stagedTagsId);
|
||||
const spinner = (await screen.findAllByRole('status'))[0];
|
||||
expect(spinner.textContent).toEqual('Loading'); // Uses <Spinner />
|
||||
});
|
||||
|
||||
it('shows spinner before the taxonomy tags query is complete', async () => {
|
||||
await act(async () => {
|
||||
renderDrawer(stagedTagsId);
|
||||
const spinner = screen.getAllByRole('status')[1];
|
||||
expect(spinner.textContent).toEqual('Loading...'); // Uses <Loading />
|
||||
});
|
||||
renderDrawer(stagedTagsId);
|
||||
const spinner = (await screen.findAllByRole('status'))[1];
|
||||
expect(spinner.textContent).toEqual('Loading...'); // Uses <Loading />
|
||||
});
|
||||
|
||||
it('shows the content display name after the query is complete in drawer variant', async () => {
|
||||
@@ -98,15 +99,12 @@ describe('<ContentTagsDrawer />', () => {
|
||||
});
|
||||
|
||||
it('shows the taxonomies data including tag numbers after the query is complete', async () => {
|
||||
await act(async () => {
|
||||
const { container } = renderDrawer(largeTagsId);
|
||||
await waitFor(() => { expect(screen.getByText('Taxonomy 1')).toBeInTheDocument(); });
|
||||
expect(screen.getByText('Taxonomy 1')).toBeInTheDocument();
|
||||
expect(screen.getByText('Taxonomy 2')).toBeInTheDocument();
|
||||
const tagCountBadges = container.getElementsByClassName('taxonomy-tags-count-chip');
|
||||
expect(tagCountBadges[0].textContent).toBe('3');
|
||||
expect(tagCountBadges[1].textContent).toBe('2');
|
||||
});
|
||||
const { container } = renderDrawer(largeTagsId);
|
||||
await screen.findByText('Taxonomy 1');
|
||||
await screen.findByText('Taxonomy 2');
|
||||
const tagCountBadges = container.getElementsByClassName('taxonomy-tags-count-chip');
|
||||
expect(tagCountBadges[0].textContent).toBe('3');
|
||||
expect(tagCountBadges[1].textContent).toBe('2');
|
||||
});
|
||||
|
||||
it('should be read only on first render on drawer variant', async () => {
|
||||
@@ -192,6 +190,26 @@ describe('<ContentTagsDrawer />', () => {
|
||||
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 () => {
|
||||
renderDrawer(stagedTagsId);
|
||||
expect(await screen.findByText('Taxonomy 1')).toBeInTheDocument();
|
||||
|
||||
@@ -14,6 +14,7 @@ import ContentTagsCollapsible from './ContentTagsCollapsible';
|
||||
import Loading from '../generic/Loading';
|
||||
import { useCreateContentTagsDrawerContext } from './ContentTagsDrawerHelper';
|
||||
import { ContentTagsDrawerContext, ContentTagsDrawerSheetContext } from './common/context';
|
||||
import { SidebarActions, useSidebarContext } from '../library-authoring/common/context/SidebarContext';
|
||||
|
||||
interface TaxonomyListProps {
|
||||
contentId: string;
|
||||
@@ -227,7 +228,6 @@ interface ContentTagsDrawerProps {
|
||||
* It is used both in interfaces of this MFE and in edx-platform interfaces such as iframe.
|
||||
* - If you want to use it as an iframe, the component obtains the `contentId` from the url parameters.
|
||||
* Functions to close the drawer are handled internally.
|
||||
* TODO: We can delete this method when is no longer used on edx-platform.
|
||||
* - If you want to use it as react component, you need to pass the content id and the close functions
|
||||
* through the component parameters.
|
||||
*/
|
||||
@@ -245,8 +245,9 @@ const ContentTagsDrawer = ({
|
||||
if (contentId === undefined) {
|
||||
throw new Error('Error: contentId cannot be null.');
|
||||
}
|
||||
const { sidebarAction } = useSidebarContext();
|
||||
|
||||
const context = useCreateContentTagsDrawerContext(contentId, !readOnly);
|
||||
const context = useCreateContentTagsDrawerContext(contentId, !readOnly, variant === 'drawer');
|
||||
const { blockingSheet } = useContext(ContentTagsDrawerSheetContext);
|
||||
|
||||
const {
|
||||
@@ -261,6 +262,7 @@ const ContentTagsDrawer = ({
|
||||
closeToast,
|
||||
setCollapsibleToInitalState,
|
||||
otherTaxonomies,
|
||||
toEditMode,
|
||||
} = context;
|
||||
|
||||
let onCloseDrawer: () => void;
|
||||
@@ -303,8 +305,13 @@ const ContentTagsDrawer = ({
|
||||
|
||||
// First call of the initial collapsible states
|
||||
React.useEffect(() => {
|
||||
setCollapsibleToInitalState();
|
||||
}, [isTaxonomyListLoaded, isContentTaxonomyTagsLoaded]);
|
||||
// Open tag edit mode when sidebarAction is JumpToManageTags
|
||||
if (sidebarAction === SidebarActions.JumpToManageTags) {
|
||||
toEditMode();
|
||||
} else {
|
||||
setCollapsibleToInitalState();
|
||||
}
|
||||
}, [isTaxonomyListLoaded, isContentTaxonomyTagsLoaded, sidebarAction, toEditMode]);
|
||||
|
||||
const renderFooter = () => {
|
||||
if (isTaxonomyListLoaded && isContentTaxonomyTagsLoaded) {
|
||||
|
||||
@@ -20,9 +20,11 @@ import { ContentTagsDrawerSheetContext } from './common/context';
|
||||
* To *use* the context, just use `useContext(ContentTagsDrawerContext)`
|
||||
* @param {string} contentId
|
||||
* @param {boolean} canTagObject
|
||||
* @param {boolean} fetchMetadata=false If true, fetches metadata for the contentId. This is used on `edx-platform`
|
||||
* and the Course/Unit Outline to show the content name as the drawer title.
|
||||
* @returns {ContentTagsDrawerContextData}
|
||||
*/
|
||||
export const useCreateContentTagsDrawerContext = (contentId, canTagObject) => {
|
||||
export const useCreateContentTagsDrawerContext = (contentId, canTagObject, fetchMetadata = false) => {
|
||||
const intl = useIntl();
|
||||
const org = extractOrgFromContentId(contentId);
|
||||
|
||||
@@ -48,7 +50,7 @@ export const useCreateContentTagsDrawerContext = (contentId, canTagObject) => {
|
||||
const updateTags = useContentTaxonomyTagsUpdater(contentId);
|
||||
|
||||
// Fetch from database
|
||||
const { data: contentData, isSuccess: isContentDataLoaded } = useContentData(contentId);
|
||||
const { data: contentData, isSuccess: isContentDataLoaded } = useContentData(contentId, fetchMetadata);
|
||||
const {
|
||||
data: contentTaxonomyTagsData,
|
||||
isSuccess: isContentTaxonomyTagsLoaded,
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import React from 'react';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
act,
|
||||
render,
|
||||
waitFor,
|
||||
fireEvent,
|
||||
@@ -74,11 +73,9 @@ describe('<ContentTagsDropDownSelector />', () => {
|
||||
}
|
||||
|
||||
it('should render taxonomy tags drop down selector loading with spinner', async () => {
|
||||
await act(async () => {
|
||||
const { getByRole } = await getComponent();
|
||||
const spinner = getByRole('status');
|
||||
expect(spinner.textContent).toEqual('Loading tags'); // Uses <Spinner />
|
||||
});
|
||||
const { getByRole } = await getComponent();
|
||||
const spinner = getByRole('status');
|
||||
expect(spinner.textContent).toEqual('Loading tags'); // Uses <Spinner />
|
||||
});
|
||||
|
||||
it('should render taxonomy tags drop down selector with no sub tags', async () => {
|
||||
@@ -99,13 +96,11 @@ describe('<ContentTagsDropDownSelector />', () => {
|
||||
},
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
const { container, getByText } = await getComponent();
|
||||
const { container, getByText } = await getComponent();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(getByText('Tag 1')).toBeInTheDocument();
|
||||
expect(container.getElementsByClassName('taxonomy-tags-arrow-drop-down').length).toBe(0);
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(getByText('Tag 1')).toBeInTheDocument();
|
||||
expect(container.getElementsByClassName('taxonomy-tags-arrow-drop-down').length).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -127,13 +122,11 @@ describe('<ContentTagsDropDownSelector />', () => {
|
||||
},
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
const { container, getByText } = await getComponent();
|
||||
const { container, getByText } = await getComponent();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(getByText('Tag 2')).toBeInTheDocument();
|
||||
expect(container.getElementsByClassName('taxonomy-tags-arrow-drop-down').length).toBe(1);
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(getByText('Tag 2')).toBeInTheDocument();
|
||||
expect(container.getElementsByClassName('taxonomy-tags-arrow-drop-down').length).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -155,47 +148,45 @@ describe('<ContentTagsDropDownSelector />', () => {
|
||||
},
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
const dataWithTagsTree = {
|
||||
...data,
|
||||
tagsTree: {
|
||||
'Tag 3': {
|
||||
explicit: false,
|
||||
children: {},
|
||||
},
|
||||
const dataWithTagsTree = {
|
||||
...data,
|
||||
tagsTree: {
|
||||
'Tag 3': {
|
||||
explicit: false,
|
||||
children: {},
|
||||
},
|
||||
};
|
||||
const { container, getByText } = await getComponent(dataWithTagsTree);
|
||||
await waitFor(() => {
|
||||
expect(getByText('Tag 2')).toBeInTheDocument();
|
||||
expect(container.getElementsByClassName('taxonomy-tags-arrow-drop-down').length).toBe(1);
|
||||
});
|
||||
},
|
||||
};
|
||||
const { container, getByText } = await getComponent(dataWithTagsTree);
|
||||
await waitFor(() => {
|
||||
expect(getByText('Tag 2')).toBeInTheDocument();
|
||||
expect(container.getElementsByClassName('taxonomy-tags-arrow-drop-down').length).toBe(1);
|
||||
});
|
||||
|
||||
// Mock useTaxonomyTagsData again since it gets called in the recursive call
|
||||
useTaxonomyTagsData.mockReturnValueOnce({
|
||||
hasMorePages: false,
|
||||
tagPages: {
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
data: [{
|
||||
value: 'Tag 3',
|
||||
externalId: null,
|
||||
childCount: 0,
|
||||
depth: 1,
|
||||
parentValue: 'Tag 2',
|
||||
id: 12346,
|
||||
subTagsUrl: null,
|
||||
}],
|
||||
},
|
||||
});
|
||||
// Mock useTaxonomyTagsData again since it gets called in the recursive call
|
||||
useTaxonomyTagsData.mockReturnValueOnce({
|
||||
hasMorePages: false,
|
||||
tagPages: {
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
data: [{
|
||||
value: 'Tag 3',
|
||||
externalId: null,
|
||||
childCount: 0,
|
||||
depth: 1,
|
||||
parentValue: 'Tag 2',
|
||||
id: 12346,
|
||||
subTagsUrl: null,
|
||||
}],
|
||||
},
|
||||
});
|
||||
|
||||
// Expand the dropdown to see the subtags selectors
|
||||
const expandToggle = container.querySelector('.taxonomy-tags-arrow-drop-down span');
|
||||
fireEvent.click(expandToggle);
|
||||
// Expand the dropdown to see the subtags selectors
|
||||
const expandToggle = container.querySelector('.taxonomy-tags-arrow-drop-down span');
|
||||
fireEvent.click(expandToggle);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(getByText('Tag 3')).toBeInTheDocument();
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(getByText('Tag 3')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -219,48 +210,46 @@ describe('<ContentTagsDropDownSelector />', () => {
|
||||
});
|
||||
|
||||
const initalSearchTerm = 'test 1';
|
||||
await act(async () => {
|
||||
const { rerender } = await getComponent({ ...data, searchTerm: initalSearchTerm });
|
||||
const { rerender } = await getComponent({ ...data, searchTerm: initalSearchTerm });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(useTaxonomyTagsData).toBeCalledWith(data.taxonomyId, null, 1, initalSearchTerm);
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(useTaxonomyTagsData).toHaveBeenCalledWith(data.taxonomyId, null, 1, initalSearchTerm);
|
||||
});
|
||||
|
||||
const updatedSearchTerm = 'test 2';
|
||||
rerender(<ContentTagsDropDownSelectorComponent
|
||||
key={`selector-${data.taxonomyId}`}
|
||||
taxonomyId={data.taxonomyId}
|
||||
level={data.level}
|
||||
tagsTree={data.tagsTree}
|
||||
searchTerm={updatedSearchTerm}
|
||||
appliedContentTagsTree={{}}
|
||||
stagedContentTagsTree={{}}
|
||||
/>);
|
||||
const updatedSearchTerm = 'test 2';
|
||||
rerender(<ContentTagsDropDownSelectorComponent
|
||||
key={`selector-${data.taxonomyId}`}
|
||||
taxonomyId={data.taxonomyId}
|
||||
level={data.level}
|
||||
tagsTree={data.tagsTree}
|
||||
searchTerm={updatedSearchTerm}
|
||||
appliedContentTagsTree={{}}
|
||||
stagedContentTagsTree={{}}
|
||||
/>);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(useTaxonomyTagsData).toBeCalledWith(data.taxonomyId, null, 1, updatedSearchTerm);
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(useTaxonomyTagsData).toHaveBeenCalledWith(data.taxonomyId, null, 1, updatedSearchTerm);
|
||||
});
|
||||
|
||||
// Clean search term
|
||||
const cleanSearchTerm = '';
|
||||
rerender(<ContentTagsDropDownSelectorComponent
|
||||
key={`selector-${data.taxonomyId}`}
|
||||
taxonomyId={data.taxonomyId}
|
||||
level={data.level}
|
||||
tagsTree={data.tagsTree}
|
||||
searchTerm={cleanSearchTerm}
|
||||
appliedContentTagsTree={{}}
|
||||
stagedContentTagsTree={{}}
|
||||
/>);
|
||||
// Clean search term
|
||||
const cleanSearchTerm = '';
|
||||
rerender(<ContentTagsDropDownSelectorComponent
|
||||
key={`selector-${data.taxonomyId}`}
|
||||
taxonomyId={data.taxonomyId}
|
||||
level={data.level}
|
||||
tagsTree={data.tagsTree}
|
||||
searchTerm={cleanSearchTerm}
|
||||
appliedContentTagsTree={{}}
|
||||
stagedContentTagsTree={{}}
|
||||
/>);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(useTaxonomyTagsData).toBeCalledWith(data.taxonomyId, null, 1, cleanSearchTerm);
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(useTaxonomyTagsData).toHaveBeenCalledWith(data.taxonomyId, null, 1, cleanSearchTerm);
|
||||
});
|
||||
});
|
||||
|
||||
it('should render "noTag" message if search doesnt return taxonomies', async () => {
|
||||
useTaxonomyTagsData.mockReturnValueOnce({
|
||||
useTaxonomyTagsData.mockReturnValue({
|
||||
hasMorePages: false,
|
||||
tagPages: {
|
||||
isLoading: false,
|
||||
@@ -271,20 +260,18 @@ describe('<ContentTagsDropDownSelector />', () => {
|
||||
});
|
||||
|
||||
const searchTerm = 'uncommon search term';
|
||||
await act(async () => {
|
||||
const { getByText } = await getComponent({ ...data, searchTerm });
|
||||
const { getByText } = await getComponent({ ...data, searchTerm });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(useTaxonomyTagsData).toBeCalledWith(data.taxonomyId, null, 1, searchTerm);
|
||||
});
|
||||
|
||||
const message = `No tags found with the search term "${searchTerm}"`;
|
||||
expect(getByText(message)).toBeInTheDocument();
|
||||
await waitFor(() => {
|
||||
expect(useTaxonomyTagsData).toHaveBeenCalledWith(data.taxonomyId, null, 1, searchTerm);
|
||||
});
|
||||
|
||||
const message = `No tags found with the search term "${searchTerm}"`;
|
||||
expect(getByText(message)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render "noTagsInTaxonomy" message if taxonomy is empty', async () => {
|
||||
useTaxonomyTagsData.mockReturnValueOnce({
|
||||
useTaxonomyTagsData.mockReturnValue({
|
||||
hasMorePages: false,
|
||||
tagPages: {
|
||||
isLoading: false,
|
||||
@@ -295,15 +282,13 @@ describe('<ContentTagsDropDownSelector />', () => {
|
||||
});
|
||||
|
||||
const searchTerm = '';
|
||||
await act(async () => {
|
||||
const { getByText } = await getComponent({ ...data, searchTerm });
|
||||
const { getByText } = await getComponent({ ...data, searchTerm });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(useTaxonomyTagsData).toBeCalledWith(data.taxonomyId, null, 1, searchTerm);
|
||||
});
|
||||
|
||||
const message = 'No tags in this taxonomy yet';
|
||||
expect(getByText(message)).toBeInTheDocument();
|
||||
await waitFor(() => {
|
||||
expect(useTaxonomyTagsData).toHaveBeenCalledWith(data.taxonomyId, null, 1, searchTerm);
|
||||
});
|
||||
|
||||
const message = 'No tags in this taxonomy yet';
|
||||
expect(getByText(message)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -70,17 +70,12 @@ export async function getContentTaxonomyTagsCount(contentId) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch meta data (eg: display_name) about the content object (unit/compoenent)
|
||||
* Fetch meta data (eg: display_name) about the content object (unit/component)
|
||||
* @param {string} contentId The id of the content object (unit/component)
|
||||
* @returns {Promise<import("./types.js").ContentData | null>}
|
||||
* @returns {Promise<import("./types.js").ContentData>}
|
||||
*/
|
||||
export async function getContentData(contentId) {
|
||||
let url;
|
||||
if (contentId.startsWith('lib-collection:')) {
|
||||
// This type of usage_key is not used to obtain collections
|
||||
// is only used in tagging.
|
||||
return null;
|
||||
}
|
||||
|
||||
if (contentId.startsWith('lb:')) {
|
||||
url = getLibraryContentDataApiUrl(contentId);
|
||||
|
||||
@@ -13,6 +13,7 @@ export async function mockContentTaxonomyTagsData(contentId: string): Promise<an
|
||||
case thisMock.languageWithTagsId: return thisMock.languageWithTags;
|
||||
case thisMock.languageWithoutTagsId: return thisMock.languageWithoutTags;
|
||||
case thisMock.largeTagsId: return thisMock.largeTags;
|
||||
case thisMock.containerTagsId: return thisMock.largeTags;
|
||||
case thisMock.emptyTagsId: return thisMock.emptyTags;
|
||||
default: throw new Error(`No mock has been set up for contentId "${contentId}"`);
|
||||
}
|
||||
@@ -204,6 +205,7 @@ mockContentTaxonomyTagsData.emptyTagsId = 'block-v1:EmptyTagsOrg+STC1+2023_1+typ
|
||||
mockContentTaxonomyTagsData.emptyTags = {
|
||||
taxonomies: [],
|
||||
};
|
||||
mockContentTaxonomyTagsData.containerTagsId = 'lct:org:lib:unit:container_tags';
|
||||
mockContentTaxonomyTagsData.applyMock = () => jest.spyOn(api, 'getContentTaxonomyTagsData').mockImplementation(mockContentTaxonomyTagsData);
|
||||
|
||||
/**
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
useMutation,
|
||||
useQueryClient,
|
||||
} from '@tanstack/react-query';
|
||||
import { useParams } from 'react-router';
|
||||
import {
|
||||
getTaxonomyTagsData,
|
||||
getContentTaxonomyTagsData,
|
||||
@@ -14,7 +15,7 @@ import {
|
||||
updateContentTaxonomyTags,
|
||||
getContentTaxonomyTagsCount,
|
||||
} from './api';
|
||||
import { libraryQueryPredicate, xblockQueryKeys } from '../../library-authoring/data/apiHooks';
|
||||
import { libraryAuthoringQueryKeys, libraryQueryPredicate, xblockQueryKeys } from '../../library-authoring/data/apiHooks';
|
||||
import { getLibraryId } from '../../generic/key-utils';
|
||||
|
||||
/** @typedef {import("../../taxonomy/data/types.js").TagListData} TagListData */
|
||||
@@ -112,11 +113,13 @@ export const useContentTaxonomyTagsData = (contentId) => (
|
||||
/**
|
||||
* Builds the query to get meta data about the content object
|
||||
* @param {string} contentId The id of the content object (unit/component)
|
||||
* @param {boolean} enabled Flag to enable/disable the query
|
||||
*/
|
||||
export const useContentData = (contentId) => (
|
||||
export const useContentData = (contentId, enabled) => (
|
||||
useQuery({
|
||||
queryKey: ['contentData', contentId],
|
||||
queryFn: () => getContentData(contentId),
|
||||
queryFn: enabled ? () => getContentData(contentId) : undefined,
|
||||
enabled,
|
||||
})
|
||||
);
|
||||
|
||||
@@ -127,6 +130,7 @@ export const useContentData = (contentId) => (
|
||||
export const useContentTaxonomyTagsUpdater = (contentId) => {
|
||||
const queryClient = useQueryClient();
|
||||
const unitIframe = window.frames['xblock-iframe'];
|
||||
const { unitId } = useParams();
|
||||
|
||||
return useMutation({
|
||||
/**
|
||||
@@ -149,13 +153,17 @@ export const useContentTaxonomyTagsUpdater = (contentId) => {
|
||||
contentPattern = contentId.replace(/\+type@.*$/, '*');
|
||||
}
|
||||
queryClient.invalidateQueries({ queryKey: ['contentTagsCount', contentPattern] });
|
||||
if (contentId.startsWith('lb:') || contentId.startsWith('lib-collection:')) {
|
||||
if (contentId.startsWith('lb:') || contentId.startsWith('lib-collection:') || contentId.startsWith('lct:')) {
|
||||
// Obtain library id from contentId
|
||||
const libraryId = getLibraryId(contentId);
|
||||
// Invalidate component metadata to update tags count
|
||||
queryClient.invalidateQueries(xblockQueryKeys.componentMetadata(contentId));
|
||||
// Invalidate content search to update tags count
|
||||
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 */ () => {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { useQuery, useMutation, useQueries } from '@tanstack/react-query';
|
||||
import { act } from '@testing-library/react';
|
||||
import { renderHook } from '@testing-library/react-hooks';
|
||||
import { act, renderHook } from '@testing-library/react';
|
||||
import {
|
||||
useTaxonomyTagsData,
|
||||
useContentTaxonomyTagsData,
|
||||
@@ -158,7 +157,7 @@ describe('useContentTaxonomyTagsUpdater', () => {
|
||||
|
||||
const contentId = 'testerContent';
|
||||
const taxonomyId = 123;
|
||||
const mutation = useContentTaxonomyTagsUpdater(contentId);
|
||||
const mutation = renderHook(() => useContentTaxonomyTagsUpdater(contentId)).result.current;
|
||||
const tagsData = [{
|
||||
taxonomy: taxonomyId,
|
||||
tags: ['tag1', 'tag2'],
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
export { default as ContentTagsDrawer } from './ContentTagsDrawer';
|
||||
export { default as ContentTagsDrawerSheet } from './ContentTagsDrawerSheet';
|
||||
export { useContentTaxonomyTagsData } from './data/apiHooks';
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
// @ts-check
|
||||
import React, { useState, useMemo } from 'react';
|
||||
import { useState, useMemo } from 'react';
|
||||
import {
|
||||
Card, Stack, Button, Collapsible, Icon,
|
||||
} from '@openedx/paragon';
|
||||
@@ -10,10 +9,19 @@ import { ContentTagsDrawerSheet } from '..';
|
||||
|
||||
import messages from '../messages';
|
||||
import { useContentTaxonomyTagsData } from '../data/apiHooks';
|
||||
import type { ContentTaxonomyTagData, Tag } from '../data/types';
|
||||
import { LoadingSpinner } from '../../generic/Loading';
|
||||
import TagsTree from '../TagsTree';
|
||||
|
||||
const TagsSidebarBody = () => {
|
||||
interface TagsSidebarBodyProps {
|
||||
readOnly: boolean
|
||||
}
|
||||
|
||||
type TagTree = {
|
||||
[key: string]: { children: TagTree, canChangeObjecttag: boolean, canDeleteObjecttag: boolean }
|
||||
};
|
||||
|
||||
const TagsSidebarBody = ({ readOnly }: TagsSidebarBodyProps) => {
|
||||
const intl = useIntl();
|
||||
const [showManageTags, setShowManageTags] = useState(false);
|
||||
const contentId = useParams().blockId;
|
||||
@@ -24,8 +32,8 @@ const TagsSidebarBody = () => {
|
||||
isSuccess: isContentTaxonomyTagsLoaded,
|
||||
} = useContentTaxonomyTagsData(contentId || '');
|
||||
|
||||
const buildTagsTree = (contentTags) => {
|
||||
const resultTree = {};
|
||||
const buildTagsTree = (contentTags: Tag[]) => {
|
||||
const resultTree: TagTree = {};
|
||||
contentTags.forEach(item => {
|
||||
let currentLevel = resultTree;
|
||||
|
||||
@@ -46,7 +54,7 @@ const TagsSidebarBody = () => {
|
||||
};
|
||||
|
||||
const tree = useMemo(() => {
|
||||
const result = [];
|
||||
const result: (Omit<ContentTaxonomyTagData, 'tags'> & { tags: TagTree })[] = [];
|
||||
if (isContentTaxonomyTagsLoaded && contentTaxonomyTagsData) {
|
||||
contentTaxonomyTagsData.taxonomies.forEach((taxonomy) => {
|
||||
result.push({
|
||||
@@ -88,7 +96,13 @@ const TagsSidebarBody = () => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Button className="mt-3 ml-2" variant="outline-primary" size="sm" onClick={() => setShowManageTags(true)}>
|
||||
<Button
|
||||
className="mt-3 ml-2"
|
||||
variant="outline-primary"
|
||||
size="sm"
|
||||
onClick={() => setShowManageTags(true)}
|
||||
disabled={readOnly}
|
||||
>
|
||||
{intl.formatMessage(messages.manageTagsButton)}
|
||||
</Button>
|
||||
</Stack>
|
||||
@@ -102,6 +116,4 @@ const TagsSidebarBody = () => {
|
||||
);
|
||||
};
|
||||
|
||||
TagsSidebarBody.propTypes = {};
|
||||
|
||||
export default TagsSidebarBody;
|
||||
@@ -1,10 +1,14 @@
|
||||
import TagsSidebarHeader from './TagsSidebarHeader';
|
||||
import TagsSidebarBody from './TagsSidebarBody';
|
||||
|
||||
const TagsSidebarControls = () => (
|
||||
interface TagsSidebarControlsProps {
|
||||
readOnly: boolean,
|
||||
}
|
||||
|
||||
const TagsSidebarControls = ({ readOnly }: TagsSidebarControlsProps) => (
|
||||
<>
|
||||
<TagsSidebarHeader />
|
||||
<TagsSidebarBody />
|
||||
<TagsSidebarBody readOnly={readOnly} />
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,22 +1,35 @@
|
||||
import fetchMock from 'fetch-mock-jest';
|
||||
import { cloneDeep } from 'lodash';
|
||||
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import MockAdapter from 'axios-mock-adapter/types';
|
||||
import { QueryClient } from '@tanstack/react-query';
|
||||
import {
|
||||
initializeMocks,
|
||||
render,
|
||||
screen,
|
||||
waitFor,
|
||||
within,
|
||||
} from '../testUtils';
|
||||
import { mockContentSearchConfig } from '../search-manager/data/api.mock';
|
||||
import mockInfoResult from './__mocks__/courseBlocksInfo.json';
|
||||
import CourseLibraries from './CourseLibraries';
|
||||
import { mockGetEntityLinksByDownstreamContext } from './data/api.mocks';
|
||||
import { CourseLibraries } from './CourseLibraries';
|
||||
import {
|
||||
mockGetEntityLinks,
|
||||
mockGetEntityLinksSummaryByDownstreamContext,
|
||||
mockFetchIndexDocuments,
|
||||
mockUseLibBlockMetadata,
|
||||
} from './data/api.mocks';
|
||||
import { libraryBlockChangesUrl } from '../course-unit/data/api';
|
||||
import { type ToastActionData } from '../generic/toast-context';
|
||||
|
||||
mockContentSearchConfig.applyMock();
|
||||
mockGetEntityLinksByDownstreamContext.applyMock();
|
||||
mockGetEntityLinks.applyMock();
|
||||
mockGetEntityLinksSummaryByDownstreamContext.applyMock();
|
||||
mockUseLibBlockMetadata.applyMock();
|
||||
|
||||
const searchEndpoint = 'http://mock.meilisearch.local/indexes/studio/search';
|
||||
const searchParamsGetMock = jest.fn();
|
||||
let axiosMock: MockAdapter;
|
||||
let mockShowToast: (message: string, action?: ToastActionData | undefined) => void;
|
||||
let queryClient: QueryClient;
|
||||
|
||||
jest.mock('../studio-home/hooks', () => ({
|
||||
useStudioHome: () => ({
|
||||
@@ -26,123 +39,246 @@ jest.mock('../studio-home/hooks', () => ({
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'), // use actual for all non-hook parts
|
||||
useSearchParams: () => [{
|
||||
get: searchParamsGetMock,
|
||||
getAll: () => [],
|
||||
}],
|
||||
}));
|
||||
|
||||
describe('<CourseLibraries />', () => {
|
||||
beforeEach(() => {
|
||||
initializeMocks();
|
||||
fetchMock.mockReset();
|
||||
|
||||
// The Meilisearch client-side API uses fetch, not Axios.
|
||||
fetchMock.post(searchEndpoint, (_url, req) => {
|
||||
const requestData = JSON.parse(req.body?.toString() ?? '');
|
||||
const filter = requestData?.filter[1];
|
||||
const mockInfoResultCopy = cloneDeep(mockInfoResult);
|
||||
const resp = mockInfoResultCopy.filter((o: { filter: string }) => o.filter === filter)[0] || {
|
||||
result: {
|
||||
hits: [],
|
||||
query: '',
|
||||
processingTimeMs: 0,
|
||||
limit: 4,
|
||||
offset: 0,
|
||||
estimatedTotalHits: 0,
|
||||
},
|
||||
};
|
||||
const { result } = resp;
|
||||
return result;
|
||||
});
|
||||
mockFetchIndexDocuments.applyMock();
|
||||
localStorage.clear();
|
||||
searchParamsGetMock.mockReturnValue('all');
|
||||
});
|
||||
|
||||
const renderCourseLibrariesPage = async (courseKey?: string) => {
|
||||
const courseId = courseKey || mockGetEntityLinksByDownstreamContext.courseKey;
|
||||
const courseId = courseKey || mockGetEntityLinks.courseKey;
|
||||
render(<CourseLibraries courseId={courseId} />);
|
||||
};
|
||||
|
||||
it('shows the spinner before the query is complete', async () => {
|
||||
// This mock will never return data (it loads forever):
|
||||
await renderCourseLibrariesPage(mockGetEntityLinksByDownstreamContext.courseKeyLoading);
|
||||
await renderCourseLibrariesPage(mockGetEntityLinksSummaryByDownstreamContext.courseKeyLoading);
|
||||
const spinner = await screen.findByRole('status');
|
||||
expect(spinner.textContent).toEqual('Loading...');
|
||||
});
|
||||
|
||||
it('shows empty state wheen no links are present', async () => {
|
||||
await renderCourseLibrariesPage(mockGetEntityLinksByDownstreamContext.courseKeyEmpty);
|
||||
it('shows empty state when no links are present', async () => {
|
||||
await renderCourseLibrariesPage(mockGetEntityLinks.courseKeyEmpty);
|
||||
const emptyMsg = await screen.findByText('This course does not use any content from libraries.');
|
||||
expect(emptyMsg).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows alert when out of sync components are present', async () => {
|
||||
await renderCourseLibrariesPage(mockGetEntityLinksByDownstreamContext.courseKey);
|
||||
const alert = await screen.findByRole('alert');
|
||||
expect(await within(alert).findByText(
|
||||
'1 library components are out of sync. Review updates to accept or ignore changes',
|
||||
)).toBeInTheDocument();
|
||||
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.findAllByRole('alert'))[0];
|
||||
expect(await within(alert).findByText(
|
||||
'5 library components are out of sync. Review updates to accept or ignore changes',
|
||||
)).toBeInTheDocument();
|
||||
expect(allTab).toHaveAttribute('aria-selected', 'true');
|
||||
|
||||
const reviewBtn = await screen.findByRole('button', { name: 'Review' });
|
||||
userEvent.click(reviewBtn);
|
||||
|
||||
expect(allTab).toHaveAttribute('aria-selected', 'false');
|
||||
expect(await screen.findByRole('tab', { name: 'Review Content Updates (1)' })).toHaveAttribute('aria-selected', 'true');
|
||||
expect(await screen.findByRole('tab', { name: 'Review Content Updates 5' })).toHaveAttribute('aria-selected', 'true');
|
||||
expect(alert).not.toBeInTheDocument();
|
||||
|
||||
// go back to all tab
|
||||
userEvent.click(allTab);
|
||||
// alert should not be back
|
||||
expect(alert).not.toBeInTheDocument();
|
||||
expect(allTab).toHaveAttribute('aria-selected', 'true');
|
||||
|
||||
// review updates button
|
||||
const reviewActionBtn = await screen.findByRole('button', { name: 'Review Updates' });
|
||||
userEvent.click(reviewActionBtn);
|
||||
expect(await screen.findByRole('tab', { name: 'Review Content Updates (1)' })).toHaveAttribute('aria-selected', 'true');
|
||||
});
|
||||
|
||||
it('hide alert on dismiss', async () => {
|
||||
await renderCourseLibrariesPage(mockGetEntityLinksByDownstreamContext.courseKey);
|
||||
const alert = await screen.findByRole('alert');
|
||||
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);
|
||||
expect(allTab).toHaveAttribute('aria-selected', 'true');
|
||||
|
||||
const alert = (await screen.findAllByRole('alert'))[0];
|
||||
expect(await within(alert).findByText(
|
||||
'1 library components are out of sync. Review updates to accept or ignore changes',
|
||||
'5 library components are out of sync. Review updates to accept or ignore changes',
|
||||
)).toBeInTheDocument();
|
||||
const dismissBtn = await screen.findByRole('button', { name: 'Dismiss' });
|
||||
userEvent.click(dismissBtn);
|
||||
expect(allTab).toHaveAttribute('aria-selected', 'true');
|
||||
waitFor(() => expect(alert).not.toBeInTheDocument());
|
||||
// review updates button
|
||||
const reviewActionBtn = await screen.findByRole('button', { name: 'Review Updates' });
|
||||
userEvent.click(reviewActionBtn);
|
||||
expect(await screen.findByRole('tab', { name: 'Review Content Updates 5' })).toHaveAttribute('aria-selected', 'true');
|
||||
});
|
||||
|
||||
it('show alert if max lastPublishedDate is greated 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 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.findAllByRole('alert'))[0];
|
||||
expect(await within(alert).findByText(
|
||||
'5 library components are out of sync. Review updates to accept or ignore changes',
|
||||
)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
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 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(alert).not.toBeInTheDocument();
|
||||
});
|
||||
screen.logTestingPlaygroundURL();
|
||||
|
||||
it('shows links split by library', async () => {
|
||||
await renderCourseLibrariesPage(mockGetEntityLinksByDownstreamContext.courseKey);
|
||||
const msg = await screen.findByText('This course contains content from these libraries.');
|
||||
expect(msg).toBeInTheDocument();
|
||||
const allButtons = await screen.findAllByRole('button');
|
||||
// total 3 components used from lib 1
|
||||
const expectedLib1Blocks = 3;
|
||||
// total 4 components used from lib 1
|
||||
const expectedLib2Blocks = 4;
|
||||
// 1 component has updates.
|
||||
const expectedLib2ToUpdate = 1;
|
||||
|
||||
const libraryCards = allButtons.filter((el) => el.classList.contains('collapsible-trigger'));
|
||||
expect(libraryCards.length).toEqual(2);
|
||||
expect(await within(libraryCards[0]).findByText('CS problems 2')).toBeInTheDocument();
|
||||
expect(await within(libraryCards[0]).findByText(`${expectedLib1Blocks} components applied`)).toBeInTheDocument();
|
||||
expect(await within(libraryCards[0]).findByText('All components up to date')).toBeInTheDocument();
|
||||
|
||||
const libParent1 = libraryCards[0].parentElement;
|
||||
expect(libParent1).not.toBeNull();
|
||||
userEvent.click(libraryCards[0]);
|
||||
const xblockCards1 = libParent1!.querySelectorAll('div.card');
|
||||
expect(xblockCards1.length).toEqual(expectedLib1Blocks);
|
||||
|
||||
expect(await within(libraryCards[1]).findByText('CS problems 3')).toBeInTheDocument();
|
||||
expect(await within(libraryCards[1]).findByText(`${expectedLib2Blocks} components applied`)).toBeInTheDocument();
|
||||
expect(await within(libraryCards[1]).findByText(`${expectedLib2ToUpdate} component out of sync`)).toBeInTheDocument();
|
||||
|
||||
const libParent2 = libraryCards[1].parentElement;
|
||||
expect(libParent2).not.toBeNull();
|
||||
userEvent.click(libraryCards[1]);
|
||||
const xblockCards2 = libParent2!.querySelectorAll('div.card');
|
||||
expect(xblockCards2.length).toEqual(expectedLib2Blocks);
|
||||
expect(screen.queryAllByRole('alert').length).toEqual(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('<CourseLibraries ReviewTab />', () => {
|
||||
beforeEach(() => {
|
||||
const mocks = initializeMocks();
|
||||
axiosMock = mocks.axiosMock;
|
||||
mockShowToast = mocks.mockShowToast;
|
||||
fetchMock.mockReset();
|
||||
mockFetchIndexDocuments.applyMock();
|
||||
localStorage.clear();
|
||||
searchParamsGetMock.mockReturnValue('review');
|
||||
queryClient = mocks.queryClient;
|
||||
});
|
||||
|
||||
const renderCourseLibrariesReviewPage = async (courseKey?: string) => {
|
||||
const courseId = courseKey || mockGetEntityLinks.courseKey;
|
||||
render(<CourseLibraries courseId={courseId} />);
|
||||
};
|
||||
|
||||
it('shows the spinner before the query is complete', async () => {
|
||||
// This mock will never return data (it loads forever):
|
||||
await renderCourseLibrariesReviewPage(mockGetEntityLinks.courseKeyLoading);
|
||||
const spinner = await screen.findByRole('status');
|
||||
expect(spinner.textContent).toEqual('Loading...');
|
||||
});
|
||||
|
||||
it('shows empty state when no readyToSync links are present', async () => {
|
||||
await renderCourseLibrariesReviewPage(mockGetEntityLinksSummaryByDownstreamContext.courseKeyUpToDate);
|
||||
const emptyMsg = await screen.findByText('All components are up to date');
|
||||
expect(emptyMsg).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows all readyToSync links', async () => {
|
||||
await renderCourseLibrariesReviewPage(mockGetEntityLinksSummaryByDownstreamContext.courseKey);
|
||||
const updateBtns = await screen.findAllByRole('button', { name: 'Update' });
|
||||
expect(updateBtns.length).toEqual(5);
|
||||
const ignoreBtns = await screen.findAllByRole('button', { name: 'Ignore' });
|
||||
expect(ignoreBtns.length).toEqual(5);
|
||||
});
|
||||
|
||||
it('update changes works', async () => {
|
||||
const mockInvalidateQueries = jest.spyOn(queryClient, 'invalidateQueries');
|
||||
const usageKey = mockGetEntityLinks.response[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']);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,241 +1,116 @@
|
||||
import React, {
|
||||
useCallback, useMemo, useState,
|
||||
useCallback, useEffect, useMemo, useState,
|
||||
} from 'react';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
Alert,
|
||||
Breadcrumb, Button, Card, Collapsible, Container, Dropdown, Hyperlink, Icon, IconButton, Layout, Stack, Tab, Tabs,
|
||||
ActionRow,
|
||||
Button,
|
||||
Card,
|
||||
Container,
|
||||
Hyperlink,
|
||||
Icon,
|
||||
Stack,
|
||||
Tab,
|
||||
Tabs,
|
||||
} from '@openedx/paragon';
|
||||
import {
|
||||
Cached, CheckCircle, KeyboardArrowDown, KeyboardArrowRight, Loop, MoreVert,
|
||||
Cached, CheckCircle, Launch, Loop, Info,
|
||||
} from '@openedx/paragon/icons';
|
||||
|
||||
import {
|
||||
countBy, groupBy, keyBy, tail, uniq,
|
||||
} from 'lodash';
|
||||
import classNames from 'classnames';
|
||||
import sumBy from 'lodash/sumBy';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import getPageHeadTitle from '../generic/utils';
|
||||
import { useModel } from '../generic/model-store';
|
||||
import messages from './messages';
|
||||
import SubHeader from '../generic/sub-header/SubHeader';
|
||||
import { useEntityLinksByDownstreamContext } from './data/apiHooks';
|
||||
import type { PublishableEntityLink } from './data/api';
|
||||
import { useFetchIndexDocuments } from '../search-manager/data/apiHooks';
|
||||
import { getItemIcon } from '../generic/block-type-utils';
|
||||
import { BlockTypeLabel } from '../search-manager';
|
||||
import AlertMessage from '../generic/alert-message';
|
||||
import type { ContentHit } from '../search-manager/data/api';
|
||||
import { SearchSortOption } from '../search-manager/data/api';
|
||||
import { useEntityLinksSummaryByDownstreamContext } from './data/apiHooks';
|
||||
import type { PublishableEntityLinkSummary } from './data/api';
|
||||
import Loading from '../generic/Loading';
|
||||
import { useStudioHome } from '../studio-home/hooks';
|
||||
import NewsstandIcon from '../generic/NewsstandIcon';
|
||||
import ReviewTabContent from './ReviewTabContent';
|
||||
import { OutOfSyncAlert } from './OutOfSyncAlert';
|
||||
import AlertMessage from '../generic/alert-message';
|
||||
|
||||
interface Props {
|
||||
courseId: string;
|
||||
}
|
||||
|
||||
interface LibraryCardProps {
|
||||
courseId: string;
|
||||
title: string;
|
||||
links: PublishableEntityLink[];
|
||||
}
|
||||
|
||||
interface ComponentInfo extends ContentHit {
|
||||
readyToSync: boolean;
|
||||
}
|
||||
|
||||
interface BlockCardProps {
|
||||
info: ComponentInfo;
|
||||
linkSummary: PublishableEntityLinkSummary;
|
||||
}
|
||||
|
||||
export enum CourseLibraryTabs {
|
||||
home = '',
|
||||
all = 'all',
|
||||
review = 'review',
|
||||
}
|
||||
|
||||
const BlockCard: React.FC<BlockCardProps> = ({ info }) => {
|
||||
const LibraryCard = ({ linkSummary }: LibraryCardProps) => {
|
||||
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={classNames(
|
||||
'my-3 shadow-none border-light-600 border',
|
||||
{ 'bg-primary-100': info.readyToSync },
|
||||
)}
|
||||
orientation="horizontal"
|
||||
key={info.usageKey}
|
||||
>
|
||||
<Card.Section
|
||||
className="py-2"
|
||||
>
|
||||
<Stack direction="vertical" gap={1}>
|
||||
<Stack direction="horizontal" gap={1} className="micro text-gray-500">
|
||||
<Icon src={componentIcon} size="xs" />
|
||||
<BlockTypeLabel blockType={info.blockType} />
|
||||
<Hyperlink className="lead ml-auto text-black" destination={getBlockLink()} target="_blank">
|
||||
{' '}
|
||||
</Hyperlink>
|
||||
<Card className="my-3 border-light-500 border shadow-none">
|
||||
<Card.Header
|
||||
title={(
|
||||
<Stack direction="horizontal" gap={2}>
|
||||
<Icon src={NewsstandIcon} />
|
||||
{linkSummary.upstreamContextTitle}
|
||||
</Stack>
|
||||
<Stack direction="horizontal" className="small" gap={1}>
|
||||
{info.readyToSync && <Icon src={Loop} size="xs" />}
|
||||
{info.formatted?.displayName}
|
||||
</Stack>
|
||||
<div className="micro">{info.formatted?.description}</div>
|
||||
<Breadcrumb
|
||||
className="micro text-gray-500"
|
||||
ariaLabel={intl.formatMessage(messages.breadcrumbAriaLabel)}
|
||||
links={breadcrumbs.map((breadcrumb) => ({ label: breadcrumb.displayName }))}
|
||||
spacer={<span className="custom-spacer">/</span>}
|
||||
linkAs="span"
|
||||
/>
|
||||
)}
|
||||
actions={(
|
||||
<ActionRow>
|
||||
<Button
|
||||
destination={`${getConfig().PUBLIC_PATH}library/${linkSummary.upstreamContextKey}`}
|
||||
target="_blank"
|
||||
className="border border-light-300"
|
||||
variant="tertiary"
|
||||
as={Hyperlink}
|
||||
size="sm"
|
||||
showLaunchIcon={false}
|
||||
iconAfter={Launch}
|
||||
>
|
||||
View Library
|
||||
</Button>
|
||||
</ActionRow>
|
||||
)}
|
||||
size="sm"
|
||||
/>
|
||||
<Card.Section>
|
||||
<Stack
|
||||
direction="horizontal"
|
||||
gap={4}
|
||||
className="x-small"
|
||||
>
|
||||
<span>
|
||||
{intl.formatMessage(messages.totalComponentLabel, { totalComponents: linkSummary.totalCount })}
|
||||
</span>
|
||||
{linkSummary.readyToSyncCount > 0 && (
|
||||
<Stack direction="horizontal" gap={1}>
|
||||
<Icon src={Loop} size="xs" />
|
||||
<span>
|
||||
{intl.formatMessage(messages.outOfSyncCountLabel, { outOfSyncCount: linkSummary.readyToSyncCount })}
|
||||
</span>
|
||||
</Stack>
|
||||
)}
|
||||
</Stack>
|
||||
</Card.Section>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
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 }) => {
|
||||
export const CourseLibraries: React.FC<Props> = ({ courseId }) => {
|
||||
const intl = useIntl();
|
||||
const courseDetails = useModel('courseDetails', courseId);
|
||||
const [tabKey, setTabKey] = useState<CourseLibraryTabs>(CourseLibraryTabs.home);
|
||||
const [showReviewAlert, setShowReviewAlert] = useState(true);
|
||||
const { data: links, isLoading } = useEntityLinksByDownstreamContext(courseId);
|
||||
const linksByLib = useMemo(() => groupBy(links, 'upstreamContextKey'), [links]);
|
||||
const outOfSyncCount = useMemo(() => countBy(links, 'readyToSync').true, [links]);
|
||||
const [searchParams] = useSearchParams();
|
||||
const [tabKey, setTabKey] = useState<CourseLibraryTabs>(
|
||||
() => searchParams.get('tab') as CourseLibraryTabs,
|
||||
);
|
||||
const [showReviewAlert, setShowReviewAlert] = useState(false);
|
||||
const { data: libraries, isLoading } = useEntityLinksSummaryByDownstreamContext(courseId);
|
||||
const outOfSyncCount = useMemo(() => sumBy(libraries, (lib) => lib.readyToSyncCount), [libraries]);
|
||||
const {
|
||||
isLoadingPage: isLoadingStudioHome,
|
||||
isFailedLoadingPage: isFailedLoadingStudioHome,
|
||||
@@ -244,33 +119,64 @@ const CourseLibraries: React.FC<Props> = ({ courseId }) => {
|
||||
|
||||
const onAlertReview = () => {
|
||||
setTabKey(CourseLibraryTabs.review);
|
||||
setShowReviewAlert(false);
|
||||
};
|
||||
const onAlertDismiss = () => {
|
||||
setShowReviewAlert(false);
|
||||
};
|
||||
|
||||
const tabChange = useCallback((selectedTab: CourseLibraryTabs) => {
|
||||
setTabKey(selectedTab);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
setTabKey((prev) => {
|
||||
if (outOfSyncCount > 0) {
|
||||
return CourseLibraryTabs.review;
|
||||
}
|
||||
if (prev) {
|
||||
return prev;
|
||||
}
|
||||
/* istanbul ignore next */
|
||||
return CourseLibraryTabs.all;
|
||||
});
|
||||
}, [outOfSyncCount]);
|
||||
|
||||
const renderLibrariesTabContent = useCallback(() => {
|
||||
if (isLoading) {
|
||||
return <Loading />;
|
||||
}
|
||||
if (links?.length === 0) {
|
||||
if (libraries?.length === 0) {
|
||||
return <small><FormattedMessage {...messages.homeTabDescriptionEmpty} /></small>;
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<small><FormattedMessage {...messages.homeTabDescription} /></small>
|
||||
{Object.entries(linksByLib).map(([libKey, libLinks]) => (
|
||||
{libraries?.map((library) => (
|
||||
<LibraryCard
|
||||
courseId={courseId}
|
||||
title={libLinks[0].upstreamContextTitle}
|
||||
links={libLinks}
|
||||
key={libKey}
|
||||
linkSummary={library}
|
||||
key={library.upstreamContextKey}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}, [links, isLoading, linksByLib]);
|
||||
}, [libraries, isLoading]);
|
||||
|
||||
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)) {
|
||||
return (
|
||||
@@ -288,16 +194,22 @@ const CourseLibraries: React.FC<Props> = ({ courseId }) => {
|
||||
</title>
|
||||
</Helmet>
|
||||
<Container size="xl" className="px-4 pt-4 mt-3">
|
||||
<ReviewAlert
|
||||
show={outOfSyncCount > 0 && tabKey === CourseLibraryTabs.home && showReviewAlert}
|
||||
outOfSyncCount={outOfSyncCount}
|
||||
onDismiss={onAlertDismiss}
|
||||
<OutOfSyncAlert
|
||||
courseId={courseId}
|
||||
onReview={onAlertReview}
|
||||
showAlert={showReviewAlert && tabKey === CourseLibraryTabs.all}
|
||||
setShowAlert={setShowReviewAlert}
|
||||
/>
|
||||
{ /* TODO: Remove this alert after implement container in this page */}
|
||||
<AlertMessage
|
||||
title={intl.formatMessage(messages.unitsUpdatesWarning)}
|
||||
icon={Info}
|
||||
variant="info"
|
||||
/>
|
||||
<SubHeader
|
||||
title={intl.formatMessage(messages.headingTitle)}
|
||||
subtitle={intl.formatMessage(messages.headingSubtitle)}
|
||||
headerActions={!showReviewAlert && tabKey === CourseLibraryTabs.home && (
|
||||
headerActions={!showReviewAlert && outOfSyncCount > 0 && tabKey === CourseLibraryTabs.all && (
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={onAlertReview}
|
||||
@@ -312,25 +224,27 @@ const CourseLibraries: React.FC<Props> = ({ courseId }) => {
|
||||
<Tabs
|
||||
id="course-library-tabs"
|
||||
activeKey={tabKey}
|
||||
onSelect={(k: CourseLibraryTabs) => setTabKey(k)}
|
||||
onSelect={tabChange}
|
||||
>
|
||||
<Tab
|
||||
eventKey={CourseLibraryTabs.home}
|
||||
eventKey={CourseLibraryTabs.all}
|
||||
title={intl.formatMessage(messages.homeTabTitle)}
|
||||
className="px-2 mt-3"
|
||||
>
|
||||
<TabContent>
|
||||
{renderLibrariesTabContent()}
|
||||
</TabContent>
|
||||
{renderLibrariesTabContent()}
|
||||
</Tab>
|
||||
<Tab
|
||||
eventKey={CourseLibraryTabs.review}
|
||||
title={intl.formatMessage(
|
||||
outOfSyncCount > 0 ? messages.reviewTabTitle : messages.reviewTabTitleEmpty,
|
||||
{ count: outOfSyncCount },
|
||||
title={(
|
||||
<Stack direction="horizontal" gap={1}>
|
||||
<Icon src={Loop} />
|
||||
{intl.formatMessage(messages.reviewTabTitle)}
|
||||
</Stack>
|
||||
)}
|
||||
notification={outOfSyncCount}
|
||||
className="px-2 mt-3"
|
||||
>
|
||||
<TabContent>Help</TabContent>
|
||||
{renderReviewTabContent()}
|
||||
</Tab>
|
||||
</Tabs>
|
||||
</section>
|
||||
@@ -338,5 +252,3 @@ const CourseLibraries: React.FC<Props> = ({ courseId }) => {
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default CourseLibraries;
|
||||
|
||||
78
src/course-libraries/OutOfSyncAlert.tsx
Normal file
78
src/course-libraries/OutOfSyncAlert.tsx
Normal file
@@ -0,0 +1,78 @@
|
||||
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>,
|
||||
]}
|
||||
/>
|
||||
);
|
||||
};
|
||||
330
src/course-libraries/ReviewTabContent.tsx
Normal file
330
src/course-libraries/ReviewTabContent.tsx
Normal file
@@ -0,0 +1,330 @@
|
||||
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;
|
||||
23
src/course-libraries/__mocks__/libBlockMetadata.json
Normal file
23
src/course-libraries/__mocks__/libBlockMetadata.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
]
|
||||
}
|
||||
22
src/course-libraries/__mocks__/linkCourseSummary.json
Normal file
22
src/course-libraries/__mocks__/linkCourseSummary.json
Normal file
@@ -0,0 +1,22 @@
|
||||
[
|
||||
{
|
||||
"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"
|
||||
}
|
||||
]
|
||||
376
src/course-libraries/__mocks__/linkDetailsFromIndex.json
Normal file
376
src/course-libraries/__mocks__/linkDetailsFromIndex.json
Normal file
@@ -0,0 +1,376 @@
|
||||
{
|
||||
"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
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,100 +1,72 @@
|
||||
[
|
||||
{
|
||||
"id": 970,
|
||||
"upstreamContextTitle": "CS problems 2",
|
||||
"upstreamVersion": 15,
|
||||
"readyToSync": false,
|
||||
"upstreamUsageKey": "lb:OpenedX:CSPROB2:html:c0c1ca28-ff25-4757-83bc-3a2c2a0fe9c8",
|
||||
"upstreamContextKey": "lib:OpenedX:CSPROB2",
|
||||
"downstreamUsageKey": "block-v1:NewOrg3+TSTCS+2025_T1+type@html+block@0dfbabb8c93140caa3a004ab621757f5",
|
||||
"downstreamContextKey": "course-v1:NewOrg3+TSTCS+2025_T1",
|
||||
"versionSynced": 15,
|
||||
"versionDeclined": 13,
|
||||
"created": "2025-02-08T14:11:23.650589Z",
|
||||
"updated": "2025-02-08T14:11:23.650589Z"
|
||||
},
|
||||
{
|
||||
"id": 971,
|
||||
"upstreamContextTitle": "CS problems 2",
|
||||
"upstreamVersion": 3,
|
||||
"readyToSync": false,
|
||||
"upstreamUsageKey": "lb:OpenedX:CSPROB2:html:fd2d3827-e633-4217-bca9-c6661086b4b2",
|
||||
"upstreamContextKey": "lib:OpenedX:CSPROB2",
|
||||
"downstreamUsageKey": "block-v1:NewOrg3+TSTCS+2025_T1+type@html+block@37a916b53c2f430989d33ddeb47e224d",
|
||||
"downstreamContextKey": "course-v1:NewOrg3+TSTCS+2025_T1",
|
||||
"versionSynced": 3,
|
||||
"versionDeclined": null,
|
||||
"created": "2025-02-08T14:11:23.650589Z",
|
||||
"updated": "2025-02-08T14:11:23.650589Z"
|
||||
},
|
||||
{
|
||||
"id": 972,
|
||||
"upstreamContextTitle": "CS problems 2",
|
||||
"upstreamVersion": 3,
|
||||
"readyToSync": false,
|
||||
"upstreamUsageKey": "lb:OpenedX:CSPROB2:video:ba2023d4-b4e4-44a5-bfc8-322203e8737f",
|
||||
"upstreamContextKey": "lib:OpenedX:CSPROB2",
|
||||
"downstreamUsageKey": "block-v1:NewOrg3+TSTCS+2025_T1+type@video+block@33518787c3e14844ad823af8ff158a83",
|
||||
"downstreamContextKey": "course-v1:NewOrg3+TSTCS+2025_T1",
|
||||
"versionSynced": 3,
|
||||
"versionDeclined": null,
|
||||
"created": "2025-02-08T14:11:23.650589Z",
|
||||
"updated": "2025-02-08T14:11:23.650589Z"
|
||||
},
|
||||
{
|
||||
"id": 974,
|
||||
"id": 875,
|
||||
"upstreamContextTitle": "CS problems 3",
|
||||
"upstreamVersion": 18,
|
||||
"readyToSync": false,
|
||||
"upstreamUsageKey": "lb:OpenedX:CSPROB3:html:ca4d2b1f-0b64-4a2d-88fa-592f7e398477",
|
||||
"upstreamContextKey": "lib:OpenedX:CSPROB3",
|
||||
"downstreamUsageKey": "block-v1:NewOrg3+TSTCS+2025_T1+type@html+block@5be06d70f09c45a581d973d215e7ad62",
|
||||
"downstreamContextKey": "course-v1:NewOrg3+TSTCS+2025_T1",
|
||||
"versionSynced": 17,
|
||||
"versionDeclined": 18,
|
||||
"created": "2025-02-12T05:38:53.967738Z",
|
||||
"updated": "2025-02-12T05:41:01.225542Z"
|
||||
},
|
||||
{
|
||||
"id": 975,
|
||||
"upstreamContextTitle": "CS problems 3",
|
||||
"upstreamVersion": 1,
|
||||
"readyToSync": false,
|
||||
"upstreamUsageKey": "lb:OpenedX:CSPROB3:html:4abdfa10-dd1a-4ebb-bad3-489000671acb",
|
||||
"upstreamContextKey": "lib:OpenedX:CSPROB3",
|
||||
"downstreamUsageKey": "block-v1:NewOrg3+TSTCS+2025_T1+type@html+block@d1b006742e0745b0948c8ba8dcd9bc07",
|
||||
"downstreamContextKey": "course-v1:NewOrg3+TSTCS+2025_T1",
|
||||
"versionSynced": 1,
|
||||
"versionDeclined": null,
|
||||
"created": "2025-02-12T05:38:55.899821Z",
|
||||
"updated": "2025-02-12T05:38:55.899821Z"
|
||||
},
|
||||
{
|
||||
"id": 976,
|
||||
"upstreamContextTitle": "CS problems 3",
|
||||
"upstreamVersion": 1,
|
||||
"readyToSync": false,
|
||||
"upstreamUsageKey": "lb:OpenedX:CSPROB3:html:6aff1b41-e406-41ff-9d31-70d02ef42deb",
|
||||
"upstreamContextKey": "lib:OpenedX:CSPROB3",
|
||||
"downstreamUsageKey": "block-v1:NewOrg3+TSTCS+2025_T1+type@html+block@276914b632eb49049b2c2568ef24fe4d",
|
||||
"downstreamContextKey": "course-v1:NewOrg3+TSTCS+2025_T1",
|
||||
"versionSynced": 1,
|
||||
"versionDeclined": null,
|
||||
"created": "2025-02-12T05:38:57.228152Z",
|
||||
"updated": "2025-02-12T05:38:57.228152Z"
|
||||
},
|
||||
{
|
||||
"id": 977,
|
||||
"upstreamContextTitle": "CS problems 3",
|
||||
"upstreamVersion": 3,
|
||||
"upstreamVersion": 10,
|
||||
"readyToSync": true,
|
||||
"upstreamUsageKey": "lb:OpenedX:CSPROB3:problem:d40264d5-80c4-4be8-bfb6-086391de1cd3",
|
||||
"upstreamContextKey": "lib:OpenedX:CSPROB3",
|
||||
"downstreamUsageKey": "block-v1:NewOrg3+TSTCS+2025_T1+type@problem+block@fcf430ac5b7a44b6ab316b53a7e67fef",
|
||||
"downstreamContextKey": "course-v1:NewOrg3+TSTCS+2025_T1",
|
||||
"downstreamUsageKey": "block-v1:OpenEdx+DemoX+CourseX+type@problem+block@problem3",
|
||||
"downstreamContextKey": "course-v1:OpenEdx+DemoX+CourseX",
|
||||
"versionSynced": 2,
|
||||
"versionDeclined": null,
|
||||
"created": "2025-02-12T05:38:58.538280Z",
|
||||
"updated": "2025-02-12T05:38:58.538280Z"
|
||||
"created": "2025-02-08T14:07:05.588484Z",
|
||||
"updated": "2025-02-08T14:07:05.588484Z"
|
||||
},
|
||||
{
|
||||
"id": 876,
|
||||
"upstreamContextTitle": "CS problems 3",
|
||||
"upstreamVersion": 10,
|
||||
"readyToSync": true,
|
||||
"upstreamUsageKey": "lb:OpenedX:CSPROB3:problem:d40264d5-80c4-4be8-bfb6-086391de1cd3",
|
||||
"upstreamContextKey": "lib:OpenedX:CSPROB3",
|
||||
"downstreamUsageKey": "block-v1:OpenEdx+DemoX+CourseX+type@problem+block@problem6",
|
||||
"downstreamContextKey": "course-v1:OpenEdx+DemoX+CourseX",
|
||||
"versionSynced": 2,
|
||||
"versionDeclined": null,
|
||||
"created": "2025-02-08T14:07:05.588484Z",
|
||||
"updated": "2025-02-08T14:07:05.588484Z"
|
||||
},
|
||||
{
|
||||
"id": 884,
|
||||
"upstreamContextTitle": "CS problems 3",
|
||||
"upstreamVersion": 26,
|
||||
"readyToSync": true,
|
||||
"upstreamUsageKey": "lb:OpenedX:CSPROB3:html:ca4d2b1f-0b64-4a2d-88fa-592f7e398477",
|
||||
"upstreamContextKey": "lib:OpenedX:CSPROB3",
|
||||
"downstreamUsageKey": "block-v1:OpenEdx+DemoX+CourseX+type@html+block@257e68e3386d4a8f8739d45b67e76a9b",
|
||||
"downstreamContextKey": "course-v1:OpenEdx+DemoX+CourseX",
|
||||
"versionSynced": 16,
|
||||
"versionDeclined": null,
|
||||
"created": "2025-02-08T14:07:05.588484Z",
|
||||
"updated": "2025-02-08T14:07:05.588484Z"
|
||||
},
|
||||
{
|
||||
"id": 889,
|
||||
"upstreamContextTitle": "CS problems 3",
|
||||
"upstreamVersion": 10,
|
||||
"readyToSync": true,
|
||||
"upstreamUsageKey": "lb:OpenedX:CSPROB3:problem:d40264d5-80c4-4be8-bfb6-086391de1cd3",
|
||||
"upstreamContextKey": "lib:OpenedX:CSPROB3",
|
||||
"downstreamUsageKey": "block-v1:OpenEdx+DemoX+CourseX+type@problem+block@a4455860b03647219ff8b01cde49cf37",
|
||||
"downstreamContextKey": "course-v1:OpenEdx+DemoX+CourseX",
|
||||
"versionSynced": 2,
|
||||
"versionDeclined": null,
|
||||
"created": "2025-02-08T14:07:05.588484Z",
|
||||
"updated": "2025-02-08T14:07:05.588484Z"
|
||||
},
|
||||
{
|
||||
"id": 890,
|
||||
"upstreamContextTitle": "CS problems 3",
|
||||
"upstreamVersion": 10,
|
||||
"readyToSync": true,
|
||||
"upstreamUsageKey": "lb:OpenedX:CSPROB3:problem:d40264d5-80c4-4be8-bfb6-086391de1cd3",
|
||||
"upstreamContextKey": "lib:OpenedX:CSPROB3",
|
||||
"downstreamUsageKey": "block-v1:OpenEdx+DemoX+CourseX+type@problem+block@210e356cfa304b0aac591af53f6a6ae0",
|
||||
"downstreamContextKey": "course-v1:OpenEdx+DemoX+CourseX",
|
||||
"versionSynced": 2,
|
||||
"versionDeclined": null,
|
||||
"created": "2025-02-08T14:07:05.588484Z",
|
||||
"updated": "2025-02-08T14:07:05.588484Z"
|
||||
}
|
||||
]
|
||||
]
|
||||
|
||||
@@ -1,36 +1,111 @@
|
||||
/* istanbul ignore file */
|
||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||
import fetchMock from 'fetch-mock-jest';
|
||||
import mockLinksResult from '../__mocks__/publishableEntityLinks.json';
|
||||
import mockSummaryResult from '../__mocks__/linkCourseSummary.json';
|
||||
import mockLinkDetailsFromIndex from '../__mocks__/linkDetailsFromIndex.json';
|
||||
import mockLibBlockMetadata from '../__mocks__/libBlockMetadata.json';
|
||||
import { createAxiosError } from '../../testUtils';
|
||||
import * as api from './api';
|
||||
import * as libApi from '../../library-authoring/data/api';
|
||||
|
||||
/**
|
||||
* Mock for `getEntityLinksByDownstreamContext()`
|
||||
* Mock for `getEntityLinks()`
|
||||
*
|
||||
* This mock returns a fixed response for the downstreamContextKey.
|
||||
*/
|
||||
export async function mockGetEntityLinksByDownstreamContext(
|
||||
downstreamContextKey: string,
|
||||
): Promise<api.PublishableEntityLink[]> {
|
||||
export async function mockGetEntityLinks(
|
||||
downstreamContextKey?: string,
|
||||
readyToSync?: boolean,
|
||||
): ReturnType<typeof api.getEntityLinks> {
|
||||
switch (downstreamContextKey) {
|
||||
case mockGetEntityLinksByDownstreamContext.invalidCourseKey:
|
||||
case mockGetEntityLinks.invalidCourseKey:
|
||||
throw createAxiosError({
|
||||
code: 404,
|
||||
message: 'Not found.',
|
||||
path: api.getEntityLinksByDownstreamContextUrl(downstreamContextKey),
|
||||
path: api.getEntityLinksByDownstreamContextUrl(),
|
||||
});
|
||||
case mockGetEntityLinksByDownstreamContext.courseKeyLoading:
|
||||
case mockGetEntityLinks.courseKeyLoading:
|
||||
return new Promise(() => {});
|
||||
case mockGetEntityLinksByDownstreamContext.courseKeyEmpty:
|
||||
case mockGetEntityLinks.courseKeyEmpty:
|
||||
return Promise.resolve([]);
|
||||
default:
|
||||
return Promise.resolve(mockGetEntityLinksByDownstreamContext.response);
|
||||
default: {
|
||||
let { response } = mockGetEntityLinks;
|
||||
if (readyToSync !== undefined) {
|
||||
response = response.filter((o) => o.readyToSync === readyToSync);
|
||||
}
|
||||
return Promise.resolve(response);
|
||||
}
|
||||
}
|
||||
}
|
||||
mockGetEntityLinksByDownstreamContext.courseKey = mockLinksResult[0].downstreamContextKey;
|
||||
mockGetEntityLinksByDownstreamContext.invalidCourseKey = 'course_key_error';
|
||||
mockGetEntityLinksByDownstreamContext.courseKeyLoading = 'courseKeyLoading';
|
||||
mockGetEntityLinksByDownstreamContext.courseKeyEmpty = 'courseKeyEmpty';
|
||||
mockGetEntityLinksByDownstreamContext.response = mockLinksResult;
|
||||
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. */
|
||||
mockGetEntityLinksByDownstreamContext.applyMock = () => {
|
||||
jest.spyOn(api, 'getEntityLinksByDownstreamContext').mockImplementation(mockGetEntityLinksByDownstreamContext);
|
||||
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:
|
||||
return Promise.resolve(mockGetEntityLinksSummaryByDownstreamContext.response);
|
||||
}
|
||||
}
|
||||
mockGetEntityLinksSummaryByDownstreamContext.courseKey = mockLinksResult[0].downstreamContextKey;
|
||||
mockGetEntityLinksSummaryByDownstreamContext.invalidCourseKey = 'course_key_error';
|
||||
mockGetEntityLinksSummaryByDownstreamContext.courseKeyLoading = 'courseKeySummaryLoading';
|
||||
mockGetEntityLinksSummaryByDownstreamContext.courseKeyEmpty = 'courseKeyEmpty';
|
||||
mockGetEntityLinksSummaryByDownstreamContext.courseKeyUpToDate = 'courseKeyUpToDate';
|
||||
mockGetEntityLinksSummaryByDownstreamContext.response = mockSummaryResult;
|
||||
/** Apply this mock. Returns a spy object that can tell you if it's been called. */
|
||||
mockGetEntityLinksSummaryByDownstreamContext.applyMock = () => {
|
||||
jest.spyOn(api, 'getEntityLinksSummaryByDownstreamContext').mockImplementation(mockGetEntityLinksSummaryByDownstreamContext);
|
||||
};
|
||||
|
||||
/**
|
||||
* Mock for multi-search from meilisearch index for link details.
|
||||
*/
|
||||
export async function mockFetchIndexDocuments() {
|
||||
return mockLinkDetailsFromIndex;
|
||||
}
|
||||
mockFetchIndexDocuments.applyMock = () => {
|
||||
fetchMock.post(
|
||||
'http://mock.meilisearch.local/multi-search',
|
||||
mockFetchIndexDocuments,
|
||||
{ overwriteRoutes: true },
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Mock for library block metadata
|
||||
*/
|
||||
export async function mockUseLibBlockMetadata() {
|
||||
return mockLibBlockMetadata;
|
||||
}
|
||||
mockUseLibBlockMetadata.applyMock = () => {
|
||||
jest.spyOn(libApi, 'getLibraryBlockMetadata').mockImplementation(mockUseLibBlockMetadata);
|
||||
};
|
||||
|
||||
@@ -3,27 +3,65 @@ import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
|
||||
const getApiBaseUrl = () => getConfig().STUDIO_BASE_URL;
|
||||
|
||||
export const getEntityLinksByDownstreamContextUrl = (downstreamContextKey: string) => `${getApiBaseUrl()}/api/contentstore/v2/upstreams/${downstreamContextKey}`;
|
||||
export const getEntityLinksByDownstreamContextUrl = () => `${getApiBaseUrl()}/api/contentstore/v2/downstreams/`;
|
||||
|
||||
export const getEntityLinksSummaryByDownstreamContextUrl = (downstreamContextKey: string) => `${getApiBaseUrl()}/api/contentstore/v2/downstreams/${downstreamContextKey}/summary`;
|
||||
|
||||
export interface PaginatedData<T> {
|
||||
next: string | null;
|
||||
previous: string | null;
|
||||
nextPageNum: number | null;
|
||||
previousPageNum: number | null;
|
||||
count: number;
|
||||
numPages: number;
|
||||
currentPage: number;
|
||||
results: T,
|
||||
}
|
||||
|
||||
export interface PublishableEntityLink {
|
||||
id: number;
|
||||
upstreamUsageKey: string;
|
||||
upstreamContextKey: string;
|
||||
upstreamContextTitle: string;
|
||||
upstreamVersion: string;
|
||||
upstreamVersion: number;
|
||||
downstreamUsageKey: string;
|
||||
downstreamContextTitle: string;
|
||||
downstreamContextKey: string;
|
||||
versionSynced: string;
|
||||
versionDeclined: string;
|
||||
versionSynced: number;
|
||||
versionDeclined: number | null;
|
||||
created: string;
|
||||
updated: string;
|
||||
readyToSync: boolean;
|
||||
}
|
||||
|
||||
export const getEntityLinksByDownstreamContext = async (
|
||||
downstreamContextKey: string,
|
||||
export interface PublishableEntityLinkSummary {
|
||||
upstreamContextKey: string;
|
||||
upstreamContextTitle: string;
|
||||
readyToSyncCount: number;
|
||||
totalCount: number;
|
||||
lastPublishedAt: string;
|
||||
}
|
||||
|
||||
export const getEntityLinks = async (
|
||||
downstreamContextKey?: string,
|
||||
readyToSync?: boolean,
|
||||
upstreamUsageKey?: string,
|
||||
): Promise<PublishableEntityLink[]> => {
|
||||
const { data } = await getAuthenticatedHttpClient()
|
||||
.get(getEntityLinksByDownstreamContextUrl(downstreamContextKey));
|
||||
.get(getEntityLinksByDownstreamContextUrl(), {
|
||||
params: {
|
||||
course_id: downstreamContextKey,
|
||||
ready_to_sync: readyToSync,
|
||||
upstream_usage_key: upstreamUsageKey,
|
||||
no_page: true,
|
||||
},
|
||||
});
|
||||
return camelCaseObject(data);
|
||||
};
|
||||
|
||||
export const getEntityLinksSummaryByDownstreamContext = async (
|
||||
downstreamContextKey: string,
|
||||
): Promise<PublishableEntityLinkSummary[]> => {
|
||||
const { data } = await getAuthenticatedHttpClient()
|
||||
.get(getEntityLinksSummaryByDownstreamContextUrl(downstreamContextKey));
|
||||
return camelCaseObject(data);
|
||||
};
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { initializeMockApp } from '@edx/frontend-platform';
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
import { renderHook } from '@testing-library/react-hooks';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
import { waitFor } from '@testing-library/react';
|
||||
import { renderHook, waitFor } from '@testing-library/react';
|
||||
import { getEntityLinksByDownstreamContextUrl } from './api';
|
||||
import { useEntityLinksByDownstreamContext } from './apiHooks';
|
||||
import { useEntityLinks } from './apiHooks';
|
||||
|
||||
let axiosMock: MockAdapter;
|
||||
|
||||
@@ -36,15 +35,24 @@ describe('course libraries api hooks', () => {
|
||||
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
|
||||
});
|
||||
|
||||
it('should create library block', async () => {
|
||||
const courseKey = 'course-v1:some+key';
|
||||
const url = getEntityLinksByDownstreamContextUrl(courseKey);
|
||||
afterEach(() => {
|
||||
axiosMock.reset();
|
||||
});
|
||||
|
||||
it('should return links for course', async () => {
|
||||
const courseId = 'course-v1:some+key';
|
||||
const url = getEntityLinksByDownstreamContextUrl();
|
||||
axiosMock.onGet(url).reply(200, []);
|
||||
const { result } = renderHook(() => useEntityLinksByDownstreamContext(courseKey), { wrapper });
|
||||
const { result } = renderHook(() => useEntityLinks({ courseId }), { wrapper });
|
||||
await waitFor(() => {
|
||||
expect(result.current.isLoading).toBeFalsy();
|
||||
});
|
||||
expect(result.current.data).toEqual([]);
|
||||
expect(axiosMock.history.get[0].url).toEqual(url);
|
||||
expect(axiosMock.history.get[0].params).toEqual({
|
||||
course_id: courseId,
|
||||
ready_to_sync: undefined,
|
||||
upstream_usage_key: undefined,
|
||||
no_page: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,20 +1,65 @@
|
||||
import {
|
||||
useQuery,
|
||||
} from '@tanstack/react-query';
|
||||
import { getEntityLinksByDownstreamContext } from './api';
|
||||
import { getEntityLinks, getEntityLinksSummaryByDownstreamContext } from './api';
|
||||
|
||||
export const courseLibrariesQueryKeys = {
|
||||
all: ['courseLibraries'],
|
||||
courseLibraries: (courseKey?: string) => [...courseLibrariesQueryKeys.all, courseKey],
|
||||
courseLibraries: (courseId?: string) => [...courseLibrariesQueryKeys.all, courseId],
|
||||
courseReadyToSyncLibraries: ({ courseId, readyToSync, upstreamUsageKey }: {
|
||||
courseId?: string,
|
||||
readyToSync?: boolean,
|
||||
upstreamUsageKey?: string,
|
||||
pageSize?: number,
|
||||
}) => {
|
||||
const key: Array<string | boolean | number> = [...courseLibrariesQueryKeys.all];
|
||||
if (courseId !== undefined) {
|
||||
key.push(courseId);
|
||||
}
|
||||
if (readyToSync !== undefined) {
|
||||
key.push(readyToSync);
|
||||
}
|
||||
if (upstreamUsageKey !== undefined) {
|
||||
key.push(upstreamUsageKey);
|
||||
}
|
||||
return key;
|
||||
},
|
||||
courseLibrariesSummary: (courseId?: string) => [...courseLibrariesQueryKeys.courseLibraries(courseId), 'summary'],
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook to fetch a content library by its ID.
|
||||
* Hook to fetch list of publishable entity links by course key.
|
||||
* (That is, get a list of the library components used in the given course.)
|
||||
*/
|
||||
export const useEntityLinksByDownstreamContext = (courseKey: string | undefined) => (
|
||||
export const useEntityLinks = ({
|
||||
courseId, readyToSync, upstreamUsageKey,
|
||||
}: {
|
||||
courseId?: string,
|
||||
readyToSync?: boolean,
|
||||
upstreamUsageKey?: string,
|
||||
}) => (
|
||||
useQuery({
|
||||
queryKey: courseLibrariesQueryKeys.courseLibraries(courseKey),
|
||||
queryFn: () => getEntityLinksByDownstreamContext(courseKey!),
|
||||
enabled: courseKey !== undefined,
|
||||
queryKey: courseLibrariesQueryKeys.courseReadyToSyncLibraries({
|
||||
courseId,
|
||||
readyToSync,
|
||||
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,
|
||||
})
|
||||
);
|
||||
|
||||
@@ -1 +1 @@
|
||||
export { default } from './CourseLibraries';
|
||||
export { CourseLibraries } from './CourseLibraries';
|
||||
|
||||
@@ -18,7 +18,7 @@ const messages = defineMessages({
|
||||
},
|
||||
homeTabDescription: {
|
||||
id: 'course-authoring.course-libraries.tab.home.description',
|
||||
defaultMessage: 'This course contains content from these libraries.',
|
||||
defaultMessage: 'Your course contains content from these libraries.',
|
||||
description: 'Description text for home tab',
|
||||
},
|
||||
homeTabDescriptionEmpty: {
|
||||
@@ -28,18 +28,18 @@ const messages = defineMessages({
|
||||
},
|
||||
reviewTabTitle: {
|
||||
id: 'course-authoring.course-libraries.tab.review.title',
|
||||
defaultMessage: 'Review Content Updates ({count})',
|
||||
defaultMessage: 'Review Content Updates',
|
||||
description: 'Tab title for review tab',
|
||||
},
|
||||
reviewTabTitleEmpty: {
|
||||
id: 'course-authoring.course-libraries.tab.review.title-no-updates',
|
||||
defaultMessage: 'Review Content Updates',
|
||||
description: 'Tab title for review tab when no updates are available',
|
||||
reviewTabDescriptionEmpty: {
|
||||
id: 'course-authoring.course-libraries.tab.home.description-no-links',
|
||||
defaultMessage: 'All components are up to date',
|
||||
description: 'Description text for home tab',
|
||||
},
|
||||
breadcrumbAriaLabel: {
|
||||
id: 'course-authoring.course-libraries.downstream-block.breadcrumb.aria-label',
|
||||
defaultMessage: 'Component breadcrumb',
|
||||
description: 'Aria label for breadcrumb in component cards in course libraries page.',
|
||||
breadcrumbLabel: {
|
||||
id: 'course-authoring.course-libraries.downstream-block.breadcrumb.label',
|
||||
defaultMessage: 'Location:',
|
||||
description: 'label for breadcrumb in component cards in course libraries page.',
|
||||
},
|
||||
totalComponentLabel: {
|
||||
id: 'course-authoring.course-libraries.libcard.total-component.label',
|
||||
@@ -58,7 +58,7 @@ const messages = defineMessages({
|
||||
},
|
||||
outOfSyncCountAlertTitle: {
|
||||
id: 'course-authoring.course-libraries.libcard.out-of-sync.alert.title',
|
||||
defaultMessage: '{outOfSyncCount} library components are out of sync. Review updates to accept or ignore changes',
|
||||
defaultMessage: '{outOfSyncCount, plural, one {# library component is} other {# library components are}} out of sync. Review updates to accept or ignore changes',
|
||||
description: 'Alert message shown when library components are out of sync',
|
||||
},
|
||||
reviewUpdatesBtn: {
|
||||
@@ -76,6 +76,51 @@ const messages = defineMessages({
|
||||
defaultMessage: 'This page cannot be shown: Libraries v2 are disabled.',
|
||||
description: 'Error message shown to users when trying to load a libraries V2 page while libraries v2 are disabled.',
|
||||
},
|
||||
cardReviewContentBtn: {
|
||||
id: 'course-authoring.course-libraries.review-tab.libcard.review-btn-text',
|
||||
defaultMessage: 'Review Updates',
|
||||
description: 'Card review button for component in review tab',
|
||||
},
|
||||
cardUpdateContentBtn: {
|
||||
id: 'course-authoring.course-libraries.review-tab.libcard.update-btn-text',
|
||||
defaultMessage: 'Update',
|
||||
description: 'Card update button for component in review tab',
|
||||
},
|
||||
cardIgnoreContentBtn: {
|
||||
id: 'course-authoring.course-libraries.review-tab.libcard.ignore-btn-text',
|
||||
defaultMessage: 'Ignore',
|
||||
description: 'Card ignore button for component in review tab',
|
||||
},
|
||||
updateSingleBlockSuccess: {
|
||||
id: 'course-authoring.course-libraries.review-tab.libcard.update-success-toast',
|
||||
defaultMessage: 'Success! "{name}" is updated',
|
||||
description: 'Success toast message when a component is updated.',
|
||||
},
|
||||
ignoreSingleBlockSuccess: {
|
||||
id: 'course-authoring.course-libraries.review-tab.libcard.ignore-success-toast',
|
||||
defaultMessage: '"{name}" will remain out of sync with library content. You will be notified when this component is updated again.',
|
||||
description: 'Success toast message when a component update is ignored.',
|
||||
},
|
||||
searchPlaceholder: {
|
||||
id: 'course-authoring.course-libraries.review-tab.search.placeholder',
|
||||
defaultMessage: 'Search',
|
||||
description: 'Search text box in review tab placeholder text',
|
||||
},
|
||||
brokenLinkTooltip: {
|
||||
id: 'course-authoring.course-libraries.home-tab.broken-link.tooltip',
|
||||
defaultMessage: 'Sourced from a library - but the upstream link is broken/invalid.',
|
||||
description: 'Tooltip text describing broken link in component listing.',
|
||||
},
|
||||
genericErrorMessage: {
|
||||
id: 'course-authoring.course-libraries.home-tab.error.message',
|
||||
defaultMessage: 'Something went wrong! Could not fetch results.',
|
||||
description: 'Generic error message displayed when fetching link data fails.',
|
||||
},
|
||||
unitsUpdatesWarning: {
|
||||
id: 'course-authoring.course-libraries.home-tab.warning.units',
|
||||
defaultMessage: 'Currently this page only tracks component updates. To check for unit updates, go to your Course Outline.',
|
||||
description: 'Warning message shown in library sync page about units updates.',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
verticalListSortingStrategy,
|
||||
} from '@dnd-kit/sortable';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { CourseAuthoringOutlineSidebarSlot } from '../plugin-slots/CourseAuthoringOutlineSidebarSlot';
|
||||
|
||||
import { LoadingSpinner } from '../generic/Loading';
|
||||
import { getProcessingNotification } from '../generic/processing-notification/data/selectors';
|
||||
@@ -35,8 +36,6 @@ import AlertMessage from '../generic/alert-message';
|
||||
import getPageHeadTitle from '../generic/utils';
|
||||
import { getCurrentItem, getProctoredExamsFlag } from './data/selectors';
|
||||
import { COURSE_BLOCK_NAMES } from './constants';
|
||||
import HeaderNavigations from './header-navigations/HeaderNavigations';
|
||||
import OutlineSideBar from './outline-sidebar/OutlineSidebar';
|
||||
import StatusBar from './status-bar/StatusBar';
|
||||
import EnableHighlightsModal from './enable-highlights-modal/EnableHighlightsModal';
|
||||
import SectionCard from './section-card/SectionCard';
|
||||
@@ -46,15 +45,16 @@ import HighlightsModal from './highlights-modal/HighlightsModal';
|
||||
import EmptyPlaceholder from './empty-placeholder/EmptyPlaceholder';
|
||||
import PublishModal from './publish-modal/PublishModal';
|
||||
import PageAlerts from './page-alerts/PageAlerts';
|
||||
import DraggableList from '../generic/drag-helper/DraggableList';
|
||||
import DraggableList from './drag-helper/DraggableList';
|
||||
import {
|
||||
canMoveSection,
|
||||
possibleUnitMoves,
|
||||
possibleSubsectionMoves,
|
||||
} from '../generic/drag-helper/utils';
|
||||
} from './drag-helper/utils';
|
||||
import { useCourseOutline } from './hooks';
|
||||
import messages from './messages';
|
||||
import { getTagsExportFile } from './data/api';
|
||||
import CourseOutlineHeaderActionsSlot from '../plugin-slots/CourseOutlineHeaderActionsSlot';
|
||||
|
||||
const CourseOutline = ({ courseId }) => {
|
||||
const intl = useIntl();
|
||||
@@ -103,13 +103,12 @@ const CourseOutline = ({ courseId }) => {
|
||||
handleNewSectionSubmit,
|
||||
handleNewSubsectionSubmit,
|
||||
handleNewUnitSubmit,
|
||||
handleAddUnitFromLibrary,
|
||||
getUnitUrl,
|
||||
handleVideoSharingOptionChange,
|
||||
handleCopyToClipboardClick,
|
||||
handlePasteClipboardClick,
|
||||
notificationDismissUrl,
|
||||
discussionsSettings,
|
||||
discussionsIncontextFeedbackUrl,
|
||||
discussionsIncontextLearnmoreUrl,
|
||||
deprecatedBlocksInfo,
|
||||
proctoringErrors,
|
||||
@@ -242,7 +241,6 @@ const CourseOutline = ({ courseId }) => {
|
||||
notificationDismissUrl={notificationDismissUrl}
|
||||
handleDismissNotification={handleDismissNotification}
|
||||
discussionsSettings={discussionsSettings}
|
||||
discussionsIncontextFeedbackUrl={discussionsIncontextFeedbackUrl}
|
||||
discussionsIncontextLearnmoreUrl={discussionsIncontextLearnmoreUrl}
|
||||
deprecatedBlocksInfo={deprecatedBlocksInfo}
|
||||
proctoringErrors={proctoringErrors}
|
||||
@@ -267,7 +265,6 @@ const CourseOutline = ({ courseId }) => {
|
||||
notificationDismissUrl={notificationDismissUrl}
|
||||
handleDismissNotification={handleDismissNotification}
|
||||
discussionsSettings={discussionsSettings}
|
||||
discussionsIncontextFeedbackUrl={discussionsIncontextFeedbackUrl}
|
||||
discussionsIncontextLearnmoreUrl={discussionsIncontextLearnmoreUrl}
|
||||
deprecatedBlocksInfo={deprecatedBlocksInfo}
|
||||
proctoringErrors={proctoringErrors}
|
||||
@@ -295,7 +292,7 @@ const CourseOutline = ({ courseId }) => {
|
||||
title={intl.formatMessage(messages.headingTitle)}
|
||||
subtitle={intl.formatMessage(messages.headingSubtitle)}
|
||||
headerActions={(
|
||||
<HeaderNavigations
|
||||
<CourseOutlineHeaderActionsSlot
|
||||
isReIndexShow={isReIndexShow}
|
||||
isSectionsExpanded={isSectionsExpanded}
|
||||
headerNavigationsActions={headerNavigationsActions}
|
||||
@@ -303,6 +300,7 @@ const CourseOutline = ({ courseId }) => {
|
||||
hasSections={Boolean(sectionsList.length)}
|
||||
courseActions={courseActions}
|
||||
errors={errors}
|
||||
sections={sections}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
@@ -377,6 +375,7 @@ const CourseOutline = ({ courseId }) => {
|
||||
section,
|
||||
section.childInfo.children,
|
||||
)}
|
||||
isSectionsExpanded={isSectionsExpanded}
|
||||
isSelfPaced={statusBarData.isSelfPaced}
|
||||
isCustomRelativeDatesActive={isCustomRelativeDatesActive}
|
||||
savingStatus={savingStatus}
|
||||
@@ -386,6 +385,7 @@ const CourseOutline = ({ courseId }) => {
|
||||
onDuplicateSubmit={handleDuplicateSubsectionSubmit}
|
||||
onOpenConfigureModal={openConfigureModal}
|
||||
onNewUnitSubmit={handleNewUnitSubmit}
|
||||
onAddUnitFromLibrary={handleAddUnitFromLibrary}
|
||||
onOrderChange={updateSubsectionOrderByIndex}
|
||||
onPasteClick={handlePasteClipboardClick}
|
||||
>
|
||||
@@ -419,7 +419,6 @@ const CourseOutline = ({ courseId }) => {
|
||||
onDuplicateSubmit={handleDuplicateUnitSubmit}
|
||||
getTitleLink={getUnitUrl}
|
||||
onOrderChange={updateUnitOrderByIndex}
|
||||
onCopyToClipboardClick={handleCopyToClipboardClick}
|
||||
discussionsSettings={discussionsSettings}
|
||||
/>
|
||||
))}
|
||||
@@ -457,7 +456,11 @@ const CourseOutline = ({ courseId }) => {
|
||||
</article>
|
||||
</Layout.Element>
|
||||
<Layout.Element>
|
||||
<OutlineSideBar courseId={courseId} />
|
||||
<CourseAuthoringOutlineSidebarSlot
|
||||
courseId={courseId}
|
||||
courseName={courseName}
|
||||
sections={sections}
|
||||
/>
|
||||
</Layout.Element>
|
||||
</Layout>
|
||||
<EnableHighlightsModal
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
@import "./header-navigations/HeaderNavigations";
|
||||
@import "./status-bar/StatusBar";
|
||||
@import "./section-card/SectionCard";
|
||||
@import "./subsection-card/SubsectionCard";
|
||||
@@ -8,3 +7,4 @@
|
||||
@import "./highlights-modal/HighlightsModal";
|
||||
@import "./publish-modal/PublishModal";
|
||||
@import "./xblock-status/XBlockStatus";
|
||||
@import "./drag-helper/SortableItem";
|
||||
|
||||
@@ -58,22 +58,18 @@ import {
|
||||
moveUnitOver,
|
||||
moveSubsection,
|
||||
moveUnit,
|
||||
} from '../generic/drag-helper/utils';
|
||||
} from './drag-helper/utils';
|
||||
import { postXBlockBaseApiUrl } from '../course-unit/data/api';
|
||||
import { COMPONENT_TYPES } from '../generic/block-type-utils/constants';
|
||||
|
||||
let axiosMock;
|
||||
let store;
|
||||
const mockPathname = '/foo-bar';
|
||||
const courseId = '123';
|
||||
const containerKey = 'lct:org:lib:unit:1';
|
||||
|
||||
window.HTMLElement.prototype.scrollIntoView = jest.fn();
|
||||
|
||||
const clipboardBroadcastChannelMock = {
|
||||
postMessage: jest.fn(),
|
||||
close: jest.fn(),
|
||||
};
|
||||
|
||||
global.BroadcastChannel = jest.fn(() => clipboardBroadcastChannelMock);
|
||||
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
useLocation: jest.fn(),
|
||||
@@ -100,6 +96,24 @@ jest.mock('./data/api', () => ({
|
||||
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();
|
||||
|
||||
jest.mock('@dnd-kit/core', () => ({
|
||||
@@ -139,7 +153,9 @@ describe('<CourseOutline />', () => {
|
||||
pathname: mockPathname,
|
||||
});
|
||||
|
||||
store = initializeStore();
|
||||
store = initializeStore({
|
||||
studioHome: { studioHomeData: { librariesV2Enabled: true } },
|
||||
});
|
||||
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
|
||||
axiosMock
|
||||
.onGet(getCourseOutlineIndexApiUrl(courseId))
|
||||
@@ -158,6 +174,10 @@ describe('<CourseOutline />', () => {
|
||||
await executeThunk(fetchCourseOutlineIndexQuery(courseId), store.dispatch);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('render CourseOutline component correctly', async () => {
|
||||
const { getByText } = render(<RootWrapper />);
|
||||
|
||||
@@ -268,13 +288,15 @@ describe('<CourseOutline />', () => {
|
||||
});
|
||||
|
||||
it('check that new section list is saved when dragged', async () => {
|
||||
const { findAllByRole } = render(<RootWrapper />);
|
||||
const courseBlockId = courseOutlineIndexMock.courseStructure.id;
|
||||
const { findAllByRole, findByTestId } = render(<RootWrapper />);
|
||||
const expandAllButton = await findByTestId('expand-collapse-all-button');
|
||||
fireEvent.click(expandAllButton);
|
||||
const [section] = store.getState().courseOutline.sectionsList;
|
||||
const sectionsDraggers = await findAllByRole('button', { name: 'Drag to reorder' });
|
||||
const draggableButton = sectionsDraggers[6];
|
||||
const draggableButton = sectionsDraggers[1];
|
||||
|
||||
axiosMock
|
||||
.onPut(getCourseBlockApiUrl(courseBlockId))
|
||||
.onPut(getCourseBlockApiUrl(section.id))
|
||||
.reply(200, { dummy: 'value' });
|
||||
|
||||
const section1 = store.getState().courseOutline.sectionsList[0].id;
|
||||
@@ -293,13 +315,15 @@ describe('<CourseOutline />', () => {
|
||||
});
|
||||
|
||||
it('check section list is restored to original order when API call fails', async () => {
|
||||
const { findAllByRole } = render(<RootWrapper />);
|
||||
const courseBlockId = courseOutlineIndexMock.courseStructure.id;
|
||||
const { findAllByRole, findByTestId } = render(<RootWrapper />);
|
||||
const expandAllButton = await findByTestId('expand-collapse-all-button');
|
||||
fireEvent.click(expandAllButton);
|
||||
const [section] = store.getState().courseOutline.sectionsList;
|
||||
const sectionsDraggers = await findAllByRole('button', { name: 'Drag to reorder' });
|
||||
const draggableButton = sectionsDraggers[6];
|
||||
const draggableButton = sectionsDraggers[1];
|
||||
|
||||
axiosMock
|
||||
.onPut(getCourseBlockApiUrl(courseBlockId))
|
||||
.onPut(getCourseBlockApiUrl(section.id))
|
||||
.reply(500);
|
||||
|
||||
const section1 = store.getState().courseOutline.sectionsList[0].id;
|
||||
@@ -374,8 +398,6 @@ describe('<CourseOutline />', () => {
|
||||
const { findAllByTestId } = render(<RootWrapper />);
|
||||
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 units = await within(subsectionElement).findAllByTestId('unit-card');
|
||||
expect(units.length).toBe(1);
|
||||
|
||||
@@ -396,6 +418,40 @@ 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 () => {
|
||||
const { getByText } = render(<RootWrapper />);
|
||||
|
||||
@@ -589,8 +645,6 @@ describe('<CourseOutline />', () => {
|
||||
await checkEditTitle(section, subsectionElement, subsection, 'New subsection name', 'subsection');
|
||||
|
||||
// check unit
|
||||
const expandBtn = await within(subsectionElement).findByTestId('subsection-card-header__expanded-btn');
|
||||
fireEvent.click(expandBtn);
|
||||
const [unit] = subsection.childInfo.children;
|
||||
const [unitElement] = await within(subsectionElement).findAllByTestId('unit-card');
|
||||
await checkEditTitle(section, unitElement, unit, 'New unit name', 'unit');
|
||||
@@ -603,8 +657,6 @@ describe('<CourseOutline />', () => {
|
||||
const [sectionElement] = await screen.findAllByTestId('section-card');
|
||||
const [subsection] = section.childInfo.children;
|
||||
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 [unitElement] = await within(subsectionElement).findAllByTestId('unit-card');
|
||||
|
||||
@@ -643,8 +695,6 @@ describe('<CourseOutline />', () => {
|
||||
const [sectionElement] = await findAllByTestId('section-card');
|
||||
const [subsection] = section.childInfo.children;
|
||||
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 [unitElement] = await within(subsectionElement).findAllByTestId('unit-card');
|
||||
|
||||
@@ -714,8 +764,6 @@ describe('<CourseOutline />', () => {
|
||||
const [sectionElement] = await findAllByTestId('section-card');
|
||||
const [subsection] = section.childInfo.children;
|
||||
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 [unitElement] = await within(subsectionElement).findAllByTestId('unit-card');
|
||||
|
||||
@@ -1424,8 +1472,6 @@ describe('<CourseOutline />', () => {
|
||||
|
||||
const [firstSection] = await findAllByTestId('section-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 unitDropdownButton = await within(firstUnit).findByTestId('unit-card-header__menu-button');
|
||||
|
||||
@@ -1785,8 +1831,6 @@ describe('<CourseOutline />', () => {
|
||||
const [, sectionElement] = await findAllByTestId('section-card');
|
||||
const [, subsection] = section.childInfo.children;
|
||||
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 [, unitElement] = await within(subsectionElement).findAllByTestId('unit-card');
|
||||
|
||||
@@ -1826,8 +1870,6 @@ describe('<CourseOutline />', () => {
|
||||
const [, sectionElement] = await findAllByTestId('section-card');
|
||||
const [firstSubsection, subsection] = section.childInfo.children;
|
||||
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 [unitElement] = await within(subsectionElement).findAllByTestId('unit-card');
|
||||
|
||||
@@ -1863,8 +1905,6 @@ describe('<CourseOutline />', () => {
|
||||
const [subsection] = secondSection.childInfo.children;
|
||||
const firstSectionLastSubsection = firstSection.childInfo.children[firstSection.childInfo.children.length - 1];
|
||||
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 [unitElement] = await within(subsectionElement).findAllByTestId('unit-card');
|
||||
|
||||
@@ -1909,8 +1949,6 @@ describe('<CourseOutline />', () => {
|
||||
const [, sectionElement] = await findAllByTestId('section-card');
|
||||
const [firstSubsection, subsection] = section.childInfo.children;
|
||||
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 unit = firstSubsection.childInfo.children[lastUnitIdx];
|
||||
const unitElement = (await within(subsectionElement).findAllByTestId('unit-card'))[lastUnitIdx];
|
||||
@@ -1948,8 +1986,6 @@ describe('<CourseOutline />', () => {
|
||||
const secondSectionLastSubsection = secondSection.childInfo.children[lastSubIndex];
|
||||
const thirdSectionFirstSubsection = thirdSection.childInfo.children[0];
|
||||
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 unit = secondSectionLastSubsection.childInfo.children[lastUnitIdx];
|
||||
const unitElement = (await within(subsectionElement).findAllByTestId('unit-card'))[lastUnitIdx];
|
||||
@@ -1994,8 +2030,6 @@ describe('<CourseOutline />', () => {
|
||||
const sections = await findAllByTestId('section-card');
|
||||
const [sectionElement] = sections;
|
||||
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
|
||||
const [firstUnit] = await within(subsectionElement).findAllByTestId('unit-card');
|
||||
|
||||
@@ -2015,8 +2049,6 @@ describe('<CourseOutline />', () => {
|
||||
const lastSection = sections[sections.length - 1];
|
||||
// it has only one subsection
|
||||
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
|
||||
const [lastUnit] = await within(lastSubsectionElement).findAllByTestId('unit-card');
|
||||
|
||||
@@ -2037,6 +2069,9 @@ describe('<CourseOutline />', () => {
|
||||
const { findAllByTestId } = render(<RootWrapper />);
|
||||
|
||||
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 subsectionsDraggers = within(sectionElement).getAllByRole('button', { name: 'Drag to reorder' });
|
||||
const draggableButton = subsectionsDraggers[1];
|
||||
@@ -2068,6 +2103,9 @@ describe('<CourseOutline />', () => {
|
||||
const { findAllByTestId } = render(<RootWrapper />);
|
||||
|
||||
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 subsectionsDraggers = within(sectionElement).getAllByRole('button', { name: 'Drag to reorder' });
|
||||
const draggableButton = subsectionsDraggers[1];
|
||||
@@ -2097,8 +2135,6 @@ describe('<CourseOutline />', () => {
|
||||
const [subsectionElement] = await within(sectionElement).findAllByTestId('subsection-card');
|
||||
const section = store.getState().courseOutline.sectionsList[2];
|
||||
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 draggableButton = unitDraggers[1];
|
||||
const sections = courseOutlineIndexMock.courseStructure.childInfo.children;
|
||||
@@ -2133,8 +2169,6 @@ describe('<CourseOutline />', () => {
|
||||
const [subsectionElement] = await within(sectionElement).findAllByTestId('subsection-card');
|
||||
const section = store.getState().courseOutline.sectionsList[2];
|
||||
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 draggableButton = unitDraggers[1];
|
||||
const sections = courseOutlineIndexMock.courseStructure.childInfo.children;
|
||||
@@ -2172,8 +2206,6 @@ describe('<CourseOutline />', () => {
|
||||
.onGet(getXBlockApiUrl(section.id))
|
||||
.reply(200, courseSectionMock);
|
||||
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 [unitElement] = await within(subsectionElement).findAllByTestId('unit-card');
|
||||
|
||||
@@ -2182,9 +2214,6 @@ describe('<CourseOutline />', () => {
|
||||
.onPost(getClipboardUrl(), {
|
||||
usage_key: unit.id,
|
||||
}).reply(200, clipboardUnit);
|
||||
// check that initialUserClipboard state is empty
|
||||
const { initialUserClipboard } = store.getState().courseOutline;
|
||||
expect(initialUserClipboard).toBeUndefined();
|
||||
|
||||
// find menu button and click on it to open menu
|
||||
const menu = await within(unitElement).findByTestId('unit-card-header__menu-button');
|
||||
@@ -2194,9 +2223,6 @@ describe('<CourseOutline />', () => {
|
||||
const copyButton = await within(unitElement).findByText(cardHeaderMessages.menuCopy.defaultMessage);
|
||||
await act(async () => fireEvent.click(copyButton));
|
||||
|
||||
// check that initialUserClipboard state is updated
|
||||
expect(store.getState().generic.clipboardData).toEqual(clipboardUnit);
|
||||
|
||||
[subsectionElement] = await within(sectionElement).findAllByTestId('subsection-card');
|
||||
// find clipboard content label
|
||||
const clipboardLabel = await within(subsectionElement).findByText(
|
||||
@@ -2260,9 +2286,14 @@ describe('<CourseOutline />', () => {
|
||||
|
||||
it('should show toats on export tags', async () => {
|
||||
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))
|
||||
.reply(200, expectedResponse);
|
||||
.withDelayInMs(500);
|
||||
delayedResponse(200, expectedResponse);
|
||||
|
||||
useLocation.mockReturnValue({
|
||||
pathname: '/foo-bar',
|
||||
hash: '#export-tags',
|
||||
@@ -2270,37 +2301,41 @@ describe('<CourseOutline />', () => {
|
||||
window.URL.createObjectURL = jest.fn().mockReturnValue('http://example.com/archivo');
|
||||
window.URL.revokeObjectURL = jest.fn();
|
||||
render(<RootWrapper />);
|
||||
expect(await screen.findByText('Please wait. Creating export file for course tags...')).toBeInTheDocument();
|
||||
await screen.findByText('Please wait. Creating export file for course tags...');
|
||||
|
||||
const expectedRequest = axiosMock.history.get.filter(request => request.url === exportTags(courseId));
|
||||
expect(expectedRequest.length).toBe(1);
|
||||
|
||||
expect(await screen.findByText('Course tags exported successfully')).toBeInTheDocument();
|
||||
await screen.findByText('Course tags exported successfully');
|
||||
});
|
||||
|
||||
it('should show toast on export tags error', async () => {
|
||||
axiosMock
|
||||
// Delay to ensure we see "Please wait."
|
||||
// Without the delay the error renders too quickly
|
||||
const delayedResponse = axiosMock
|
||||
.onGet(exportTags(courseId))
|
||||
.reply(404);
|
||||
.withDelayInMs(500);
|
||||
delayedResponse(404);
|
||||
|
||||
useLocation.mockReturnValue({
|
||||
pathname: '/foo-bar',
|
||||
hash: '#export-tags',
|
||||
});
|
||||
|
||||
render(<RootWrapper />);
|
||||
expect(await screen.findByText('Please wait. Creating export file for course tags...')).toBeInTheDocument();
|
||||
expect(await screen.findByText('An error has occurred creating the file')).toBeInTheDocument();
|
||||
await screen.findByText('Please wait. Creating export file for course tags...');
|
||||
await screen.findByText('An error has occurred creating the file');
|
||||
});
|
||||
|
||||
it('displays an alert and sets status to DENIED when API responds with 403', async () => {
|
||||
it('sets status to DENIED when API responds with 403', async () => {
|
||||
axiosMock
|
||||
.onGet(getCourseOutlineIndexApiUrl(courseId))
|
||||
.reply(403);
|
||||
|
||||
const { getByRole } = render(<RootWrapper />);
|
||||
const { getByTestId } = render(<RootWrapper />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(getByRole('alert')).toBeInTheDocument();
|
||||
expect(getByTestId('redux-provider')).toBeInTheDocument();
|
||||
const { outlineIndexLoadingStatus } = store.getState().courseOutline.loadingStatus;
|
||||
expect(outlineIndexLoadingStatus).toEqual(RequestStatus.DENIED);
|
||||
});
|
||||
|
||||
@@ -60,7 +60,7 @@ module.exports = {
|
||||
highlightsEnabledForMessaging: false,
|
||||
highlightsEnabled: true,
|
||||
highlightsPreviewOnly: false,
|
||||
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',
|
||||
highlightsDocUrl: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/manage_course_highlight_emails.html',
|
||||
enableProctoredExams: true,
|
||||
createZendeskTickets: true,
|
||||
enableTimedExams: true,
|
||||
@@ -128,7 +128,7 @@ module.exports = {
|
||||
],
|
||||
highlightsEnabled: true,
|
||||
highlightsPreviewOnly: false,
|
||||
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',
|
||||
highlightsDocUrl: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/manage_course_highlight_emails.html',
|
||||
childInfo: {
|
||||
category: 'sequential',
|
||||
displayName: 'Subsection',
|
||||
@@ -292,6 +292,11 @@ module.exports = {
|
||||
selectedPartitionIndex: -1,
|
||||
selectedGroupsLabel: '',
|
||||
},
|
||||
upstreamInfo: {
|
||||
readyToSync: false,
|
||||
upstreamRef: undefined,
|
||||
versionSynced: undefined,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -517,7 +522,7 @@ module.exports = {
|
||||
],
|
||||
highlightsEnabled: true,
|
||||
highlightsPreviewOnly: false,
|
||||
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',
|
||||
highlightsDocUrl: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/manage_course_highlight_emails.html',
|
||||
childInfo: {
|
||||
category: 'sequential',
|
||||
displayName: 'Subsection',
|
||||
@@ -675,6 +680,11 @@ module.exports = {
|
||||
selectedPartitionIndex: -1,
|
||||
selectedGroupsLabel: '',
|
||||
},
|
||||
upstreamInfo: {
|
||||
readyToSync: false,
|
||||
upstreamRef: undefined,
|
||||
versionSynced: undefined,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@vertical_2dbb0072785e',
|
||||
@@ -759,6 +769,11 @@ module.exports = {
|
||||
selectedPartitionIndex: -1,
|
||||
selectedGroupsLabel: '',
|
||||
},
|
||||
upstreamInfo: {
|
||||
readyToSync: false,
|
||||
upstreamRef: undefined,
|
||||
versionSynced: undefined,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@vertical_98cf62510471',
|
||||
@@ -843,6 +858,11 @@ module.exports = {
|
||||
selectedPartitionIndex: -1,
|
||||
selectedGroupsLabel: '',
|
||||
},
|
||||
upstreamInfo: {
|
||||
readyToSync: false,
|
||||
upstreamRef: undefined,
|
||||
versionSynced: undefined,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@vertical_d32bf9b2242c',
|
||||
@@ -927,6 +947,11 @@ module.exports = {
|
||||
selectedPartitionIndex: -1,
|
||||
selectedGroupsLabel: '',
|
||||
},
|
||||
upstreamInfo: {
|
||||
readyToSync: false,
|
||||
upstreamRef: undefined,
|
||||
versionSynced: undefined,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@4e592689563243c484af947465eaef0d',
|
||||
@@ -1011,6 +1036,11 @@ module.exports = {
|
||||
selectedPartitionIndex: -1,
|
||||
selectedGroupsLabel: '',
|
||||
},
|
||||
upstreamInfo: {
|
||||
readyToSync: false,
|
||||
upstreamRef: undefined,
|
||||
versionSynced: undefined,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -1196,6 +1226,11 @@ module.exports = {
|
||||
selectedPartitionIndex: -1,
|
||||
selectedGroupsLabel: '',
|
||||
},
|
||||
upstreamInfo: {
|
||||
readyToSync: false,
|
||||
upstreamRef: undefined,
|
||||
versionSynced: undefined,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@vertical_aae927868e55',
|
||||
@@ -1280,6 +1315,11 @@ module.exports = {
|
||||
selectedPartitionIndex: -1,
|
||||
selectedGroupsLabel: '',
|
||||
},
|
||||
upstreamInfo: {
|
||||
readyToSync: false,
|
||||
upstreamRef: undefined,
|
||||
versionSynced: undefined,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@vertical_c037f3757df1',
|
||||
@@ -1364,6 +1404,11 @@ module.exports = {
|
||||
selectedPartitionIndex: -1,
|
||||
selectedGroupsLabel: '',
|
||||
},
|
||||
upstreamInfo: {
|
||||
readyToSync: false,
|
||||
upstreamRef: undefined,
|
||||
versionSynced: undefined,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@vertical_bc69a47c6fae',
|
||||
@@ -1448,6 +1493,11 @@ module.exports = {
|
||||
selectedPartitionIndex: -1,
|
||||
selectedGroupsLabel: '',
|
||||
},
|
||||
upstreamInfo: {
|
||||
readyToSync: false,
|
||||
upstreamRef: undefined,
|
||||
versionSynced: undefined,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@8f89194410954e768bde1764985454a7',
|
||||
@@ -1532,6 +1582,11 @@ module.exports = {
|
||||
selectedPartitionIndex: -1,
|
||||
selectedGroupsLabel: '',
|
||||
},
|
||||
upstreamInfo: {
|
||||
readyToSync: false,
|
||||
upstreamRef: undefined,
|
||||
versionSynced: undefined,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -1717,6 +1772,11 @@ module.exports = {
|
||||
selectedPartitionIndex: -1,
|
||||
selectedGroupsLabel: '',
|
||||
},
|
||||
upstreamInfo: {
|
||||
readyToSync: false,
|
||||
upstreamRef: undefined,
|
||||
versionSynced: undefined,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -1837,7 +1897,7 @@ module.exports = {
|
||||
highlights: [],
|
||||
highlightsEnabled: true,
|
||||
highlightsPreviewOnly: false,
|
||||
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',
|
||||
highlightsDocUrl: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/manage_course_highlight_emails.html',
|
||||
childInfo: {
|
||||
category: 'sequential',
|
||||
displayName: 'Subsection',
|
||||
@@ -1995,6 +2055,11 @@ module.exports = {
|
||||
selectedPartitionIndex: -1,
|
||||
selectedGroupsLabel: '',
|
||||
},
|
||||
upstreamInfo: {
|
||||
readyToSync: false,
|
||||
upstreamRef: undefined,
|
||||
versionSynced: undefined,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@vertical_f04afeac0131',
|
||||
@@ -2079,6 +2144,11 @@ module.exports = {
|
||||
selectedPartitionIndex: -1,
|
||||
selectedGroupsLabel: '',
|
||||
},
|
||||
upstreamInfo: {
|
||||
readyToSync: false,
|
||||
upstreamRef: undefined,
|
||||
versionSynced: undefined,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@b6662b497c094bcc9b870d8270c90c93',
|
||||
@@ -2163,6 +2233,11 @@ module.exports = {
|
||||
selectedPartitionIndex: -1,
|
||||
selectedGroupsLabel: '',
|
||||
},
|
||||
upstreamInfo: {
|
||||
readyToSync: false,
|
||||
upstreamRef: undefined,
|
||||
versionSynced: undefined,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@f91d8d31f7cf48ce990f8d8745ae4cfa',
|
||||
@@ -2247,6 +2322,11 @@ module.exports = {
|
||||
selectedPartitionIndex: -1,
|
||||
selectedGroupsLabel: '',
|
||||
},
|
||||
upstreamInfo: {
|
||||
readyToSync: false,
|
||||
upstreamRef: undefined,
|
||||
versionSynced: undefined,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@vertical_ac391cde8a91',
|
||||
@@ -2331,6 +2411,11 @@ module.exports = {
|
||||
selectedPartitionIndex: -1,
|
||||
selectedGroupsLabel: '',
|
||||
},
|
||||
upstreamInfo: {
|
||||
readyToSync: false,
|
||||
upstreamRef: undefined,
|
||||
versionSynced: undefined,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@vertical_36e0beb03f0a',
|
||||
@@ -2415,6 +2500,11 @@ module.exports = {
|
||||
selectedPartitionIndex: -1,
|
||||
selectedGroupsLabel: '',
|
||||
},
|
||||
upstreamInfo: {
|
||||
readyToSync: false,
|
||||
upstreamRef: undefined,
|
||||
versionSynced: undefined,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@1b0e2c2c84884b95b1c99fb678cc964c',
|
||||
@@ -2499,6 +2589,11 @@ module.exports = {
|
||||
selectedPartitionIndex: -1,
|
||||
selectedGroupsLabel: '',
|
||||
},
|
||||
upstreamInfo: {
|
||||
readyToSync: false,
|
||||
upstreamRef: undefined,
|
||||
versionSynced: undefined,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@c7e98fd39a6944edb6b286c32e1150ff',
|
||||
@@ -2583,6 +2678,11 @@ module.exports = {
|
||||
selectedPartitionIndex: -1,
|
||||
selectedGroupsLabel: '',
|
||||
},
|
||||
upstreamInfo: {
|
||||
readyToSync: false,
|
||||
upstreamRef: undefined,
|
||||
versionSynced: undefined,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@d6eaa391d2be41dea20b8b1bfbcb1c45',
|
||||
@@ -2667,6 +2767,11 @@ module.exports = {
|
||||
selectedPartitionIndex: -1,
|
||||
selectedGroupsLabel: '',
|
||||
},
|
||||
upstreamInfo: {
|
||||
readyToSync: false,
|
||||
upstreamRef: undefined,
|
||||
versionSynced: undefined,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -2787,7 +2892,7 @@ module.exports = {
|
||||
highlights: [],
|
||||
highlightsEnabled: true,
|
||||
highlightsPreviewOnly: false,
|
||||
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',
|
||||
highlightsDocUrl: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/manage_course_highlight_emails.html',
|
||||
childInfo: {
|
||||
category: 'sequential',
|
||||
displayName: 'Subsection',
|
||||
@@ -2945,6 +3050,11 @@ module.exports = {
|
||||
selectedPartitionIndex: -1,
|
||||
selectedGroupsLabel: '',
|
||||
},
|
||||
upstreamInfo: {
|
||||
readyToSync: false,
|
||||
upstreamRef: undefined,
|
||||
versionSynced: undefined,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -3044,7 +3154,6 @@ module.exports = {
|
||||
blocks: [],
|
||||
advanceSettingsUrl: '/settings/advanced/course-v1:edx+101+y76',
|
||||
},
|
||||
discussionsIncontextFeedbackUrl: '',
|
||||
discussionsIncontextLearnmoreUrl: '',
|
||||
initialState: {
|
||||
expandedLocators: [
|
||||
|
||||
@@ -6,7 +6,6 @@ module.exports = {
|
||||
blocks: [],
|
||||
advanceSettingsUrl: '/settings/advanced/course-v1:edx+101+y76',
|
||||
},
|
||||
discussionsIncontextFeedbackUrl: '',
|
||||
discussionsIncontextLearnmoreUrl: '',
|
||||
initialState: {
|
||||
expandedLocators: [
|
||||
|
||||
@@ -55,7 +55,7 @@ module.exports = {
|
||||
highlights: [],
|
||||
highlights_enabled: true,
|
||||
highlights_preview_only: false,
|
||||
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',
|
||||
highlights_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/manage_course_highlight_emails.html',
|
||||
child_info: {
|
||||
category: 'sequential',
|
||||
display_name: 'Subsection',
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// @ts-check
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
@@ -10,11 +11,13 @@ import {
|
||||
Hyperlink,
|
||||
Icon,
|
||||
IconButton,
|
||||
IconButtonWithTooltip,
|
||||
useToggle,
|
||||
} from '@openedx/paragon';
|
||||
import {
|
||||
MoreVert as MoveVertIcon,
|
||||
EditOutline as EditIcon,
|
||||
Sync as SyncIcon,
|
||||
} from '@openedx/paragon/icons';
|
||||
|
||||
import { useContentTagsCount } from '../../generic/data/apiHooks';
|
||||
@@ -54,6 +57,9 @@ const CardHeader = ({
|
||||
discussionEnabled,
|
||||
discussionsSettings,
|
||||
parentInfo,
|
||||
extraActionsComponent,
|
||||
onClickSync,
|
||||
readyToSync,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const [searchParams] = useSearchParams();
|
||||
@@ -129,12 +135,28 @@ const CardHeader = ({
|
||||
) : (
|
||||
<>
|
||||
{titleComponent}
|
||||
<IconButton
|
||||
className="item-card-edit-icon"
|
||||
<IconButtonWithTooltip
|
||||
className={classNames(
|
||||
'item-card-button-icon',
|
||||
{
|
||||
'item-card-button-icon-disabled': isDisabledEditField,
|
||||
},
|
||||
)}
|
||||
data-testid={`${namePrefix}-edit-button`}
|
||||
alt={intl.formatMessage(messages.altButtonEdit)}
|
||||
alt={intl.formatMessage(
|
||||
isDisabledEditField ? messages.cannotEditTooltip : messages.altButtonRename,
|
||||
)}
|
||||
tooltipContent={(
|
||||
<div>
|
||||
{intl.formatMessage(
|
||||
isDisabledEditField ? messages.cannotEditTooltip : messages.altButtonRename,
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
iconAs={EditIcon}
|
||||
onClick={onClickEdit}
|
||||
// @ts-ignore
|
||||
disabled={isDisabledEditField}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
@@ -145,6 +167,16 @@ const CardHeader = ({
|
||||
{ getConfig().ENABLE_TAGGING_TAXONOMY_PAGES === 'true' && !!contentTagCount && (
|
||||
<TagCount count={contentTagCount} onClick={openManageTagsDrawer} />
|
||||
)}
|
||||
{extraActionsComponent}
|
||||
{readyToSync && (
|
||||
<IconButtonWithTooltip
|
||||
data-testid={`${namePrefix}-sync-button`}
|
||||
alt={intl.formatMessage(messages.readyToSyncButtonAlt)}
|
||||
iconAs={SyncIcon}
|
||||
tooltipContent={<div>{intl.formatMessage(messages.readyToSyncButtonAlt)}</div>}
|
||||
onClick={onClickSync}
|
||||
/>
|
||||
)}
|
||||
<Dropdown data-testid={`${namePrefix}-card-header__menu`} onClick={onClickMenuButton}>
|
||||
<Dropdown.Toggle
|
||||
className="item-card-header__menu"
|
||||
@@ -176,6 +208,7 @@ const CardHeader = ({
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item
|
||||
data-testid={`${namePrefix}-card-header__menu-configure-button`}
|
||||
disabled={isDisabledEditField}
|
||||
onClick={onClickConfigure}
|
||||
>
|
||||
{intl.formatMessage(messages.menuConfigure)}
|
||||
@@ -183,6 +216,7 @@ const CardHeader = ({
|
||||
{getConfig().ENABLE_TAGGING_TAXONOMY_PAGES === 'true' && (
|
||||
<Dropdown.Item
|
||||
data-testid={`${namePrefix}-card-header__menu-manage-tags-button`}
|
||||
disabled={isDisabledEditField}
|
||||
onClick={openManageTagsDrawer}
|
||||
>
|
||||
{intl.formatMessage(messages.menuManageTags)}
|
||||
@@ -252,6 +286,9 @@ CardHeader.defaultProps = {
|
||||
discussionsSettings: {},
|
||||
parentInfo: {},
|
||||
cardId: '',
|
||||
extraActionsComponent: null,
|
||||
readyToSync: false,
|
||||
onClickSync: null,
|
||||
};
|
||||
|
||||
CardHeader.propTypes = {
|
||||
@@ -295,6 +332,11 @@ CardHeader.propTypes = {
|
||||
isTimeLimited: PropTypes.bool,
|
||||
graded: PropTypes.bool,
|
||||
}),
|
||||
// An optional component that is rendered before the dropdown. This is used by the Subsection
|
||||
// and Unit card components to render their plugin slots.
|
||||
extraActionsComponent: PropTypes.node,
|
||||
onClickSync: PropTypes.func,
|
||||
readyToSync: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default CardHeader;
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
color: $black;
|
||||
}
|
||||
|
||||
.item-card-edit-icon {
|
||||
.item-card-button-icon {
|
||||
opacity: 0;
|
||||
transition: opacity .3s linear;
|
||||
margin-right: .5rem;
|
||||
@@ -23,8 +23,14 @@
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.item-card-edit-icon {
|
||||
.item-card-button-icon {
|
||||
opacity: 1;
|
||||
|
||||
&.item-card-button-icon-disabled {
|
||||
pointer-events: all;
|
||||
opacity: .5;
|
||||
cursor: default;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -240,6 +240,35 @@ describe('<CardHeader />', () => {
|
||||
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 () => {
|
||||
const { findByText, findByTestId } = renderComponent();
|
||||
|
||||
@@ -339,4 +368,19 @@ describe('<CardHeader />', () => {
|
||||
renderComponent();
|
||||
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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -29,9 +29,9 @@ const messages = defineMessages({
|
||||
id: 'course-authoring.course-outline.card.status-badge.draft-unpublished-changes',
|
||||
defaultMessage: 'Draft (Unpublished changes)',
|
||||
},
|
||||
altButtonEdit: {
|
||||
altButtonRename: {
|
||||
id: 'course-authoring.course-outline.card.button.edit.alt',
|
||||
defaultMessage: 'Edit',
|
||||
defaultMessage: 'Rename',
|
||||
},
|
||||
menuPublish: {
|
||||
id: 'course-authoring.course-outline.card.menu.publish',
|
||||
@@ -77,6 +77,16 @@ const messages = defineMessages({
|
||||
id: 'course-authoring.course-outline.card.menu.manageTags',
|
||||
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.',
|
||||
},
|
||||
cannotEditTooltip: {
|
||||
id: 'course-authoring.course-outline.card.button.edit.disable.tooltip',
|
||||
defaultMessage: 'This object was added from a library, so it cannot be edited.',
|
||||
description: 'Tooltip text of button when the object was added from a library.',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
|
||||
@@ -28,7 +28,6 @@ export const getCourseReindexApiUrl = (reindexLink) => `${getApiBaseUrl()}${rein
|
||||
export const getXBlockBaseApiUrl = () => `${getApiBaseUrl()}/xblock/`;
|
||||
export const getCourseItemApiUrl = (itemId) => `${getXBlockBaseApiUrl()}${itemId}`;
|
||||
export const getXBlockApiUrl = (blockId) => `${getXBlockBaseApiUrl()}outline/${blockId}`;
|
||||
export const getClipboardUrl = () => `${getApiBaseUrl()}/api/content-staging/v1/clipboard/`;
|
||||
export const exportTags = (courseId) => `${getApiBaseUrl()}/api/content_tagging/v1/object_tags/${courseId}/export/`;
|
||||
|
||||
/**
|
||||
@@ -36,7 +35,6 @@ export const exportTags = (courseId) => `${getApiBaseUrl()}/api/content_tagging/
|
||||
* @property {string} courseReleaseDate
|
||||
* @property {Object} courseStructure
|
||||
* @property {Object} deprecatedBlocksInfo
|
||||
* @property {string} discussionsIncontextFeedbackUrl
|
||||
* @property {string} discussionsIncontextLearnmoreUrl
|
||||
* @property {Object} initialState
|
||||
* @property {Object} initialUserClipboard
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { RequestStatus } from '../../data/constants';
|
||||
import { updateClipboardData } from '../../generic/data/slice';
|
||||
import { NOTIFICATION_MESSAGES } from '../../constants';
|
||||
import { COURSE_BLOCK_NAMES } from '../constants';
|
||||
import {
|
||||
@@ -54,6 +53,7 @@ import {
|
||||
setPasteFileNotices,
|
||||
updateCourseLaunchQueryStatus,
|
||||
} from './slice';
|
||||
import { createCourseXblock } from '../../course-unit/data/api';
|
||||
|
||||
export function fetchCourseOutlineIndexQuery(courseId) {
|
||||
return async (dispatch) => {
|
||||
@@ -71,7 +71,6 @@ export function fetchCourseOutlineIndexQuery(courseId) {
|
||||
},
|
||||
} = outlineIndex;
|
||||
dispatch(fetchOutlineIndexSuccess(outlineIndex));
|
||||
dispatch(updateClipboardData(outlineIndex.initialUserClipboard));
|
||||
dispatch(updateStatusBar({
|
||||
courseReleaseDate,
|
||||
highlightsEnabledForMessaging,
|
||||
@@ -542,6 +541,26 @@ 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(
|
||||
parentId,
|
||||
blockIds,
|
||||
|
||||
@@ -25,7 +25,7 @@ const HeaderNavigations = ({
|
||||
} = headerNavigationsActions;
|
||||
|
||||
return (
|
||||
<nav className="header-navigations ml-auto">
|
||||
<>
|
||||
{courseActions.childAddable && (
|
||||
<OverlayTrigger
|
||||
placement="bottom"
|
||||
@@ -66,6 +66,8 @@ const HeaderNavigations = ({
|
||||
{hasSections && (
|
||||
<Button
|
||||
variant="outline-primary"
|
||||
id="expand-collapse-all-button"
|
||||
data-testid="expand-collapse-all-button"
|
||||
iconBefore={isSectionsExpanded ? ArrowUpIcon : ArrowDownIcon}
|
||||
onClick={handleExpandAll}
|
||||
>
|
||||
@@ -90,7 +92,7 @@ const HeaderNavigations = ({
|
||||
{intl.formatMessage(messages.viewLiveButton)}
|
||||
</Button>
|
||||
</OverlayTrigger>
|
||||
</nav>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
.header-navigations {
|
||||
display: flex;
|
||||
gap: .75rem;
|
||||
}
|
||||
@@ -4,7 +4,6 @@ import { useNavigate } from 'react-router-dom';
|
||||
import { useToggle } from '@openedx/paragon';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
|
||||
import { copyToClipboard } from '../generic/data/thunks';
|
||||
import { getSavingStatus as getGenericSavingStatus } from '../generic/data/selectors';
|
||||
import { getWaffleFlags } from '../data/selectors';
|
||||
import { RequestStatus } from '../data/constants';
|
||||
@@ -54,6 +53,7 @@ import {
|
||||
setUnitOrderListQuery,
|
||||
pasteClipboardContent,
|
||||
dismissNotificationQuery,
|
||||
addUnitFromLibrary,
|
||||
} from './data/thunk';
|
||||
|
||||
const useCourseOutline = ({ courseId }) => {
|
||||
@@ -67,13 +67,13 @@ const useCourseOutline = ({ courseId }) => {
|
||||
lmsLink,
|
||||
notificationDismissUrl,
|
||||
discussionsSettings,
|
||||
discussionsIncontextFeedbackUrl,
|
||||
discussionsIncontextLearnmoreUrl,
|
||||
deprecatedBlocksInfo,
|
||||
proctoringErrors,
|
||||
mfeProctoredExamSettingsUrl,
|
||||
advanceSettingsUrl,
|
||||
} = useSelector(getOutlineIndexData);
|
||||
|
||||
const { outlineIndexLoadingStatus, reIndexLoadingStatus } = useSelector(getLoadingStatus);
|
||||
const statusBarData = useSelector(getStatusBarData);
|
||||
const savingStatus = useSelector(getSavingStatus);
|
||||
@@ -97,10 +97,6 @@ const useCourseOutline = ({ courseId }) => {
|
||||
|
||||
const isSavingStatusFailed = savingStatus === RequestStatus.FAILED || genericSavingStatus === RequestStatus.FAILED;
|
||||
|
||||
const handleCopyToClipboardClick = (usageKey) => {
|
||||
dispatch(copyToClipboard(usageKey));
|
||||
};
|
||||
|
||||
const handlePasteClipboardClick = (parentLocator, sectionId) => {
|
||||
dispatch(pasteClipboardContent(parentLocator, sectionId));
|
||||
};
|
||||
@@ -133,6 +129,10 @@ const useCourseOutline = ({ courseId }) => {
|
||||
dispatch(addNewUnitQuery(subsectionId, openUnitPage));
|
||||
};
|
||||
|
||||
const handleAddUnitFromLibrary = (body) => {
|
||||
dispatch(addUnitFromLibrary(body, openUnitPage));
|
||||
};
|
||||
|
||||
const headerNavigationsActions = {
|
||||
handleNewSection: handleNewSectionSubmit,
|
||||
handleReIndex: () => {
|
||||
@@ -341,12 +341,11 @@ const useCourseOutline = ({ courseId }) => {
|
||||
getUnitUrl,
|
||||
openUnitPage,
|
||||
handleNewUnitSubmit,
|
||||
handleAddUnitFromLibrary,
|
||||
handleVideoSharingOptionChange,
|
||||
handleCopyToClipboardClick,
|
||||
handlePasteClipboardClick,
|
||||
notificationDismissUrl,
|
||||
discussionsSettings,
|
||||
discussionsIncontextFeedbackUrl,
|
||||
discussionsIncontextLearnmoreUrl,
|
||||
deprecatedBlocksInfo,
|
||||
proctoringErrors,
|
||||
|
||||
@@ -26,7 +26,6 @@ const OutlineSideBar = ({ courseId }) => {
|
||||
|
||||
return (
|
||||
<HelpSidebar
|
||||
intl={intl}
|
||||
courseId={courseId}
|
||||
showOtherSettings={false}
|
||||
className="outline-sidebar mt-4"
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
import {
|
||||
Alert, Button, Hyperlink, Truncate,
|
||||
} from '@openedx/paragon';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
|
||||
import ErrorAlert from '../../editors/sharedComponents/ErrorAlerts/ErrorAlert';
|
||||
import { RequestStatus } from '../../data/constants';
|
||||
@@ -24,13 +24,13 @@ import advancedSettingsMessages from '../../advanced-settings/messages';
|
||||
import { getPasteFileNotices } from '../data/selectors';
|
||||
import { dismissError, removePasteFileNotices } from '../data/slice';
|
||||
import { API_ERROR_TYPES } from '../constants';
|
||||
import { OutOfSyncAlert } from '../../course-libraries/OutOfSyncAlert';
|
||||
|
||||
const PageAlerts = ({
|
||||
courseId,
|
||||
notificationDismissUrl,
|
||||
handleDismissNotification,
|
||||
discussionsSettings,
|
||||
discussionsIncontextFeedbackUrl,
|
||||
discussionsIncontextLearnmoreUrl,
|
||||
deprecatedBlocksInfo,
|
||||
proctoringErrors,
|
||||
@@ -48,6 +48,8 @@ const PageAlerts = ({
|
||||
localStorage.getItem(discussionAlertDismissKey) === null,
|
||||
);
|
||||
const { newFiles, conflictingFiles, errorFiles } = useSelector(getPasteFileNotices);
|
||||
const [showOutOfSyncAlert, setShowOutOfSyncAlert] = useState(false);
|
||||
const navigate = useNavigate();
|
||||
|
||||
const getAssetsUrl = () => {
|
||||
if (getConfig().ENABLE_ASSETS_PAGE === 'true') {
|
||||
@@ -111,13 +113,6 @@ const PageAlerts = ({
|
||||
platformName: process.env.SITE_NAME,
|
||||
})}
|
||||
</div>
|
||||
<Hyperlink
|
||||
showLaunchIcon={false}
|
||||
destination={discussionsIncontextFeedbackUrl}
|
||||
target="_blank"
|
||||
>
|
||||
{intl.formatMessage(messages.discussionNotificationFeedback)}
|
||||
</Hyperlink>
|
||||
</Alert>
|
||||
);
|
||||
};
|
||||
@@ -419,6 +414,15 @@ const PageAlerts = ({
|
||||
);
|
||||
};
|
||||
|
||||
const renderOutOfSyncAlert = () => (
|
||||
<OutOfSyncAlert
|
||||
courseId={courseId}
|
||||
onReview={() => navigate(`/course/${courseId}/libraries?tab=review`)}
|
||||
showAlert={showOutOfSyncAlert}
|
||||
setShowAlert={setShowOutOfSyncAlert}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{configurationErrors()}
|
||||
@@ -432,6 +436,7 @@ const PageAlerts = ({
|
||||
{errorFilesPasteAlert()}
|
||||
{conflictingFilesPasteAlert()}
|
||||
{newFilesPasteAlert()}
|
||||
{renderOutOfSyncAlert()}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -440,7 +445,6 @@ PageAlerts.defaultProps = {
|
||||
notificationDismissUrl: '',
|
||||
handleDismissNotification: null,
|
||||
discussionsSettings: {},
|
||||
discussionsIncontextFeedbackUrl: '',
|
||||
discussionsIncontextLearnmoreUrl: '',
|
||||
deprecatedBlocksInfo: {},
|
||||
proctoringErrors: [],
|
||||
@@ -457,7 +461,6 @@ PageAlerts.propTypes = {
|
||||
discussionsSettings: PropTypes.shape({
|
||||
providerType: PropTypes.string,
|
||||
}),
|
||||
discussionsIncontextFeedbackUrl: PropTypes.string,
|
||||
discussionsIncontextLearnmoreUrl: PropTypes.string,
|
||||
deprecatedBlocksInfo: PropTypes.shape({
|
||||
blocks: PropTypes.arrayOf(PropTypes.arrayOf(PropTypes.string)),
|
||||
|
||||
@@ -5,7 +5,6 @@ import {
|
||||
render,
|
||||
fireEvent,
|
||||
screen,
|
||||
waitFor,
|
||||
} from '@testing-library/react';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { AppProvider } from '@edx/frontend-platform/react';
|
||||
@@ -28,6 +27,13 @@ jest.mock('react-redux', () => ({
|
||||
useSelector: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('../../course-libraries/data/apiHooks', () => ({
|
||||
useEntityLinksSummaryByDownstreamContext: () => ({
|
||||
data: [],
|
||||
isLoading: false,
|
||||
}),
|
||||
}));
|
||||
|
||||
let store;
|
||||
const handleDismissNotification = jest.fn();
|
||||
|
||||
@@ -36,7 +42,6 @@ const pageAlertsData = {
|
||||
notificationDismissUrl: '',
|
||||
handleDismissNotification: null,
|
||||
discussionsSettings: {},
|
||||
discussionsIncontextFeedbackUrl: '',
|
||||
discussionsIncontextLearnmoreUrl: '',
|
||||
deprecatedBlocksInfo: {},
|
||||
proctoringErrors: [],
|
||||
@@ -70,9 +75,9 @@ describe('<PageAlerts />', () => {
|
||||
useSelector.mockReturnValue({});
|
||||
});
|
||||
|
||||
it('renders null when no alerts are present', () => {
|
||||
it('renders null when no alerts are present', async () => {
|
||||
renderComponent();
|
||||
expect(screen.queryByTestId('browser-router')).toBeEmptyDOMElement();
|
||||
expect(await screen.findByTestId('browser-router')).toBeEmptyDOMElement();
|
||||
});
|
||||
|
||||
it('renders configuration alerts', async () => {
|
||||
@@ -95,7 +100,6 @@ describe('<PageAlerts />', () => {
|
||||
discussionsSettings: {
|
||||
providerType: 'openedx',
|
||||
},
|
||||
discussionsIncontextFeedbackUrl: 'some-feedback-url',
|
||||
discussionsIncontextLearnmoreUrl: 'some-learn-more-url',
|
||||
});
|
||||
|
||||
@@ -108,12 +112,6 @@ describe('<PageAlerts />', () => {
|
||||
fireEvent.click(dismissBtn);
|
||||
const discussionAlertDismissKey = `discussionAlertDismissed-${pageAlertsData.courseId}`;
|
||||
expect(localStorage.getItem(discussionAlertDismissKey)).toBe('true');
|
||||
|
||||
await waitFor(() => {
|
||||
const feedbackLink = screen.queryByText(messages.discussionNotificationFeedback.defaultMessage);
|
||||
expect(feedbackLink).toBeInTheDocument();
|
||||
expect(feedbackLink).toHaveAttribute('href', 'some-feedback-url');
|
||||
});
|
||||
});
|
||||
|
||||
it('renders deprecation warning alerts', async () => {
|
||||
|
||||
@@ -13,8 +13,8 @@ import classNames from 'classnames';
|
||||
import { setCurrentItem, setCurrentSection } from '../data/slice';
|
||||
import { RequestStatus } from '../../data/constants';
|
||||
import CardHeader from '../card-header/CardHeader';
|
||||
import SortableItem from '../../generic/drag-helper/SortableItem';
|
||||
import { DragContext } from '../../generic/drag-helper/DragContextProvider';
|
||||
import SortableItem from '../drag-helper/SortableItem';
|
||||
import { DragContext } from '../drag-helper/DragContextProvider';
|
||||
import TitleButton from '../card-header/TitleButton';
|
||||
import XBlockStatus from '../xblock-status/XBlockStatus';
|
||||
import { getItemStatus, getItemStatusBorder, scrollToElement } from '../utils';
|
||||
|
||||
@@ -1,30 +1,37 @@
|
||||
// @ts-check
|
||||
import React, {
|
||||
useContext, useEffect, useState, useRef,
|
||||
useContext, useEffect, useState, useRef, useCallback,
|
||||
} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Button, useToggle } from '@openedx/paragon';
|
||||
import { Button, StandardModal, useToggle } from '@openedx/paragon';
|
||||
import { Add as IconAdd } from '@openedx/paragon/icons';
|
||||
import classNames from 'classnames';
|
||||
import { isEmpty } from 'lodash';
|
||||
|
||||
import CourseOutlineSubsectionCardExtraActionsSlot from '../../plugin-slots/CourseOutlineSubsectionCardExtraActionsSlot';
|
||||
import { setCurrentItem, setCurrentSection, setCurrentSubsection } from '../data/slice';
|
||||
import { RequestStatus } from '../../data/constants';
|
||||
import CardHeader from '../card-header/CardHeader';
|
||||
import SortableItem from '../../generic/drag-helper/SortableItem';
|
||||
import { DragContext } from '../../generic/drag-helper/DragContextProvider';
|
||||
import { useCopyToClipboard, PasteComponent } from '../../generic/clipboard';
|
||||
import SortableItem from '../drag-helper/SortableItem';
|
||||
import { DragContext } from '../drag-helper/DragContextProvider';
|
||||
import { useClipboard, PasteComponent } from '../../generic/clipboard';
|
||||
import TitleButton from '../card-header/TitleButton';
|
||||
import XBlockStatus from '../xblock-status/XBlockStatus';
|
||||
import { getItemStatus, getItemStatusBorder, scrollToElement } from '../utils';
|
||||
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 = ({
|
||||
section,
|
||||
subsection,
|
||||
isSectionsExpanded,
|
||||
isSelfPaced,
|
||||
isCustomRelativeDatesActive,
|
||||
children,
|
||||
@@ -36,6 +43,7 @@ const SubsectionCard = ({
|
||||
onOpenDeleteModal,
|
||||
onDuplicateSubmit,
|
||||
onNewUnitSubmit,
|
||||
onAddUnitFromLibrary,
|
||||
onOrderChange,
|
||||
onOpenConfigureModal,
|
||||
onPasteClick,
|
||||
@@ -49,7 +57,18 @@ const SubsectionCard = ({
|
||||
const isScrolledToElement = locatorId === subsection.id;
|
||||
const [isFormOpen, openForm, closeForm] = useToggle(false);
|
||||
const namePrefix = 'subsection';
|
||||
const { sharedClipboardData, showPasteUnit } = useCopyToClipboard();
|
||||
const { sharedClipboardData, showPasteUnit } = useClipboard();
|
||||
// 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 {
|
||||
id,
|
||||
@@ -80,7 +99,7 @@ const SubsectionCard = ({
|
||||
|
||||
return false;
|
||||
};
|
||||
const [isExpanded, setIsExpanded] = useState(containsSearchResult() || !isHeaderVisible);
|
||||
const [isExpanded, setIsExpanded] = useState(containsSearchResult() || !isHeaderVisible || isSectionsExpanded);
|
||||
const subsectionStatus = getItemStatus({
|
||||
published,
|
||||
visibilityState,
|
||||
@@ -88,6 +107,10 @@ const SubsectionCard = ({
|
||||
});
|
||||
const borderStyle = getItemStatusBorder(subsectionStatus);
|
||||
|
||||
useEffect(() => {
|
||||
setIsExpanded(isSectionsExpanded);
|
||||
}, [isSectionsExpanded]);
|
||||
|
||||
const handleExpandContent = () => {
|
||||
setIsExpanded((prevState) => !prevState);
|
||||
};
|
||||
@@ -127,6 +150,13 @@ const SubsectionCard = ({
|
||||
/>
|
||||
);
|
||||
|
||||
const extraActionsComponent = (
|
||||
<CourseOutlineSubsectionCardExtraActionsSlot
|
||||
subsection={subsection}
|
||||
section={section}
|
||||
/>
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (activeId === id && isExpanded) {
|
||||
setIsExpanded(false);
|
||||
@@ -164,89 +194,129 @@ const SubsectionCard = ({
|
||||
&& !(isHeaderVisible === false)
|
||||
);
|
||||
|
||||
const handleSelectLibraryUnit = useCallback((selectedUnit) => {
|
||||
onAddUnitFromLibrary({
|
||||
type: COMPONENT_TYPES.libraryV2,
|
||||
category: ContainerType.Vertical,
|
||||
parentLocator: id,
|
||||
libraryContentKey: selectedUnit.usageKey,
|
||||
});
|
||||
closeAddLibraryUnitModal();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<SortableItem
|
||||
id={id}
|
||||
category={category}
|
||||
key={id}
|
||||
isDraggable={isDraggable}
|
||||
isDroppable={actions.childAddable}
|
||||
componentStyle={{
|
||||
background: '#f8f7f6',
|
||||
...borderStyle,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className={`subsection-card ${isScrolledToElement ? 'highlight' : ''}`}
|
||||
data-testid="subsection-card"
|
||||
ref={currentRef}
|
||||
<>
|
||||
<SortableItem
|
||||
id={id}
|
||||
category={category}
|
||||
key={id}
|
||||
isDraggable={isDraggable}
|
||||
isDroppable={actions.childAddable}
|
||||
componentStyle={{
|
||||
background: '#f8f7f6',
|
||||
...borderStyle,
|
||||
}}
|
||||
>
|
||||
{isHeaderVisible && (
|
||||
<>
|
||||
<CardHeader
|
||||
title={displayName}
|
||||
status={subsectionStatus}
|
||||
cardId={id}
|
||||
hasChanges={hasChanges}
|
||||
onClickMenuButton={handleClickMenuButton}
|
||||
onClickPublish={onOpenPublishModal}
|
||||
onClickEdit={openForm}
|
||||
onClickDelete={onOpenDeleteModal}
|
||||
onClickMoveUp={handleSubsectionMoveUp}
|
||||
onClickMoveDown={handleSubsectionMoveDown}
|
||||
onClickConfigure={onOpenConfigureModal}
|
||||
isFormOpen={isFormOpen}
|
||||
closeForm={closeForm}
|
||||
onEditSubmit={handleEditSubmit}
|
||||
isDisabledEditField={savingStatus === RequestStatus.IN_PROGRESS}
|
||||
onClickDuplicate={onDuplicateSubmit}
|
||||
titleComponent={titleComponent}
|
||||
namePrefix={namePrefix}
|
||||
actions={actions}
|
||||
proctoringExamConfigurationLink={proctoringExamConfigurationLink}
|
||||
isSequential
|
||||
/>
|
||||
<div className="subsection-card__content item-children" data-testid="subsection-card__content">
|
||||
<XBlockStatus
|
||||
isSelfPaced={isSelfPaced}
|
||||
isCustomRelativeDatesActive={isCustomRelativeDatesActive}
|
||||
blockData={subsection}
|
||||
<div
|
||||
className={`subsection-card ${isScrolledToElement ? 'highlight' : ''}`}
|
||||
data-testid="subsection-card"
|
||||
ref={currentRef}
|
||||
>
|
||||
{isHeaderVisible && (
|
||||
<>
|
||||
<CardHeader
|
||||
title={displayName}
|
||||
status={subsectionStatus}
|
||||
cardId={id}
|
||||
hasChanges={hasChanges}
|
||||
onClickMenuButton={handleClickMenuButton}
|
||||
onClickPublish={onOpenPublishModal}
|
||||
onClickEdit={openForm}
|
||||
onClickDelete={onOpenDeleteModal}
|
||||
onClickMoveUp={handleSubsectionMoveUp}
|
||||
onClickMoveDown={handleSubsectionMoveDown}
|
||||
onClickConfigure={onOpenConfigureModal}
|
||||
isFormOpen={isFormOpen}
|
||||
closeForm={closeForm}
|
||||
onEditSubmit={handleEditSubmit}
|
||||
isDisabledEditField={savingStatus === RequestStatus.IN_PROGRESS}
|
||||
onClickDuplicate={onDuplicateSubmit}
|
||||
titleComponent={titleComponent}
|
||||
namePrefix={namePrefix}
|
||||
actions={actions}
|
||||
proctoringExamConfigurationLink={proctoringExamConfigurationLink}
|
||||
isSequential
|
||||
extraActionsComponent={extraActionsComponent}
|
||||
/>
|
||||
</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 && (
|
||||
<PasteComponent
|
||||
<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"
|
||||
text={intl.formatMessage(messages.pasteButton)}
|
||||
clipboardData={sharedClipboardData}
|
||||
onClick={handlePasteButtonClick}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</SortableItem>
|
||||
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>
|
||||
</SortableItem>
|
||||
<StandardModal
|
||||
title={intl.formatMessage(messages.unitPickerModalTitle)}
|
||||
isOpen={isAddLibraryUnitModalOpen}
|
||||
onClose={closeAddLibraryUnitModal}
|
||||
isOverflowVisible={false}
|
||||
size="xl"
|
||||
>
|
||||
<ComponentPicker
|
||||
showOnlyPublished
|
||||
extraFilter={['block_type = "unit"']}
|
||||
componentPickerMode="single"
|
||||
onComponentSelected={handleSelectLibraryUnit}
|
||||
visibleTabs={[ContentType.units]}
|
||||
/>
|
||||
</StandardModal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -289,6 +359,7 @@ SubsectionCard.propTypes = {
|
||||
}).isRequired,
|
||||
}).isRequired,
|
||||
children: PropTypes.node,
|
||||
isSectionsExpanded: PropTypes.bool.isRequired,
|
||||
isSelfPaced: PropTypes.bool.isRequired,
|
||||
isCustomRelativeDatesActive: PropTypes.bool.isRequired,
|
||||
onOpenPublishModal: PropTypes.func.isRequired,
|
||||
@@ -297,6 +368,7 @@ SubsectionCard.propTypes = {
|
||||
onOpenDeleteModal: PropTypes.func.isRequired,
|
||||
onDuplicateSubmit: PropTypes.func.isRequired,
|
||||
onNewUnitSubmit: PropTypes.func.isRequired,
|
||||
onAddUnitFromLibrary: PropTypes.func.isRequired,
|
||||
index: PropTypes.number.isRequired,
|
||||
getPossibleMoves: PropTypes.func.isRequired,
|
||||
onOrderChange: PropTypes.func.isRequired,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import {
|
||||
act, render, fireEvent, within,
|
||||
act, render, fireEvent, within, screen,
|
||||
} from '@testing-library/react';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { AppProvider } from '@edx/frontend-platform/react';
|
||||
@@ -10,9 +10,12 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import initializeStore from '../../store';
|
||||
import SubsectionCard from './SubsectionCard';
|
||||
import cardHeaderMessages from '../card-header/messages';
|
||||
import { COMPONENT_TYPES } from '../../generic/block-type-utils/constants';
|
||||
|
||||
let store;
|
||||
const mockPathname = '/foo-bar';
|
||||
const containerKey = 'lct:org:lib:unit:1';
|
||||
const handleOnAddUnitFromLibrary = jest.fn();
|
||||
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
@@ -21,12 +24,30 @@ jest.mock('react-router-dom', () => ({
|
||||
}),
|
||||
}));
|
||||
|
||||
const clipboardBroadcastChannelMock = {
|
||||
postMessage: jest.fn(),
|
||||
close: jest.fn(),
|
||||
};
|
||||
jest.mock('react-redux', () => ({
|
||||
...jest.requireActual('react-redux'),
|
||||
useSelector: () => ({
|
||||
librariesV2Enabled: true,
|
||||
}),
|
||||
}));
|
||||
|
||||
global.BroadcastChannel = jest.fn(() => clipboardBroadcastChannelMock);
|
||||
// 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 unit = {
|
||||
id: 'unit-1',
|
||||
@@ -87,6 +108,7 @@ const renderComponent = (props, entry = '/') => render(
|
||||
onOpenHighlightsModal={jest.fn()}
|
||||
onOpenDeleteModal={jest.fn()}
|
||||
onNewUnitSubmit={jest.fn()}
|
||||
onAddUnitFromLibrary={handleOnAddUnitFromLibrary}
|
||||
isCustomRelativeDatesActive={false}
|
||||
onEditClick={jest.fn()}
|
||||
savingStatus=""
|
||||
@@ -254,4 +276,31 @@ describe('<SubsectionCard />', () => {
|
||||
expect(cardUnits).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,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,10 +4,22 @@ const messages = defineMessages({
|
||||
newUnitButton: {
|
||||
id: 'course-authoring.course-outline.subsection.button.new-unit',
|
||||
defaultMessage: 'New unit',
|
||||
description: 'Message of the button to create a new unit in a subsection.',
|
||||
},
|
||||
pasteButton: {
|
||||
id: 'course-authoring.course-outline.subsection.button.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.',
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -1,18 +1,28 @@
|
||||
// @ts-check
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import React, {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useToggle } from '@openedx/paragon';
|
||||
import { isEmpty } from 'lodash';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
|
||||
import CourseOutlineUnitCardExtraActionsSlot from '../../plugin-slots/CourseOutlineUnitCardExtraActionsSlot';
|
||||
import { setCurrentItem, setCurrentSection, setCurrentSubsection } from '../data/slice';
|
||||
import { fetchCourseSectionQuery } from '../data/thunk';
|
||||
import { RequestStatus } from '../../data/constants';
|
||||
import { isUnitReadOnly } from '../../course-unit/data/utils';
|
||||
import CardHeader from '../card-header/CardHeader';
|
||||
import SortableItem from '../../generic/drag-helper/SortableItem';
|
||||
import SortableItem from '../drag-helper/SortableItem';
|
||||
import TitleLink from '../card-header/TitleLink';
|
||||
import XBlockStatus from '../xblock-status/XBlockStatus';
|
||||
import { getItemStatus, getItemStatusBorder, scrollToElement } from '../utils';
|
||||
import { useClipboard } from '../../generic/clipboard';
|
||||
import { PreviewLibraryXBlockChanges } from '../../course-unit/preview-changes';
|
||||
|
||||
const UnitCard = ({
|
||||
unit,
|
||||
@@ -30,7 +40,6 @@ const UnitCard = ({
|
||||
onDuplicateSubmit,
|
||||
getTitleLink,
|
||||
onOrderChange,
|
||||
onCopyToClipboardClick,
|
||||
discussionsSettings,
|
||||
}) => {
|
||||
const currentRef = useRef(null);
|
||||
@@ -39,8 +48,11 @@ const UnitCard = ({
|
||||
const locatorId = searchParams.get('show');
|
||||
const isScrolledToElement = locatorId === unit.id;
|
||||
const [isFormOpen, openForm, closeForm] = useToggle(false);
|
||||
const [isSyncModalOpen, openSyncModal, closeSyncModal] = useToggle(false);
|
||||
const namePrefix = 'unit';
|
||||
|
||||
const { copyToClipboard } = useClipboard();
|
||||
|
||||
const {
|
||||
id,
|
||||
category,
|
||||
@@ -52,8 +64,24 @@ const UnitCard = ({
|
||||
isHeaderVisible = true,
|
||||
enableCopyPasteUnits = false,
|
||||
discussionEnabled,
|
||||
upstreamInfo,
|
||||
} = 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
|
||||
const actions = { ...unitActions };
|
||||
// add actions to control display of move up & down menu buton.
|
||||
@@ -98,9 +126,13 @@ const UnitCard = ({
|
||||
};
|
||||
|
||||
const handleCopyClick = () => {
|
||||
onCopyToClipboardClick(unit.id);
|
||||
copyToClipboard(id);
|
||||
};
|
||||
|
||||
const handleOnPostChangeSync = useCallback(async () => {
|
||||
await dispatch(fetchCourseSectionQuery([section.id]));
|
||||
}, [dispatch, section]);
|
||||
|
||||
const titleComponent = (
|
||||
<TitleLink
|
||||
title={displayName}
|
||||
@@ -109,6 +141,14 @@ const UnitCard = ({
|
||||
/>
|
||||
);
|
||||
|
||||
const extraActionsComponent = (
|
||||
<CourseOutlineUnitCardExtraActionsSlot
|
||||
unit={unit}
|
||||
subsection={subsection}
|
||||
section={section}
|
||||
/>
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
// if this items has been newly added, scroll to it.
|
||||
// we need to check section.shouldScroll as whole section is fetched when a
|
||||
@@ -133,58 +173,71 @@ const UnitCard = ({
|
||||
const isDraggable = actions.draggable && (actions.allowMoveUp || actions.allowMoveDown);
|
||||
|
||||
return (
|
||||
<SortableItem
|
||||
id={id}
|
||||
category={category}
|
||||
key={id}
|
||||
isDraggable={isDraggable}
|
||||
isDroppable={actions.childAddable}
|
||||
componentStyle={{
|
||||
background: '#fdfdfd',
|
||||
...borderStyle,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className={`unit-card ${isScrolledToElement ? 'highlight' : ''}`}
|
||||
data-testid="unit-card"
|
||||
ref={currentRef}
|
||||
<>
|
||||
<SortableItem
|
||||
id={id}
|
||||
category={category}
|
||||
key={id}
|
||||
isDraggable={isDraggable}
|
||||
isDroppable={actions.childAddable}
|
||||
componentStyle={{
|
||||
background: '#fdfdfd',
|
||||
...borderStyle,
|
||||
}}
|
||||
>
|
||||
<CardHeader
|
||||
title={displayName}
|
||||
status={unitStatus}
|
||||
hasChanges={hasChanges}
|
||||
cardId={id}
|
||||
onClickMenuButton={handleClickMenuButton}
|
||||
onClickPublish={onOpenPublishModal}
|
||||
onClickConfigure={onOpenConfigureModal}
|
||||
onClickEdit={openForm}
|
||||
onClickDelete={onOpenDeleteModal}
|
||||
onClickMoveUp={handleUnitMoveUp}
|
||||
onClickMoveDown={handleUnitMoveDown}
|
||||
isFormOpen={isFormOpen}
|
||||
closeForm={closeForm}
|
||||
onEditSubmit={handleEditSubmit}
|
||||
isDisabledEditField={savingStatus === RequestStatus.IN_PROGRESS}
|
||||
onClickDuplicate={onDuplicateSubmit}
|
||||
titleComponent={titleComponent}
|
||||
namePrefix={namePrefix}
|
||||
actions={actions}
|
||||
isVertical
|
||||
enableCopyPasteUnits={enableCopyPasteUnits}
|
||||
onClickCopy={handleCopyClick}
|
||||
discussionEnabled={discussionEnabled}
|
||||
discussionsSettings={discussionsSettings}
|
||||
parentInfo={parentInfo}
|
||||
/>
|
||||
<div className="unit-card__content item-children" data-testid="unit-card__content">
|
||||
<XBlockStatus
|
||||
isSelfPaced={isSelfPaced}
|
||||
isCustomRelativeDatesActive={isCustomRelativeDatesActive}
|
||||
blockData={unit}
|
||||
<div
|
||||
className={`unit-card ${isScrolledToElement ? 'highlight' : ''}`}
|
||||
data-testid="unit-card"
|
||||
ref={currentRef}
|
||||
>
|
||||
<CardHeader
|
||||
title={displayName}
|
||||
status={unitStatus}
|
||||
hasChanges={hasChanges}
|
||||
cardId={id}
|
||||
onClickMenuButton={handleClickMenuButton}
|
||||
onClickPublish={onOpenPublishModal}
|
||||
onClickConfigure={onOpenConfigureModal}
|
||||
onClickEdit={openForm}
|
||||
onClickDelete={onOpenDeleteModal}
|
||||
onClickMoveUp={handleUnitMoveUp}
|
||||
onClickMoveDown={handleUnitMoveDown}
|
||||
onClickSync={openSyncModal}
|
||||
isFormOpen={isFormOpen}
|
||||
closeForm={closeForm}
|
||||
onEditSubmit={handleEditSubmit}
|
||||
isDisabledEditField={readOnly || savingStatus === RequestStatus.IN_PROGRESS}
|
||||
onClickDuplicate={onDuplicateSubmit}
|
||||
titleComponent={titleComponent}
|
||||
namePrefix={namePrefix}
|
||||
actions={actions}
|
||||
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>
|
||||
</div>
|
||||
</SortableItem>
|
||||
</SortableItem>
|
||||
{blockSyncData && (
|
||||
<PreviewLibraryXBlockChanges
|
||||
blockData={blockSyncData}
|
||||
isModalOpen={isSyncModalOpen}
|
||||
closeModal={closeSyncModal}
|
||||
postChange={handleOnPostChangeSync}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -210,6 +263,11 @@ UnitCard.propTypes = {
|
||||
isHeaderVisible: PropTypes.bool,
|
||||
enableCopyPasteUnits: PropTypes.bool,
|
||||
discussionEnabled: PropTypes.bool,
|
||||
upstreamInfo: PropTypes.shape({
|
||||
readyToSync: PropTypes.bool.isRequired,
|
||||
upstreamRef: PropTypes.string.isRequired,
|
||||
versionSynced: PropTypes.number.isRequired,
|
||||
}).isRequired,
|
||||
}).isRequired,
|
||||
subsection: PropTypes.shape({
|
||||
id: PropTypes.string.isRequired,
|
||||
@@ -241,7 +299,6 @@ UnitCard.propTypes = {
|
||||
onOrderChange: PropTypes.func.isRequired,
|
||||
isSelfPaced: PropTypes.bool.isRequired,
|
||||
isCustomRelativeDatesActive: PropTypes.bool.isRequired,
|
||||
onCopyToClipboardClick: PropTypes.func.isRequired,
|
||||
discussionsSettings: PropTypes.shape({
|
||||
providerType: PropTypes.string,
|
||||
enableGradedUnits: PropTypes.bool,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
act, render, fireEvent, within,
|
||||
act, render, fireEvent, within, screen,
|
||||
waitFor,
|
||||
} from '@testing-library/react';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { AppProvider } from '@edx/frontend-platform/react';
|
||||
@@ -12,6 +12,17 @@ import UnitCard from './UnitCard';
|
||||
import cardMessages from '../card-header/messages';
|
||||
|
||||
let store;
|
||||
const mockUseAcceptLibraryBlockChanges = jest.fn();
|
||||
const mockUseIgnoreLibraryBlockChanges = jest.fn();
|
||||
|
||||
jest.mock('../../course-unit/data/apiHooks', () => ({
|
||||
useAcceptLibraryBlockChanges: () => ({
|
||||
mutateAsync: mockUseAcceptLibraryBlockChanges,
|
||||
}),
|
||||
useIgnoreLibraryBlockChanges: () => ({
|
||||
mutateAsync: mockUseIgnoreLibraryBlockChanges,
|
||||
}),
|
||||
}));
|
||||
|
||||
const section = {
|
||||
id: '1',
|
||||
@@ -44,6 +55,11 @@ const unit = {
|
||||
duplicable: true,
|
||||
},
|
||||
isHeaderVisible: true,
|
||||
upstreamInfo: {
|
||||
readyToSync: true,
|
||||
upstreamRef: 'lct:org1:lib1:unit:1',
|
||||
versionSynced: 1,
|
||||
},
|
||||
};
|
||||
|
||||
const queryClient = new QueryClient();
|
||||
@@ -62,7 +78,6 @@ const renderComponent = (props) => render(
|
||||
onOpenPublishModal={jest.fn()}
|
||||
onOpenDeleteModal={jest.fn()}
|
||||
onOpenConfigureModal={jest.fn()}
|
||||
onCopyToClipboardClick={jest.fn()}
|
||||
savingStatus=""
|
||||
onEditSubmit={jest.fn()}
|
||||
onDuplicateSubmit={jest.fn()}
|
||||
@@ -149,4 +164,51 @@ describe('<UnitCard />', () => {
|
||||
});
|
||||
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 for unit changes at this time')).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 for unit changes at this time')).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());
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,7 +6,7 @@ import { initializeMockApp } from '@edx/frontend-platform';
|
||||
import { IntlProvider, injectIntl } from '@edx/frontend-platform/i18n';
|
||||
import { AppProvider } from '@edx/frontend-platform/react';
|
||||
import {
|
||||
act, fireEvent, render, waitFor,
|
||||
fireEvent, render, waitFor,
|
||||
} from '@testing-library/react';
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
|
||||
@@ -78,16 +78,15 @@ describe('<CourseRerun />', () => {
|
||||
it('shows the spinner before the query is complete', async () => {
|
||||
useSelector.mockReturnValue({ organizationLoadingStatus: RequestStatus.IN_PROGRESS });
|
||||
|
||||
await act(async () => {
|
||||
const { getByRole } = render(<RootWrapper />);
|
||||
const spinner = getByRole('status');
|
||||
expect(spinner.textContent).toEqual('Loading...');
|
||||
});
|
||||
const { findByRole } = render(<RootWrapper />);
|
||||
const spinner = await findByRole('status');
|
||||
expect(spinner.textContent).toEqual('Loading...');
|
||||
});
|
||||
|
||||
it('should show footer', () => {
|
||||
const { getByText } = render(<RootWrapper />);
|
||||
expect(getByText('Looking for help with Studio?')).toBeInTheDocument();
|
||||
expect(getByText('LMS')).toHaveAttribute('href', process.env.LMS_BASE_URL);
|
||||
it('should show footer', async () => {
|
||||
const { findByText } = render(<RootWrapper />);
|
||||
await findByText('Looking for help with Studio?');
|
||||
const lmsElement = await findByText('LMS');
|
||||
expect(lmsElement).toHaveAttribute('href', process.env.LMS_BASE_URL);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,7 +7,8 @@ import {
|
||||
ActionRow,
|
||||
Button,
|
||||
} from '@openedx/paragon';
|
||||
import { StudioFooter } from '@edx/frontend-component-footer';
|
||||
import { StudioFooterSlot } from '@edx/frontend-component-footer';
|
||||
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
|
||||
import Header from '../header';
|
||||
@@ -88,7 +89,7 @@ const CourseRerun = () => {
|
||||
isQueryPending={savingStatus === RequestStatus.PENDING}
|
||||
/>
|
||||
</div>
|
||||
<StudioFooter />
|
||||
<StudioFooterSlot />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -3,14 +3,14 @@ import PropTypes from 'prop-types';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import {
|
||||
Container, Layout, Stack, Button, TransitionReplace,
|
||||
Alert, Container, Layout, Button, TransitionReplace,
|
||||
} from '@openedx/paragon';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { useIntl, injectIntl } from '@edx/frontend-platform/i18n';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
Warning as WarningIcon,
|
||||
CheckCircle as CheckCircleIcon,
|
||||
} from '@openedx/paragon/icons';
|
||||
import { CourseAuthoringUnitSidebarSlot } from '../plugin-slots/CourseAuthoringUnitSidebarSlot';
|
||||
|
||||
import { getProcessingNotification } from '../generic/processing-notification/data/selectors';
|
||||
import SubHeader from '../generic/sub-header/SubHeader';
|
||||
@@ -25,25 +25,23 @@ import Loading from '../generic/Loading';
|
||||
import AddComponent from './add-component/AddComponent';
|
||||
import HeaderTitle from './header-title/HeaderTitle';
|
||||
import Breadcrumbs from './breadcrumbs/Breadcrumbs';
|
||||
import HeaderNavigations from './header-navigations/HeaderNavigations';
|
||||
import Sequence from './course-sequence';
|
||||
import Sidebar from './sidebar';
|
||||
import { useCourseUnit, useLayoutGrid, useScrollToLastPosition } from './hooks';
|
||||
import messages from './messages';
|
||||
import PublishControls from './sidebar/PublishControls';
|
||||
import LocationInfo from './sidebar/LocationInfo';
|
||||
import TagsSidebarControls from '../content-tags-drawer/tags-sidebar-controls';
|
||||
import { PasteNotificationAlert } from './clipboard';
|
||||
import XBlockContainerIframe from './xblock-container-iframe';
|
||||
import MoveModal from './move-modal';
|
||||
import PreviewLibraryXBlockChanges from './preview-changes';
|
||||
import IframePreviewLibraryXBlockChanges from './preview-changes';
|
||||
import CourseUnitHeaderActionsSlot from '../plugin-slots/CourseUnitHeaderActionsSlot';
|
||||
|
||||
const CourseUnit = ({ courseId }) => {
|
||||
const { blockId } = useParams();
|
||||
const intl = useIntl();
|
||||
const {
|
||||
courseUnit,
|
||||
isLoading,
|
||||
sequenceId,
|
||||
courseUnitLoadingStatus,
|
||||
unitTitle,
|
||||
unitCategory,
|
||||
errorMessage,
|
||||
@@ -52,6 +50,7 @@ const CourseUnit = ({ courseId }) => {
|
||||
isTitleEditFormOpen,
|
||||
isUnitVerticalType,
|
||||
isUnitLibraryType,
|
||||
isSplitTestType,
|
||||
staticFileNotices,
|
||||
currentlyVisibleToStudents,
|
||||
unitXBlockActions,
|
||||
@@ -72,9 +71,12 @@ const CourseUnit = ({ courseId }) => {
|
||||
handleRollbackMovedXBlock,
|
||||
handleCloseXBlockMovedAlert,
|
||||
handleNavigateToTargetUnit,
|
||||
addComponentTemplateData,
|
||||
} = useCourseUnit({ courseId, blockId });
|
||||
const layoutGrid = useLayoutGrid(unitCategory, isUnitLibraryType);
|
||||
|
||||
const readOnly = !!courseUnit.readOnly;
|
||||
|
||||
useEffect(() => {
|
||||
document.title = getPageHeadTitle('', unitTitle);
|
||||
}, [unitTitle]);
|
||||
@@ -136,6 +138,24 @@ const CourseUnit = ({ courseId }) => {
|
||||
/>
|
||||
) : null}
|
||||
</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
|
||||
hideBorder
|
||||
title={(
|
||||
@@ -154,9 +174,11 @@ const CourseUnit = ({ courseId }) => {
|
||||
/>
|
||||
)}
|
||||
headerActions={(
|
||||
<HeaderNavigations
|
||||
unitCategory={unitCategory}
|
||||
<CourseUnitHeaderActionsSlot
|
||||
category={unitCategory}
|
||||
headerNavigationsActions={headerNavigationsActions}
|
||||
unitTitle={unitTitle}
|
||||
verticalBlocks={courseVerticalChildren.children}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
@@ -188,20 +210,27 @@ const CourseUnit = ({ courseId }) => {
|
||||
<XBlockContainerIframe
|
||||
courseId={courseId}
|
||||
blockId={blockId}
|
||||
isUnitVerticalType={isUnitVerticalType}
|
||||
courseUnitLoadingStatus={courseUnitLoadingStatus}
|
||||
unitXBlockActions={unitXBlockActions}
|
||||
courseVerticalChildren={courseVerticalChildren.children}
|
||||
handleConfigureSubmit={handleConfigureSubmit}
|
||||
/>
|
||||
{isUnitVerticalType && (
|
||||
{!readOnly && (
|
||||
<AddComponent
|
||||
blockId={blockId}
|
||||
parentLocator={blockId}
|
||||
isSplitTestType={isSplitTestType}
|
||||
isUnitVerticalType={isUnitVerticalType}
|
||||
handleCreateNewCourseXBlock={handleCreateNewCourseXBlock}
|
||||
addComponentTemplateData={addComponentTemplateData}
|
||||
/>
|
||||
)}
|
||||
{showPasteXBlock && canPasteComponent && isUnitVerticalType && (
|
||||
{!readOnly && showPasteXBlock && canPasteComponent && isUnitVerticalType && (
|
||||
<PasteComponent
|
||||
clipboardData={sharedClipboardData}
|
||||
onClick={handleCreateNewCourseXBlock}
|
||||
onClick={
|
||||
() => handleCreateNewCourseXBlock({ stagedContent: 'clipboard', parentLocator: blockId })
|
||||
}
|
||||
text={intl.formatMessage(messages.pasteButtonText)}
|
||||
/>
|
||||
)}
|
||||
@@ -211,26 +240,18 @@ const CourseUnit = ({ courseId }) => {
|
||||
closeModal={closeMoveModal}
|
||||
courseId={courseId}
|
||||
/>
|
||||
<PreviewLibraryXBlockChanges />
|
||||
<IframePreviewLibraryXBlockChanges />
|
||||
</Layout.Element>
|
||||
<Layout.Element>
|
||||
<Stack gap={3}>
|
||||
{isUnitVerticalType && (
|
||||
<>
|
||||
<Sidebar data-testid="course-unit-sidebar">
|
||||
<PublishControls blockId={blockId} />
|
||||
</Sidebar>
|
||||
{getConfig().ENABLE_TAGGING_TAXONOMY_PAGES === 'true' && (
|
||||
<Sidebar className="tags-sidebar">
|
||||
<TagsSidebarControls />
|
||||
</Sidebar>
|
||||
)}
|
||||
<Sidebar data-testid="course-unit-location-sidebar">
|
||||
<LocationInfo />
|
||||
</Sidebar>
|
||||
</>
|
||||
)}
|
||||
</Stack>
|
||||
<CourseAuthoringUnitSidebarSlot
|
||||
courseId={courseId}
|
||||
blockId={blockId}
|
||||
unitTitle={unitTitle}
|
||||
xBlocks={courseVerticalChildren.children}
|
||||
readOnly={readOnly}
|
||||
isUnitVerticalType={isUnitVerticalType}
|
||||
isSplitTestType={isSplitTestType}
|
||||
/>
|
||||
</Layout.Element>
|
||||
</Layout>
|
||||
</section>
|
||||
@@ -253,4 +274,4 @@ CourseUnit.propTypes = {
|
||||
courseId: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(CourseUnit);
|
||||
export default CourseUnit;
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
@import "./header-title/HeaderTitle";
|
||||
@import "./move-modal";
|
||||
@import "./preview-changes";
|
||||
@import "./xblock-container-iframe";
|
||||
|
||||
.course-unit {
|
||||
min-width: 900px;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import {
|
||||
act, render, waitFor, within, screen,
|
||||
} from '@testing-library/react';
|
||||
@@ -16,7 +17,6 @@ import { cloneDeep, set } from 'lodash';
|
||||
|
||||
import {
|
||||
getCourseSectionVerticalApiUrl,
|
||||
getCourseUnitApiUrl,
|
||||
getCourseVerticalChildrenApiUrl,
|
||||
getCourseOutlineInfoUrl,
|
||||
getXBlockBaseApiUrl,
|
||||
@@ -27,7 +27,6 @@ import {
|
||||
deleteUnitItemQuery,
|
||||
editCourseUnitVisibilityAndData,
|
||||
fetchCourseSectionVerticalData,
|
||||
fetchCourseUnitQuery,
|
||||
fetchCourseVerticalChildrenData,
|
||||
getCourseOutlineInfoQuery,
|
||||
patchUnitItemQuery,
|
||||
@@ -36,21 +35,18 @@ import initializeStore from '../store';
|
||||
import {
|
||||
courseCreateXblockMock,
|
||||
courseSectionVerticalMock,
|
||||
courseUnitIndexMock,
|
||||
courseUnitMock,
|
||||
courseVerticalChildrenMock,
|
||||
clipboardMockResponse,
|
||||
courseOutlineInfoMock,
|
||||
} from './__mocks__';
|
||||
import { clipboardUnit, clipboardXBlock } from '../__mocks__';
|
||||
import { clipboardUnit } from '../__mocks__';
|
||||
import { executeThunk } from '../utils';
|
||||
import { IFRAME_FEATURE_POLICY } from '../constants';
|
||||
import pasteComponentMessages from '../generic/clipboard/paste-component/messages';
|
||||
import pasteNotificationsMessages from './clipboard/paste-notification/messages';
|
||||
import headerNavigationsMessages from './header-navigations/messages';
|
||||
import headerTitleMessages from './header-title/messages';
|
||||
import courseSequenceMessages from './course-sequence/messages';
|
||||
import sidebarMessages from './sidebar/messages';
|
||||
import { extractCourseUnitId } from './sidebar/utils';
|
||||
import CourseUnit from './CourseUnit';
|
||||
|
||||
@@ -60,16 +56,21 @@ import configureModalMessages from '../generic/configure-modal/messages';
|
||||
import { getContentTaxonomyTagsApiUrl, getContentTaxonomyTagsCountApiUrl } from '../content-tags-drawer/data/api';
|
||||
import addComponentMessages from './add-component/messages';
|
||||
import { messageTypes, PUBLISH_TYPES, UNIT_VISIBILITY_STATES } from './constants';
|
||||
import { IframeProvider } from './context/iFrameContext';
|
||||
import { IframeProvider } from '../generic/hooks/context/iFrameContext';
|
||||
import moveModalMessages from './move-modal/messages';
|
||||
import xblockContainerIframeMessages from './xblock-container-iframe/messages';
|
||||
import headerNavigationsMessages from './header-navigations/messages';
|
||||
import sidebarMessages from './sidebar/messages';
|
||||
import messages from './messages';
|
||||
import * as selectors from '../data/selectors';
|
||||
|
||||
let axiosMock;
|
||||
let store;
|
||||
let queryClient;
|
||||
const courseId = '123';
|
||||
const blockId = '567890';
|
||||
const unitDisplayName = courseUnitIndexMock.metadata.display_name;
|
||||
const sequenceId = 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@19a30717eff543078a5d94ae9d6c18a5';
|
||||
const unitDisplayName = courseSectionVerticalMock.xblock_info.display_name;
|
||||
const mockedUsedNavigate = jest.fn();
|
||||
const userName = 'openedx';
|
||||
const handleConfigureSubmitMock = jest.fn();
|
||||
@@ -87,56 +88,10 @@ const postXBlockBody = {
|
||||
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
useParams: () => ({ blockId }),
|
||||
useParams: () => ({ blockId, sequenceId }),
|
||||
useNavigate: () => mockedUsedNavigate,
|
||||
}));
|
||||
|
||||
jest.mock('@tanstack/react-query', () => ({
|
||||
useQuery: jest.fn(({ queryKey }) => {
|
||||
const taxonomyApiHooksModule = jest.requireActual('../taxonomy/data/apiHooks');
|
||||
const actualQueryKeys = taxonomyApiHooksModule.taxonomyQueryKeys;
|
||||
|
||||
if (queryKey[0] === 'contentTaxonomyTags') {
|
||||
return {
|
||||
data: {
|
||||
taxonomies: [],
|
||||
},
|
||||
isSuccess: true,
|
||||
};
|
||||
} if (queryKey[0] === 'contentTagsCount') {
|
||||
return {
|
||||
data: 17,
|
||||
isSuccess: true,
|
||||
};
|
||||
}
|
||||
if (actualQueryKeys.all.includes(queryKey[0])) {
|
||||
return {
|
||||
data: {
|
||||
results: [],
|
||||
},
|
||||
isSuccess: true,
|
||||
};
|
||||
}
|
||||
return {
|
||||
data: {},
|
||||
isSuccess: true,
|
||||
};
|
||||
}),
|
||||
useQueryClient: jest.fn(() => ({
|
||||
setQueryData: jest.fn(),
|
||||
})),
|
||||
useMutation: jest.fn(() => ({
|
||||
mutateAsync: jest.fn(),
|
||||
})),
|
||||
}));
|
||||
|
||||
const clipboardBroadcastChannelMock = {
|
||||
postMessage: jest.fn(),
|
||||
close: jest.fn(),
|
||||
};
|
||||
|
||||
global.BroadcastChannel = jest.fn(() => clipboardBroadcastChannelMock);
|
||||
|
||||
/**
|
||||
* Simulates receiving a post message event for testing purposes.
|
||||
* This can be used to mimic events like deletion or other actions
|
||||
@@ -157,7 +112,9 @@ const RootWrapper = () => (
|
||||
<AppProvider store={store}>
|
||||
<IntlProvider locale="en">
|
||||
<IframeProvider>
|
||||
<CourseUnit courseId={courseId} />
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<CourseUnit courseId={courseId} />
|
||||
</QueryClientProvider>
|
||||
</IframeProvider>
|
||||
</IntlProvider>
|
||||
</AppProvider>
|
||||
@@ -176,55 +133,58 @@ describe('<CourseUnit />', () => {
|
||||
window.scrollTo = jest.fn();
|
||||
global.localStorage.clear();
|
||||
store = initializeStore();
|
||||
queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
retry: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
|
||||
axiosMock
|
||||
.onGet(getClipboardUrl())
|
||||
.reply(200, clipboardUnit);
|
||||
axiosMock
|
||||
.onGet(getCourseUnitApiUrl(courseId))
|
||||
.reply(200, courseUnitIndexMock);
|
||||
await executeThunk(fetchCourseUnitQuery(courseId), store.dispatch);
|
||||
axiosMock
|
||||
.onGet(getCourseSectionVerticalApiUrl(blockId))
|
||||
.reply(200, courseSectionVerticalMock);
|
||||
await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch);
|
||||
await executeThunk(fetchCourseSectionVerticalData(blockId, courseId), store.dispatch);
|
||||
axiosMock
|
||||
.onGet(getCourseVerticalChildrenApiUrl(blockId))
|
||||
.reply(200, courseVerticalChildrenMock);
|
||||
await executeThunk(fetchCourseVerticalChildrenData(blockId), store.dispatch);
|
||||
axiosMock
|
||||
.onGet(getContentTaxonomyTagsApiUrl(blockId))
|
||||
.reply(200, {});
|
||||
.reply(200, { taxonomies: [] });
|
||||
axiosMock
|
||||
.onGet(getContentTaxonomyTagsCountApiUrl(blockId))
|
||||
.reply(200, 17);
|
||||
});
|
||||
|
||||
it('render CourseUnit component correctly', async () => {
|
||||
const { getByText, getByRole, getByTestId } = render(<RootWrapper />);
|
||||
const currentSectionName = courseUnitIndexMock.ancestor_info.ancestors[1].display_name;
|
||||
const currentSubSectionName = courseUnitIndexMock.ancestor_info.ancestors[1].display_name;
|
||||
render(<RootWrapper />);
|
||||
const currentSectionName = courseSectionVerticalMock.xblock_info.ancestor_info.ancestors[1].display_name;
|
||||
const currentSubSectionName = courseSectionVerticalMock.xblock_info.ancestor_info.ancestors[1].display_name;
|
||||
|
||||
await waitFor(() => {
|
||||
const unitHeaderTitle = getByTestId('unit-header-title');
|
||||
expect(getByText(unitDisplayName)).toBeInTheDocument();
|
||||
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(getByRole('button', { name: headerNavigationsMessages.viewLiveButton.defaultMessage })).toBeInTheDocument();
|
||||
expect(getByRole('button', { name: headerNavigationsMessages.previewButton.defaultMessage })).toBeInTheDocument();
|
||||
expect(getByRole('button', { name: currentSectionName })).toBeInTheDocument();
|
||||
expect(getByRole('button', { name: currentSubSectionName })).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: headerNavigationsMessages.viewLiveButton.defaultMessage })).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: headerNavigationsMessages.previewButton.defaultMessage })).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: currentSectionName })).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: currentSubSectionName })).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('renders the course unit iframe with correct attributes', async () => {
|
||||
const { getByTitle } = render(<RootWrapper />);
|
||||
render(<RootWrapper />);
|
||||
|
||||
await waitFor(() => {
|
||||
const iframe = getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
|
||||
const iframe = screen.getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
|
||||
expect(iframe).toHaveAttribute('src', `${getConfig().STUDIO_BASE_URL}/container_embed/${blockId}`);
|
||||
expect(iframe).toHaveAttribute('allow', IFRAME_FEATURE_POLICY);
|
||||
expect(iframe).toHaveAttribute('style', 'width: 100%; height: 0px;');
|
||||
expect(iframe).toHaveAttribute('style', 'height: 0px;');
|
||||
expect(iframe).toHaveAttribute('scrolling', 'no');
|
||||
expect(iframe).toHaveAttribute('referrerpolicy', 'origin');
|
||||
expect(iframe).toHaveAttribute('loading', 'lazy');
|
||||
@@ -233,40 +193,39 @@ describe('<CourseUnit />', () => {
|
||||
});
|
||||
|
||||
it('adjusts iframe height dynamically based on courseXBlockDropdownHeight postMessage event', async () => {
|
||||
const { getByTitle } = render(<RootWrapper />);
|
||||
render(<RootWrapper />);
|
||||
|
||||
await waitFor(() => {
|
||||
const iframe = getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
|
||||
expect(iframe).toHaveAttribute('style', 'width: 100%; height: 0px;');
|
||||
simulatePostMessageEvent(messageTypes.toggleCourseXBlockDropdown, {
|
||||
courseXBlockDropdownHeight: 200,
|
||||
});
|
||||
expect(iframe).toHaveAttribute('style', 'width: 100%; height: 200px;');
|
||||
let iframe = await screen.findByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
|
||||
expect(iframe).toHaveAttribute('style', 'height: 0px;');
|
||||
simulatePostMessageEvent(messageTypes.toggleCourseXBlockDropdown, {
|
||||
courseXBlockDropdownHeight: 200,
|
||||
});
|
||||
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 () => {
|
||||
const { getByTitle, getByTestId } = render(<RootWrapper />);
|
||||
render(<RootWrapper />);
|
||||
|
||||
await waitFor(() => {
|
||||
const xblocksIframe = getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
|
||||
const xblocksIframe = screen.getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
|
||||
expect(xblocksIframe).toBeInTheDocument();
|
||||
simulatePostMessageEvent(messageTypes.studioAjaxError, {
|
||||
error: 'Some error text...',
|
||||
});
|
||||
});
|
||||
expect(getByTestId('saving-error-alert')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('saving-error-alert')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders XBlock iframe and opens legacy edit modal on editXBlock message', async () => {
|
||||
const { getByTitle } = render(<RootWrapper />);
|
||||
render(<RootWrapper />);
|
||||
|
||||
await waitFor(() => {
|
||||
const xblocksIframe = getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
|
||||
const xblocksIframe = screen.getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
|
||||
expect(xblocksIframe).toBeInTheDocument();
|
||||
simulatePostMessageEvent(messageTypes.editXBlock, { id: blockId });
|
||||
|
||||
const legacyXBlockEditModalIframe = getByTitle(
|
||||
const legacyXBlockEditModalIframe = screen.getByTitle(
|
||||
xblockContainerIframeMessages.legacyEditModalIframeTitle.defaultMessage,
|
||||
);
|
||||
expect(legacyXBlockEditModalIframe).toBeInTheDocument();
|
||||
@@ -274,27 +233,24 @@ describe('<CourseUnit />', () => {
|
||||
});
|
||||
|
||||
it('renders the xBlocks iframe and opens the tags drawer on postMessage event', async () => {
|
||||
const { getByTitle, getByText } = render(<RootWrapper />);
|
||||
render(<RootWrapper />);
|
||||
|
||||
await waitFor(() => {
|
||||
const xblocksIframe = getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
|
||||
expect(xblocksIframe).toBeInTheDocument();
|
||||
});
|
||||
await screen.findByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
|
||||
|
||||
simulatePostMessageEvent(messageTypes.openManageTags, { contentId: blockId });
|
||||
|
||||
expect(getByText(tagsDrawerMessages.headerSubtitle.defaultMessage)).toBeInTheDocument();
|
||||
await screen.findByText(tagsDrawerMessages.headerSubtitle.defaultMessage);
|
||||
});
|
||||
|
||||
it('closes the legacy edit modal when closeXBlockEditorModal message is received', async () => {
|
||||
const { getByTitle, queryByTitle } = render(<RootWrapper />);
|
||||
render(<RootWrapper />);
|
||||
|
||||
await waitFor(() => {
|
||||
const xblocksIframe = getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
|
||||
const xblocksIframe = screen.getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
|
||||
expect(xblocksIframe).toBeInTheDocument();
|
||||
simulatePostMessageEvent(messageTypes.closeXBlockEditorModal, { id: blockId });
|
||||
|
||||
const legacyXBlockEditModalIframe = queryByTitle(
|
||||
const legacyXBlockEditModalIframe = screen.queryByTitle(
|
||||
xblockContainerIframeMessages.legacyEditModalIframeTitle.defaultMessage,
|
||||
);
|
||||
expect(legacyXBlockEditModalIframe).not.toBeInTheDocument();
|
||||
@@ -302,29 +258,32 @@ describe('<CourseUnit />', () => {
|
||||
});
|
||||
|
||||
it('closes legacy edit modal and updates course unit sidebar after saveEditedXBlockData message', async () => {
|
||||
const { getByTitle, queryByTitle, getByTestId } = render(<RootWrapper />);
|
||||
render(<RootWrapper />);
|
||||
|
||||
await waitFor(() => {
|
||||
const xblocksIframe = getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
|
||||
const xblocksIframe = screen.getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
|
||||
expect(xblocksIframe).toBeInTheDocument();
|
||||
simulatePostMessageEvent(messageTypes.saveEditedXBlockData);
|
||||
|
||||
const legacyXBlockEditModalIframe = queryByTitle(
|
||||
const legacyXBlockEditModalIframe = screen.queryByTitle(
|
||||
xblockContainerIframeMessages.legacyEditModalIframeTitle.defaultMessage,
|
||||
);
|
||||
expect(legacyXBlockEditModalIframe).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
axiosMock
|
||||
.onGet(getCourseUnitApiUrl(blockId))
|
||||
.onGet(getCourseSectionVerticalApiUrl(blockId))
|
||||
.reply(200, {
|
||||
...courseUnitIndexMock,
|
||||
has_changes: true,
|
||||
published_by: userName,
|
||||
...courseSectionVerticalMock,
|
||||
xblock_info: {
|
||||
...courseSectionVerticalMock.xblock_info,
|
||||
has_changes: true,
|
||||
published_by: userName,
|
||||
},
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
const courseUnitSidebar = getByTestId('course-unit-sidebar');
|
||||
const courseUnitSidebar = screen.getByTestId('course-unit-sidebar');
|
||||
expect(
|
||||
within(courseUnitSidebar).getByText(sidebarMessages.sidebarTitleDraftUnpublishedChanges.defaultMessage),
|
||||
).toBeInTheDocument();
|
||||
@@ -343,24 +302,27 @@ describe('<CourseUnit />', () => {
|
||||
});
|
||||
|
||||
it('updates course unit sidebar after receiving refreshPositions message', async () => {
|
||||
const { getByTitle, getByTestId } = render(<RootWrapper />);
|
||||
render(<RootWrapper />);
|
||||
|
||||
await waitFor(() => {
|
||||
const xblocksIframe = getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
|
||||
const xblocksIframe = screen.getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
|
||||
expect(xblocksIframe).toBeInTheDocument();
|
||||
simulatePostMessageEvent(messageTypes.refreshPositions);
|
||||
});
|
||||
|
||||
axiosMock
|
||||
.onGet(getCourseUnitApiUrl(blockId))
|
||||
.onGet(getCourseSectionVerticalApiUrl(blockId))
|
||||
.reply(200, {
|
||||
...courseUnitIndexMock,
|
||||
has_changes: true,
|
||||
published_by: userName,
|
||||
...courseSectionVerticalMock,
|
||||
xblock_info: {
|
||||
...courseSectionVerticalMock.xblock_info,
|
||||
has_changes: true,
|
||||
published_by: userName,
|
||||
},
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
const courseUnitSidebar = getByTestId('course-unit-sidebar');
|
||||
const courseUnitSidebar = screen.getByTestId('course-unit-sidebar');
|
||||
expect(
|
||||
within(courseUnitSidebar).getByText(sidebarMessages.sidebarTitleDraftUnpublishedChanges.defaultMessage),
|
||||
).toBeInTheDocument();
|
||||
@@ -379,12 +341,10 @@ describe('<CourseUnit />', () => {
|
||||
});
|
||||
|
||||
it('checks whether xblock is removed when the corresponding delete button is clicked and the sidebar is the updated', async () => {
|
||||
const {
|
||||
getByTitle, getByText, queryByRole, getAllByRole, getByRole,
|
||||
} = render(<RootWrapper />);
|
||||
render(<RootWrapper />);
|
||||
|
||||
await waitFor(() => {
|
||||
const iframe = getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
|
||||
await waitFor(async () => {
|
||||
const iframe = screen.getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
|
||||
expect(iframe).toHaveAttribute(
|
||||
'aria-label',
|
||||
xblockContainerIframeMessages.xblockIframeLabel.defaultMessage
|
||||
@@ -395,16 +355,15 @@ describe('<CourseUnit />', () => {
|
||||
usageId: courseVerticalChildrenMock.children[0].block_id,
|
||||
});
|
||||
|
||||
expect(getByText(/Delete this component?/i)).toBeInTheDocument();
|
||||
expect(getByText(/Deleting this component is permanent and cannot be undone./i)).toBeInTheDocument();
|
||||
expect(screen.getByText(/Delete this component?/i)).toBeInTheDocument();
|
||||
expect(screen.getByText(/Deleting this component is permanent and cannot be undone./i)).toBeInTheDocument();
|
||||
|
||||
expect(getByRole('dialog')).toBeInTheDocument();
|
||||
const dialog = screen.getByRole('dialog');
|
||||
expect(dialog).toBeInTheDocument();
|
||||
|
||||
// Find the Cancel and Delete buttons within the iframe by their specific classes
|
||||
const cancelButton = getAllByRole('button', { name: /Cancel/i })
|
||||
.find(({ classList }) => classList.contains('btn-tertiary'));
|
||||
const deleteButton = getAllByRole('button', { name: /Delete/i })
|
||||
.find(({ classList }) => classList.contains('btn-primary'));
|
||||
const cancelButton = await within(dialog).findByRole('button', { name: /Cancel/i });
|
||||
const deleteButton = await within(dialog).findByRole('button', { name: /Delete/i });
|
||||
|
||||
expect(cancelButton).toBeInTheDocument();
|
||||
|
||||
@@ -412,7 +371,7 @@ describe('<CourseUnit />', () => {
|
||||
usageId: courseVerticalChildrenMock.children[0].block_id,
|
||||
});
|
||||
|
||||
expect(getByRole('dialog')).toBeInTheDocument();
|
||||
expect(screen.getByRole('dialog')).toBeInTheDocument();
|
||||
userEvent.click(deleteButton);
|
||||
});
|
||||
|
||||
@@ -422,30 +381,36 @@ describe('<CourseUnit />', () => {
|
||||
})
|
||||
.reply(200, { dummy: 'value' });
|
||||
axiosMock
|
||||
.onGet(getCourseUnitApiUrl(blockId))
|
||||
.onGet(getCourseSectionVerticalApiUrl(blockId))
|
||||
.reply(200, {
|
||||
...courseUnitIndexMock,
|
||||
visibility_state: UNIT_VISIBILITY_STATES.live,
|
||||
has_changes: false,
|
||||
published_by: userName,
|
||||
...courseSectionVerticalMock,
|
||||
xblock_info: {
|
||||
...courseSectionVerticalMock.xblock_info,
|
||||
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(getByText(sidebarMessages.sidebarTitlePublishedAndLive.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByText(
|
||||
expect(screen.getByText(sidebarMessages.sidebarTitlePublishedAndLive.defaultMessage)).toBeInTheDocument();
|
||||
expect(screen.getByText(
|
||||
sidebarMessages.publishLastPublished.defaultMessage
|
||||
.replace('{publishedOn}', courseUnitIndexMock.published_on)
|
||||
.replace('{publishedOn}', courseSectionVerticalMock.xblock_info.published_on)
|
||||
.replace('{publishedBy}', userName),
|
||||
)).toBeInTheDocument();
|
||||
expect(queryByRole('button', { name: sidebarMessages.actionButtonPublishTitle.defaultMessage })).not.toBeInTheDocument();
|
||||
expect(getByText(unitDisplayName)).toBeInTheDocument();
|
||||
expect(screen.queryByRole('button', { name: sidebarMessages.actionButtonPublishTitle.defaultMessage })).not.toBeInTheDocument();
|
||||
expect(screen.getByText(unitDisplayName)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
axiosMock
|
||||
.onDelete(getXBlockBaseApiUrl(courseVerticalChildrenMock.children[0].block_id))
|
||||
.reply(200, { dummy: 'value' });
|
||||
axiosMock
|
||||
.onGet(getCourseSectionVerticalApiUrl(courseId))
|
||||
.reply(200, courseSectionVerticalMock);
|
||||
await executeThunk(deleteUnitItemQuery(
|
||||
courseId,
|
||||
courseVerticalChildrenMock.children[0].block_id,
|
||||
@@ -466,43 +431,41 @@ describe('<CourseUnit />', () => {
|
||||
await executeThunk(fetchCourseVerticalChildrenData(blockId), store.dispatch);
|
||||
|
||||
axiosMock
|
||||
.onGet(getCourseUnitApiUrl(blockId))
|
||||
.reply(200, courseUnitIndexMock);
|
||||
.onGet(getCourseSectionVerticalApiUrl(blockId))
|
||||
.reply(200, courseSectionVerticalMock);
|
||||
await executeThunk(editCourseUnitVisibilityAndData(blockId, PUBLISH_TYPES.makePublic, true), store.dispatch);
|
||||
|
||||
await waitFor(() => {
|
||||
const iframe = getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
|
||||
const iframe = screen.getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
|
||||
expect(iframe).toHaveAttribute(
|
||||
'aria-label',
|
||||
xblockContainerIframeMessages.xblockIframeLabel.defaultMessage
|
||||
.replace('{xblockCount}', updatedCourseVerticalChildren.length),
|
||||
);
|
||||
// after removing the xblock, the sidebar status changes to Draft (unpublished changes)
|
||||
expect(getByText(sidebarMessages.sidebarTitleDraftUnpublishedChanges.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByText(sidebarMessages.visibilityStaffAndLearnersTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByText(sidebarMessages.releaseStatusTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByText(sidebarMessages.sidebarBodyNote.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByText(sidebarMessages.visibilityWillBeVisibleToTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByText(sidebarMessages.visibilityCheckboxTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByText(sidebarMessages.actionButtonPublishTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByText(sidebarMessages.actionButtonDiscardChangesTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByText(courseUnitIndexMock.release_date)).toBeInTheDocument();
|
||||
expect(getByText(
|
||||
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(courseSectionVerticalMock.xblock_info.release_date)).toBeInTheDocument();
|
||||
expect(screen.getByText(
|
||||
sidebarMessages.publishInfoDraftSaved.defaultMessage
|
||||
.replace('{editedOn}', courseUnitIndexMock.edited_on)
|
||||
.replace('{editedBy}', courseUnitIndexMock.edited_by),
|
||||
.replace('{editedOn}', courseSectionVerticalMock.xblock_info.edited_on)
|
||||
.replace('{editedBy}', courseSectionVerticalMock.xblock_info.edited_by),
|
||||
)).toBeInTheDocument();
|
||||
expect(getByText(
|
||||
expect(screen.getByText(
|
||||
sidebarMessages.releaseInfoWithSection.defaultMessage
|
||||
.replace('{sectionName}', courseUnitIndexMock.release_date_from),
|
||||
.replace('{sectionName}', courseSectionVerticalMock.xblock_info.release_date_from),
|
||||
)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('checks if xblock is a duplicate when the corresponding duplicate button is clicked and if the sidebar status is updated', async () => {
|
||||
const {
|
||||
getByTitle, getByRole, getByText, queryByRole,
|
||||
} = render(<RootWrapper />);
|
||||
render(<RootWrapper />);
|
||||
|
||||
simulatePostMessageEvent(messageTypes.duplicateXBlock, {
|
||||
id: courseVerticalChildrenMock.children[0].block_id,
|
||||
@@ -518,8 +481,14 @@ describe('<CourseUnit />', () => {
|
||||
const updatedCourseVerticalChildren = [
|
||||
...courseVerticalChildrenMock.children,
|
||||
{
|
||||
...courseVerticalChildrenMock.children[0],
|
||||
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: '',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
@@ -531,9 +500,9 @@ describe('<CourseUnit />', () => {
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
userEvent.click(getByRole('button', { name: sidebarMessages.actionButtonPublishTitle.defaultMessage }));
|
||||
userEvent.click(screen.getByRole('button', { name: sidebarMessages.actionButtonPublishTitle.defaultMessage }));
|
||||
|
||||
const iframe = getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
|
||||
const iframe = screen.getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
|
||||
expect(iframe).toHaveAttribute(
|
||||
'aria-label',
|
||||
xblockContainerIframeMessages.xblockIframeLabel.defaultMessage
|
||||
@@ -551,34 +520,37 @@ describe('<CourseUnit />', () => {
|
||||
})
|
||||
.reply(200, { dummy: 'value' });
|
||||
axiosMock
|
||||
.onGet(getCourseUnitApiUrl(blockId))
|
||||
.onGet(getCourseSectionVerticalApiUrl(blockId))
|
||||
.reply(200, {
|
||||
...courseUnitIndexMock,
|
||||
visibility_state: UNIT_VISIBILITY_STATES.live,
|
||||
has_changes: false,
|
||||
published_by: userName,
|
||||
...courseSectionVerticalMock,
|
||||
xblock_info: {
|
||||
...courseSectionVerticalMock.xblock_info,
|
||||
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(getByText(sidebarMessages.sidebarTitlePublishedAndLive.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByText(
|
||||
expect(screen.getByText(sidebarMessages.sidebarTitlePublishedAndLive.defaultMessage)).toBeInTheDocument();
|
||||
expect(screen.getByText(
|
||||
sidebarMessages.publishLastPublished.defaultMessage
|
||||
.replace('{publishedOn}', courseUnitIndexMock.published_on)
|
||||
.replace('{publishedOn}', courseSectionVerticalMock.xblock_info.published_on)
|
||||
.replace('{publishedBy}', userName),
|
||||
)).toBeInTheDocument();
|
||||
expect(queryByRole('button', { name: sidebarMessages.actionButtonPublishTitle.defaultMessage })).not.toBeInTheDocument();
|
||||
expect(getByText(unitDisplayName)).toBeInTheDocument();
|
||||
expect(screen.queryByRole('button', { name: sidebarMessages.actionButtonPublishTitle.defaultMessage })).not.toBeInTheDocument();
|
||||
expect(screen.getByText(unitDisplayName)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
axiosMock
|
||||
.onGet(getCourseUnitApiUrl(blockId))
|
||||
.reply(200, courseUnitIndexMock);
|
||||
.onGet(getCourseSectionVerticalApiUrl(blockId))
|
||||
.reply(200, courseSectionVerticalMock);
|
||||
await executeThunk(editCourseUnitVisibilityAndData(blockId, PUBLISH_TYPES.makePublic, true), store.dispatch);
|
||||
|
||||
await waitFor(() => {
|
||||
const iframe = getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
|
||||
const iframe = screen.getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
|
||||
expect(iframe).toHaveAttribute(
|
||||
'aria-label',
|
||||
xblockContainerIframeMessages.xblockIframeLabel.defaultMessage
|
||||
@@ -586,23 +558,23 @@ describe('<CourseUnit />', () => {
|
||||
);
|
||||
|
||||
// after duplicate the xblock, the sidebar status changes to Draft (unpublished changes)
|
||||
expect(getByText(sidebarMessages.sidebarTitleDraftUnpublishedChanges.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByText(sidebarMessages.visibilityStaffAndLearnersTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByText(sidebarMessages.releaseStatusTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByText(sidebarMessages.sidebarBodyNote.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByText(sidebarMessages.visibilityWillBeVisibleToTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByText(sidebarMessages.visibilityCheckboxTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByText(sidebarMessages.actionButtonPublishTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByText(sidebarMessages.actionButtonDiscardChangesTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByText(courseUnitIndexMock.release_date)).toBeInTheDocument();
|
||||
expect(getByText(
|
||||
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(courseSectionVerticalMock.xblock_info.release_date)).toBeInTheDocument();
|
||||
expect(screen.getByText(
|
||||
sidebarMessages.publishInfoDraftSaved.defaultMessage
|
||||
.replace('{editedOn}', courseUnitIndexMock.edited_on)
|
||||
.replace('{editedBy}', courseUnitIndexMock.edited_by),
|
||||
.replace('{editedOn}', courseSectionVerticalMock.xblock_info.edited_on)
|
||||
.replace('{editedBy}', courseSectionVerticalMock.xblock_info.edited_by),
|
||||
)).toBeInTheDocument();
|
||||
expect(getByText(
|
||||
expect(screen.getByText(
|
||||
sidebarMessages.releaseInfoWithSection.defaultMessage
|
||||
.replace('{sectionName}', courseUnitIndexMock.release_date_from),
|
||||
.replace('{sectionName}', courseSectionVerticalMock.xblock_info.release_date_from),
|
||||
)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -610,19 +582,19 @@ describe('<CourseUnit />', () => {
|
||||
it('handles CourseUnit header action buttons', async () => {
|
||||
const { open } = window;
|
||||
window.open = jest.fn();
|
||||
const { getByRole } = render(<RootWrapper />);
|
||||
render(<RootWrapper />);
|
||||
const {
|
||||
draft_preview_link: draftPreviewLink,
|
||||
published_preview_link: publishedPreviewLink,
|
||||
} = courseSectionVerticalMock;
|
||||
|
||||
await waitFor(() => {
|
||||
const viewLiveButton = getByRole('button', { name: headerNavigationsMessages.viewLiveButton.defaultMessage });
|
||||
const viewLiveButton = screen.getByRole('button', { name: headerNavigationsMessages.viewLiveButton.defaultMessage });
|
||||
userEvent.click(viewLiveButton);
|
||||
expect(window.open).toHaveBeenCalled();
|
||||
expect(window.open).toHaveBeenCalledWith(publishedPreviewLink, '_blank');
|
||||
|
||||
const previewButton = getByRole('button', { name: headerNavigationsMessages.previewButton.defaultMessage });
|
||||
const previewButton = screen.getByRole('button', { name: headerNavigationsMessages.previewButton.defaultMessage });
|
||||
userEvent.click(previewButton);
|
||||
expect(window.open).toHaveBeenCalled();
|
||||
expect(window.open).toHaveBeenCalledWith(draftPreviewLink, '_blank');
|
||||
@@ -632,12 +604,7 @@ describe('<CourseUnit />', () => {
|
||||
});
|
||||
|
||||
it('checks courseUnit title changing when edit query is successfully', async () => {
|
||||
const {
|
||||
findByText,
|
||||
queryByRole,
|
||||
getByRole,
|
||||
getByTestId,
|
||||
} = render(<RootWrapper />);
|
||||
render(<RootWrapper />);
|
||||
let editTitleButton = null;
|
||||
let titleEditField = null;
|
||||
const newDisplayName = `${unitDisplayName} new`;
|
||||
@@ -650,12 +617,15 @@ describe('<CourseUnit />', () => {
|
||||
}))
|
||||
.reply(200, { dummy: 'value' });
|
||||
axiosMock
|
||||
.onGet(getCourseUnitApiUrl(blockId))
|
||||
.onGet(getCourseSectionVerticalApiUrl(blockId))
|
||||
.reply(200, {
|
||||
...courseUnitIndexMock,
|
||||
metadata: {
|
||||
...courseUnitIndexMock.metadata,
|
||||
display_name: newDisplayName,
|
||||
...courseSectionVerticalMock,
|
||||
xblock_info: {
|
||||
...courseSectionVerticalMock.xblock_info,
|
||||
metadata: {
|
||||
...courseSectionVerticalMock.xblock_info.metadata,
|
||||
display_name: newDisplayName,
|
||||
},
|
||||
},
|
||||
});
|
||||
axiosMock
|
||||
@@ -673,7 +643,7 @@ describe('<CourseUnit />', () => {
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
const unitHeaderTitle = getByTestId('unit-header-title');
|
||||
const unitHeaderTitle = screen.getByTestId('unit-header-title');
|
||||
editTitleButton = within(unitHeaderTitle)
|
||||
.getByRole('button', { name: headerTitleMessages.altButtonEdit.defaultMessage });
|
||||
titleEditField = within(unitHeaderTitle)
|
||||
@@ -681,7 +651,7 @@ describe('<CourseUnit />', () => {
|
||||
});
|
||||
expect(titleEditField).not.toBeInTheDocument();
|
||||
userEvent.click(editTitleButton);
|
||||
titleEditField = getByRole('textbox', { name: headerTitleMessages.ariaLabelButtonEdit.defaultMessage });
|
||||
titleEditField = screen.getByRole('textbox', { name: headerTitleMessages.ariaLabelButtonEdit.defaultMessage });
|
||||
|
||||
await userEvent.clear(titleEditField);
|
||||
await userEvent.type(titleEditField, newDisplayName);
|
||||
@@ -689,9 +659,10 @@ describe('<CourseUnit />', () => {
|
||||
|
||||
expect(titleEditField).toHaveValue(newDisplayName);
|
||||
|
||||
titleEditField = queryByRole('textbox', { name: headerTitleMessages.ariaLabelButtonEdit.defaultMessage });
|
||||
titleEditField = screen.queryByRole('textbox', { name: headerTitleMessages.ariaLabelButtonEdit.defaultMessage });
|
||||
titleEditField = screen.queryByRole('textbox', { name: headerTitleMessages.ariaLabelButtonEdit.defaultMessage });
|
||||
expect(titleEditField).not.toBeInTheDocument();
|
||||
expect(await findByText(newDisplayName)).toBeInTheDocument();
|
||||
expect(await screen.findByText(newDisplayName)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('doesn\'t handle creating xblock and displays an error message', async () => {
|
||||
@@ -711,15 +682,14 @@ describe('<CourseUnit />', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('handle creating Problem xblock and navigate to editor page', async () => {
|
||||
const { courseKey, locator } = courseCreateXblockMock;
|
||||
it('handle creating Problem xblock and showing editor modal', async () => {
|
||||
axiosMock
|
||||
.onPost(postXBlockBaseApiUrl({ type: 'problem', category: 'problem', parentLocator: blockId }))
|
||||
.reply(200, courseCreateXblockMock);
|
||||
const { getByText, getByRole } = render(<RootWrapper />);
|
||||
render(<RootWrapper />);
|
||||
|
||||
await waitFor(() => {
|
||||
userEvent.click(getByRole('button', { name: sidebarMessages.actionButtonPublishTitle.defaultMessage }));
|
||||
userEvent.click(screen.getByRole('button', { name: sidebarMessages.actionButtonPublishTitle.defaultMessage }));
|
||||
});
|
||||
|
||||
axiosMock
|
||||
@@ -728,93 +698,57 @@ describe('<CourseUnit />', () => {
|
||||
})
|
||||
.reply(200, { dummy: 'value' });
|
||||
axiosMock
|
||||
.onGet(getCourseUnitApiUrl(blockId))
|
||||
.onGet(getCourseSectionVerticalApiUrl(blockId))
|
||||
.reply(200, {
|
||||
...courseUnitIndexMock,
|
||||
visibility_state: UNIT_VISIBILITY_STATES.live,
|
||||
has_changes: false,
|
||||
published_by: userName,
|
||||
...courseSectionVerticalMock,
|
||||
xblock_info: {
|
||||
...courseSectionVerticalMock.xblock_info,
|
||||
visibility_state: UNIT_VISIBILITY_STATES.live,
|
||||
has_changes: false,
|
||||
published_by: userName,
|
||||
},
|
||||
});
|
||||
|
||||
await executeThunk(editCourseUnitVisibilityAndData(blockId, PUBLISH_TYPES.makePublic, true), store.dispatch);
|
||||
|
||||
await waitFor(() => {
|
||||
const problemButton = getByRole('button', {
|
||||
const problemButton = screen.getByRole('button', {
|
||||
name: new RegExp(`problem ${addComponentMessages.buttonText.defaultMessage} Problem`, 'i'),
|
||||
hidden: true,
|
||||
});
|
||||
|
||||
userEvent.click(problemButton);
|
||||
expect(mockedUsedNavigate).toHaveBeenCalled();
|
||||
expect(mockedUsedNavigate).toHaveBeenCalledWith(`/course/${courseKey}/editor/problem/${locator}`);
|
||||
});
|
||||
|
||||
axiosMock
|
||||
.onGet(getCourseUnitApiUrl(blockId))
|
||||
.reply(200, courseUnitIndexMock);
|
||||
.onGet(getCourseSectionVerticalApiUrl(blockId))
|
||||
.reply(200, courseSectionVerticalMock);
|
||||
|
||||
await executeThunk(editCourseUnitVisibilityAndData(blockId, PUBLISH_TYPES.makePublic, true), store.dispatch);
|
||||
|
||||
// after creating problem xblock, the sidebar status changes to Draft (unpublished changes)
|
||||
expect(getByText(sidebarMessages.sidebarTitleDraftUnpublishedChanges.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByText(sidebarMessages.visibilityStaffAndLearnersTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByText(sidebarMessages.releaseStatusTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByText(sidebarMessages.sidebarBodyNote.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByText(sidebarMessages.visibilityWillBeVisibleToTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByText(sidebarMessages.visibilityCheckboxTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByText(sidebarMessages.actionButtonPublishTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByText(sidebarMessages.actionButtonDiscardChangesTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByText(courseUnitIndexMock.release_date)).toBeInTheDocument();
|
||||
expect(getByText(
|
||||
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(courseSectionVerticalMock.xblock_info.release_date)).toBeInTheDocument();
|
||||
expect(screen.getByText(
|
||||
sidebarMessages.publishInfoDraftSaved.defaultMessage
|
||||
.replace('{editedOn}', courseUnitIndexMock.edited_on)
|
||||
.replace('{editedBy}', courseUnitIndexMock.edited_by),
|
||||
.replace('{editedOn}', courseSectionVerticalMock.xblock_info.edited_on)
|
||||
.replace('{editedBy}', courseSectionVerticalMock.xblock_info.edited_by),
|
||||
)).toBeInTheDocument();
|
||||
expect(getByText(
|
||||
expect(screen.getByText(
|
||||
sidebarMessages.releaseInfoWithSection.defaultMessage
|
||||
.replace('{sectionName}', courseUnitIndexMock.release_date_from),
|
||||
.replace('{sectionName}', courseSectionVerticalMock.xblock_info.release_date_from),
|
||||
)).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 () => {
|
||||
const { getByRole, getAllByTestId } = render(<RootWrapper />);
|
||||
render(<RootWrapper />);
|
||||
let units = null;
|
||||
const updatedCourseSectionVerticalData = cloneDeep(courseSectionVerticalMock);
|
||||
const updatedAncestorsChild = updatedCourseSectionVerticalData.xblock_info.ancestor_info.ancestors[0];
|
||||
@@ -824,7 +758,7 @@ describe('<CourseUnit />', () => {
|
||||
]);
|
||||
|
||||
await waitFor(async () => {
|
||||
units = getAllByTestId('course-unit-btn');
|
||||
units = screen.getAllByTestId('course-unit-btn');
|
||||
const courseUnits = courseSectionVerticalMock.xblock_info.ancestor_info.ancestors[0].child_info.children;
|
||||
expect(units).toHaveLength(courseUnits.length);
|
||||
});
|
||||
@@ -841,8 +775,8 @@ describe('<CourseUnit />', () => {
|
||||
|
||||
await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch);
|
||||
|
||||
const addNewUnitBtn = getByRole('button', { name: courseSequenceMessages.newUnitBtnText.defaultMessage });
|
||||
units = getAllByTestId('course-unit-btn');
|
||||
const addNewUnitBtn = screen.getByRole('button', { name: courseSequenceMessages.newUnitBtnText.defaultMessage });
|
||||
units = screen.getAllByTestId('course-unit-btn');
|
||||
const updatedCourseUnits = updatedCourseSectionVerticalData
|
||||
.xblock_info.ancestor_info.ancestors[0].child_info.children;
|
||||
|
||||
@@ -854,7 +788,7 @@ describe('<CourseUnit />', () => {
|
||||
});
|
||||
|
||||
it('the sequence unit is updated after changing the unit header', async () => {
|
||||
const { getAllByTestId, getByTestId } = render(<RootWrapper />);
|
||||
render(<RootWrapper />);
|
||||
const updatedCourseSectionVerticalData = cloneDeep(courseSectionVerticalMock);
|
||||
const updatedAncestorsChild = updatedCourseSectionVerticalData.xblock_info.ancestor_info.ancestors[0];
|
||||
set(updatedCourseSectionVerticalData, 'xblock_info.ancestor_info.ancestors[0].child_info.children', [
|
||||
@@ -871,12 +805,15 @@ describe('<CourseUnit />', () => {
|
||||
},
|
||||
}))
|
||||
.reply(200, { dummy: 'value' })
|
||||
.onGet(getCourseUnitApiUrl(blockId))
|
||||
.onGet(getCourseSectionVerticalApiUrl(blockId))
|
||||
.reply(200, {
|
||||
...courseUnitIndexMock,
|
||||
metadata: {
|
||||
...courseUnitIndexMock.metadata,
|
||||
display_name: newDisplayName,
|
||||
...courseSectionVerticalMock,
|
||||
xblock_info: {
|
||||
...courseSectionVerticalMock.xblock_info,
|
||||
metadata: {
|
||||
...courseSectionVerticalMock.xblock_info.metadata,
|
||||
display_name: newDisplayName,
|
||||
},
|
||||
},
|
||||
})
|
||||
.onGet(getCourseSectionVerticalApiUrl(blockId))
|
||||
@@ -886,7 +823,7 @@ describe('<CourseUnit />', () => {
|
||||
|
||||
await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch);
|
||||
|
||||
const unitHeaderTitle = getByTestId('unit-header-title');
|
||||
const unitHeaderTitle = screen.getByTestId('unit-header-title');
|
||||
|
||||
const editTitleButton = within(unitHeaderTitle).getByRole('button', { name: headerTitleMessages.altButtonEdit.defaultMessage });
|
||||
userEvent.click(editTitleButton);
|
||||
@@ -898,20 +835,21 @@ describe('<CourseUnit />', () => {
|
||||
await userEvent.tab();
|
||||
|
||||
await waitFor(async () => {
|
||||
const units = getAllByTestId('course-unit-btn');
|
||||
const units = screen.getAllByTestId('course-unit-btn');
|
||||
expect(units.some(unit => unit.title === newDisplayName)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
it('handles creating Video xblock and navigates to editor page', async () => {
|
||||
const { courseKey, locator } = courseCreateXblockMock;
|
||||
it('handles creating Video xblock and showing editor modal using videogalleryflow', async () => {
|
||||
const waffleSpy = jest.spyOn(selectors, 'getWaffleFlags').mockReturnValue({ useVideoGalleryFlow: true });
|
||||
|
||||
axiosMock
|
||||
.onPost(postXBlockBaseApiUrl({ type: 'video', category: 'video', parentLocator: blockId }))
|
||||
.reply(200, courseCreateXblockMock);
|
||||
const { getByText, queryByRole, getByRole } = render(<RootWrapper />);
|
||||
render(<RootWrapper />);
|
||||
|
||||
await waitFor(() => {
|
||||
userEvent.click(getByRole('button', { name: sidebarMessages.actionButtonPublishTitle.defaultMessage }));
|
||||
userEvent.click(screen.getByRole('button', { name: sidebarMessages.actionButtonPublishTitle.defaultMessage }));
|
||||
});
|
||||
|
||||
axiosMock
|
||||
@@ -920,96 +858,181 @@ describe('<CourseUnit />', () => {
|
||||
})
|
||||
.reply(200, { dummy: 'value' });
|
||||
axiosMock
|
||||
.onGet(getCourseUnitApiUrl(blockId))
|
||||
.onGet(getCourseSectionVerticalApiUrl(blockId))
|
||||
.reply(200, {
|
||||
...courseUnitIndexMock,
|
||||
visibility_state: UNIT_VISIBILITY_STATES.live,
|
||||
has_changes: false,
|
||||
published_by: userName,
|
||||
...courseSectionVerticalMock,
|
||||
xblock_info: {
|
||||
...courseSectionVerticalMock.xblock_info,
|
||||
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(getByText(sidebarMessages.sidebarTitlePublishedAndLive.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByText(
|
||||
sidebarMessages.publishLastPublished.defaultMessage
|
||||
.replace('{publishedOn}', courseUnitIndexMock.published_on)
|
||||
.replace('{publishedBy}', userName),
|
||||
)).toBeInTheDocument();
|
||||
expect(queryByRole('button', { name: sidebarMessages.actionButtonPublishTitle.defaultMessage })).not.toBeInTheDocument();
|
||||
|
||||
const videoButton = getByRole('button', {
|
||||
name: new RegExp(`${addComponentMessages.buttonText.defaultMessage} Video`, 'i'),
|
||||
});
|
||||
|
||||
userEvent.click(videoButton);
|
||||
expect(mockedUsedNavigate).toHaveBeenCalled();
|
||||
expect(mockedUsedNavigate).toHaveBeenCalledWith(`/course/${courseKey}/editor/video/${locator}`);
|
||||
expect(screen.getByText(sidebarMessages.sidebarTitlePublishedAndLive.defaultMessage)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
expect(screen.getByText(
|
||||
sidebarMessages.publishLastPublished.defaultMessage
|
||||
.replace('{publishedOn}', courseSectionVerticalMock.xblock_info.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);
|
||||
.onGet(getCourseSectionVerticalApiUrl(blockId))
|
||||
.reply(200, courseSectionVerticalMock);
|
||||
|
||||
await executeThunk(editCourseUnitVisibilityAndData(blockId, PUBLISH_TYPES.makePublic, true), store.dispatch);
|
||||
|
||||
// after creating video xblock, the sidebar status changes to Draft (unpublished changes)
|
||||
expect(getByText(sidebarMessages.sidebarTitleDraftUnpublishedChanges.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByText(sidebarMessages.visibilityStaffAndLearnersTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByText(sidebarMessages.releaseStatusTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByText(sidebarMessages.sidebarBodyNote.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByText(sidebarMessages.visibilityWillBeVisibleToTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByText(sidebarMessages.visibilityCheckboxTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByText(sidebarMessages.actionButtonPublishTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByText(sidebarMessages.actionButtonDiscardChangesTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByText(courseUnitIndexMock.release_date)).toBeInTheDocument();
|
||||
expect(getByText(
|
||||
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(courseSectionVerticalMock.xblock_info.release_date)).toBeInTheDocument();
|
||||
expect(screen.getByText(
|
||||
sidebarMessages.publishInfoDraftSaved.defaultMessage
|
||||
.replace('{editedOn}', courseUnitIndexMock.edited_on)
|
||||
.replace('{editedBy}', courseUnitIndexMock.edited_by),
|
||||
.replace('{editedOn}', courseSectionVerticalMock.xblock_info.edited_on)
|
||||
.replace('{editedBy}', courseSectionVerticalMock.xblock_info.edited_by),
|
||||
)).toBeInTheDocument();
|
||||
expect(getByText(
|
||||
expect(screen.getByText(
|
||||
sidebarMessages.releaseInfoWithSection.defaultMessage
|
||||
.replace('{sectionName}', courseUnitIndexMock.release_date_from),
|
||||
.replace('{sectionName}', courseSectionVerticalMock.xblock_info.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(getCourseSectionVerticalApiUrl(blockId))
|
||||
.reply(200, {
|
||||
...courseSectionVerticalMock,
|
||||
xblock_info: {
|
||||
...courseSectionVerticalMock.xblock_info,
|
||||
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
|
||||
.replace('{publishedOn}', courseSectionVerticalMock.xblock_info.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);
|
||||
});
|
||||
|
||||
/** TODO -- fix this test.
|
||||
await waitFor(() => {
|
||||
expect(getByRole('textbox', { name: /paste your video id or url/i })).toBeInTheDocument();
|
||||
});
|
||||
*/
|
||||
|
||||
axiosMock
|
||||
.onGet(getCourseSectionVerticalApiUrl(blockId))
|
||||
.reply(200, courseSectionVerticalMock);
|
||||
|
||||
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(courseSectionVerticalMock.xblock_info.release_date)).toBeInTheDocument();
|
||||
expect(screen.getByText(
|
||||
sidebarMessages.publishInfoDraftSaved.defaultMessage
|
||||
.replace('{editedOn}', courseSectionVerticalMock.xblock_info.edited_on)
|
||||
.replace('{editedBy}', courseSectionVerticalMock.xblock_info.edited_by),
|
||||
)).toBeInTheDocument();
|
||||
expect(screen.getByText(
|
||||
sidebarMessages.releaseInfoWithSection.defaultMessage
|
||||
.replace('{sectionName}', courseSectionVerticalMock.xblock_info.release_date_from),
|
||||
)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders course unit details for a draft with unpublished changes', async () => {
|
||||
const { getByText } = render(<RootWrapper />);
|
||||
render(<RootWrapper />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(getByText(sidebarMessages.sidebarTitleDraftUnpublishedChanges.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByText(sidebarMessages.visibilityStaffAndLearnersTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByText(sidebarMessages.releaseStatusTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByText(sidebarMessages.sidebarBodyNote.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByText(sidebarMessages.visibilityWillBeVisibleToTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByText(sidebarMessages.visibilityCheckboxTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByText(sidebarMessages.actionButtonPublishTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByText(sidebarMessages.actionButtonDiscardChangesTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByText(courseUnitIndexMock.release_date)).toBeInTheDocument();
|
||||
expect(getByText(
|
||||
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(courseSectionVerticalMock.xblock_info.release_date)).toBeInTheDocument();
|
||||
expect(screen.getByText(
|
||||
sidebarMessages.publishInfoDraftSaved.defaultMessage
|
||||
.replace('{editedOn}', courseUnitIndexMock.edited_on)
|
||||
.replace('{editedBy}', courseUnitIndexMock.edited_by),
|
||||
.replace('{editedOn}', courseSectionVerticalMock.xblock_info.edited_on)
|
||||
.replace('{editedBy}', courseSectionVerticalMock.xblock_info.edited_by),
|
||||
)).toBeInTheDocument();
|
||||
expect(getByText(
|
||||
expect(screen.getByText(
|
||||
sidebarMessages.releaseInfoWithSection.defaultMessage
|
||||
.replace('{sectionName}', courseUnitIndexMock.release_date_from),
|
||||
.replace('{sectionName}', courseSectionVerticalMock.xblock_info.release_date_from),
|
||||
)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('renders course unit details in the sidebar', async () => {
|
||||
const { getByText } = render(<RootWrapper />);
|
||||
const courseUnitLocationId = extractCourseUnitId(courseUnitIndexMock.id);
|
||||
render(<RootWrapper />);
|
||||
const courseUnitLocationId = extractCourseUnitId(courseSectionVerticalMock.xblock_info.id);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(getByText(sidebarMessages.sidebarHeaderUnitLocationTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByText(sidebarMessages.unitLocationTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByText(courseUnitLocationId)).toBeInTheDocument();
|
||||
expect(getByText(sidebarMessages.unitLocationDescription.defaultMessage
|
||||
expect(screen.getByText(sidebarMessages.sidebarHeaderUnitLocationTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(screen.getByText(sidebarMessages.unitLocationTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(screen.getByText(courseUnitLocationId)).toBeInTheDocument();
|
||||
expect(screen.getByText(sidebarMessages.unitLocationDescription.defaultMessage
|
||||
.replace('{id}', courseUnitLocationId))).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -1030,13 +1053,16 @@ describe('<CourseUnit />', () => {
|
||||
render(<RootWrapper />);
|
||||
|
||||
axiosMock
|
||||
.onGet(getCourseUnitApiUrl(courseId))
|
||||
.onGet(getCourseSectionVerticalApiUrl(courseId))
|
||||
.reply(200, {
|
||||
...courseUnitIndexMock,
|
||||
currently_visible_to_students: false,
|
||||
...courseSectionVerticalMock,
|
||||
xblock_info: {
|
||||
...courseSectionVerticalMock.xblock_info,
|
||||
currently_visible_to_students: false,
|
||||
},
|
||||
});
|
||||
|
||||
await executeThunk(fetchCourseUnitQuery(courseId), store.dispatch);
|
||||
await executeThunk(fetchCourseSectionVerticalData(courseId), store.dispatch);
|
||||
|
||||
await waitFor(() => {
|
||||
const alert = screen.queryAllByRole('alert').find(
|
||||
@@ -1047,13 +1073,13 @@ describe('<CourseUnit />', () => {
|
||||
});
|
||||
|
||||
it('should toggle visibility from sidebar and update course unit state accordingly', async () => {
|
||||
const { getByRole, getByTestId } = render(<RootWrapper />);
|
||||
render(<RootWrapper />);
|
||||
let courseUnitSidebar;
|
||||
let draftUnpublishedChangesHeading;
|
||||
let visibilityCheckbox;
|
||||
|
||||
await waitFor(() => {
|
||||
courseUnitSidebar = getByTestId('course-unit-sidebar');
|
||||
courseUnitSidebar = screen.getByTestId('course-unit-sidebar');
|
||||
|
||||
draftUnpublishedChangesHeading = within(courseUnitSidebar)
|
||||
.getByText(sidebarMessages.sidebarTitleDraftUnpublishedChanges.defaultMessage);
|
||||
@@ -1073,11 +1099,14 @@ describe('<CourseUnit />', () => {
|
||||
})
|
||||
.reply(200, { dummy: 'value' });
|
||||
axiosMock
|
||||
.onGet(getCourseUnitApiUrl(blockId))
|
||||
.onGet(getCourseSectionVerticalApiUrl(blockId))
|
||||
.reply(200, {
|
||||
...courseUnitIndexMock,
|
||||
visibility_state: UNIT_VISIBILITY_STATES.staffOnly,
|
||||
has_explicit_staff_lock: true,
|
||||
...courseSectionVerticalMock,
|
||||
xblock_info: {
|
||||
...courseSectionVerticalMock.xblock_info,
|
||||
visibility_state: UNIT_VISIBILITY_STATES.staffOnly,
|
||||
has_explicit_staff_lock: true,
|
||||
},
|
||||
});
|
||||
|
||||
await executeThunk(editCourseUnitVisibilityAndData(blockId, PUBLISH_TYPES.republish, true), store.dispatch);
|
||||
@@ -1090,7 +1119,7 @@ describe('<CourseUnit />', () => {
|
||||
|
||||
userEvent.click(visibilityCheckbox);
|
||||
|
||||
const modalNotification = getByRole('dialog');
|
||||
const modalNotification = screen.getByRole('dialog');
|
||||
const makeVisibilityBtn = within(modalNotification).getByRole('button', { name: sidebarMessages.modalMakeVisibilityActionButtonText.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' });
|
||||
@@ -1110,8 +1139,8 @@ describe('<CourseUnit />', () => {
|
||||
})
|
||||
.reply(200, { dummy: 'value' });
|
||||
axiosMock
|
||||
.onGet(getCourseUnitApiUrl(blockId))
|
||||
.reply(200, courseUnitIndexMock);
|
||||
.onGet(getCourseSectionVerticalApiUrl(blockId))
|
||||
.reply(200, courseSectionVerticalMock);
|
||||
|
||||
await executeThunk(editCourseUnitVisibilityAndData(blockId, PUBLISH_TYPES.republish, null), store.dispatch);
|
||||
|
||||
@@ -1122,12 +1151,12 @@ describe('<CourseUnit />', () => {
|
||||
});
|
||||
|
||||
it('should publish course unit after click on the "Publish" button', async () => {
|
||||
const { getByTestId } = render(<RootWrapper />);
|
||||
render(<RootWrapper />);
|
||||
let courseUnitSidebar;
|
||||
let publishBtn;
|
||||
|
||||
await waitFor(() => {
|
||||
courseUnitSidebar = getByTestId('course-unit-sidebar');
|
||||
courseUnitSidebar = screen.getByTestId('course-unit-sidebar');
|
||||
publishBtn = within(courseUnitSidebar).queryByRole('button', { name: sidebarMessages.actionButtonPublishTitle.defaultMessage });
|
||||
expect(publishBtn).toBeInTheDocument();
|
||||
|
||||
@@ -1140,12 +1169,15 @@ describe('<CourseUnit />', () => {
|
||||
})
|
||||
.reply(200, { dummy: 'value' });
|
||||
axiosMock
|
||||
.onGet(getCourseUnitApiUrl(blockId))
|
||||
.onGet(getCourseSectionVerticalApiUrl(blockId))
|
||||
.reply(200, {
|
||||
...courseUnitIndexMock,
|
||||
visibility_state: UNIT_VISIBILITY_STATES.live,
|
||||
has_changes: false,
|
||||
published_by: userName,
|
||||
...courseSectionVerticalMock,
|
||||
xblock_info: {
|
||||
...courseSectionVerticalMock.xblock_info,
|
||||
visibility_state: UNIT_VISIBILITY_STATES.live,
|
||||
has_changes: false,
|
||||
published_by: userName,
|
||||
},
|
||||
});
|
||||
|
||||
await executeThunk(editCourseUnitVisibilityAndData(blockId, PUBLISH_TYPES.makePublic, true), store.dispatch);
|
||||
@@ -1154,19 +1186,19 @@ describe('<CourseUnit />', () => {
|
||||
.getByText(sidebarMessages.sidebarTitlePublishedAndLive.defaultMessage)).toBeInTheDocument();
|
||||
expect(within(courseUnitSidebar).getByText(
|
||||
sidebarMessages.publishLastPublished.defaultMessage
|
||||
.replace('{publishedOn}', courseUnitIndexMock.published_on)
|
||||
.replace('{publishedOn}', courseSectionVerticalMock.xblock_info.published_on)
|
||||
.replace('{publishedBy}', userName),
|
||||
)).toBeInTheDocument();
|
||||
expect(publishBtn).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should discard changes after click on the "Discard changes" button', async () => {
|
||||
const { getByTestId, getByRole } = render(<RootWrapper />);
|
||||
render(<RootWrapper />);
|
||||
let courseUnitSidebar;
|
||||
let discardChangesBtn;
|
||||
|
||||
await waitFor(() => {
|
||||
courseUnitSidebar = getByTestId('course-unit-sidebar');
|
||||
courseUnitSidebar = screen.getByTestId('course-unit-sidebar');
|
||||
|
||||
const draftUnpublishedChangesHeading = within(courseUnitSidebar)
|
||||
.getByText(sidebarMessages.sidebarTitleDraftUnpublishedChanges.defaultMessage);
|
||||
@@ -1176,7 +1208,7 @@ describe('<CourseUnit />', () => {
|
||||
|
||||
userEvent.click(discardChangesBtn);
|
||||
|
||||
const modalNotification = getByRole('dialog');
|
||||
const modalNotification = screen.getByRole('dialog');
|
||||
expect(modalNotification).toBeInTheDocument();
|
||||
expect(within(modalNotification)
|
||||
.getByText(sidebarMessages.modalDiscardUnitChangesDescription.defaultMessage)).toBeInTheDocument();
|
||||
@@ -1196,9 +1228,14 @@ describe('<CourseUnit />', () => {
|
||||
})
|
||||
.reply(200, { dummy: 'value' });
|
||||
axiosMock
|
||||
.onGet(getCourseUnitApiUrl(blockId))
|
||||
.onGet(getCourseSectionVerticalApiUrl(blockId))
|
||||
.reply(200, {
|
||||
...courseUnitIndexMock, published: true, has_changes: false,
|
||||
...courseSectionVerticalMock,
|
||||
xblock_info: {
|
||||
...courseSectionVerticalMock.xblock_info,
|
||||
published: true,
|
||||
has_changes: false,
|
||||
},
|
||||
});
|
||||
|
||||
await executeThunk(editCourseUnitVisibilityAndData(
|
||||
@@ -1213,7 +1250,7 @@ describe('<CourseUnit />', () => {
|
||||
});
|
||||
|
||||
it('should toggle visibility from header configure modal and update course unit state accordingly', async () => {
|
||||
const { getByRole, getByTestId } = render(<RootWrapper />);
|
||||
render(<RootWrapper />);
|
||||
let courseUnitSidebar;
|
||||
let sidebarVisibilityCheckbox;
|
||||
let modalVisibilityCheckbox;
|
||||
@@ -1221,16 +1258,16 @@ describe('<CourseUnit />', () => {
|
||||
let restrictAccessSelect;
|
||||
|
||||
await waitFor(() => {
|
||||
courseUnitSidebar = getByTestId('course-unit-sidebar');
|
||||
courseUnitSidebar = screen.getByTestId('course-unit-sidebar');
|
||||
sidebarVisibilityCheckbox = within(courseUnitSidebar)
|
||||
.getByLabelText(sidebarMessages.visibilityCheckboxTitle.defaultMessage);
|
||||
expect(sidebarVisibilityCheckbox).not.toBeChecked();
|
||||
|
||||
const headerConfigureBtn = getByRole('button', { name: /settings/i });
|
||||
const headerConfigureBtn = screen.getByRole('button', { name: /settings/i });
|
||||
expect(headerConfigureBtn).toBeInTheDocument();
|
||||
|
||||
userEvent.click(headerConfigureBtn);
|
||||
configureModal = getByTestId('configure-modal');
|
||||
configureModal = screen.getByTestId('configure-modal');
|
||||
restrictAccessSelect = within(configureModal)
|
||||
.getByRole('combobox', { name: configureModalMessages.restrictAccessTo.defaultMessage });
|
||||
expect(within(configureModal)
|
||||
@@ -1255,17 +1292,20 @@ describe('<CourseUnit />', () => {
|
||||
});
|
||||
|
||||
axiosMock
|
||||
.onPost(getXBlockBaseApiUrl(courseUnitIndexMock.id), {
|
||||
.onPost(getXBlockBaseApiUrl(courseSectionVerticalMock.xblock_info.id), {
|
||||
publish: null,
|
||||
metadata: { visible_to_staff_only: true, group_access: { 50: [2] }, discussion_enabled: true },
|
||||
})
|
||||
.reply(200, { dummy: 'value' });
|
||||
axiosMock
|
||||
.onGet(getCourseUnitApiUrl(blockId))
|
||||
.replyOnce(200, {
|
||||
...courseUnitIndexMock,
|
||||
visibility_state: UNIT_VISIBILITY_STATES.staffOnly,
|
||||
has_explicit_staff_lock: true,
|
||||
.onGet(getCourseSectionVerticalApiUrl(blockId))
|
||||
.reply(200, {
|
||||
...courseSectionVerticalMock,
|
||||
xblock_info: {
|
||||
...courseSectionVerticalMock.xblock_info,
|
||||
visibility_state: UNIT_VISIBILITY_STATES.staffOnly,
|
||||
has_explicit_staff_lock: true,
|
||||
},
|
||||
});
|
||||
|
||||
const modalSaveBtn = within(configureModal)
|
||||
@@ -1286,8 +1326,8 @@ describe('<CourseUnit />', () => {
|
||||
...getConfig(),
|
||||
ENABLE_TAGGING_TAXONOMY_PAGES: 'true',
|
||||
});
|
||||
const { getByText } = render(<RootWrapper />);
|
||||
await waitFor(() => { expect(getByText('Unit tags')).toBeInTheDocument(); });
|
||||
render(<RootWrapper />);
|
||||
await waitFor(() => { expect(screen.getByText('Unit tags')).toBeInTheDocument(); });
|
||||
});
|
||||
|
||||
it('hides the Tags sidebar when not enabled', async () => {
|
||||
@@ -1295,35 +1335,28 @@ describe('<CourseUnit />', () => {
|
||||
...getConfig(),
|
||||
ENABLE_TAGGING_TAXONOMY_PAGES: 'false',
|
||||
});
|
||||
const { queryByText } = render(<RootWrapper />);
|
||||
await waitFor(() => { expect(queryByText('Unit tags')).not.toBeInTheDocument(); });
|
||||
render(<RootWrapper />);
|
||||
await waitFor(() => { expect(screen.queryByText('Unit tags')).not.toBeInTheDocument(); });
|
||||
});
|
||||
|
||||
describe('Copy paste functionality', () => {
|
||||
it('should copy a unit, paste it as a new unit, and update the course section vertical data', async () => {
|
||||
const {
|
||||
getAllByTestId, getByRole,
|
||||
} = render(<RootWrapper />);
|
||||
|
||||
axiosMock
|
||||
.onGet(getCourseUnitApiUrl(courseId))
|
||||
.reply(200, {
|
||||
...courseUnitIndexMock,
|
||||
enable_copy_paste_units: true,
|
||||
});
|
||||
render(<RootWrapper />);
|
||||
|
||||
axiosMock
|
||||
.onGet(getCourseSectionVerticalApiUrl(blockId))
|
||||
.reply(200, {
|
||||
...courseSectionVerticalMock,
|
||||
user_clipboard: clipboardUnit,
|
||||
xblock_info: {
|
||||
...courseSectionVerticalMock.xblock_info,
|
||||
enable_copy_paste_units: true,
|
||||
},
|
||||
});
|
||||
|
||||
await executeThunk(fetchCourseUnitQuery(courseId), store.dispatch);
|
||||
await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch);
|
||||
|
||||
userEvent.click(getByRole('button', { name: sidebarMessages.actionButtonCopyUnitTitle.defaultMessage }));
|
||||
userEvent.click(getByRole('button', { name: courseSequenceMessages.pasteAsNewUnitLink.defaultMessage }));
|
||||
userEvent.click(screen.getByRole('button', { name: sidebarMessages.actionButtonCopyUnitTitle.defaultMessage }));
|
||||
userEvent.click(screen.getByRole('button', { name: courseSequenceMessages.pasteAsNewUnitLink.defaultMessage }));
|
||||
|
||||
let units = null;
|
||||
const updatedCourseSectionVerticalData = cloneDeep(courseSectionVerticalMock);
|
||||
@@ -1334,7 +1367,7 @@ describe('<CourseUnit />', () => {
|
||||
]);
|
||||
|
||||
await waitFor(() => {
|
||||
units = getAllByTestId('course-unit-btn');
|
||||
units = screen.getAllByTestId('course-unit-btn');
|
||||
const courseUnits = courseSectionVerticalMock.xblock_info.ancestor_info.ancestors[0].child_info.children;
|
||||
expect(units).toHaveLength(courseUnits.length);
|
||||
});
|
||||
@@ -1350,7 +1383,7 @@ describe('<CourseUnit />', () => {
|
||||
|
||||
await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch);
|
||||
|
||||
units = getAllByTestId('course-unit-btn');
|
||||
units = screen.getAllByTestId('course-unit-btn');
|
||||
const updatedCourseUnits = updatedCourseSectionVerticalData
|
||||
.xblock_info.ancestor_info.ancestors[0].child_info.children;
|
||||
|
||||
@@ -1361,7 +1394,7 @@ describe('<CourseUnit />', () => {
|
||||
});
|
||||
|
||||
it('should increase the number of course XBlocks after copying and pasting a block', async () => {
|
||||
const { getByRole, getByTitle } = render(<RootWrapper />);
|
||||
render(<RootWrapper />);
|
||||
|
||||
simulatePostMessageEvent(messageTypes.copyXBlock, {
|
||||
id: courseVerticalChildrenMock.children[0].block_id,
|
||||
@@ -1371,22 +1404,17 @@ describe('<CourseUnit />', () => {
|
||||
.onGet(getCourseSectionVerticalApiUrl(blockId))
|
||||
.reply(200, {
|
||||
...courseSectionVerticalMock,
|
||||
user_clipboard: clipboardXBlock,
|
||||
xblock_info: {
|
||||
...courseSectionVerticalMock.xblock_info,
|
||||
enable_copy_paste_units: true,
|
||||
},
|
||||
});
|
||||
axiosMock
|
||||
.onGet(getCourseUnitApiUrl(courseId))
|
||||
.reply(200, {
|
||||
...courseUnitIndexMock,
|
||||
enable_copy_paste_units: true,
|
||||
});
|
||||
await executeThunk(fetchCourseUnitQuery(courseId), store.dispatch);
|
||||
await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch);
|
||||
|
||||
userEvent.click(getByRole('button', { name: sidebarMessages.actionButtonCopyUnitTitle.defaultMessage }));
|
||||
userEvent.click(getByRole('button', { name: messages.pasteButtonText.defaultMessage }));
|
||||
userEvent.click(screen.getByRole('button', { name: sidebarMessages.actionButtonCopyUnitTitle.defaultMessage }));
|
||||
|
||||
await waitFor(() => {
|
||||
const iframe = getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
|
||||
const iframe = screen.getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
|
||||
expect(iframe).toHaveAttribute(
|
||||
'aria-label',
|
||||
xblockContainerIframeMessages.xblockIframeLabel.defaultMessage
|
||||
@@ -1422,7 +1450,7 @@ describe('<CourseUnit />', () => {
|
||||
await executeThunk(fetchCourseVerticalChildrenData(blockId), store.dispatch);
|
||||
|
||||
await waitFor(() => {
|
||||
const iframe = getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
|
||||
const iframe = screen.getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
|
||||
expect(iframe).toHaveAttribute(
|
||||
'aria-label',
|
||||
xblockContainerIframeMessages.xblockIframeLabel.defaultMessage
|
||||
@@ -1432,29 +1460,22 @@ describe('<CourseUnit />', () => {
|
||||
});
|
||||
|
||||
it('displays a notification about new files after pasting a component', async () => {
|
||||
const {
|
||||
queryByTestId, getByTestId, getByRole,
|
||||
} = render(<RootWrapper />);
|
||||
|
||||
axiosMock
|
||||
.onGet(getCourseUnitApiUrl(courseId))
|
||||
.reply(200, {
|
||||
...courseUnitIndexMock,
|
||||
enable_copy_paste_units: true,
|
||||
});
|
||||
render(<RootWrapper />);
|
||||
|
||||
axiosMock
|
||||
.onGet(getCourseSectionVerticalApiUrl(blockId))
|
||||
.reply(200, {
|
||||
...courseSectionVerticalMock,
|
||||
user_clipboard: clipboardUnit,
|
||||
xblock_info: {
|
||||
...courseSectionVerticalMock.xblock_info,
|
||||
enable_copy_paste_units: true,
|
||||
},
|
||||
});
|
||||
|
||||
await executeThunk(fetchCourseUnitQuery(courseId), store.dispatch);
|
||||
await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch);
|
||||
|
||||
userEvent.click(getByRole('button', { name: sidebarMessages.actionButtonCopyUnitTitle.defaultMessage }));
|
||||
userEvent.click(getByRole('button', { name: courseSequenceMessages.pasteAsNewUnitLink.defaultMessage }));
|
||||
userEvent.click(screen.getByRole('button', { name: sidebarMessages.actionButtonCopyUnitTitle.defaultMessage }));
|
||||
userEvent.click(screen.getByRole('button', { name: courseSequenceMessages.pasteAsNewUnitLink.defaultMessage }));
|
||||
|
||||
const updatedCourseSectionVerticalData = cloneDeep(courseSectionVerticalMock);
|
||||
const updatedAncestorsChild = updatedCourseSectionVerticalData.xblock_info.ancestor_info.ancestors[0];
|
||||
@@ -1473,7 +1494,7 @@ describe('<CourseUnit />', () => {
|
||||
global.localStorage.setItem('staticFileNotices', JSON.stringify(clipboardMockResponse.staticFileNotices));
|
||||
await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch);
|
||||
await executeThunk(createNewCourseXBlock(camelCaseObject(postXBlockBody), null, blockId), store.dispatch);
|
||||
const newFilesAlert = getByTestId('has-new-files-alert');
|
||||
const newFilesAlert = screen.getByTestId('has-new-files-alert');
|
||||
|
||||
expect(within(newFilesAlert)
|
||||
.getByText(pasteNotificationsMessages.hasNewFilesTitle.defaultMessage)).toBeInTheDocument();
|
||||
@@ -1487,33 +1508,26 @@ describe('<CourseUnit />', () => {
|
||||
|
||||
userEvent.click(within(newFilesAlert).getByText(/Dismiss/i));
|
||||
|
||||
expect(queryByTestId('has-new-files-alert')).toBeNull();
|
||||
expect(screen.queryByTestId('has-new-files-alert')).toBeNull();
|
||||
});
|
||||
|
||||
it('displays a notification about conflicting errors after pasting a component', async () => {
|
||||
const {
|
||||
queryByTestId, getByTestId, getByRole,
|
||||
} = render(<RootWrapper />);
|
||||
|
||||
axiosMock
|
||||
.onGet(getCourseUnitApiUrl(courseId))
|
||||
.reply(200, {
|
||||
...courseUnitIndexMock,
|
||||
enable_copy_paste_units: true,
|
||||
});
|
||||
render(<RootWrapper />);
|
||||
|
||||
axiosMock
|
||||
.onGet(getCourseSectionVerticalApiUrl(blockId))
|
||||
.reply(200, {
|
||||
...courseSectionVerticalMock,
|
||||
user_clipboard: clipboardUnit,
|
||||
xblock_info: {
|
||||
...courseSectionVerticalMock.xblock_info,
|
||||
enable_copy_paste_units: true,
|
||||
},
|
||||
});
|
||||
|
||||
await executeThunk(fetchCourseUnitQuery(courseId), store.dispatch);
|
||||
await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch);
|
||||
|
||||
userEvent.click(getByRole('button', { name: sidebarMessages.actionButtonCopyUnitTitle.defaultMessage }));
|
||||
userEvent.click(getByRole('button', { name: courseSequenceMessages.pasteAsNewUnitLink.defaultMessage }));
|
||||
userEvent.click(screen.getByRole('button', { name: sidebarMessages.actionButtonCopyUnitTitle.defaultMessage }));
|
||||
userEvent.click(screen.getByRole('button', { name: courseSequenceMessages.pasteAsNewUnitLink.defaultMessage }));
|
||||
|
||||
const updatedCourseSectionVerticalData = cloneDeep(courseSectionVerticalMock);
|
||||
const updatedAncestorsChild = updatedCourseSectionVerticalData.xblock_info.ancestor_info.ancestors[0];
|
||||
@@ -1534,7 +1548,7 @@ describe('<CourseUnit />', () => {
|
||||
global.localStorage.setItem('staticFileNotices', JSON.stringify(clipboardMockResponse.staticFileNotices));
|
||||
await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch);
|
||||
await executeThunk(createNewCourseXBlock(camelCaseObject(postXBlockBody), null, blockId), store.dispatch);
|
||||
const conflictingErrorsAlert = getByTestId('has-conflicting-errors-alert');
|
||||
const conflictingErrorsAlert = screen.getByTestId('has-conflicting-errors-alert');
|
||||
|
||||
expect(within(conflictingErrorsAlert)
|
||||
.getByText(pasteNotificationsMessages.hasConflictingErrorsTitle.defaultMessage)).toBeInTheDocument();
|
||||
@@ -1548,33 +1562,26 @@ describe('<CourseUnit />', () => {
|
||||
|
||||
userEvent.click(within(conflictingErrorsAlert).getByText(/Dismiss/i));
|
||||
|
||||
expect(queryByTestId('has-conflicting-errors-alert')).toBeNull();
|
||||
expect(screen.queryByTestId('has-conflicting-errors-alert')).toBeNull();
|
||||
});
|
||||
|
||||
it('displays a notification about error files after pasting a component', async () => {
|
||||
const {
|
||||
queryByTestId, getByTestId, getByRole,
|
||||
} = render(<RootWrapper />);
|
||||
|
||||
axiosMock
|
||||
.onGet(getCourseUnitApiUrl(courseId))
|
||||
.reply(200, {
|
||||
...courseUnitIndexMock,
|
||||
enable_copy_paste_units: true,
|
||||
});
|
||||
render(<RootWrapper />);
|
||||
|
||||
axiosMock
|
||||
.onGet(getCourseSectionVerticalApiUrl(blockId))
|
||||
.reply(200, {
|
||||
...courseSectionVerticalMock,
|
||||
user_clipboard: clipboardUnit,
|
||||
xblock_info: {
|
||||
...courseSectionVerticalMock.xblock_info,
|
||||
enable_copy_paste_units: true,
|
||||
},
|
||||
});
|
||||
|
||||
await executeThunk(fetchCourseUnitQuery(courseId), store.dispatch);
|
||||
await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch);
|
||||
|
||||
userEvent.click(getByRole('button', { name: sidebarMessages.actionButtonCopyUnitTitle.defaultMessage }));
|
||||
userEvent.click(getByRole('button', { name: courseSequenceMessages.pasteAsNewUnitLink.defaultMessage }));
|
||||
userEvent.click(screen.getByRole('button', { name: sidebarMessages.actionButtonCopyUnitTitle.defaultMessage }));
|
||||
userEvent.click(screen.getByRole('button', { name: courseSequenceMessages.pasteAsNewUnitLink.defaultMessage }));
|
||||
|
||||
const updatedCourseSectionVerticalData = cloneDeep(courseSectionVerticalMock);
|
||||
const updatedAncestorsChild = updatedCourseSectionVerticalData.xblock_info.ancestor_info.ancestors[0];
|
||||
@@ -1595,7 +1602,7 @@ describe('<CourseUnit />', () => {
|
||||
global.localStorage.setItem('staticFileNotices', JSON.stringify(clipboardMockResponse.staticFileNotices));
|
||||
await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch);
|
||||
await executeThunk(createNewCourseXBlock(camelCaseObject(postXBlockBody), null, blockId), store.dispatch);
|
||||
const errorFilesAlert = getByTestId('has-error-files-alert');
|
||||
const errorFilesAlert = screen.getByTestId('has-error-files-alert');
|
||||
|
||||
expect(within(errorFilesAlert)
|
||||
.getByText(pasteNotificationsMessages.hasErrorsTitle.defaultMessage)).toBeInTheDocument();
|
||||
@@ -1604,11 +1611,11 @@ describe('<CourseUnit />', () => {
|
||||
|
||||
userEvent.click(within(errorFilesAlert).getByText(/Dismiss/i));
|
||||
|
||||
expect(queryByTestId('has-error-files')).toBeNull();
|
||||
expect(screen.queryByTestId('has-error-files')).toBeNull();
|
||||
});
|
||||
|
||||
it('should hide the "Paste component" block if canPasteComponent is false', async () => {
|
||||
const { queryByText, queryByRole } = render(<RootWrapper />);
|
||||
render(<RootWrapper />);
|
||||
|
||||
axiosMock
|
||||
.onGet(getCourseVerticalChildrenApiUrl(blockId))
|
||||
@@ -1619,10 +1626,10 @@ describe('<CourseUnit />', () => {
|
||||
|
||||
await executeThunk(fetchCourseVerticalChildrenData(blockId), store.dispatch);
|
||||
|
||||
expect(queryByRole('button', {
|
||||
expect(screen.queryByRole('button', {
|
||||
name: messages.pasteButtonText.defaultMessage,
|
||||
})).not.toBeInTheDocument();
|
||||
expect(queryByText(
|
||||
expect(screen.queryByText(
|
||||
pasteComponentMessages.pasteButtonWhatsInClipboardText.defaultMessage,
|
||||
)).not.toBeInTheDocument();
|
||||
});
|
||||
@@ -1656,58 +1663,42 @@ describe('<CourseUnit />', () => {
|
||||
});
|
||||
|
||||
it('should display "Move Modal" on receive trigger message', async () => {
|
||||
const {
|
||||
getByText,
|
||||
getByRole,
|
||||
} = render(<RootWrapper />);
|
||||
render(<RootWrapper />);
|
||||
|
||||
await act(async () => {
|
||||
await waitFor(() => {
|
||||
expect(getByText(unitDisplayName))
|
||||
.toBeInTheDocument();
|
||||
});
|
||||
await screen.findByText(unitDisplayName);
|
||||
|
||||
axiosMock
|
||||
.onGet(getCourseOutlineInfoUrl(courseId))
|
||||
.reply(200, courseOutlineInfoMock);
|
||||
await executeThunk(getCourseOutlineInfoQuery(courseId), store.dispatch);
|
||||
axiosMock
|
||||
.onGet(getCourseOutlineInfoUrl(courseId))
|
||||
.reply(200, courseOutlineInfoMock);
|
||||
await executeThunk(getCourseOutlineInfoQuery(courseId), store.dispatch);
|
||||
|
||||
window.dispatchEvent(messageEvent);
|
||||
});
|
||||
window.dispatchEvent(messageEvent);
|
||||
|
||||
expect(getByText(
|
||||
await screen.findByText(
|
||||
moveModalMessages.moveModalTitle.defaultMessage.replace('{displayName}', requestData.title),
|
||||
)).toBeInTheDocument();
|
||||
expect(getByRole('button', { name: moveModalMessages.moveModalSubmitButton.defaultMessage })).toBeInTheDocument();
|
||||
expect(getByRole('button', { name: moveModalMessages.moveModalCancelButton.defaultMessage })).toBeInTheDocument();
|
||||
);
|
||||
expect(screen.getByRole('button', { name: moveModalMessages.moveModalSubmitButton.defaultMessage })).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: moveModalMessages.moveModalCancelButton.defaultMessage })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should navigates to xBlock current unit', async () => {
|
||||
const {
|
||||
getByText,
|
||||
getByRole,
|
||||
} = render(<RootWrapper />);
|
||||
render(<RootWrapper />);
|
||||
|
||||
await act(async () => {
|
||||
await waitFor(() => {
|
||||
expect(getByText(unitDisplayName))
|
||||
.toBeInTheDocument();
|
||||
});
|
||||
await screen.findByText(unitDisplayName);
|
||||
|
||||
axiosMock
|
||||
.onGet(getCourseOutlineInfoUrl(courseId))
|
||||
.reply(200, courseOutlineInfoMock);
|
||||
await executeThunk(getCourseOutlineInfoQuery(courseId), store.dispatch);
|
||||
axiosMock
|
||||
.onGet(getCourseOutlineInfoUrl(courseId))
|
||||
.reply(200, courseOutlineInfoMock);
|
||||
await executeThunk(getCourseOutlineInfoQuery(courseId), store.dispatch);
|
||||
|
||||
window.dispatchEvent(messageEvent);
|
||||
});
|
||||
window.dispatchEvent(messageEvent);
|
||||
|
||||
expect(getByText(
|
||||
await screen.findByText(
|
||||
moveModalMessages.moveModalTitle.defaultMessage.replace('{displayName}', requestData.title),
|
||||
)).toBeInTheDocument();
|
||||
);
|
||||
|
||||
const currentSection = courseOutlineInfoMock.child_info.children[1];
|
||||
const currentSectionItemBtn = getByRole('button', {
|
||||
const currentSectionItemBtn = screen.getByRole('button', {
|
||||
name: `${currentSection.display_name} ${moveModalMessages.moveModalOutlineItemCurrentLocationText.defaultMessage} ${moveModalMessages.moveModalOutlineItemViewText.defaultMessage}`,
|
||||
});
|
||||
expect(currentSectionItemBtn).toBeInTheDocument();
|
||||
@@ -1715,7 +1706,7 @@ describe('<CourseUnit />', () => {
|
||||
|
||||
await waitFor(() => {
|
||||
const currentSubsection = currentSection.child_info.children[0];
|
||||
const currentSubsectionItemBtn = getByRole('button', {
|
||||
const currentSubsectionItemBtn = screen.getByRole('button', {
|
||||
name: `${currentSubsection.display_name} ${moveModalMessages.moveModalOutlineItemCurrentLocationText.defaultMessage} ${moveModalMessages.moveModalOutlineItemViewText.defaultMessage}`,
|
||||
});
|
||||
expect(currentSubsectionItemBtn).toBeInTheDocument();
|
||||
@@ -1723,7 +1714,7 @@ describe('<CourseUnit />', () => {
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
const currentComponentLocationText = getByText(
|
||||
const currentComponentLocationText = screen.getByText(
|
||||
moveModalMessages.moveModalOutlineItemCurrentComponentLocationText.defaultMessage,
|
||||
);
|
||||
expect(currentComponentLocationText).toBeInTheDocument();
|
||||
@@ -1731,39 +1722,31 @@ describe('<CourseUnit />', () => {
|
||||
});
|
||||
|
||||
it('should allow move operation and handles it successfully', async () => {
|
||||
const {
|
||||
getByText,
|
||||
getByRole,
|
||||
} = render(<RootWrapper />);
|
||||
render(<RootWrapper />);
|
||||
|
||||
axiosMock
|
||||
.onPatch(postXBlockBaseApiUrl())
|
||||
.reply(200, {});
|
||||
|
||||
axiosMock
|
||||
.onGet(getCourseUnitApiUrl(blockId))
|
||||
.reply(200, courseUnitIndexMock);
|
||||
.onGet(getCourseSectionVerticalApiUrl(blockId))
|
||||
.reply(200, courseSectionVerticalMock);
|
||||
|
||||
await act(async () => {
|
||||
await waitFor(() => {
|
||||
expect(getByText(unitDisplayName))
|
||||
.toBeInTheDocument();
|
||||
});
|
||||
await screen.findByText(unitDisplayName);
|
||||
|
||||
axiosMock
|
||||
.onGet(getCourseOutlineInfoUrl(courseId))
|
||||
.reply(200, courseOutlineInfoMock);
|
||||
await executeThunk(getCourseOutlineInfoQuery(courseId), store.dispatch);
|
||||
axiosMock
|
||||
.onGet(getCourseOutlineInfoUrl(courseId))
|
||||
.reply(200, courseOutlineInfoMock);
|
||||
await executeThunk(getCourseOutlineInfoQuery(courseId), store.dispatch);
|
||||
|
||||
window.dispatchEvent(messageEvent);
|
||||
});
|
||||
window.dispatchEvent(messageEvent);
|
||||
|
||||
expect(getByText(
|
||||
await screen.findByText(
|
||||
moveModalMessages.moveModalTitle.defaultMessage.replace('{displayName}', requestData.title),
|
||||
)).toBeInTheDocument();
|
||||
);
|
||||
|
||||
const currentSection = courseOutlineInfoMock.child_info.children[1];
|
||||
const currentSectionItemBtn = getByRole('button', {
|
||||
const currentSectionItemBtn = screen.getByRole('button', {
|
||||
name: `${currentSection.display_name} ${moveModalMessages.moveModalOutlineItemCurrentLocationText.defaultMessage} ${moveModalMessages.moveModalOutlineItemViewText.defaultMessage}`,
|
||||
});
|
||||
expect(currentSectionItemBtn).toBeInTheDocument();
|
||||
@@ -1771,7 +1754,7 @@ describe('<CourseUnit />', () => {
|
||||
|
||||
const currentSubsection = currentSection.child_info.children[1];
|
||||
await waitFor(() => {
|
||||
const currentSubsectionItemBtn = getByRole('button', {
|
||||
const currentSubsectionItemBtn = screen.getByRole('button', {
|
||||
name: `${currentSubsection.display_name} ${moveModalMessages.moveModalOutlineItemViewText.defaultMessage}`,
|
||||
});
|
||||
expect(currentSubsectionItemBtn).toBeInTheDocument();
|
||||
@@ -1780,14 +1763,14 @@ describe('<CourseUnit />', () => {
|
||||
|
||||
await waitFor(() => {
|
||||
const currentUnit = currentSubsection.child_info.children[0];
|
||||
const currentUnitItemBtn = getByRole('button', {
|
||||
const currentUnitItemBtn = screen.getByRole('button', {
|
||||
name: `${currentUnit.display_name} ${moveModalMessages.moveModalOutlineItemViewText.defaultMessage}`,
|
||||
});
|
||||
expect(currentUnitItemBtn).toBeInTheDocument();
|
||||
userEvent.click(currentUnitItemBtn);
|
||||
});
|
||||
|
||||
const moveModalBtn = getByRole('button', {
|
||||
const moveModalBtn = screen.getByRole('button', {
|
||||
name: moveModalMessages.moveModalSubmitButton.defaultMessage,
|
||||
});
|
||||
expect(moveModalBtn).toBeInTheDocument();
|
||||
@@ -1801,10 +1784,7 @@ describe('<CourseUnit />', () => {
|
||||
});
|
||||
|
||||
it('should display "Move Confirmation" alert after moving and undo operations', async () => {
|
||||
const {
|
||||
queryByRole,
|
||||
getByText,
|
||||
} = render(<RootWrapper />);
|
||||
render(<RootWrapper />);
|
||||
|
||||
axiosMock
|
||||
.onPatch(postXBlockBaseApiUrl())
|
||||
@@ -1821,18 +1801,18 @@ describe('<CourseUnit />', () => {
|
||||
|
||||
simulatePostMessageEvent(messageTypes.rollbackMovedXBlock, { locator: requestData.sourceLocator });
|
||||
|
||||
const dismissButton = queryByRole('button', {
|
||||
const dismissButton = screen.queryByRole('button', {
|
||||
name: /dismiss/i, hidden: true,
|
||||
});
|
||||
const undoButton = queryByRole('button', {
|
||||
const undoButton = screen.queryByRole('button', {
|
||||
name: messages.undoMoveButton.defaultMessage, hidden: true,
|
||||
});
|
||||
const newLocationButton = queryByRole('button', {
|
||||
const newLocationButton = screen.queryByRole('button', {
|
||||
name: messages.newLocationButton.defaultMessage, hidden: true,
|
||||
});
|
||||
|
||||
expect(getByText(messages.alertMoveSuccessTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByText(`${requestData.title} has been moved`)).toBeInTheDocument();
|
||||
expect(screen.getByText(messages.alertMoveSuccessTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(screen.getByText(`${requestData.title} has been moved`)).toBeInTheDocument();
|
||||
expect(dismissButton).toBeInTheDocument();
|
||||
expect(undoButton).toBeInTheDocument();
|
||||
expect(newLocationButton).toBeInTheDocument();
|
||||
@@ -1840,9 +1820,9 @@ describe('<CourseUnit />', () => {
|
||||
userEvent.click(undoButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(getByText(messages.alertMoveCancelTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(screen.getByText(messages.alertMoveCancelTitle.defaultMessage)).toBeInTheDocument();
|
||||
});
|
||||
expect(getByText(
|
||||
expect(screen.getByText(
|
||||
messages.alertMoveCancelDescription.defaultMessage.replace('{title}', requestData.title),
|
||||
)).toBeInTheDocument();
|
||||
expect(dismissButton).toBeInTheDocument();
|
||||
@@ -1851,9 +1831,7 @@ describe('<CourseUnit />', () => {
|
||||
});
|
||||
|
||||
it('should navigate to new location by button click', async () => {
|
||||
const {
|
||||
queryByRole,
|
||||
} = render(<RootWrapper />);
|
||||
render(<RootWrapper />);
|
||||
|
||||
axiosMock
|
||||
.onPatch(postXBlockBaseApiUrl())
|
||||
@@ -1868,7 +1846,7 @@ describe('<CourseUnit />', () => {
|
||||
callbackFn: requestData.callbackFn,
|
||||
}), store.dispatch);
|
||||
|
||||
const newLocationButton = queryByRole('button', {
|
||||
const newLocationButton = screen.queryByRole('button', {
|
||||
name: messages.newLocationButton.defaultMessage, hidden: true,
|
||||
});
|
||||
userEvent.click(newLocationButton);
|
||||
@@ -1880,17 +1858,15 @@ describe('<CourseUnit />', () => {
|
||||
});
|
||||
|
||||
describe('XBlock restrict access', () => {
|
||||
it('opens xblock restrict access modal successfully', () => {
|
||||
const {
|
||||
getByTitle, getByTestId,
|
||||
} = render(<RootWrapper />);
|
||||
it('opens xblock restrict access modal successfully', async () => {
|
||||
render(<RootWrapper />);
|
||||
|
||||
const modalSubtitleText = configureModalMessages.restrictAccessTo.defaultMessage;
|
||||
const modalCancelBtnText = configureModalMessages.cancelButton.defaultMessage;
|
||||
const modalSaveBtnText = configureModalMessages.saveButton.defaultMessage;
|
||||
|
||||
waitFor(() => {
|
||||
const iframe = getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
|
||||
await waitFor(() => {
|
||||
const iframe = screen.getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
|
||||
const usageId = courseVerticalChildrenMock.children[0].block_id;
|
||||
expect(iframe).toBeInTheDocument();
|
||||
|
||||
@@ -1899,8 +1875,8 @@ describe('<CourseUnit />', () => {
|
||||
});
|
||||
});
|
||||
|
||||
waitFor(() => {
|
||||
const configureModal = getByTestId('configure-modal');
|
||||
await waitFor(() => {
|
||||
const configureModal = screen.getByTestId('configure-modal');
|
||||
|
||||
expect(within(configureModal).getByText(modalSubtitleText)).toBeInTheDocument();
|
||||
expect(within(configureModal).getByRole('button', { name: modalCancelBtnText })).toBeInTheDocument();
|
||||
@@ -1909,20 +1885,18 @@ describe('<CourseUnit />', () => {
|
||||
});
|
||||
|
||||
it('closes xblock restrict access modal when cancel button is clicked', async () => {
|
||||
const {
|
||||
getByTitle, queryByTestId, getByTestId,
|
||||
} = render(<RootWrapper />);
|
||||
render(<RootWrapper />);
|
||||
|
||||
waitFor(() => {
|
||||
const iframe = getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
|
||||
await waitFor(() => {
|
||||
const iframe = screen.getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
|
||||
expect(iframe).toBeInTheDocument();
|
||||
simulatePostMessageEvent(messageTypes.manageXBlockAccess, {
|
||||
usageId: courseVerticalChildrenMock.children[0].block_id,
|
||||
});
|
||||
});
|
||||
|
||||
waitFor(() => {
|
||||
const configureModal = getByTestId('configure-modal');
|
||||
await waitFor(() => {
|
||||
const configureModal = screen.getByTestId('configure-modal');
|
||||
expect(configureModal).toBeInTheDocument();
|
||||
userEvent.click(within(configureModal).getByRole('button', {
|
||||
name: configureModalMessages.cancelButton.defaultMessage,
|
||||
@@ -1930,7 +1904,7 @@ describe('<CourseUnit />', () => {
|
||||
expect(handleConfigureSubmitMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
expect(queryByTestId('configure-modal')).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId('configure-modal')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('handles submit xblock restrict access data when save button is clicked', async () => {
|
||||
@@ -1941,90 +1915,103 @@ describe('<CourseUnit />', () => {
|
||||
})
|
||||
.reply(200, { dummy: 'value' });
|
||||
|
||||
const {
|
||||
getByTitle, getByRole, getByTestId,
|
||||
} = render(<RootWrapper />);
|
||||
render(<RootWrapper />);
|
||||
|
||||
const accessGroupName1 = userPartitionInfoFormatted.selectablePartitions[0].groups[0].name;
|
||||
const accessGroupName2 = userPartitionInfoFormatted.selectablePartitions[0].groups[1].name;
|
||||
|
||||
waitFor(() => {
|
||||
const iframe = getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
|
||||
await waitFor(() => {
|
||||
const iframe = screen.getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
|
||||
expect(iframe).toBeInTheDocument();
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
simulatePostMessageEvent(messageTypes.manageXBlockAccess, {
|
||||
usageId: courseVerticalChildrenMock.children[0].block_id,
|
||||
});
|
||||
});
|
||||
|
||||
waitFor(() => {
|
||||
const configureModal = getByTestId('configure-modal');
|
||||
expect(configureModal).toBeInTheDocument();
|
||||
const configureModal = await waitFor(() => screen.getByTestId('configure-modal'));
|
||||
expect(configureModal).toBeInTheDocument();
|
||||
|
||||
expect(within(configureModal).queryByText(accessGroupName1)).not.toBeInTheDocument();
|
||||
expect(within(configureModal).queryByText(accessGroupName2)).not.toBeInTheDocument();
|
||||
expect(within(configureModal).queryByText(accessGroupName1)).not.toBeInTheDocument();
|
||||
expect(within(configureModal).queryByText(accessGroupName2)).not.toBeInTheDocument();
|
||||
|
||||
const restrictAccessSelect = getByRole('combobox', {
|
||||
name: configureModalMessages.restrictAccessTo.defaultMessage,
|
||||
});
|
||||
|
||||
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 restrictAccessSelect = screen.getByRole('combobox', {
|
||||
name: configureModalMessages.restrictAccessTo.defaultMessage,
|
||||
});
|
||||
|
||||
await userEvent.selectOptions(restrictAccessSelect, '0');
|
||||
|
||||
await waitFor(() => {
|
||||
userPartitionInfoFormatted.selectablePartitions[0].groups.forEach((group) => {
|
||||
const checkbox = within(configureModal).getByRole('checkbox', { name: group.name });
|
||||
expect(checkbox).not.toBeChecked();
|
||||
expect(checkbox).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
const group1Checkbox = within(configureModal).getByRole('checkbox', { name: accessGroupName1 });
|
||||
await userEvent.click(group1Checkbox);
|
||||
expect(group1Checkbox).toBeChecked();
|
||||
|
||||
const saveModalBtnText = within(configureModal).getByRole('button', {
|
||||
name: configureModalMessages.saveButton.defaultMessage,
|
||||
});
|
||||
|
||||
expect(saveModalBtnText).toBeInTheDocument();
|
||||
await userEvent.click(saveModalBtnText);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(axiosMock.history.post.length).toBeGreaterThan(0);
|
||||
expect(axiosMock.history.post[0].url).toBe(getXBlockBaseApiUrl(id));
|
||||
});
|
||||
|
||||
expect(screen.queryByTestId('configure-modal')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('renders and navigates to the new HTML XBlock editor after xblock duplicating', async () => {
|
||||
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);
|
||||
const checkLegacyEditModalOnEditMessage = async () => {
|
||||
render(<RootWrapper />);
|
||||
|
||||
await waitFor(() => {
|
||||
const iframe = getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
|
||||
expect(iframe).toBeInTheDocument();
|
||||
simulatePostMessageEvent(messageTypes.currentXBlockId, {
|
||||
id: targetBlockId,
|
||||
});
|
||||
const editButton = screen.getByTestId('header-edit-button');
|
||||
expect(editButton).toBeInTheDocument();
|
||||
const xblocksIframe = screen.getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
|
||||
expect(xblocksIframe).toBeInTheDocument();
|
||||
userEvent.click(editButton);
|
||||
});
|
||||
};
|
||||
|
||||
const checkRenderVisibilityModal = async (headingMessageId) => {
|
||||
const { findByRole, getByTestId } = render(<RootWrapper />);
|
||||
let configureModal;
|
||||
let restrictAccessSelect;
|
||||
|
||||
const headerConfigureBtn = await findByRole('button', { name: /settings/i });
|
||||
await userEvent.click(headerConfigureBtn);
|
||||
|
||||
await waitFor(() => {
|
||||
configureModal = getByTestId('configure-modal');
|
||||
restrictAccessSelect = within(configureModal)
|
||||
.getByRole('combobox', { name: configureModalMessages.restrictAccessTo.defaultMessage });
|
||||
expect(within(configureModal)
|
||||
.getByRole('heading', { name: configureModalMessages[headingMessageId].defaultMessage })).toBeInTheDocument();
|
||||
expect(within(configureModal)
|
||||
.queryByText(configureModalMessages.unitVisibility.defaultMessage)).not.toBeInTheDocument();
|
||||
expect(within(configureModal)
|
||||
.getByText(configureModalMessages.restrictAccessTo.defaultMessage)).toBeInTheDocument();
|
||||
expect(restrictAccessSelect).toBeInTheDocument();
|
||||
expect(restrictAccessSelect).toHaveValue('-1');
|
||||
});
|
||||
|
||||
waitFor(() => {
|
||||
simulatePostMessageEvent(messageTypes.duplicateXBlock, {});
|
||||
simulatePostMessageEvent(messageTypes.newXBlockEditor, {});
|
||||
expect(mockedUsedNavigate)
|
||||
.toHaveBeenCalledWith(`/course/${courseId}/editor/html/${targetBlockId}`, { replace: true });
|
||||
});
|
||||
});
|
||||
const modalSaveBtn = within(configureModal)
|
||||
.getByRole('button', { name: configureModalMessages.saveButton.defaultMessage });
|
||||
userEvent.click(modalSaveBtn);
|
||||
};
|
||||
|
||||
describe('Library Content page', () => {
|
||||
const newUnitId = '12345';
|
||||
const sequenceId = courseSectionVerticalMock.subsection_location;
|
||||
|
||||
beforeEach(async () => {
|
||||
axiosMock
|
||||
@@ -2043,27 +2030,29 @@ describe('<CourseUnit />', () => {
|
||||
await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch);
|
||||
});
|
||||
|
||||
it('navigates to library content page on receive window event', () => {
|
||||
it('navigates to library content page on receive window event', async () => {
|
||||
render(<RootWrapper />);
|
||||
|
||||
simulatePostMessageEvent(messageTypes.handleViewXBlockContent, { usageId: newUnitId });
|
||||
expect(mockedUsedNavigate).toHaveBeenCalledWith(`/course/${courseId}/container/${newUnitId}/${sequenceId}`);
|
||||
await waitFor(() => {
|
||||
simulatePostMessageEvent(messageTypes.handleViewXBlockContent, { usageId: newUnitId });
|
||||
expect(mockedUsedNavigate).toHaveBeenCalledWith(`/course/${courseId}/container/${newUnitId}/${sequenceId}`);
|
||||
});
|
||||
});
|
||||
|
||||
it('should render library content page correctly', async () => {
|
||||
const {
|
||||
getByText,
|
||||
findByText,
|
||||
getByRole,
|
||||
queryByRole,
|
||||
getByTestId,
|
||||
findByTestId,
|
||||
} = render(<RootWrapper />);
|
||||
|
||||
const currentSectionName = courseUnitIndexMock.ancestor_info.ancestors[1].display_name;
|
||||
const currentSubSectionName = courseUnitIndexMock.ancestor_info.ancestors[1].display_name;
|
||||
const currentSectionName = courseSectionVerticalMock.xblock_info.ancestor_info.ancestors[1].display_name;
|
||||
const currentSubSectionName = courseSectionVerticalMock.xblock_info.ancestor_info.ancestors[1].display_name;
|
||||
|
||||
const unitHeaderTitle = await findByTestId('unit-header-title');
|
||||
await findByText(unitDisplayName);
|
||||
await waitFor(() => {
|
||||
const unitHeaderTitle = getByTestId('unit-header-title');
|
||||
expect(getByText(unitDisplayName)).toBeInTheDocument();
|
||||
expect(within(unitHeaderTitle).getByRole('button', { name: headerTitleMessages.altButtonEdit.defaultMessage })).toBeInTheDocument();
|
||||
expect(within(unitHeaderTitle).getByRole('button', { name: headerTitleMessages.altButtonSettings.defaultMessage })).toBeInTheDocument();
|
||||
expect(getByRole('button', { name: currentSectionName })).toBeInTheDocument();
|
||||
@@ -2077,5 +2066,242 @@ describe('<CourseUnit />', () => {
|
||||
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';
|
||||
|
||||
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);
|
||||
});
|
||||
|
||||
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 = courseSectionVerticalMock.xblock_info.ancestor_info.ancestors[1].display_name;
|
||||
const currentSubSectionName = courseSectionVerticalMock.xblock_info.ancestor_info.ancestors[1].display_name;
|
||||
const helpLinkUrl = 'https://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/latest/developing_course/course_components.html#components-that-contain-other-components';
|
||||
|
||||
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
|
||||
.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(getCourseSectionVerticalApiUrl(courseId))
|
||||
.reply(200, {
|
||||
...courseSectionVerticalMock,
|
||||
xblock_info: {
|
||||
...courseSectionVerticalMock.xblock_info,
|
||||
upstreamInfo: {
|
||||
...courseSectionVerticalMock.xblock_info,
|
||||
upstreamRef: 'lct:org:lib:unit:unit-1',
|
||||
upstreamLink: 'some-link',
|
||||
},
|
||||
},
|
||||
});
|
||||
await executeThunk(fetchCourseSectionVerticalData(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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,7 +5,7 @@ module.exports = {
|
||||
has_children: true,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
|
||||
unit_level_discussions: false,
|
||||
child_info: {
|
||||
category: 'chapter',
|
||||
@@ -18,7 +18,7 @@ module.exports = {
|
||||
has_children: true,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
|
||||
child_info: {
|
||||
category: 'sequential',
|
||||
display_name: 'Subsection',
|
||||
@@ -30,7 +30,7 @@ module.exports = {
|
||||
has_children: true,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
|
||||
child_info: {
|
||||
category: 'vertical',
|
||||
display_name: 'Unit',
|
||||
@@ -42,7 +42,7 @@ module.exports = {
|
||||
has_children: true,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
|
||||
child_info: {
|
||||
children: [
|
||||
{
|
||||
@@ -52,7 +52,7 @@ module.exports = {
|
||||
has_children: false,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@html+block@f7cc083ff66d442eafafd48152881276',
|
||||
@@ -61,7 +61,7 @@ module.exports = {
|
||||
has_children: false,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@video+block@0b9e39477cf34507a7a48f74be381fdd',
|
||||
@@ -70,7 +70,7 @@ module.exports = {
|
||||
has_children: false,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@video+block@6e72ebc448694e42ac56553af74304e7',
|
||||
@@ -79,7 +79,7 @@ module.exports = {
|
||||
has_children: false,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -94,7 +94,7 @@ module.exports = {
|
||||
has_children: true,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -106,7 +106,7 @@ module.exports = {
|
||||
has_children: true,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
|
||||
child_info: {
|
||||
category: 'sequential',
|
||||
display_name: 'Subsection',
|
||||
@@ -118,7 +118,7 @@ module.exports = {
|
||||
has_children: true,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
|
||||
child_info: {
|
||||
category: 'vertical',
|
||||
display_name: 'Unit',
|
||||
@@ -130,7 +130,7 @@ module.exports = {
|
||||
has_children: true,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
|
||||
child_info: {
|
||||
children: [
|
||||
{
|
||||
@@ -140,7 +140,7 @@ module.exports = {
|
||||
has_children: false,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -152,7 +152,7 @@ module.exports = {
|
||||
has_children: true,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
|
||||
child_info: {
|
||||
children: [
|
||||
{
|
||||
@@ -162,7 +162,7 @@ module.exports = {
|
||||
has_children: false,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@video+block@7e9b434e6de3435ab99bd3fb25bde807',
|
||||
@@ -171,7 +171,7 @@ module.exports = {
|
||||
has_children: false,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@discussion+block@412dc8dbb6674014862237b23c1f643f',
|
||||
@@ -180,7 +180,7 @@ module.exports = {
|
||||
has_children: false,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -192,7 +192,7 @@ module.exports = {
|
||||
has_children: true,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
|
||||
child_info: {
|
||||
children: [
|
||||
{
|
||||
@@ -202,7 +202,7 @@ module.exports = {
|
||||
has_children: false,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@video+block@5c90cffecd9b48b188cbfea176bf7fe9',
|
||||
@@ -211,7 +211,7 @@ module.exports = {
|
||||
has_children: false,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@discussion+block@722085be27c84ac693cfebc8ac5da700',
|
||||
@@ -220,7 +220,7 @@ module.exports = {
|
||||
has_children: false,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -232,7 +232,7 @@ module.exports = {
|
||||
has_children: true,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
|
||||
child_info: {
|
||||
children: [
|
||||
{
|
||||
@@ -242,7 +242,7 @@ module.exports = {
|
||||
has_children: false,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@discussion+block@9f9e1373cc8243b985c8750cc8acec7d',
|
||||
@@ -251,7 +251,7 @@ module.exports = {
|
||||
has_children: false,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -263,7 +263,7 @@ module.exports = {
|
||||
has_children: true,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
|
||||
child_info: {
|
||||
children: [
|
||||
{
|
||||
@@ -273,7 +273,7 @@ module.exports = {
|
||||
has_children: false,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@video+block@636541acbae448d98ab484b028c9a7f6',
|
||||
@@ -282,7 +282,7 @@ module.exports = {
|
||||
has_children: false,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@discussion+block@e2cb0e0994f84b0abfa5f4ae42ed9d44',
|
||||
@@ -291,7 +291,7 @@ module.exports = {
|
||||
has_children: false,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -303,7 +303,7 @@ module.exports = {
|
||||
has_children: true,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
|
||||
child_info: {
|
||||
children: [
|
||||
{
|
||||
@@ -313,7 +313,7 @@ module.exports = {
|
||||
has_children: false,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@discussion+block@3169f89efde2452993f2f2d9bc74f5b2',
|
||||
@@ -322,7 +322,7 @@ module.exports = {
|
||||
has_children: false,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -334,7 +334,7 @@ module.exports = {
|
||||
has_children: true,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
|
||||
child_info: {
|
||||
children: [
|
||||
{
|
||||
@@ -344,7 +344,7 @@ module.exports = {
|
||||
has_children: false,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@discussion+block@1c8d47c425724346a7968fa1bc745dcd',
|
||||
@@ -353,7 +353,7 @@ module.exports = {
|
||||
has_children: false,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -365,7 +365,7 @@ module.exports = {
|
||||
has_children: true,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
|
||||
child_info: {
|
||||
children: [
|
||||
{
|
||||
@@ -375,7 +375,7 @@ module.exports = {
|
||||
has_children: false,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@html+block@2574c523e97b477a9d72fbb37bfb995f',
|
||||
@@ -384,7 +384,7 @@ module.exports = {
|
||||
has_children: false,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@problem+block@932e6f2ce8274072a355a94560216d1a',
|
||||
@@ -393,7 +393,7 @@ module.exports = {
|
||||
has_children: false,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@problem+block@303034da25524878a2e66fb57c91cf85',
|
||||
@@ -402,7 +402,7 @@ module.exports = {
|
||||
has_children: false,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@discussion+block@ffa5817d49e14fec83ad6187cbe16358',
|
||||
@@ -411,7 +411,7 @@ module.exports = {
|
||||
has_children: false,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -423,7 +423,7 @@ module.exports = {
|
||||
has_children: true,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
|
||||
child_info: {
|
||||
children: [
|
||||
{
|
||||
@@ -433,7 +433,7 @@ module.exports = {
|
||||
has_children: false,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -448,7 +448,7 @@ module.exports = {
|
||||
has_children: true,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
|
||||
child_info: {
|
||||
category: 'vertical',
|
||||
display_name: 'Unit',
|
||||
@@ -460,7 +460,7 @@ module.exports = {
|
||||
has_children: true,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
|
||||
child_info: {
|
||||
children: [
|
||||
{
|
||||
@@ -470,7 +470,7 @@ module.exports = {
|
||||
has_children: false,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@discussion+block@e5eac7e1a5a24f5fa7ed77bb6d136591',
|
||||
@@ -479,7 +479,7 @@ module.exports = {
|
||||
has_children: false,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -491,7 +491,7 @@ module.exports = {
|
||||
has_children: true,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
|
||||
child_info: {
|
||||
children: [
|
||||
{
|
||||
@@ -501,7 +501,7 @@ module.exports = {
|
||||
has_children: false,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@discussion+block@5ab88e67d46049b9aa694cb240c39cef',
|
||||
@@ -510,7 +510,7 @@ module.exports = {
|
||||
has_children: false,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -522,7 +522,7 @@ module.exports = {
|
||||
has_children: true,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
|
||||
child_info: {
|
||||
children: [
|
||||
{
|
||||
@@ -532,7 +532,7 @@ module.exports = {
|
||||
has_children: false,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@discussion+block@67c26b1e826e47aaa29757f62bcd1ad0',
|
||||
@@ -541,7 +541,7 @@ module.exports = {
|
||||
has_children: false,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -553,7 +553,7 @@ module.exports = {
|
||||
has_children: true,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
|
||||
child_info: {
|
||||
children: [
|
||||
{
|
||||
@@ -563,7 +563,7 @@ module.exports = {
|
||||
has_children: false,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@discussion+block@870371212ba04dcf9536d7c7b8f3109e',
|
||||
@@ -572,7 +572,7 @@ module.exports = {
|
||||
has_children: false,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -584,7 +584,7 @@ module.exports = {
|
||||
has_children: true,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
|
||||
child_info: {
|
||||
children: [
|
||||
{
|
||||
@@ -594,7 +594,7 @@ module.exports = {
|
||||
has_children: false,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@discussion+block@4d672c5893cb4f1dad0de67d2008522e',
|
||||
@@ -603,7 +603,7 @@ module.exports = {
|
||||
has_children: false,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -615,7 +615,7 @@ module.exports = {
|
||||
has_children: true,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
|
||||
child_info: {
|
||||
children: [
|
||||
{
|
||||
@@ -625,7 +625,7 @@ module.exports = {
|
||||
has_children: false,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@discussion+block@501aed9d902349eeb2191fa505548de2',
|
||||
@@ -634,7 +634,7 @@ module.exports = {
|
||||
has_children: false,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -646,7 +646,7 @@ module.exports = {
|
||||
has_children: true,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
|
||||
child_info: {
|
||||
children: [
|
||||
{
|
||||
@@ -656,7 +656,7 @@ module.exports = {
|
||||
has_children: false,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@discussion+block@6244918637ed4ff4b5f94a840a7e4b43',
|
||||
@@ -665,7 +665,7 @@ module.exports = {
|
||||
has_children: false,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -677,7 +677,7 @@ module.exports = {
|
||||
has_children: true,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
|
||||
child_info: {
|
||||
children: [],
|
||||
},
|
||||
@@ -695,7 +695,7 @@ module.exports = {
|
||||
has_children: true,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
|
||||
child_info: {
|
||||
category: 'sequential',
|
||||
display_name: 'Subsection',
|
||||
@@ -707,7 +707,7 @@ module.exports = {
|
||||
has_children: true,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
|
||||
child_info: {
|
||||
category: 'vertical',
|
||||
display_name: 'Unit',
|
||||
@@ -719,7 +719,7 @@ module.exports = {
|
||||
has_children: true,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
|
||||
child_info: {
|
||||
children: [
|
||||
{
|
||||
@@ -729,7 +729,7 @@ module.exports = {
|
||||
has_children: false,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -741,7 +741,7 @@ module.exports = {
|
||||
has_children: true,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
|
||||
child_info: {
|
||||
children: [
|
||||
{
|
||||
@@ -751,7 +751,7 @@ module.exports = {
|
||||
has_children: false,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@discussion+block@6f7a6670f87147149caeff6afa07a526',
|
||||
@@ -760,7 +760,7 @@ module.exports = {
|
||||
has_children: false,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -772,7 +772,7 @@ module.exports = {
|
||||
has_children: true,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
|
||||
child_info: {
|
||||
children: [
|
||||
{
|
||||
@@ -782,7 +782,7 @@ module.exports = {
|
||||
has_children: false,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@discussion+block@e0d7423118ab432582d03e8e8dad8e36',
|
||||
@@ -791,7 +791,7 @@ module.exports = {
|
||||
has_children: false,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -803,7 +803,7 @@ module.exports = {
|
||||
has_children: true,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
|
||||
child_info: {
|
||||
children: [
|
||||
{
|
||||
@@ -813,7 +813,7 @@ module.exports = {
|
||||
has_children: false,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@discussion+block@03f051f9a8814881a3783d2511613aa6',
|
||||
@@ -822,7 +822,7 @@ module.exports = {
|
||||
has_children: false,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -834,7 +834,7 @@ module.exports = {
|
||||
has_children: true,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
|
||||
child_info: {
|
||||
children: [
|
||||
{
|
||||
@@ -844,7 +844,7 @@ module.exports = {
|
||||
has_children: false,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -859,7 +859,7 @@ module.exports = {
|
||||
has_children: true,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
|
||||
child_info: {
|
||||
category: 'vertical',
|
||||
display_name: 'Unit',
|
||||
@@ -871,7 +871,7 @@ module.exports = {
|
||||
has_children: true,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
|
||||
child_info: {
|
||||
children: [
|
||||
{
|
||||
@@ -881,7 +881,7 @@ module.exports = {
|
||||
has_children: false,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -893,7 +893,7 @@ module.exports = {
|
||||
has_children: true,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
|
||||
child_info: {
|
||||
children: [
|
||||
{
|
||||
@@ -903,7 +903,7 @@ module.exports = {
|
||||
has_children: false,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@problem+block@python_grader',
|
||||
@@ -912,7 +912,7 @@ module.exports = {
|
||||
has_children: false,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@discussion+block@c6cd4bea43454aaea60ad01beb0cf213',
|
||||
@@ -921,7 +921,7 @@ module.exports = {
|
||||
has_children: false,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -933,7 +933,7 @@ module.exports = {
|
||||
has_children: true,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
|
||||
child_info: {
|
||||
children: [
|
||||
{
|
||||
@@ -943,7 +943,7 @@ module.exports = {
|
||||
has_children: false,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@problem+block@free_form_simulation',
|
||||
@@ -952,7 +952,7 @@ module.exports = {
|
||||
has_children: false,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@problem+block@logic_gate_problem',
|
||||
@@ -961,7 +961,7 @@ module.exports = {
|
||||
has_children: false,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@discussion+block@4f06b358a96f4d1dae57d6d81acd06f2',
|
||||
@@ -970,7 +970,7 @@ module.exports = {
|
||||
has_children: false,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -982,7 +982,7 @@ module.exports = {
|
||||
has_children: true,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
|
||||
child_info: {
|
||||
children: [
|
||||
{
|
||||
@@ -992,7 +992,7 @@ module.exports = {
|
||||
has_children: false,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@problem+block@700x_proteinmake',
|
||||
@@ -1001,7 +1001,7 @@ module.exports = {
|
||||
has_children: false,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@discussion+block@ed01bcd164e64038a78964a16eac3edc',
|
||||
@@ -1010,7 +1010,7 @@ module.exports = {
|
||||
has_children: false,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -1022,7 +1022,7 @@ module.exports = {
|
||||
has_children: true,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
|
||||
child_info: {
|
||||
children: [
|
||||
{
|
||||
@@ -1032,7 +1032,7 @@ module.exports = {
|
||||
has_children: false,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -1047,7 +1047,7 @@ module.exports = {
|
||||
has_children: true,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
|
||||
child_info: {
|
||||
category: 'vertical',
|
||||
display_name: 'Unit',
|
||||
@@ -1059,7 +1059,7 @@ module.exports = {
|
||||
has_children: true,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
|
||||
child_info: {
|
||||
children: [
|
||||
{
|
||||
@@ -1069,7 +1069,7 @@ module.exports = {
|
||||
has_children: false,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@discussion+block@12ad4f3ff4c14114a6e629b00e000976',
|
||||
@@ -1078,7 +1078,7 @@ module.exports = {
|
||||
has_children: false,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -1096,7 +1096,7 @@ module.exports = {
|
||||
has_children: true,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
|
||||
child_info: {
|
||||
category: 'sequential',
|
||||
display_name: 'Subsection',
|
||||
@@ -1108,7 +1108,7 @@ module.exports = {
|
||||
has_children: true,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
|
||||
child_info: {
|
||||
category: 'vertical',
|
||||
display_name: 'Unit',
|
||||
@@ -1120,7 +1120,7 @@ module.exports = {
|
||||
has_children: true,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
|
||||
child_info: {
|
||||
children: [
|
||||
{
|
||||
@@ -1130,7 +1130,7 @@ module.exports = {
|
||||
has_children: false,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -1142,7 +1142,7 @@ module.exports = {
|
||||
has_children: true,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
|
||||
child_info: {
|
||||
children: [
|
||||
{
|
||||
@@ -1152,7 +1152,7 @@ module.exports = {
|
||||
has_children: false,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@discussion+block@discussion_5deb6081620d',
|
||||
@@ -1161,7 +1161,7 @@ module.exports = {
|
||||
has_children: false,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -1173,7 +1173,7 @@ module.exports = {
|
||||
has_children: true,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
|
||||
child_info: {
|
||||
children: [
|
||||
{
|
||||
@@ -1183,7 +1183,7 @@ module.exports = {
|
||||
has_children: false,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -1195,7 +1195,7 @@ module.exports = {
|
||||
has_children: true,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
|
||||
child_info: {
|
||||
children: [
|
||||
{
|
||||
@@ -1205,7 +1205,7 @@ module.exports = {
|
||||
has_children: false,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -1220,7 +1220,7 @@ module.exports = {
|
||||
has_children: true,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
|
||||
child_info: {
|
||||
category: 'vertical',
|
||||
display_name: 'Unit',
|
||||
@@ -1232,7 +1232,7 @@ module.exports = {
|
||||
has_children: true,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
|
||||
child_info: {
|
||||
children: [
|
||||
{
|
||||
@@ -1242,7 +1242,7 @@ module.exports = {
|
||||
has_children: false,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -1257,7 +1257,7 @@ module.exports = {
|
||||
has_children: true,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
|
||||
child_info: {
|
||||
category: 'vertical',
|
||||
display_name: 'Unit',
|
||||
@@ -1269,7 +1269,7 @@ module.exports = {
|
||||
has_children: true,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
|
||||
child_info: {
|
||||
children: [
|
||||
{
|
||||
@@ -1279,7 +1279,7 @@ module.exports = {
|
||||
has_children: false,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@html+block@55cbc99f262443d886a25cf84594eafb',
|
||||
@@ -1288,7 +1288,7 @@ module.exports = {
|
||||
has_children: false,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@discussion+block@ade92343df3d4953a40ab3adc8805390',
|
||||
@@ -1297,7 +1297,7 @@ module.exports = {
|
||||
has_children: false,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -1315,7 +1315,7 @@ module.exports = {
|
||||
has_children: true,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
|
||||
child_info: {
|
||||
category: 'sequential',
|
||||
display_name: 'Subsection',
|
||||
@@ -1327,7 +1327,7 @@ module.exports = {
|
||||
has_children: true,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
|
||||
child_info: {
|
||||
category: 'vertical',
|
||||
display_name: 'Unit',
|
||||
@@ -1339,7 +1339,7 @@ module.exports = {
|
||||
has_children: true,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
|
||||
child_info: {
|
||||
children: [
|
||||
{
|
||||
@@ -1349,7 +1349,7 @@ module.exports = {
|
||||
has_children: false,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -1361,7 +1361,7 @@ module.exports = {
|
||||
has_children: true,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
|
||||
child_info: {
|
||||
children: [
|
||||
{
|
||||
@@ -1371,7 +1371,7 @@ module.exports = {
|
||||
has_children: false,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@discussion+block@4aba537a78774bd5a862485a8563c345',
|
||||
@@ -1380,7 +1380,7 @@ module.exports = {
|
||||
has_children: false,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -1392,7 +1392,7 @@ module.exports = {
|
||||
has_children: true,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
|
||||
child_info: {
|
||||
children: [
|
||||
{
|
||||
@@ -1402,7 +1402,7 @@ module.exports = {
|
||||
has_children: false,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@discussion+block@f480df4ce91347c5ae4301ddf6146238',
|
||||
@@ -1411,7 +1411,7 @@ module.exports = {
|
||||
has_children: false,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -1423,7 +1423,7 @@ module.exports = {
|
||||
has_children: true,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
|
||||
child_info: {
|
||||
children: [
|
||||
{
|
||||
@@ -1433,7 +1433,7 @@ module.exports = {
|
||||
has_children: false,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@discussion+block@b8cec2a19ebf463f90cd3544c7927b0e',
|
||||
@@ -1442,7 +1442,7 @@ module.exports = {
|
||||
has_children: false,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -1454,7 +1454,7 @@ module.exports = {
|
||||
has_children: true,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
|
||||
child_info: {
|
||||
children: [
|
||||
{
|
||||
@@ -1464,7 +1464,7 @@ module.exports = {
|
||||
has_children: false,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@problem+block@d1b84dcd39b0423d9e288f27f0f7f242',
|
||||
@@ -1473,7 +1473,7 @@ module.exports = {
|
||||
has_children: false,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@discussion+block@cd177caa62444fbca48aa8f843f09eac',
|
||||
@@ -1482,7 +1482,7 @@ module.exports = {
|
||||
has_children: false,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -1494,7 +1494,7 @@ module.exports = {
|
||||
has_children: true,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
|
||||
child_info: {
|
||||
children: [
|
||||
{
|
||||
@@ -1504,7 +1504,7 @@ module.exports = {
|
||||
has_children: false,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@discussion+block@ddede76df71045ffa16de9d1481d2119',
|
||||
@@ -1513,7 +1513,7 @@ module.exports = {
|
||||
has_children: false,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -1525,7 +1525,7 @@ module.exports = {
|
||||
has_children: true,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
|
||||
child_info: {
|
||||
children: [
|
||||
{
|
||||
@@ -1535,7 +1535,7 @@ module.exports = {
|
||||
has_children: false,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@discussion+block@1a810b1a3b2447b998f0917d0e5a802b',
|
||||
@@ -1544,7 +1544,7 @@ module.exports = {
|
||||
has_children: false,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -1556,7 +1556,7 @@ module.exports = {
|
||||
has_children: true,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
|
||||
child_info: {
|
||||
children: [
|
||||
{
|
||||
@@ -1566,7 +1566,7 @@ module.exports = {
|
||||
has_children: false,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@discussion+block@23e6eda482c04335af2bb265beacaf59',
|
||||
@@ -1575,7 +1575,7 @@ module.exports = {
|
||||
has_children: false,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -1587,7 +1587,7 @@ module.exports = {
|
||||
has_children: true,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
|
||||
child_info: {
|
||||
children: [
|
||||
{
|
||||
@@ -1597,7 +1597,7 @@ module.exports = {
|
||||
has_children: false,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@html+block@6b6bee43c7c641509da71c9299cc9f5a',
|
||||
@@ -1606,7 +1606,7 @@ module.exports = {
|
||||
has_children: false,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -1624,7 +1624,7 @@ module.exports = {
|
||||
has_children: true,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
|
||||
child_info: {
|
||||
category: 'sequential',
|
||||
display_name: 'Subsection',
|
||||
@@ -1636,7 +1636,7 @@ module.exports = {
|
||||
has_children: true,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
|
||||
child_info: {
|
||||
category: 'vertical',
|
||||
display_name: 'Unit',
|
||||
@@ -1648,7 +1648,7 @@ module.exports = {
|
||||
has_children: true,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
|
||||
child_info: {
|
||||
children: [
|
||||
{
|
||||
@@ -1658,7 +1658,7 @@ module.exports = {
|
||||
has_children: false,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -1673,7 +1673,7 @@ module.exports = {
|
||||
has_children: true,
|
||||
video_sharing_enabled: true,
|
||||
video_sharing_options: 'per-video',
|
||||
video_sharing_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/open-release-quince.master/developing_course/social_sharing.html',
|
||||
video_sharing_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/social_sharing.html',
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -1310,7 +1310,7 @@ module.exports = {
|
||||
highlights: [],
|
||||
highlights_enabled: true,
|
||||
highlights_preview_only: false,
|
||||
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',
|
||||
highlights_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/manage_course_highlight_emails.html',
|
||||
ancestor_has_staff_lock: false,
|
||||
user_partition_info: {
|
||||
selectable_partitions: [
|
||||
@@ -1396,7 +1396,7 @@ module.exports = {
|
||||
highlights_enabled_for_messaging: false,
|
||||
highlights_enabled: true,
|
||||
highlights_preview_only: false,
|
||||
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',
|
||||
highlights_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/manage_course_highlight_emails.html',
|
||||
enable_proctored_exams: false,
|
||||
create_zendesk_tickets: true,
|
||||
enable_timed_exams: true,
|
||||
|
||||
@@ -1,1123 +0,0 @@
|
||||
module.exports = {
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@867dddb6f55d410caaa9c1eb9c6743ec',
|
||||
display_name: 'Getting Started',
|
||||
category: 'vertical',
|
||||
has_children: true,
|
||||
edited_on: 'Jan 03, 2024 at 12:06 UTC',
|
||||
published: true,
|
||||
published_on: 'Dec 28, 2023 at 10:00 UTC',
|
||||
studio_url: '/container/block-v1:edX+DemoX+Demo_Course+type@vertical+block@867dddb6f55d410caaa9c1eb9c6743ec',
|
||||
released_to_students: true,
|
||||
release_date: 'Feb 05, 2013 at 05:00 UTC',
|
||||
visibility_state: 'needs_attention',
|
||||
has_explicit_staff_lock: false,
|
||||
start: '2013-02-05T05:00:00Z',
|
||||
graded: false,
|
||||
due_date: '',
|
||||
due: null,
|
||||
relative_weeks_due: null,
|
||||
format: null,
|
||||
course_graders: [
|
||||
'Homework',
|
||||
'Exam',
|
||||
],
|
||||
has_changes: true,
|
||||
actions: {
|
||||
deletable: true,
|
||||
draggable: true,
|
||||
childAddable: true,
|
||||
duplicable: true,
|
||||
},
|
||||
explanatory_message: null,
|
||||
group_access: {},
|
||||
user_partitions: [
|
||||
{
|
||||
id: 50,
|
||||
name: 'Enrollment Track Groups',
|
||||
scheme: 'enrollment_track',
|
||||
groups: [
|
||||
{
|
||||
id: 2,
|
||||
name: 'Verified Certificate',
|
||||
selected: false,
|
||||
deleted: false,
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
name: 'Audit',
|
||||
selected: false,
|
||||
deleted: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
show_correctness: 'always',
|
||||
discussion_enabled: true,
|
||||
data: '',
|
||||
metadata: {
|
||||
display_name: 'Getting Started',
|
||||
xml_attributes: {
|
||||
filename: [
|
||||
'vertical/867dddb6f55d410caaa9c1eb9c6743ec.xml',
|
||||
'vertical/867dddb6f55d410caaa9c1eb9c6743ec.xml',
|
||||
],
|
||||
},
|
||||
},
|
||||
ancestor_info: {
|
||||
ancestors: [
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@19a30717eff543078a5d94ae9d6c18a5',
|
||||
display_name: 'Lesson 1 - Getting Started',
|
||||
category: 'sequential',
|
||||
has_children: true,
|
||||
edited_on: 'Jan 03, 2024 at 12:06 UTC',
|
||||
published: true,
|
||||
published_on: 'Dec 28, 2023 at 10:00 UTC',
|
||||
studio_url: '/course/course-v1:edX+DemoX+Demo_Course?show=block-v1%3AedX%2BDemoX%2BDemo_Course%2Btype%40sequential%2Bblock%4019a30717eff543078a5d94ae9d6c18a5',
|
||||
released_to_students: true,
|
||||
release_date: 'Feb 05, 2013 at 05:00 UTC',
|
||||
visibility_state: 'needs_attention',
|
||||
has_explicit_staff_lock: false,
|
||||
start: '2013-02-05T05:00:00Z',
|
||||
graded: false,
|
||||
due_date: '',
|
||||
due: null,
|
||||
relative_weeks_due: null,
|
||||
format: null,
|
||||
course_graders: [
|
||||
'Homework',
|
||||
'Exam',
|
||||
],
|
||||
has_changes: null,
|
||||
actions: {
|
||||
deletable: true,
|
||||
draggable: true,
|
||||
childAddable: true,
|
||||
duplicable: true,
|
||||
},
|
||||
explanatory_message: null,
|
||||
group_access: {},
|
||||
user_partitions: [
|
||||
{
|
||||
id: 50,
|
||||
name: 'Enrollment Track Groups',
|
||||
scheme: 'enrollment_track',
|
||||
groups: [
|
||||
{
|
||||
id: 2,
|
||||
name: 'Verified Certificate',
|
||||
selected: false,
|
||||
deleted: false,
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
name: 'Audit',
|
||||
selected: false,
|
||||
deleted: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
show_correctness: 'always',
|
||||
hide_after_due: false,
|
||||
is_proctored_exam: false,
|
||||
was_exam_ever_linked_with_external: false,
|
||||
online_proctoring_rules: '',
|
||||
is_practice_exam: false,
|
||||
is_onboarding_exam: false,
|
||||
is_time_limited: false,
|
||||
exam_review_rules: '',
|
||||
default_time_limit_minutes: null,
|
||||
proctoring_exam_configuration_link: null,
|
||||
supports_onboarding: false,
|
||||
show_review_rules: true,
|
||||
child_info: {
|
||||
category: 'vertical',
|
||||
display_name: 'Unit',
|
||||
children: [
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@867dddb6f55d410caaa9c1eb9c6743ec',
|
||||
display_name: 'Getting Started',
|
||||
category: 'vertical',
|
||||
has_children: true,
|
||||
edited_on: 'Jan 03, 2024 at 12:06 UTC',
|
||||
published: true,
|
||||
published_on: 'Dec 28, 2023 at 10:00 UTC',
|
||||
studio_url: '/container/block-v1:edX+DemoX+Demo_Course+type@vertical+block@867dddb6f55d410caaa9c1eb9c6743ec',
|
||||
released_to_students: true,
|
||||
release_date: 'Feb 05, 2013 at 05:00 UTC',
|
||||
visibility_state: 'needs_attention',
|
||||
has_explicit_staff_lock: false,
|
||||
start: '2013-02-05T05:00:00Z',
|
||||
graded: false,
|
||||
due_date: '',
|
||||
due: null,
|
||||
relative_weeks_due: null,
|
||||
format: null,
|
||||
course_graders: [
|
||||
'Homework',
|
||||
'Exam',
|
||||
],
|
||||
has_changes: true,
|
||||
actions: {
|
||||
deletable: true,
|
||||
draggable: true,
|
||||
childAddable: true,
|
||||
duplicable: true,
|
||||
},
|
||||
explanatory_message: null,
|
||||
group_access: {},
|
||||
user_partitions: [
|
||||
{
|
||||
id: 50,
|
||||
name: 'Enrollment Track Groups',
|
||||
scheme: 'enrollment_track',
|
||||
groups: [
|
||||
{
|
||||
id: 2,
|
||||
name: 'Verified Certificate',
|
||||
selected: false,
|
||||
deleted: false,
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
name: 'Audit',
|
||||
selected: false,
|
||||
deleted: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
show_correctness: 'always',
|
||||
discussion_enabled: true,
|
||||
ancestor_has_staff_lock: false,
|
||||
user_partition_info: {
|
||||
selectable_partitions: [
|
||||
{
|
||||
id: 50,
|
||||
name: 'Enrollment Track Groups',
|
||||
scheme: 'enrollment_track',
|
||||
groups: [
|
||||
{
|
||||
id: 2,
|
||||
name: 'Verified Certificate',
|
||||
selected: false,
|
||||
deleted: false,
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
name: 'Audit',
|
||||
selected: false,
|
||||
deleted: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
selected_partition_index: -1,
|
||||
selected_groups_label: '',
|
||||
},
|
||||
enable_copy_paste_units: false,
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@4f6c1b4e316a419ab5b6bf30e6c708e9',
|
||||
display_name: 'Working with Videos',
|
||||
category: 'vertical',
|
||||
has_children: true,
|
||||
edited_on: 'Dec 28, 2023 at 10:00 UTC',
|
||||
published: true,
|
||||
published_on: 'Dec 28, 2023 at 10:00 UTC',
|
||||
studio_url: '/container/block-v1:edX+DemoX+Demo_Course+type@vertical+block@4f6c1b4e316a419ab5b6bf30e6c708e9',
|
||||
released_to_students: true,
|
||||
release_date: 'Feb 05, 2013 at 05:00 UTC',
|
||||
visibility_state: 'live',
|
||||
has_explicit_staff_lock: false,
|
||||
start: '2013-02-05T05:00:00Z',
|
||||
graded: false,
|
||||
due_date: '',
|
||||
due: null,
|
||||
relative_weeks_due: null,
|
||||
format: null,
|
||||
course_graders: [
|
||||
'Homework',
|
||||
'Exam',
|
||||
],
|
||||
has_changes: false,
|
||||
actions: {
|
||||
deletable: true,
|
||||
draggable: true,
|
||||
childAddable: true,
|
||||
duplicable: true,
|
||||
},
|
||||
explanatory_message: null,
|
||||
group_access: {},
|
||||
user_partitions: [
|
||||
{
|
||||
id: 50,
|
||||
name: 'Enrollment Track Groups',
|
||||
scheme: 'enrollment_track',
|
||||
groups: [
|
||||
{
|
||||
id: 2,
|
||||
name: 'Verified Certificate',
|
||||
selected: false,
|
||||
deleted: false,
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
name: 'Audit',
|
||||
selected: false,
|
||||
deleted: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
show_correctness: 'always',
|
||||
discussion_enabled: true,
|
||||
ancestor_has_staff_lock: false,
|
||||
user_partition_info: {
|
||||
selectable_partitions: [
|
||||
{
|
||||
id: 50,
|
||||
name: 'Enrollment Track Groups',
|
||||
scheme: 'enrollment_track',
|
||||
groups: [
|
||||
{
|
||||
id: 2,
|
||||
name: 'Verified Certificate',
|
||||
selected: false,
|
||||
deleted: false,
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
name: 'Audit',
|
||||
selected: false,
|
||||
deleted: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
selected_partition_index: -1,
|
||||
selected_groups_label: '',
|
||||
},
|
||||
enable_copy_paste_units: false,
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@3dc16db8d14842e38324e95d4030b8a0',
|
||||
display_name: 'Videos on edX',
|
||||
category: 'vertical',
|
||||
has_children: true,
|
||||
edited_on: 'Dec 28, 2023 at 10:00 UTC',
|
||||
published: true,
|
||||
published_on: 'Dec 28, 2023 at 10:00 UTC',
|
||||
studio_url: '/container/block-v1:edX+DemoX+Demo_Course+type@vertical+block@3dc16db8d14842e38324e95d4030b8a0',
|
||||
released_to_students: true,
|
||||
release_date: 'Feb 05, 2013 at 05:00 UTC',
|
||||
visibility_state: 'live',
|
||||
has_explicit_staff_lock: false,
|
||||
start: '2013-02-05T05:00:00Z',
|
||||
graded: false,
|
||||
due_date: '',
|
||||
due: null,
|
||||
relative_weeks_due: null,
|
||||
format: null,
|
||||
course_graders: [
|
||||
'Homework',
|
||||
'Exam',
|
||||
],
|
||||
has_changes: false,
|
||||
actions: {
|
||||
deletable: true,
|
||||
draggable: true,
|
||||
childAddable: true,
|
||||
duplicable: true,
|
||||
},
|
||||
explanatory_message: null,
|
||||
group_access: {},
|
||||
user_partitions: [
|
||||
{
|
||||
id: 50,
|
||||
name: 'Enrollment Track Groups',
|
||||
scheme: 'enrollment_track',
|
||||
groups: [
|
||||
{
|
||||
id: 2,
|
||||
name: 'Verified Certificate',
|
||||
selected: false,
|
||||
deleted: false,
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
name: 'Audit',
|
||||
selected: false,
|
||||
deleted: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
show_correctness: 'always',
|
||||
discussion_enabled: true,
|
||||
ancestor_has_staff_lock: false,
|
||||
user_partition_info: {
|
||||
selectable_partitions: [
|
||||
{
|
||||
id: 50,
|
||||
name: 'Enrollment Track Groups',
|
||||
scheme: 'enrollment_track',
|
||||
groups: [
|
||||
{
|
||||
id: 2,
|
||||
name: 'Verified Certificate',
|
||||
selected: false,
|
||||
deleted: false,
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
name: 'Audit',
|
||||
selected: false,
|
||||
deleted: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
selected_partition_index: -1,
|
||||
selected_groups_label: '',
|
||||
},
|
||||
enable_copy_paste_units: false,
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@4a1bba2a403f40bca5ec245e945b0d76',
|
||||
display_name: 'Video Demonstrations',
|
||||
category: 'vertical',
|
||||
has_children: true,
|
||||
edited_on: 'Dec 28, 2023 at 10:00 UTC',
|
||||
published: true,
|
||||
published_on: 'Dec 28, 2023 at 10:00 UTC',
|
||||
studio_url: '/container/block-v1:edX+DemoX+Demo_Course+type@vertical+block@4a1bba2a403f40bca5ec245e945b0d76',
|
||||
released_to_students: true,
|
||||
release_date: 'Feb 05, 2013 at 05:00 UTC',
|
||||
visibility_state: 'live',
|
||||
has_explicit_staff_lock: false,
|
||||
start: '2013-02-05T05:00:00Z',
|
||||
graded: false,
|
||||
due_date: '',
|
||||
due: null,
|
||||
relative_weeks_due: null,
|
||||
format: null,
|
||||
course_graders: [
|
||||
'Homework',
|
||||
'Exam',
|
||||
],
|
||||
has_changes: false,
|
||||
actions: {
|
||||
deletable: true,
|
||||
draggable: true,
|
||||
childAddable: true,
|
||||
duplicable: true,
|
||||
},
|
||||
explanatory_message: null,
|
||||
group_access: {},
|
||||
user_partitions: [
|
||||
{
|
||||
id: 50,
|
||||
name: 'Enrollment Track Groups',
|
||||
scheme: 'enrollment_track',
|
||||
groups: [
|
||||
{
|
||||
id: 2,
|
||||
name: 'Verified Certificate',
|
||||
selected: false,
|
||||
deleted: false,
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
name: 'Audit',
|
||||
selected: false,
|
||||
deleted: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
show_correctness: 'always',
|
||||
discussion_enabled: true,
|
||||
ancestor_has_staff_lock: false,
|
||||
user_partition_info: {
|
||||
selectable_partitions: [
|
||||
{
|
||||
id: 50,
|
||||
name: 'Enrollment Track Groups',
|
||||
scheme: 'enrollment_track',
|
||||
groups: [
|
||||
{
|
||||
id: 2,
|
||||
name: 'Verified Certificate',
|
||||
selected: false,
|
||||
deleted: false,
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
name: 'Audit',
|
||||
selected: false,
|
||||
deleted: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
selected_partition_index: -1,
|
||||
selected_groups_label: '',
|
||||
},
|
||||
enable_copy_paste_units: false,
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@256f17a44983429fb1a60802203ee4e0',
|
||||
display_name: 'Video Presentation Styles',
|
||||
category: 'vertical',
|
||||
has_children: true,
|
||||
edited_on: 'Dec 28, 2023 at 10:00 UTC',
|
||||
published: true,
|
||||
published_on: 'Dec 28, 2023 at 10:00 UTC',
|
||||
studio_url: '/container/block-v1:edX+DemoX+Demo_Course+type@vertical+block@256f17a44983429fb1a60802203ee4e0',
|
||||
released_to_students: true,
|
||||
release_date: 'Feb 05, 2013 at 05:00 UTC',
|
||||
visibility_state: 'live',
|
||||
has_explicit_staff_lock: false,
|
||||
start: '2013-02-05T05:00:00Z',
|
||||
graded: false,
|
||||
due_date: '',
|
||||
due: null,
|
||||
relative_weeks_due: null,
|
||||
format: null,
|
||||
course_graders: [
|
||||
'Homework',
|
||||
'Exam',
|
||||
],
|
||||
has_changes: false,
|
||||
actions: {
|
||||
deletable: true,
|
||||
draggable: true,
|
||||
childAddable: true,
|
||||
duplicable: true,
|
||||
},
|
||||
explanatory_message: null,
|
||||
group_access: {},
|
||||
user_partitions: [
|
||||
{
|
||||
id: 50,
|
||||
name: 'Enrollment Track Groups',
|
||||
scheme: 'enrollment_track',
|
||||
groups: [
|
||||
{
|
||||
id: 2,
|
||||
name: 'Verified Certificate',
|
||||
selected: false,
|
||||
deleted: false,
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
name: 'Audit',
|
||||
selected: false,
|
||||
deleted: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
show_correctness: 'always',
|
||||
discussion_enabled: true,
|
||||
ancestor_has_staff_lock: false,
|
||||
user_partition_info: {
|
||||
selectable_partitions: [
|
||||
{
|
||||
id: 50,
|
||||
name: 'Enrollment Track Groups',
|
||||
scheme: 'enrollment_track',
|
||||
groups: [
|
||||
{
|
||||
id: 2,
|
||||
name: 'Verified Certificate',
|
||||
selected: false,
|
||||
deleted: false,
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
name: 'Audit',
|
||||
selected: false,
|
||||
deleted: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
selected_partition_index: -1,
|
||||
selected_groups_label: '',
|
||||
},
|
||||
enable_copy_paste_units: false,
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@e3601c0abee6427d8c17e6d6f8fdddd1',
|
||||
display_name: 'Interactive Questions',
|
||||
category: 'vertical',
|
||||
has_children: true,
|
||||
edited_on: 'Dec 28, 2023 at 10:00 UTC',
|
||||
published: true,
|
||||
published_on: 'Dec 28, 2023 at 10:00 UTC',
|
||||
studio_url: '/container/block-v1:edX+DemoX+Demo_Course+type@vertical+block@e3601c0abee6427d8c17e6d6f8fdddd1',
|
||||
released_to_students: true,
|
||||
release_date: 'Feb 05, 2013 at 05:00 UTC',
|
||||
visibility_state: 'live',
|
||||
has_explicit_staff_lock: false,
|
||||
start: '2013-02-05T05:00:00Z',
|
||||
graded: false,
|
||||
due_date: '',
|
||||
due: null,
|
||||
relative_weeks_due: null,
|
||||
format: null,
|
||||
course_graders: [
|
||||
'Homework',
|
||||
'Exam',
|
||||
],
|
||||
has_changes: false,
|
||||
actions: {
|
||||
deletable: true,
|
||||
draggable: true,
|
||||
childAddable: true,
|
||||
duplicable: true,
|
||||
},
|
||||
explanatory_message: null,
|
||||
group_access: {},
|
||||
user_partitions: [
|
||||
{
|
||||
id: 50,
|
||||
name: 'Enrollment Track Groups',
|
||||
scheme: 'enrollment_track',
|
||||
groups: [
|
||||
{
|
||||
id: 2,
|
||||
name: 'Verified Certificate',
|
||||
selected: false,
|
||||
deleted: false,
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
name: 'Audit',
|
||||
selected: false,
|
||||
deleted: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
show_correctness: 'always',
|
||||
discussion_enabled: true,
|
||||
ancestor_has_staff_lock: false,
|
||||
user_partition_info: {
|
||||
selectable_partitions: [
|
||||
{
|
||||
id: 50,
|
||||
name: 'Enrollment Track Groups',
|
||||
scheme: 'enrollment_track',
|
||||
groups: [
|
||||
{
|
||||
id: 2,
|
||||
name: 'Verified Certificate',
|
||||
selected: false,
|
||||
deleted: false,
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
name: 'Audit',
|
||||
selected: false,
|
||||
deleted: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
selected_partition_index: -1,
|
||||
selected_groups_label: '',
|
||||
},
|
||||
enable_copy_paste_units: false,
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@a79d59cd72034188a71d388f4954a606',
|
||||
display_name: 'Exciting Labs and Tools',
|
||||
category: 'vertical',
|
||||
has_children: true,
|
||||
edited_on: 'Dec 28, 2023 at 10:00 UTC',
|
||||
published: true,
|
||||
published_on: 'Dec 28, 2023 at 10:00 UTC',
|
||||
studio_url: '/container/block-v1:edX+DemoX+Demo_Course+type@vertical+block@a79d59cd72034188a71d388f4954a606',
|
||||
released_to_students: true,
|
||||
release_date: 'Feb 05, 2013 at 05:00 UTC',
|
||||
visibility_state: 'live',
|
||||
has_explicit_staff_lock: false,
|
||||
start: '2013-02-05T05:00:00Z',
|
||||
graded: false,
|
||||
due_date: '',
|
||||
due: null,
|
||||
relative_weeks_due: null,
|
||||
format: null,
|
||||
course_graders: [
|
||||
'Homework',
|
||||
'Exam',
|
||||
],
|
||||
has_changes: false,
|
||||
actions: {
|
||||
deletable: true,
|
||||
draggable: true,
|
||||
childAddable: true,
|
||||
duplicable: true,
|
||||
},
|
||||
explanatory_message: null,
|
||||
group_access: {},
|
||||
user_partitions: [
|
||||
{
|
||||
id: 50,
|
||||
name: 'Enrollment Track Groups',
|
||||
scheme: 'enrollment_track',
|
||||
groups: [
|
||||
{
|
||||
id: 2,
|
||||
name: 'Verified Certificate',
|
||||
selected: false,
|
||||
deleted: false,
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
name: 'Audit',
|
||||
selected: false,
|
||||
deleted: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
show_correctness: 'always',
|
||||
discussion_enabled: true,
|
||||
ancestor_has_staff_lock: false,
|
||||
user_partition_info: {
|
||||
selectable_partitions: [
|
||||
{
|
||||
id: 50,
|
||||
name: 'Enrollment Track Groups',
|
||||
scheme: 'enrollment_track',
|
||||
groups: [
|
||||
{
|
||||
id: 2,
|
||||
name: 'Verified Certificate',
|
||||
selected: false,
|
||||
deleted: false,
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
name: 'Audit',
|
||||
selected: false,
|
||||
deleted: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
selected_partition_index: -1,
|
||||
selected_groups_label: '',
|
||||
},
|
||||
enable_copy_paste_units: false,
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@134df56c516a4a0dbb24dd5facef746e',
|
||||
display_name: 'Reading Assignments',
|
||||
category: 'vertical',
|
||||
has_children: true,
|
||||
edited_on: 'Dec 28, 2023 at 10:00 UTC',
|
||||
published: true,
|
||||
published_on: 'Dec 28, 2023 at 10:00 UTC',
|
||||
studio_url: '/container/block-v1:edX+DemoX+Demo_Course+type@vertical+block@134df56c516a4a0dbb24dd5facef746e',
|
||||
released_to_students: true,
|
||||
release_date: 'Feb 05, 2013 at 05:00 UTC',
|
||||
visibility_state: 'live',
|
||||
has_explicit_staff_lock: false,
|
||||
start: '2013-02-05T05:00:00Z',
|
||||
graded: false,
|
||||
due_date: '',
|
||||
due: null,
|
||||
relative_weeks_due: null,
|
||||
format: null,
|
||||
course_graders: [
|
||||
'Homework',
|
||||
'Exam',
|
||||
],
|
||||
has_changes: false,
|
||||
actions: {
|
||||
deletable: true,
|
||||
draggable: true,
|
||||
childAddable: true,
|
||||
duplicable: true,
|
||||
},
|
||||
explanatory_message: null,
|
||||
group_access: {},
|
||||
user_partitions: [
|
||||
{
|
||||
id: 50,
|
||||
name: 'Enrollment Track Groups',
|
||||
scheme: 'enrollment_track',
|
||||
groups: [
|
||||
{
|
||||
id: 2,
|
||||
name: 'Verified Certificate',
|
||||
selected: false,
|
||||
deleted: false,
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
name: 'Audit',
|
||||
selected: false,
|
||||
deleted: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
show_correctness: 'always',
|
||||
discussion_enabled: true,
|
||||
ancestor_has_staff_lock: false,
|
||||
user_partition_info: {
|
||||
selectable_partitions: [
|
||||
{
|
||||
id: 50,
|
||||
name: 'Enrollment Track Groups',
|
||||
scheme: 'enrollment_track',
|
||||
groups: [
|
||||
{
|
||||
id: 2,
|
||||
name: 'Verified Certificate',
|
||||
selected: false,
|
||||
deleted: false,
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
name: 'Audit',
|
||||
selected: false,
|
||||
deleted: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
selected_partition_index: -1,
|
||||
selected_groups_label: '',
|
||||
},
|
||||
enable_copy_paste_units: false,
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@d91b9e5d8bc64d57a1332d06bf2f2193',
|
||||
display_name: 'When Are Your Exams? ',
|
||||
category: 'vertical',
|
||||
has_children: true,
|
||||
edited_on: 'Dec 28, 2023 at 10:00 UTC',
|
||||
published: true,
|
||||
published_on: 'Dec 28, 2023 at 10:00 UTC',
|
||||
studio_url: '/container/block-v1:edX+DemoX+Demo_Course+type@vertical+block@d91b9e5d8bc64d57a1332d06bf2f2193',
|
||||
released_to_students: true,
|
||||
release_date: 'Feb 05, 2013 at 05:00 UTC',
|
||||
visibility_state: 'live',
|
||||
has_explicit_staff_lock: false,
|
||||
start: '2013-02-05T05:00:00Z',
|
||||
graded: false,
|
||||
due_date: '',
|
||||
due: null,
|
||||
relative_weeks_due: null,
|
||||
format: null,
|
||||
course_graders: [
|
||||
'Homework',
|
||||
'Exam',
|
||||
],
|
||||
has_changes: false,
|
||||
actions: {
|
||||
deletable: true,
|
||||
draggable: true,
|
||||
childAddable: true,
|
||||
duplicable: true,
|
||||
},
|
||||
explanatory_message: null,
|
||||
group_access: {},
|
||||
user_partitions: [
|
||||
{
|
||||
id: 50,
|
||||
name: 'Enrollment Track Groups',
|
||||
scheme: 'enrollment_track',
|
||||
groups: [
|
||||
{
|
||||
id: 2,
|
||||
name: 'Verified Certificate',
|
||||
selected: false,
|
||||
deleted: false,
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
name: 'Audit',
|
||||
selected: false,
|
||||
deleted: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
show_correctness: 'always',
|
||||
discussion_enabled: true,
|
||||
ancestor_has_staff_lock: false,
|
||||
user_partition_info: {
|
||||
selectable_partitions: [
|
||||
{
|
||||
id: 50,
|
||||
name: 'Enrollment Track Groups',
|
||||
scheme: 'enrollment_track',
|
||||
groups: [
|
||||
{
|
||||
id: 2,
|
||||
name: 'Verified Certificate',
|
||||
selected: false,
|
||||
deleted: false,
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
name: 'Audit',
|
||||
selected: false,
|
||||
deleted: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
selected_partition_index: -1,
|
||||
selected_groups_label: '',
|
||||
},
|
||||
enable_copy_paste_units: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
ancestor_has_staff_lock: false,
|
||||
user_partition_info: {
|
||||
selectable_partitions: [
|
||||
{
|
||||
id: 50,
|
||||
name: 'Enrollment Track Groups',
|
||||
scheme: 'enrollment_track',
|
||||
groups: [
|
||||
{
|
||||
id: 2,
|
||||
name: 'Verified Certificate',
|
||||
selected: false,
|
||||
deleted: false,
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
name: 'Audit',
|
||||
selected: false,
|
||||
deleted: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
selected_partition_index: -1,
|
||||
selected_groups_label: '',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@chapter+block@graded_interactions',
|
||||
display_name: 'Example Week 1: Getting Started',
|
||||
category: 'chapter',
|
||||
has_children: true,
|
||||
edited_on: 'Jan 03, 2024 at 12:06 UTC',
|
||||
published: true,
|
||||
published_on: 'Dec 28, 2023 at 10:00 UTC',
|
||||
studio_url: '/course/course-v1:edX+DemoX+Demo_Course?show=block-v1%3AedX%2BDemoX%2BDemo_Course%2Btype%40chapter%2Bblock%40interactive_demonstrations',
|
||||
released_to_students: true,
|
||||
release_date: 'Feb 05, 2013 at 05:00 UTC',
|
||||
visibility_state: 'live',
|
||||
has_explicit_staff_lock: false,
|
||||
start: '2013-02-05T05:00:00Z',
|
||||
graded: false,
|
||||
due_date: '',
|
||||
due: null,
|
||||
relative_weeks_due: null,
|
||||
format: null,
|
||||
course_graders: [
|
||||
'Homework',
|
||||
'Exam',
|
||||
],
|
||||
has_changes: null,
|
||||
actions: {
|
||||
deletable: true,
|
||||
draggable: true,
|
||||
childAddable: true,
|
||||
duplicable: true,
|
||||
},
|
||||
explanatory_message: null,
|
||||
group_access: {},
|
||||
user_partitions: [
|
||||
{
|
||||
id: 50,
|
||||
name: 'Enrollment Track Groups',
|
||||
scheme: 'enrollment_track',
|
||||
groups: [
|
||||
{
|
||||
id: 2,
|
||||
name: 'Verified Certificate',
|
||||
selected: false,
|
||||
deleted: false,
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
name: 'Audit',
|
||||
selected: false,
|
||||
deleted: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
show_correctness: 'always',
|
||||
highlights: [],
|
||||
highlights_enabled: true,
|
||||
highlights_preview_only: false,
|
||||
highlights_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/latest/developing_course/course_sections.html#set-section-highlights-for-weekly-course-highlight-messages',
|
||||
ancestor_has_staff_lock: false,
|
||||
user_partition_info: {
|
||||
selectable_partitions: [
|
||||
{
|
||||
id: 50,
|
||||
name: 'Enrollment Track Groups',
|
||||
scheme: 'enrollment_track',
|
||||
groups: [
|
||||
{
|
||||
id: 2,
|
||||
name: 'Verified Certificate',
|
||||
selected: false,
|
||||
deleted: false,
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
name: 'Audit',
|
||||
selected: false,
|
||||
deleted: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
selected_partition_index: -1,
|
||||
selected_groups_label: '',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@course+block@course',
|
||||
display_name: 'Demonstration Course',
|
||||
category: 'course',
|
||||
has_children: true,
|
||||
unit_level_discussions: false,
|
||||
edited_on: 'Jan 03, 2024 at 12:06 UTC',
|
||||
published: true,
|
||||
published_on: 'Jan 03, 2024 at 08:57 UTC',
|
||||
studio_url: '/course/course-v1:edX+DemoX+Demo_Course',
|
||||
released_to_students: true,
|
||||
release_date: 'Feb 05, 2013 at 05:00 UTC',
|
||||
visibility_state: null,
|
||||
has_explicit_staff_lock: false,
|
||||
start: '2013-02-05T05:00:00Z',
|
||||
graded: false,
|
||||
due_date: '',
|
||||
due: null,
|
||||
relative_weeks_due: null,
|
||||
format: null,
|
||||
course_graders: [
|
||||
'Homework',
|
||||
'Exam',
|
||||
],
|
||||
has_changes: null,
|
||||
actions: {
|
||||
deletable: true,
|
||||
draggable: true,
|
||||
childAddable: true,
|
||||
duplicable: true,
|
||||
},
|
||||
explanatory_message: null,
|
||||
group_access: {},
|
||||
user_partitions: [
|
||||
{
|
||||
id: 50,
|
||||
name: 'Enrollment Track Groups',
|
||||
scheme: 'enrollment_track',
|
||||
groups: [
|
||||
{
|
||||
id: 2,
|
||||
name: 'Verified Certificate',
|
||||
selected: false,
|
||||
deleted: false,
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
name: 'Audit',
|
||||
selected: false,
|
||||
deleted: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
show_correctness: 'always',
|
||||
highlights_enabled_for_messaging: false,
|
||||
highlights_enabled: true,
|
||||
highlights_preview_only: false,
|
||||
highlights_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/latest/developing_course/course_sections.html#set-section-highlights-for-weekly-course-highlight-messages',
|
||||
enable_proctored_exams: false,
|
||||
create_zendesk_tickets: true,
|
||||
enable_timed_exams: true,
|
||||
ancestor_has_staff_lock: false,
|
||||
user_partition_info: {
|
||||
selectable_partitions: [
|
||||
{
|
||||
id: 50,
|
||||
name: 'Enrollment Track Groups',
|
||||
scheme: 'enrollment_track',
|
||||
groups: [
|
||||
{
|
||||
id: 2,
|
||||
name: 'Verified Certificate',
|
||||
selected: false,
|
||||
deleted: false,
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
name: 'Audit',
|
||||
selected: false,
|
||||
deleted: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
selected_partition_index: -1,
|
||||
selected_groups_label: '',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
ancestor_has_staff_lock: false,
|
||||
user_partition_info: {
|
||||
selectable_partitions: [
|
||||
{
|
||||
id: 50,
|
||||
name: 'Enrollment Track Groups',
|
||||
scheme: 'enrollment_track',
|
||||
groups: [
|
||||
{
|
||||
id: 2,
|
||||
name: 'Verified Certificate',
|
||||
selected: false,
|
||||
deleted: false,
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
name: 'Audit',
|
||||
selected: false,
|
||||
deleted: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
selected_partition_index: -1,
|
||||
selected_groups_label: '',
|
||||
},
|
||||
enable_copy_paste_units: false,
|
||||
edited_by: 'edx',
|
||||
published_by: null,
|
||||
currently_visible_to_students: true,
|
||||
has_partition_group_components: false,
|
||||
release_date_from: 'Section "Example Week 1: Getting Started"',
|
||||
staff_lock_from: null,
|
||||
};
|
||||
@@ -1,4 +1,3 @@
|
||||
export { default as courseUnitIndexMock } from './courseUnitIndex';
|
||||
export { default as courseSectionVerticalMock } from './courseSectionVertical';
|
||||
export { default as courseUnitMock } from './courseUnit';
|
||||
export { default as courseCreateXblockMock } from './courseCreateXblock';
|
||||
|
||||
@@ -1,39 +1,60 @@
|
||||
import { useCallback, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { useIntl, FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
ActionRow, Button, StandardModal, useToggle,
|
||||
} from '@openedx/paragon';
|
||||
|
||||
import { getCourseSectionVertical } from '../data/selectors';
|
||||
import { getWaffleFlags } from '../../data/selectors';
|
||||
import { COMPONENT_TYPES } from '../../generic/block-type-utils/constants';
|
||||
import ComponentModalView from './add-component-modals/ComponentModalView';
|
||||
import AddComponentButton from './add-component-btn';
|
||||
import messages from './messages';
|
||||
import { ComponentPicker } from '../../library-authoring/component-picker';
|
||||
import { messageTypes } from '../constants';
|
||||
import { useIframe } from '../context/hooks';
|
||||
import { useIframe } from '../../generic/hooks/context/hooks';
|
||||
import { useEventListener } from '../../generic/hooks';
|
||||
import VideoSelectorPage from '../../editors/VideoSelectorPage';
|
||||
import EditorPage from '../../editors/EditorPage';
|
||||
|
||||
const AddComponent = ({ blockId, handleCreateNewCourseXBlock }) => {
|
||||
const navigate = useNavigate();
|
||||
const AddComponent = ({
|
||||
parentLocator,
|
||||
isSplitTestType,
|
||||
isUnitVerticalType,
|
||||
addComponentTemplateData,
|
||||
handleCreateNewCourseXBlock,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const [isOpenAdvanced, openAdvanced, closeAdvanced] = useToggle(false);
|
||||
const [isOpenHtml, openHtml, closeHtml] = useToggle(false);
|
||||
const [isOpenOpenAssessment, openOpenAssessment, closeOpenAssessment] = useToggle(false);
|
||||
const { componentTemplates = {} } = useSelector(getCourseSectionVertical);
|
||||
const blockId = addComponentTemplateData.parentLocator || parentLocator;
|
||||
const [isAddLibraryContentModalOpen, showAddLibraryContentModal, closeAddLibraryContentModal] = useToggle();
|
||||
const [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 [selectedComponents, setSelectedComponents] = useState([]);
|
||||
const [usageId, setUsageId] = useState(null);
|
||||
const { sendMessageToIframe } = useIframe();
|
||||
const { useVideoGalleryFlow, useReactMarkdownEditor } = useSelector(getWaffleFlags);
|
||||
|
||||
const receiveMessage = useCallback(({ data: { type } }) => {
|
||||
const receiveMessage = useCallback(({ data: { type, payload } }) => {
|
||||
if (type === messageTypes.showMultipleComponentPicker) {
|
||||
showSelectLibraryContentModal();
|
||||
}
|
||||
}, [showSelectLibraryContentModal]);
|
||||
if (type === messageTypes.showSingleComponentPicker) {
|
||||
setUsageId(payload.usageId);
|
||||
showAddLibraryContentModal();
|
||||
}
|
||||
}, [showSelectLibraryContentModal, showAddLibraryContentModal, setUsageId]);
|
||||
|
||||
useEventListener('message', receiveMessage);
|
||||
|
||||
@@ -42,15 +63,21 @@ const AddComponent = ({ blockId, handleCreateNewCourseXBlock }) => {
|
||||
closeSelectLibraryContentModal();
|
||||
}, [selectedComponents]);
|
||||
|
||||
const onXBlockSave = useCallback(/* istanbul ignore next */ () => {
|
||||
closeXBlockEditorModal();
|
||||
closeVideoSelectorModal();
|
||||
sendMessageToIframe(messageTypes.refreshXBlock, null);
|
||||
}, [closeXBlockEditorModal, closeVideoSelectorModal, sendMessageToIframe]);
|
||||
|
||||
const handleLibraryV2Selection = useCallback((selection) => {
|
||||
handleCreateNewCourseXBlock({
|
||||
type: COMPONENT_TYPES.libraryV2,
|
||||
category: selection.blockType,
|
||||
parentLocator: blockId,
|
||||
parentLocator: usageId || blockId,
|
||||
libraryContentKey: selection.usageKey,
|
||||
});
|
||||
closeAddLibraryContentModal();
|
||||
}, []);
|
||||
}, [usageId]);
|
||||
|
||||
const handleCreateNewXBlock = (type, moduleName) => {
|
||||
switch (type) {
|
||||
@@ -59,12 +86,28 @@ const AddComponent = ({ blockId, handleCreateNewCourseXBlock }) => {
|
||||
handleCreateNewCourseXBlock({ type, parentLocator: blockId });
|
||||
break;
|
||||
case COMPONENT_TYPES.problem:
|
||||
case COMPONENT_TYPES.video:
|
||||
handleCreateNewCourseXBlock({ type, parentLocator: blockId }, ({ courseKey, locator }) => {
|
||||
localStorage.setItem('createXBlockLastYPosition', window.scrollY);
|
||||
navigate(`/course/${courseKey}/editor/${type}/${locator}`);
|
||||
setCourseId(courseKey);
|
||||
setBlockType(type);
|
||||
setNewBlockId(locator);
|
||||
showXBlockEditorModal();
|
||||
});
|
||||
break;
|
||||
case COMPONENT_TYPES.video:
|
||||
handleCreateNewCourseXBlock(
|
||||
{ type, parentLocator: blockId },
|
||||
/* istanbul ignore next */ ({ courseKey, locator }) => {
|
||||
setCourseId(courseKey);
|
||||
setBlockType(type);
|
||||
setNewBlockId(locator);
|
||||
if (useVideoGalleryFlow) {
|
||||
showVideoSelectorModal();
|
||||
} else {
|
||||
showXBlockEditorModal();
|
||||
}
|
||||
},
|
||||
);
|
||||
break;
|
||||
// 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).
|
||||
case COMPONENT_TYPES.library:
|
||||
@@ -77,127 +120,185 @@ const AddComponent = ({ blockId, handleCreateNewCourseXBlock }) => {
|
||||
showAddLibraryContentModal();
|
||||
break;
|
||||
case COMPONENT_TYPES.advanced:
|
||||
handleCreateNewCourseXBlock({
|
||||
type: moduleName, category: moduleName, parentLocator: blockId,
|
||||
});
|
||||
handleCreateNewCourseXBlock({ type: moduleName, category: moduleName, parentLocator: blockId });
|
||||
break;
|
||||
case COMPONENT_TYPES.openassessment:
|
||||
handleCreateNewCourseXBlock({
|
||||
boilerplate: moduleName, category: type, parentLocator: blockId,
|
||||
});
|
||||
handleCreateNewCourseXBlock({ boilerplate: moduleName, category: type, parentLocator: blockId });
|
||||
break;
|
||||
case COMPONENT_TYPES.html:
|
||||
handleCreateNewCourseXBlock({
|
||||
type,
|
||||
boilerplate: moduleName,
|
||||
parentLocator: blockId,
|
||||
}, ({ courseKey, locator }) => {
|
||||
localStorage.setItem('createXBlockLastYPosition', window.scrollY);
|
||||
navigate(`/course/${courseKey}/editor/html/${locator}`);
|
||||
}, /* istanbul ignore next */ ({ courseKey, locator }) => {
|
||||
setCourseId(courseKey);
|
||||
setBlockType(type);
|
||||
setNewBlockId(locator);
|
||||
showXBlockEditorModal();
|
||||
});
|
||||
break;
|
||||
default:
|
||||
}
|
||||
};
|
||||
|
||||
if (!Object.keys(componentTemplates).length) {
|
||||
return null;
|
||||
if (isUnitVerticalType || isSplitTestType) {
|
||||
return (
|
||||
<div className="py-4">
|
||||
{Object.keys(componentTemplates).length && isUnitVerticalType ? (
|
||||
<>
|
||||
<h5 className="h3 mb-4 text-center">{intl.formatMessage(messages.title)}</h5>
|
||||
<ul className="new-component-type list-unstyled m-0 d-flex flex-wrap justify-content-center">
|
||||
{componentTemplates.map((component) => {
|
||||
const { type, displayName, beta } = component;
|
||||
let modalParams;
|
||||
|
||||
if (!component.templates.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
switch (type) {
|
||||
case COMPONENT_TYPES.advanced:
|
||||
modalParams = {
|
||||
open: openAdvanced,
|
||||
close: closeAdvanced,
|
||||
isOpen: isOpenAdvanced,
|
||||
};
|
||||
break;
|
||||
case COMPONENT_TYPES.html:
|
||||
modalParams = {
|
||||
open: openHtml,
|
||||
close: closeHtml,
|
||||
isOpen: isOpenHtml,
|
||||
};
|
||||
break;
|
||||
case COMPONENT_TYPES.openassessment:
|
||||
modalParams = {
|
||||
open: openOpenAssessment,
|
||||
close: closeOpenAssessment,
|
||||
isOpen: isOpenOpenAssessment,
|
||||
};
|
||||
break;
|
||||
default:
|
||||
return (
|
||||
<li key={type}>
|
||||
<AddComponentButton
|
||||
onClick={() => handleCreateNewXBlock(type)}
|
||||
displayName={displayName}
|
||||
type={type}
|
||||
beta={beta}
|
||||
/>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ComponentModalView
|
||||
key={type}
|
||||
component={component}
|
||||
handleCreateNewXBlock={handleCreateNewXBlock}
|
||||
modalParams={modalParams}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</>
|
||||
) : null}
|
||||
<StandardModal
|
||||
title={
|
||||
isAddLibraryContentModalOpen
|
||||
? intl.formatMessage(messages.singleComponentPickerModalTitle)
|
||||
: intl.formatMessage(messages.multipleComponentPickerModalTitle)
|
||||
}
|
||||
isOpen={isAddLibraryContentModalOpen || isSelectLibraryContentModalOpen}
|
||||
onClose={() => {
|
||||
closeAddLibraryContentModal();
|
||||
closeSelectLibraryContentModal();
|
||||
}}
|
||||
isOverflowVisible={false}
|
||||
size="xl"
|
||||
footerNode={
|
||||
isSelectLibraryContentModalOpen && (
|
||||
<ActionRow>
|
||||
<Button onClick={onComponentSelectionSubmit}>
|
||||
<FormattedMessage {...messages.multipleComponentPickerModalBtn} />
|
||||
</Button>
|
||||
</ActionRow>
|
||||
)
|
||||
}
|
||||
>
|
||||
<ComponentPicker
|
||||
showOnlyPublished
|
||||
extraFilter={['NOT block_type = "unit"']}
|
||||
componentPickerMode={isAddLibraryContentModalOpen ? 'single' : 'multiple'}
|
||||
onComponentSelected={handleLibraryV2Selection}
|
||||
onChangeComponentSelection={setSelectedComponents}
|
||||
/>
|
||||
</StandardModal>
|
||||
<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 (
|
||||
<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>
|
||||
);
|
||||
return null;
|
||||
};
|
||||
|
||||
AddComponent.propTypes = {
|
||||
blockId: PropTypes.string.isRequired,
|
||||
isSplitTestType: PropTypes.bool.isRequired,
|
||||
isUnitVerticalType: PropTypes.bool.isRequired,
|
||||
parentLocator: PropTypes.string.isRequired,
|
||||
handleCreateNewCourseXBlock: PropTypes.func.isRequired,
|
||||
addComponentTemplateData: {
|
||||
blockId: PropTypes.string.isRequired,
|
||||
model: PropTypes.shape({
|
||||
displayName: PropTypes.string.isRequired,
|
||||
category: PropTypes.string,
|
||||
type: PropTypes.string.isRequired,
|
||||
templates: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
boilerplateName: PropTypes.string,
|
||||
category: PropTypes.string,
|
||||
displayName: PropTypes.string.isRequired,
|
||||
supportLevel: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]),
|
||||
}),
|
||||
),
|
||||
supportLegend: PropTypes.shape({
|
||||
allowUnsupportedXblocks: PropTypes.bool,
|
||||
documentationLabel: PropTypes.string,
|
||||
showLegend: PropTypes.bool,
|
||||
}),
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
export default AddComponent;
|
||||
|
||||
@@ -18,7 +18,7 @@ import { courseSectionVerticalMock } from '../__mocks__';
|
||||
import { COMPONENT_TYPES } from '../../generic/block-type-utils/constants';
|
||||
import AddComponent from './AddComponent';
|
||||
import messages from './messages';
|
||||
import { IframeProvider } from '../context/iFrameContext';
|
||||
import { IframeProvider } from '../../generic/hooks/context/iFrameContext';
|
||||
import { messageTypes } from '../constants';
|
||||
|
||||
let store;
|
||||
@@ -52,7 +52,7 @@ jest.mock('../../library-authoring/component-picker', () => ({
|
||||
}));
|
||||
|
||||
const mockSendMessageToIframe = jest.fn();
|
||||
jest.mock('../context/hooks', () => ({
|
||||
jest.mock('../../generic/hooks/context/hooks', () => ({
|
||||
useIframe: () => ({
|
||||
sendMessageToIframe: mockSendMessageToIframe,
|
||||
}),
|
||||
@@ -64,6 +64,9 @@ const renderComponent = (props) => render(
|
||||
<IframeProvider>
|
||||
<AddComponent
|
||||
blockId={blockId}
|
||||
isUnitVerticalType
|
||||
parentLocator={blockId}
|
||||
addComponentTemplateData={{}}
|
||||
handleCreateNewCourseXBlock={handleCreateNewCourseXBlockMock}
|
||||
{...props}
|
||||
/>
|
||||
|
||||
@@ -14,6 +14,7 @@ const ComponentModalView = ({
|
||||
component,
|
||||
modalParams,
|
||||
handleCreateNewXBlock,
|
||||
isRequestedModalView,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const dispatch = useDispatch();
|
||||
@@ -30,15 +31,19 @@ const ComponentModalView = ({
|
||||
setModuleTitle('');
|
||||
};
|
||||
|
||||
const renderAddComponentButton = () => (
|
||||
<li>
|
||||
<AddComponentButton
|
||||
onClick={open}
|
||||
type={type}
|
||||
displayName={displayName}
|
||||
/>
|
||||
</li>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<li>
|
||||
<AddComponentButton
|
||||
onClick={open}
|
||||
type={type}
|
||||
displayName={displayName}
|
||||
/>
|
||||
</li>
|
||||
{!isRequestedModalView && renderAddComponentButton()}
|
||||
<ModalContainer
|
||||
isOpen={isOpen}
|
||||
close={close}
|
||||
@@ -92,6 +97,10 @@ const ComponentModalView = ({
|
||||
);
|
||||
};
|
||||
|
||||
ComponentModalView.defaultProps = {
|
||||
isRequestedModalView: false,
|
||||
};
|
||||
|
||||
ComponentModalView.propTypes = {
|
||||
modalParams: PropTypes.shape({
|
||||
open: PropTypes.func,
|
||||
@@ -117,6 +126,7 @@ ComponentModalView.propTypes = {
|
||||
showLegend: PropTypes.bool,
|
||||
}),
|
||||
}).isRequired,
|
||||
isRequestedModalView: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default ComponentModalView;
|
||||
|
||||
@@ -31,6 +31,11 @@ const messages = defineMessages({
|
||||
defaultMessage: 'Add selected components',
|
||||
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: {
|
||||
id: 'course-authoring.course-unit.modal.container.title',
|
||||
defaultMessage: 'Add {componentTitle} component',
|
||||
@@ -58,27 +63,27 @@ const messages = defineMessages({
|
||||
},
|
||||
modalComponentSupportTooltipFullySupported: {
|
||||
id: 'course-authoring.course-unit.modal.component.support.tooltip.fully-supported',
|
||||
defaultMessage: 'Fully supported tools and features are available on edX, are '
|
||||
+ 'fully tested, have user interfaces where applicable, and are documented in the '
|
||||
+ 'official edX guides that are available on docs.edx.org.',
|
||||
defaultMessage: 'Fully supported tools and features are available for Open edX installations, '
|
||||
+ 'are fully tested, have user interfaces where applicable, and are documented in the '
|
||||
+ 'official Open edX guides that are available on docs.openedx.org.',
|
||||
description: 'Message for support status tooltip for modules with full platform support',
|
||||
},
|
||||
modalComponentSupportTooltipNotSupported: {
|
||||
id: 'course-authoring.course-unit.modal.component.support.tooltip.not-supported',
|
||||
defaultMessage: 'Tools with no support are not maintained by edX, and might be '
|
||||
+ 'deprecated in the future. They are not recommended for use in courses due to '
|
||||
+ 'non-compliance with one or more of the base requirements, such as testing, '
|
||||
+ 'accessibility, internationalization, and documentation.',
|
||||
defaultMessage: 'Tools with no support are not maintained by the Open edX community, '
|
||||
+ 'and might be deprecated in the future. They are not recommended for use in '
|
||||
+ 'courses due to non-compliance with one or more of the base requirements, such as '
|
||||
+ 'testing, accessibility, internationalization, and documentation.',
|
||||
description: 'Message for support status tooltip for modules which is not supported',
|
||||
},
|
||||
modalComponentSupportTooltipProvisionallySupported: {
|
||||
id: 'course-authoring.course-unit.modal.component.support.tooltip.provisionally-support',
|
||||
defaultMessage: 'Provisionally supported tools might lack the robustness of functionality '
|
||||
+ 'that your courses require. edX does not have control over the quality of the software, '
|
||||
+ 'that your courses require. Open edX does not have control over the quality of the software, '
|
||||
+ 'or of the content that can be provided using these tools. Test these tools thoroughly '
|
||||
+ 'before using them in your course, especially in graded sections. Complete documentation '
|
||||
+ 'might not be available for provisionally supported tools, or documentation might be '
|
||||
+ 'available from sources other than edX.',
|
||||
+ 'available from sources other than the Open edX community.',
|
||||
description: 'Message for support status tooltip for modules with provisional platform support',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.sub-header-title .sub-header-breadcrumbs {
|
||||
.sub-header-breadcrumbs {
|
||||
.dropdown-toggle::after {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@@ -5,11 +5,11 @@ import {
|
||||
} from '../../testUtils';
|
||||
|
||||
import { executeThunk } from '../../utils';
|
||||
import { getCourseSectionVerticalApiUrl, getCourseUnitApiUrl } from '../data/api';
|
||||
import { getCourseSectionVerticalApiUrl } from '../data/api';
|
||||
import { getApiWaffleFlagsUrl } from '../../data/api';
|
||||
import { fetchWaffleFlags } from '../../data/thunks';
|
||||
import { fetchCourseSectionVerticalData, fetchCourseUnitQuery } from '../data/thunk';
|
||||
import { courseSectionVerticalMock, courseUnitIndexMock } from '../__mocks__';
|
||||
import { fetchCourseSectionVerticalData } from '../data/thunk';
|
||||
import { courseSectionVerticalMock } from '../__mocks__';
|
||||
import Breadcrumbs from './Breadcrumbs';
|
||||
|
||||
let axiosMock;
|
||||
@@ -43,9 +43,9 @@ describe('<Breadcrumbs />', () => {
|
||||
reduxStore = mocks.reduxStore;
|
||||
|
||||
axiosMock
|
||||
.onGet(getCourseUnitApiUrl(courseId))
|
||||
.reply(200, courseUnitIndexMock);
|
||||
await executeThunk(fetchCourseUnitQuery(courseId), reduxStore.dispatch);
|
||||
.onGet(getCourseSectionVerticalApiUrl(courseId))
|
||||
.reply(200, courseSectionVerticalMock);
|
||||
await executeThunk(fetchCourseSectionVerticalData(courseId), reduxStore.dispatch);
|
||||
axiosMock
|
||||
.onGet(getCourseSectionVerticalApiUrl(courseId))
|
||||
.reply(200, courseSectionVerticalMock);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user