Compare commits
1 Commits
release/te
...
test_hyper
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f16ccfe9cf |
3
.env
3
.env
@@ -45,4 +45,5 @@ ENABLE_HOME_PAGE_COURSE_API_V2=true
|
||||
ENABLE_CHECKLIST_QUALITY=''
|
||||
ENABLE_GRADING_METHOD_IN_PROBLEMS=false
|
||||
# "Multi-level" blocks are unsupported in libraries
|
||||
LIBRARY_UNSUPPORTED_BLOCKS="conditional,step-builder,problem-builder"
|
||||
# TODO: Missing support for ORA2
|
||||
LIBRARY_UNSUPPORTED_BLOCKS="conditional,step-builder,problem-builder,openassessment"
|
||||
|
||||
@@ -40,4 +40,5 @@ ENABLE_HOME_PAGE_COURSE_API_V2=true
|
||||
ENABLE_CHECKLIST_QUALITY=true
|
||||
ENABLE_GRADING_METHOD_IN_PROBLEMS=false
|
||||
# "Multi-level" blocks are unsupported in libraries
|
||||
LIBRARY_UNSUPPORTED_BLOCKS="conditional,step-builder,problem-builder"
|
||||
# TODO: Missing support for ORA2
|
||||
LIBRARY_UNSUPPORTED_BLOCKS="conditional,step-builder,problem-builder,openassessment"
|
||||
|
||||
@@ -10,5 +10,4 @@ coverage:
|
||||
threshold: 0%
|
||||
ignore:
|
||||
- "src/grading-settings/grading-scale/react-ranger.js"
|
||||
- "src/generic/DraggableList/verticalSortableList.ts"
|
||||
- "src/index.js"
|
||||
|
||||
66
package-lock.json
generated
66
package-lock.json
generated
@@ -10,7 +10,6 @@
|
||||
"license": "AGPL-3.0",
|
||||
"dependencies": {
|
||||
"@codemirror/lang-html": "^6.0.0",
|
||||
"@codemirror/lang-markdown": "^6.0.0",
|
||||
"@codemirror/lang-xml": "^6.0.0",
|
||||
"@codemirror/lint": "^6.2.1",
|
||||
"@codemirror/state": "^6.0.0",
|
||||
@@ -21,7 +20,7 @@
|
||||
"@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.6.0",
|
||||
"@edx/frontend-component-footer": "^14.3.0",
|
||||
"@edx/frontend-component-header": "^6.2.0",
|
||||
"@edx/frontend-enterprise-hotjar": "^7.2.0",
|
||||
"@edx/frontend-platform": "^8.3.1",
|
||||
@@ -37,7 +36,8 @@
|
||||
"@openedx-plugins/course-app-wiki": "file:plugins/course-apps/wiki",
|
||||
"@openedx-plugins/course-app-xpert_unit_summary": "file:plugins/course-apps/xpert_unit_summary",
|
||||
"@openedx/frontend-build": "^14.3.3",
|
||||
"@openedx/frontend-plugin-framework": "^1.7.0",
|
||||
"@openedx/frontend-plugin-framework": "^1.6.0",
|
||||
"@openedx/frontend-slot-footer": "^1.2.0",
|
||||
"@openedx/paragon": "^22.16.0",
|
||||
"@redux-devtools/extension": "^3.3.0",
|
||||
"@reduxjs/toolkit": "1.9.7",
|
||||
@@ -2033,21 +2033,6 @@
|
||||
"@lezer/javascript": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/lang-markdown": {
|
||||
"version": "6.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/lang-markdown/-/lang-markdown-6.3.2.tgz",
|
||||
"integrity": "sha512-c/5MYinGbFxYl4itE9q/rgN/sMTjOr8XL5OWnC+EaRMLfCbVUmmubTJfdgpfcSS2SCaT7b+Q+xi3l6CgoE+BsA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/autocomplete": "^6.7.1",
|
||||
"@codemirror/lang-html": "^6.0.0",
|
||||
"@codemirror/language": "^6.3.0",
|
||||
"@codemirror/state": "^6.0.0",
|
||||
"@codemirror/view": "^6.0.0",
|
||||
"@lezer/common": "^1.2.1",
|
||||
"@lezer/markdown": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/lang-xml": {
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/lang-xml/-/lang-xml-6.1.0.tgz",
|
||||
@@ -2344,9 +2329,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@edx/frontend-component-footer": {
|
||||
"version": "14.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@edx/frontend-component-footer/-/frontend-component-footer-14.6.0.tgz",
|
||||
"integrity": "sha512-cgRhom6W/WErQ9yvLmfgB6ANBs+rBDLOH73NcvJIhfwWgAg67q+MLUscIbcX9N/9Yykk+kb7Ytr3CDefiKS7HA==",
|
||||
"version": "14.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@edx/frontend-component-footer/-/frontend-component-footer-14.3.0.tgz",
|
||||
"integrity": "sha512-domQOIsAf+b1YiQvpt245Cfz6OgrKKw3TJrDIFS+J70Mn98MpCGGg55mBraOzTfopsouzp5bN03F1PLkXyjnEQ==",
|
||||
"license": "AGPL-3.0",
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-svg-core": "6.7.2",
|
||||
@@ -2354,7 +2339,6 @@
|
||||
"@fortawesome/free-regular-svg-icons": "6.7.2",
|
||||
"@fortawesome/free-solid-svg-icons": "6.7.2",
|
||||
"@fortawesome/react-fontawesome": "0.2.2",
|
||||
"@openedx/frontend-plugin-framework": "^1.7.0",
|
||||
"classnames": "^2.5.1",
|
||||
"jest-environment-jsdom": "^29.7.0",
|
||||
"lodash": "^4.17.21",
|
||||
@@ -2369,9 +2353,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@edx/frontend-component-header": {
|
||||
"version": "6.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@edx/frontend-component-header/-/frontend-component-header-6.4.0.tgz",
|
||||
"integrity": "sha512-RNV3XRXhhN9QlhAoP26CjzoRIPlLSYDp3PZCnK6g6kIHgxC9dCpu2PTZdxV2AVChqVuxtZK5zLbk9yeAtf4U/A==",
|
||||
"version": "6.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@edx/frontend-component-header/-/frontend-component-header-6.2.0.tgz",
|
||||
"integrity": "sha512-rM/+NtvPAQk+RmAA/fhXnsneeta/CGi319Wei/or6aW7ETpSmMkfoYM4MKv+JPhF/vLMxqBzz6lwzefF9D62Lw==",
|
||||
"license": "AGPL-3.0",
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-svg-core": "6.6.0",
|
||||
@@ -2379,7 +2363,7 @@
|
||||
"@fortawesome/free-regular-svg-icons": "6.6.0",
|
||||
"@fortawesome/free-solid-svg-icons": "6.6.0",
|
||||
"@fortawesome/react-fontawesome": "^0.2.0",
|
||||
"@openedx/frontend-plugin-framework": "^1.7.0",
|
||||
"@openedx/frontend-plugin-framework": "^1.6.0",
|
||||
"axios-mock-adapter": "1.22.0",
|
||||
"babel-polyfill": "6.26.0",
|
||||
"classnames": "^2.5.1",
|
||||
@@ -3885,16 +3869,6 @@
|
||||
"@lezer/common": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@lezer/markdown": {
|
||||
"version": "1.4.2",
|
||||
"resolved": "https://registry.npmjs.org/@lezer/markdown/-/markdown-1.4.2.tgz",
|
||||
"integrity": "sha512-iYewCigG/517D0xJPQd7RGaCjZAFwROiH8T9h7OTtz0bRVtkxzFhGBFJ9JGKgBBs4uuo1cvxzyQ5iKhDLMcLUQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@lezer/common": "^1.0.0",
|
||||
"@lezer/highlight": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@lezer/xml": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@lezer/xml/-/xml-1.0.6.tgz",
|
||||
@@ -4183,9 +4157,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@openedx/frontend-plugin-framework": {
|
||||
"version": "1.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@openedx/frontend-plugin-framework/-/frontend-plugin-framework-1.7.0.tgz",
|
||||
"integrity": "sha512-8tGkuHvtzhbqb9dU4sXUtR0K44+Hjh1uGR6DvhZAt9wSKQC1v4RBk34ef8DFzQhoNQa/Jtn6BJuta4Un6MmHmw==",
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@openedx/frontend-plugin-framework/-/frontend-plugin-framework-1.6.0.tgz",
|
||||
"integrity": "sha512-zgP+/hs/cvcPmFOgVm2xt/qgX1nheNsfipzCO7I3bON4hHyOhmOyzwFZJ7pz7GzCJwKlMVguh3HcJgf4p/BPKQ==",
|
||||
"license": "AGPL-3.0",
|
||||
"dependencies": {
|
||||
"@edx/brand": "npm:@openedx/brand-openedx@^1.2.2",
|
||||
@@ -4268,6 +4242,20 @@
|
||||
"@babel/runtime": "^7.9.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@openedx/frontend-slot-footer": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@openedx/frontend-slot-footer/-/frontend-slot-footer-1.2.0.tgz",
|
||||
"integrity": "sha512-bJuqgdiAlPRj1QuUOJWtNqGTCTcdsk4vHeOM3jRkxtWycq+j1JpGnnZEWAmjoRv9dDKr39vt2buNrmvj0sCTbA==",
|
||||
"license": "AGPL-3.0",
|
||||
"dependencies": {
|
||||
"@openedx/frontend-plugin-framework": "^1.5.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@edx/frontend-component-footer": "*",
|
||||
"react": "^17.0.0 || ^18.0.0",
|
||||
"react-dom": "^17.0.0 || ^18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@openedx/paragon": {
|
||||
"version": "22.17.0",
|
||||
"resolved": "https://registry.npmjs.org/@openedx/paragon/-/paragon-22.17.0.tgz",
|
||||
|
||||
@@ -35,7 +35,6 @@
|
||||
"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",
|
||||
@@ -45,7 +44,7 @@
|
||||
"@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.6.0",
|
||||
"@edx/frontend-component-footer": "^14.3.0",
|
||||
"@edx/frontend-component-header": "^6.2.0",
|
||||
"@edx/frontend-enterprise-hotjar": "^7.2.0",
|
||||
"@edx/frontend-platform": "^8.3.1",
|
||||
@@ -61,7 +60,8 @@
|
||||
"@openedx-plugins/course-app-wiki": "file:plugins/course-apps/wiki",
|
||||
"@openedx-plugins/course-app-xpert_unit_summary": "file:plugins/course-apps/xpert_unit_summary",
|
||||
"@openedx/frontend-build": "^14.3.3",
|
||||
"@openedx/frontend-plugin-framework": "^1.7.0",
|
||||
"@openedx/frontend-plugin-framework": "^1.6.0",
|
||||
"@openedx/frontend-slot-footer": "^1.2.0",
|
||||
"@openedx/paragon": "^22.16.0",
|
||||
"@redux-devtools/extension": "^3.3.0",
|
||||
"@reduxjs/toolkit": "1.9.7",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { Form, Hyperlink } from '@openedx/paragon';
|
||||
import 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,10 +107,12 @@ const BbbSettings = ({
|
||||
)}
|
||||
</>
|
||||
</>
|
||||
|
||||
);
|
||||
};
|
||||
|
||||
BbbSettings.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
values: PropTypes.shape({
|
||||
consumerKey: PropTypes.string,
|
||||
consumerSecret: PropTypes.string,
|
||||
@@ -125,4 +127,4 @@ BbbSettings.propTypes = {
|
||||
setFieldValue: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default BbbSettings;
|
||||
export default injectIntl(BbbSettings);
|
||||
|
||||
@@ -1,43 +1,42 @@
|
||||
import React from 'react';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import PropTypes from 'prop-types';
|
||||
import FormikControl from 'CourseAuthoring/generic/FormikControl';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
const LiveCommonFields = ({
|
||||
intl,
|
||||
values,
|
||||
}) => {
|
||||
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"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
}) => (
|
||||
<>
|
||||
<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,
|
||||
@@ -46,4 +45,4 @@ LiveCommonFields.propTypes = {
|
||||
}).isRequired,
|
||||
};
|
||||
|
||||
export default LiveCommonFields;
|
||||
export default injectIntl(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 { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { injectIntl, intlShape } 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,7 +130,8 @@ const LiveSettings = ({
|
||||
};
|
||||
|
||||
LiveSettings.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default LiveSettings;
|
||||
export default injectIntl(LiveSettings);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import PropTypes from 'prop-types';
|
||||
import FormikControl from 'CourseAuthoring/generic/FormikControl';
|
||||
|
||||
@@ -8,38 +8,37 @@ import { providerNames } from './constants';
|
||||
import LiveCommonFields from './LiveCommonFields';
|
||||
|
||||
const ZoomSettings = ({
|
||||
intl,
|
||||
values,
|
||||
}) => {
|
||||
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"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
}) => (
|
||||
// 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,
|
||||
@@ -52,4 +51,4 @@ ZoomSettings.propTypes = {
|
||||
}).isRequired,
|
||||
};
|
||||
|
||||
export default ZoomSettings;
|
||||
export default injectIntl(ZoomSettings);
|
||||
|
||||
@@ -8,7 +8,7 @@ import PropTypes from 'prop-types';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
|
||||
import { useIntl, FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
import { injectIntl, intlShape, FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
ActionRow, Alert, Badge, Form, Hyperlink, ModalDialog, StatefulButton,
|
||||
} from '@openedx/paragon';
|
||||
@@ -25,8 +25,7 @@ import { PagesAndResourcesContext } from 'CourseAuthoring/pages-and-resources/Pa
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
const ProctoringSettings = ({ onClose }) => {
|
||||
const intl = useIntl();
|
||||
const ProctoringSettings = ({ intl, onClose }) => {
|
||||
const initialFormValues = {
|
||||
enableProctoredExams: false,
|
||||
proctoringProvider: false,
|
||||
@@ -653,9 +652,10 @@ const ProctoringSettings = ({ onClose }) => {
|
||||
};
|
||||
|
||||
ProctoringSettings.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
ProctoringSettings.defaultProps = {};
|
||||
|
||||
export default ProctoringSettings;
|
||||
export default injectIntl(ProctoringSettings);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import * as Yup from 'yup';
|
||||
@@ -8,8 +8,7 @@ import { useAppSetting } from 'CourseAuthoring/utils';
|
||||
import AppSettingsModal from 'CourseAuthoring/pages-and-resources/app-settings-modal/AppSettingsModal';
|
||||
import messages from './messages';
|
||||
|
||||
const ProgressSettings = ({ onClose }) => {
|
||||
const intl = useIntl();
|
||||
const ProgressSettings = ({ intl, onClose }) => {
|
||||
const [disableProgressGraph, saveSetting] = useAppSetting('disableProgressGraph');
|
||||
const showProgressGraphSetting = getConfig().ENABLE_PROGRESS_GRAPH_SETTINGS.toString().toLowerCase() === 'true';
|
||||
|
||||
@@ -49,7 +48,8 @@ const ProgressSettings = ({ onClose }) => {
|
||||
};
|
||||
|
||||
ProgressSettings.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default ProgressSettings;
|
||||
export default injectIntl(ProgressSettings);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { Button, Form, TransitionReplace } from '@openedx/paragon';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { useState } from 'react';
|
||||
@@ -30,9 +30,8 @@ const TeamTypeNameMessage = {
|
||||
};
|
||||
|
||||
const GroupEditor = ({
|
||||
group, onDelete, onChange, onBlur, fieldNameCommonBase, errors,
|
||||
intl, 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);
|
||||
@@ -150,6 +149,7 @@ 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 GroupEditor;
|
||||
export default injectIntl(GroupEditor);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { Button, Form } from '@openedx/paragon';
|
||||
import { Add } from '@openedx/paragon/icons';
|
||||
|
||||
@@ -17,9 +17,9 @@ import messages from './messages';
|
||||
setupYupExtensions();
|
||||
|
||||
const TeamSettings = ({
|
||||
intl,
|
||||
onClose,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const [teamsConfiguration, saveSettings] = useAppSetting('teamsConfiguration');
|
||||
const blankNewGroup = {
|
||||
name: '',
|
||||
@@ -166,7 +166,8 @@ const TeamSettings = ({
|
||||
};
|
||||
|
||||
TeamSettings.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default TeamSettings;
|
||||
export default injectIntl(TeamSettings);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import * as Yup from 'yup';
|
||||
@@ -8,8 +8,7 @@ import { useAppSetting } from 'CourseAuthoring/utils';
|
||||
import AppSettingsModal from 'CourseAuthoring/pages-and-resources/app-settings-modal/AppSettingsModal';
|
||||
import messages from './messages';
|
||||
|
||||
const WikiSettings = ({ onClose }) => {
|
||||
const intl = useIntl();
|
||||
const WikiSettings = ({ intl, onClose }) => {
|
||||
const [enablePublicWiki, saveSetting] = useAppSetting('allowPublicWikiAccess');
|
||||
const handleSettingsSave = (values) => saveSetting(values.enablePublicWiki);
|
||||
|
||||
@@ -33,7 +32,7 @@ const WikiSettings = ({ onClose }) => {
|
||||
label={intl.formatMessage(messages.enablePublicWikiLabel)}
|
||||
helpText={intl.formatMessage(messages.enablePublicWikiHelp)}
|
||||
onChange={handleChange}
|
||||
onBlur={handleBlur}
|
||||
onBlue={handleBlur}
|
||||
checked={values.enablePublicWiki}
|
||||
/>
|
||||
)
|
||||
@@ -43,7 +42,8 @@ const WikiSettings = ({ onClose }) => {
|
||||
};
|
||||
|
||||
WikiSettings.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default WikiSettings;
|
||||
export default injectIntl(WikiSettings);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { useCallback, useContext, useEffect } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { PagesAndResourcesContext } from 'CourseAuthoring/pages-and-resources/PagesAndResourcesProvider';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
@@ -9,8 +10,7 @@ import messages from './messages';
|
||||
|
||||
import { fetchXpertSettings } from './data/thunks';
|
||||
|
||||
const XpertUnitSummarySettings = () => {
|
||||
const intl = useIntl();
|
||||
const XpertUnitSummarySettings = ({ intl }) => {
|
||||
const { path: pagesAndResourcesPath, courseId } = useContext(PagesAndResourcesContext);
|
||||
const dispatch = useDispatch();
|
||||
const navigate = useNavigate();
|
||||
@@ -38,4 +38,8 @@ const XpertUnitSummarySettings = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default XpertUnitSummarySettings;
|
||||
XpertUnitSummarySettings.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(XpertUnitSummarySettings);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
ActionRow,
|
||||
Alert,
|
||||
@@ -70,40 +70,38 @@ AppSettingsForm.defaultProps = {
|
||||
};
|
||||
|
||||
const SettingsModalBase = ({
|
||||
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>
|
||||
);
|
||||
};
|
||||
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>
|
||||
);
|
||||
|
||||
SettingsModalBase.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
title: PropTypes.string.isRequired,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
variant: PropTypes.oneOf(['default', 'dark']).isRequired,
|
||||
@@ -117,11 +115,11 @@ SettingsModalBase.defaultProps = {
|
||||
};
|
||||
|
||||
const ResetUnitsButton = ({
|
||||
intl,
|
||||
courseId,
|
||||
checked,
|
||||
visible,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const resetStatusRequestStatus = useSelector(getResetStatus);
|
||||
const dispatch = useDispatch();
|
||||
|
||||
@@ -187,6 +185,7 @@ const ResetUnitsButton = ({
|
||||
};
|
||||
|
||||
ResetUnitsButton.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
courseId: PropTypes.string.isRequired,
|
||||
checked: PropTypes.oneOf(['true', 'false']).isRequired,
|
||||
visible: PropTypes.bool,
|
||||
@@ -197,6 +196,7 @@ ResetUnitsButton.defaultProps = {
|
||||
};
|
||||
|
||||
const SettingsModal = ({
|
||||
intl,
|
||||
appId,
|
||||
title,
|
||||
children,
|
||||
@@ -213,7 +213,6 @@ const SettingsModal = ({
|
||||
allUnitsEnabledText,
|
||||
noUnitsEnabledText,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const { courseId } = useContext(PagesAndResourcesContext);
|
||||
const loadingStatus = useSelector(getLoadingStatus);
|
||||
const updateSettingsRequestStatus = useSelector(getSavingStatus);
|
||||
@@ -373,6 +372,7 @@ const SettingsModal = ({
|
||||
>
|
||||
{allUnitsEnabledText}
|
||||
<ResetUnitsButton
|
||||
intl={intl}
|
||||
courseId={courseId}
|
||||
checked={formikProps.values.checked}
|
||||
visible={formikProps.values.checked === 'true'}
|
||||
@@ -385,6 +385,7 @@ const SettingsModal = ({
|
||||
>
|
||||
{noUnitsEnabledText}
|
||||
<ResetUnitsButton
|
||||
intl={intl}
|
||||
courseId={courseId}
|
||||
checked={formikProps.values.checked}
|
||||
visible={formikProps.values.checked === 'false'}
|
||||
@@ -422,6 +423,7 @@ const SettingsModal = ({
|
||||
};
|
||||
|
||||
SettingsModal.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
title: PropTypes.string.isRequired,
|
||||
appId: PropTypes.string.isRequired,
|
||||
children: PropTypes.func,
|
||||
@@ -448,4 +450,4 @@ SettingsModal.defaultProps = {
|
||||
enableReinitialize: false,
|
||||
};
|
||||
|
||||
export default SettingsModal;
|
||||
export default injectIntl(SettingsModal);
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useDispatch, useSelector } from 'react-redux';
|
||||
import {
|
||||
useLocation,
|
||||
} from 'react-router-dom';
|
||||
import { StudioFooterSlot } from '@edx/frontend-component-footer';
|
||||
import { StudioFooterSlot } from '@openedx/frontend-slot-footer';
|
||||
import Header from './header';
|
||||
import { fetchCourseDetail, fetchWaffleFlags } from './data/thunks';
|
||||
import { useModel } from './generic/model-store';
|
||||
|
||||
@@ -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 './textbooks';
|
||||
import { Textbooks } from 'CourseAuthoring/textbooks';
|
||||
import CourseAuthoringPage from './CourseAuthoringPage';
|
||||
import { PagesAndResources } from './pages-and-resources';
|
||||
import EditorContainer from './editors/EditorContainer';
|
||||
|
||||
@@ -2,7 +2,7 @@ import React from 'react';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import { Container } from '@openedx/paragon';
|
||||
import { StudioFooterSlot } from '@edx/frontend-component-footer';
|
||||
import { StudioFooterSlot } from '@openedx/frontend-slot-footer';
|
||||
|
||||
import Header from '../header';
|
||||
import messages from './messages';
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
/* eslint-disable import/prefer-default-export */
|
||||
import {
|
||||
camelCaseObject,
|
||||
getConfig,
|
||||
} from '@edx/frontend-platform';
|
||||
import { camelCaseObject, getConfig } from '@edx/frontend-platform';
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
import { camelCase } from 'lodash';
|
||||
import { convertObjectToSnakeCase } from '../../utils';
|
||||
|
||||
const getApiBaseUrl = () => getConfig().STUDIO_BASE_URL;
|
||||
@@ -19,19 +14,7 @@ const getProctoringErrorsApiUrl = () => `${getApiBaseUrl()}/api/contentstore/v1/
|
||||
export async function getCourseAdvancedSettings(courseId) {
|
||||
const { data } = await getAuthenticatedHttpClient()
|
||||
.get(`${getCourseAdvancedSettingsApiUrl(courseId)}?fetch_all=0`);
|
||||
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;
|
||||
return camelCaseObject(data);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -43,19 +26,7 @@ export async function getCourseAdvancedSettings(courseId) {
|
||||
export async function updateCourseAdvancedSettings(courseId, settings) {
|
||||
const { data } = await getAuthenticatedHttpClient()
|
||||
.patch(`${getCourseAdvancedSettingsApiUrl(courseId)}`, convertObjectToSnakeCase(settings));
|
||||
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;
|
||||
return camelCaseObject(data);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -65,17 +36,5 @@ export async function updateCourseAdvancedSettings(courseId, settings) {
|
||||
*/
|
||||
export async function getProctoringExamErrors(courseId) {
|
||||
const { data } = await getAuthenticatedHttpClient().get(`${getProctoringErrorsApiUrl()}${courseId}`);
|
||||
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;
|
||||
return camelCaseObject(data);
|
||||
}
|
||||
|
||||
@@ -1,236 +0,0 @@
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
import {
|
||||
getCourseAdvancedSettings,
|
||||
updateCourseAdvancedSettings,
|
||||
getProctoringExamErrors,
|
||||
} from './api';
|
||||
|
||||
jest.mock('@edx/frontend-platform/auth', () => ({
|
||||
getAuthenticatedHttpClient: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('courseSettings API', () => {
|
||||
const mockHttpClient = {
|
||||
get: jest.fn(),
|
||||
patch: jest.fn(),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
getAuthenticatedHttpClient.mockReturnValue(mockHttpClient);
|
||||
});
|
||||
|
||||
describe('getCourseAdvancedSettings', () => {
|
||||
it('should fetch and unformat course advanced settings', async () => {
|
||||
const fakeData = {
|
||||
key_snake_case: {
|
||||
display_name: 'To come camelCase',
|
||||
testCamelCase: 'This key must not be formatted',
|
||||
PascalCase: 'To come camelCase',
|
||||
'kebab-case': 'To come camelCase',
|
||||
UPPER_CASE: 'To come camelCase',
|
||||
lowercase: 'This key must not be formatted',
|
||||
UPPERCASE: 'To come lowercase',
|
||||
'Title Case': 'To come camelCase',
|
||||
'dot.case': 'To come camelCase',
|
||||
SCREAMING_SNAKE_CASE: 'To come camelCase',
|
||||
MixedCase: 'To come camelCase',
|
||||
'Train-Case': 'To come camelCase',
|
||||
nestedOption: {
|
||||
anotherOption: 'To come camelCase',
|
||||
},
|
||||
// value is an object with various cases
|
||||
// this contain must not be formatted to camelCase
|
||||
value: {
|
||||
snake_case: 'snake_case',
|
||||
camelCase: 'camelCase',
|
||||
PascalCase: 'PascalCase',
|
||||
'kebab-case': 'kebab-case',
|
||||
UPPER_CASE: 'UPPER_CASE',
|
||||
lowercase: 'lowercase',
|
||||
UPPERCASE: 'UPPERCASE',
|
||||
'Title Case': 'Title Case',
|
||||
'dot.case': 'dot.case',
|
||||
SCREAMING_SNAKE_CASE: 'SCREAMING_SNAKE_CASE',
|
||||
MixedCase: 'MixedCase',
|
||||
'Train-Case': 'Train-Case',
|
||||
nestedOption: {
|
||||
anotherOption: 'nestedContent',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
const expected = {
|
||||
keySnakeCase: {
|
||||
displayName: 'To come camelCase',
|
||||
testCamelCase: 'This key must not be formatted',
|
||||
pascalCase: 'To come camelCase',
|
||||
kebabCase: 'To come camelCase',
|
||||
upperCase: 'To come camelCase',
|
||||
lowercase: 'This key must not be formatted',
|
||||
uppercase: 'To come lowercase',
|
||||
titleCase: 'To come camelCase',
|
||||
dotCase: 'To come camelCase',
|
||||
screamingSnakeCase: 'To come camelCase',
|
||||
mixedCase: 'To come camelCase',
|
||||
trainCase: 'To come camelCase',
|
||||
nestedOption: {
|
||||
anotherOption: 'To come camelCase',
|
||||
},
|
||||
value: fakeData.key_snake_case.value,
|
||||
},
|
||||
};
|
||||
|
||||
mockHttpClient.get.mockResolvedValue({ data: fakeData });
|
||||
|
||||
const result = await getCourseAdvancedSettings('course-v1:Test+T101+2024');
|
||||
expect(mockHttpClient.get).toHaveBeenCalledWith(
|
||||
`${process.env.STUDIO_BASE_URL}/api/contentstore/v0/advanced_settings/course-v1:Test+T101+2024?fetch_all=0`,
|
||||
);
|
||||
expect(result).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateCourseAdvancedSettings', () => {
|
||||
it('should update and unformat course advanced settings', async () => {
|
||||
const fakeData = {
|
||||
key_snake_case: {
|
||||
display_name: 'To come camelCase',
|
||||
testCamelCase: 'This key must not be formatted', // because already be camelCase
|
||||
PascalCase: 'To come camelCase',
|
||||
'kebab-case': 'To come camelCase',
|
||||
UPPER_CASE: 'To come camelCase',
|
||||
lowercase: 'This key must not be formatted', // because camelCase in lowercase not formatted
|
||||
UPPERCASE: 'To come lowercase', // because camelCase in UPPERCASE format to lowercase
|
||||
'Title Case': 'To come camelCase',
|
||||
'dot.case': 'To come camelCase',
|
||||
SCREAMING_SNAKE_CASE: 'To come camelCase',
|
||||
MixedCase: 'To come camelCase',
|
||||
'Train-Case': 'To come camelCase',
|
||||
nestedOption: {
|
||||
anotherOption: 'To come camelCase',
|
||||
},
|
||||
// value is an object with various cases
|
||||
// this contain must not be formatted to camelCase
|
||||
value: {
|
||||
snake_case: 'snake_case',
|
||||
camelCase: 'camelCase',
|
||||
PascalCase: 'PascalCase',
|
||||
'kebab-case': 'kebab-case',
|
||||
UPPER_CASE: 'UPPER_CASE',
|
||||
lowercase: 'lowercase',
|
||||
UPPERCASE: 'UPPERCASE',
|
||||
'Title Case': 'Title Case',
|
||||
'dot.case': 'dot.case',
|
||||
SCREAMING_SNAKE_CASE: 'SCREAMING_SNAKE_CASE',
|
||||
MixedCase: 'MixedCase',
|
||||
'Train-Case': 'Train-Case',
|
||||
nestedOption: {
|
||||
anotherOption: 'nestedContent',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
const expected = {
|
||||
keySnakeCase: {
|
||||
displayName: 'To come camelCase',
|
||||
testCamelCase: 'This key must not be formatted',
|
||||
pascalCase: 'To come camelCase',
|
||||
kebabCase: 'To come camelCase',
|
||||
upperCase: 'To come camelCase',
|
||||
lowercase: 'This key must not be formatted',
|
||||
uppercase: 'To come lowercase',
|
||||
titleCase: 'To come camelCase',
|
||||
dotCase: 'To come camelCase',
|
||||
screamingSnakeCase: 'To come camelCase',
|
||||
mixedCase: 'To come camelCase',
|
||||
trainCase: 'To come camelCase',
|
||||
nestedOption: {
|
||||
anotherOption: 'To come camelCase',
|
||||
},
|
||||
value: fakeData.key_snake_case.value,
|
||||
},
|
||||
};
|
||||
|
||||
mockHttpClient.patch.mockResolvedValue({ data: fakeData });
|
||||
|
||||
const result = await updateCourseAdvancedSettings('course-v1:Test+T101+2024', {});
|
||||
expect(mockHttpClient.patch).toHaveBeenCalledWith(
|
||||
`${process.env.STUDIO_BASE_URL}/api/contentstore/v0/advanced_settings/course-v1:Test+T101+2024`,
|
||||
{},
|
||||
);
|
||||
expect(result).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getProctoringExamErrors', () => {
|
||||
it('should fetch proctoring errors and return unformat object', async () => {
|
||||
const fakeData = {
|
||||
key_snake_case: {
|
||||
display_name: 'To come camelCase',
|
||||
testCamelCase: 'This key must not be formatted',
|
||||
PascalCase: 'To come camelCase',
|
||||
'kebab-case': 'To come camelCase',
|
||||
UPPER_CASE: 'To come camelCase',
|
||||
lowercase: 'This key must not be formatted',
|
||||
UPPERCASE: 'To come lowercase',
|
||||
'Title Case': 'To come camelCase',
|
||||
'dot.case': 'To come camelCase',
|
||||
SCREAMING_SNAKE_CASE: 'To come camelCase',
|
||||
MixedCase: 'To come camelCase',
|
||||
'Train-Case': 'To come camelCase',
|
||||
nestedOption: {
|
||||
anotherOption: 'To come camelCase',
|
||||
},
|
||||
// value is an object with various cases
|
||||
// this contain must not be formatted to camelCase
|
||||
value: {
|
||||
snake_case: 'snake_case',
|
||||
camelCase: 'camelCase',
|
||||
PascalCase: 'PascalCase',
|
||||
'kebab-case': 'kebab-case',
|
||||
UPPER_CASE: 'UPPER_CASE',
|
||||
lowercase: 'lowercase',
|
||||
UPPERCASE: 'UPPERCASE',
|
||||
'Title Case': 'Title Case',
|
||||
'dot.case': 'dot.case',
|
||||
SCREAMING_SNAKE_CASE: 'SCREAMING_SNAKE_CASE',
|
||||
MixedCase: 'MixedCase',
|
||||
'Train-Case': 'Train-Case',
|
||||
nestedOption: {
|
||||
anotherOption: 'nestedContent',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
const expected = {
|
||||
keySnakeCase: {
|
||||
displayName: 'To come camelCase',
|
||||
testCamelCase: 'This key must not be formatted',
|
||||
pascalCase: 'To come camelCase',
|
||||
kebabCase: 'To come camelCase',
|
||||
upperCase: 'To come camelCase',
|
||||
lowercase: 'This key must not be formatted',
|
||||
uppercase: 'To come lowercase',
|
||||
titleCase: 'To come camelCase',
|
||||
dotCase: 'To come camelCase',
|
||||
screamingSnakeCase: 'To come camelCase',
|
||||
mixedCase: 'To come camelCase',
|
||||
trainCase: 'To come camelCase',
|
||||
nestedOption: {
|
||||
anotherOption: 'To come camelCase',
|
||||
},
|
||||
value: fakeData.key_snake_case.value,
|
||||
},
|
||||
};
|
||||
|
||||
mockHttpClient.get.mockResolvedValue({ data: fakeData });
|
||||
|
||||
const result = await getProctoringExamErrors('course-v1:Test+T101+2024');
|
||||
expect(mockHttpClient.get).toHaveBeenCalledWith(
|
||||
`${process.env.STUDIO_BASE_URL}/api/contentstore/v1/proctoring_errors/course-v1:Test+T101+2024`,
|
||||
);
|
||||
expect(result).toEqual(expected);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -21,7 +21,6 @@ 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,11 +40,6 @@ 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}>
|
||||
@@ -190,26 +184,6 @@ 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,7 +14,6 @@ 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;
|
||||
@@ -245,7 +244,6 @@ const ContentTagsDrawer = ({
|
||||
if (contentId === undefined) {
|
||||
throw new Error('Error: contentId cannot be null.');
|
||||
}
|
||||
const { sidebarAction } = useSidebarContext();
|
||||
|
||||
const context = useCreateContentTagsDrawerContext(contentId, !readOnly, variant === 'drawer');
|
||||
const { blockingSheet } = useContext(ContentTagsDrawerSheetContext);
|
||||
@@ -262,7 +260,6 @@ const ContentTagsDrawer = ({
|
||||
closeToast,
|
||||
setCollapsibleToInitalState,
|
||||
otherTaxonomies,
|
||||
toEditMode,
|
||||
} = context;
|
||||
|
||||
let onCloseDrawer: () => void;
|
||||
@@ -305,13 +302,8 @@ const ContentTagsDrawer = ({
|
||||
|
||||
// First call of the initial collapsible states
|
||||
React.useEffect(() => {
|
||||
// Open tag edit mode when sidebarAction is JumpToManageTags
|
||||
if (sidebarAction === SidebarActions.JumpToManageTags) {
|
||||
toEditMode();
|
||||
} else {
|
||||
setCollapsibleToInitalState();
|
||||
}
|
||||
}, [isTaxonomyListLoaded, isContentTaxonomyTagsLoaded, sidebarAction, toEditMode]);
|
||||
setCollapsibleToInitalState();
|
||||
}, [isTaxonomyListLoaded, isContentTaxonomyTagsLoaded]);
|
||||
|
||||
const renderFooter = () => {
|
||||
if (isTaxonomyListLoaded && isContentTaxonomyTagsLoaded) {
|
||||
|
||||
@@ -7,7 +7,6 @@ import {
|
||||
useMutation,
|
||||
useQueryClient,
|
||||
} from '@tanstack/react-query';
|
||||
import { useParams } from 'react-router';
|
||||
import {
|
||||
getTaxonomyTagsData,
|
||||
getContentTaxonomyTagsData,
|
||||
@@ -15,7 +14,7 @@ import {
|
||||
updateContentTaxonomyTags,
|
||||
getContentTaxonomyTagsCount,
|
||||
} from './api';
|
||||
import { libraryAuthoringQueryKeys, libraryQueryPredicate, xblockQueryKeys } from '../../library-authoring/data/apiHooks';
|
||||
import { libraryQueryPredicate, xblockQueryKeys } from '../../library-authoring/data/apiHooks';
|
||||
import { getLibraryId } from '../../generic/key-utils';
|
||||
|
||||
/** @typedef {import("../../taxonomy/data/types.js").TagListData} TagListData */
|
||||
@@ -130,7 +129,6 @@ export const useContentData = (contentId, enabled) => (
|
||||
export const useContentTaxonomyTagsUpdater = (contentId) => {
|
||||
const queryClient = useQueryClient();
|
||||
const unitIframe = window.frames['xblock-iframe'];
|
||||
const { unitId } = useParams();
|
||||
|
||||
return useMutation({
|
||||
/**
|
||||
@@ -160,10 +158,6 @@ export const useContentTaxonomyTagsUpdater = (contentId) => {
|
||||
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 */ () => {
|
||||
|
||||
@@ -157,7 +157,7 @@ describe('useContentTaxonomyTagsUpdater', () => {
|
||||
|
||||
const contentId = 'testerContent';
|
||||
const taxonomyId = 123;
|
||||
const mutation = renderHook(() => useContentTaxonomyTagsUpdater(contentId)).result.current;
|
||||
const mutation = useContentTaxonomyTagsUpdater(contentId);
|
||||
const tagsData = [{
|
||||
taxonomy: taxonomyId,
|
||||
tags: ['tag1', 'tag2'],
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useState, useMemo } from 'react';
|
||||
// @ts-check
|
||||
import React, { useState, useMemo } from 'react';
|
||||
import {
|
||||
Card, Stack, Button, Collapsible, Icon,
|
||||
} from '@openedx/paragon';
|
||||
@@ -9,19 +10,10 @@ 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';
|
||||
|
||||
interface TagsSidebarBodyProps {
|
||||
readOnly: boolean
|
||||
}
|
||||
|
||||
type TagTree = {
|
||||
[key: string]: { children: TagTree, canChangeObjecttag: boolean, canDeleteObjecttag: boolean }
|
||||
};
|
||||
|
||||
const TagsSidebarBody = ({ readOnly }: TagsSidebarBodyProps) => {
|
||||
const TagsSidebarBody = () => {
|
||||
const intl = useIntl();
|
||||
const [showManageTags, setShowManageTags] = useState(false);
|
||||
const contentId = useParams().blockId;
|
||||
@@ -32,8 +24,8 @@ const TagsSidebarBody = ({ readOnly }: TagsSidebarBodyProps) => {
|
||||
isSuccess: isContentTaxonomyTagsLoaded,
|
||||
} = useContentTaxonomyTagsData(contentId || '');
|
||||
|
||||
const buildTagsTree = (contentTags: Tag[]) => {
|
||||
const resultTree: TagTree = {};
|
||||
const buildTagsTree = (contentTags) => {
|
||||
const resultTree = {};
|
||||
contentTags.forEach(item => {
|
||||
let currentLevel = resultTree;
|
||||
|
||||
@@ -54,7 +46,7 @@ const TagsSidebarBody = ({ readOnly }: TagsSidebarBodyProps) => {
|
||||
};
|
||||
|
||||
const tree = useMemo(() => {
|
||||
const result: (Omit<ContentTaxonomyTagData, 'tags'> & { tags: TagTree })[] = [];
|
||||
const result = [];
|
||||
if (isContentTaxonomyTagsLoaded && contentTaxonomyTagsData) {
|
||||
contentTaxonomyTagsData.taxonomies.forEach((taxonomy) => {
|
||||
result.push({
|
||||
@@ -96,13 +88,7 @@ const TagsSidebarBody = ({ readOnly }: TagsSidebarBodyProps) => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Button
|
||||
className="mt-3 ml-2"
|
||||
variant="outline-primary"
|
||||
size="sm"
|
||||
onClick={() => setShowManageTags(true)}
|
||||
disabled={readOnly}
|
||||
>
|
||||
<Button className="mt-3 ml-2" variant="outline-primary" size="sm" onClick={() => setShowManageTags(true)}>
|
||||
{intl.formatMessage(messages.manageTagsButton)}
|
||||
</Button>
|
||||
</Stack>
|
||||
@@ -116,4 +102,6 @@ const TagsSidebarBody = ({ readOnly }: TagsSidebarBodyProps) => {
|
||||
);
|
||||
};
|
||||
|
||||
TagsSidebarBody.propTypes = {};
|
||||
|
||||
export default TagsSidebarBody;
|
||||
@@ -1,14 +1,10 @@
|
||||
import TagsSidebarHeader from './TagsSidebarHeader';
|
||||
import TagsSidebarBody from './TagsSidebarBody';
|
||||
|
||||
interface TagsSidebarControlsProps {
|
||||
readOnly: boolean,
|
||||
}
|
||||
|
||||
const TagsSidebarControls = ({ readOnly }: TagsSidebarControlsProps) => (
|
||||
const TagsSidebarControls = () => (
|
||||
<>
|
||||
<TagsSidebarHeader />
|
||||
<TagsSidebarBody readOnly={readOnly} />
|
||||
<TagsSidebarBody />
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -82,7 +82,7 @@ describe('<CourseLibraries />', () => {
|
||||
expect(reviewTab).toHaveAttribute('aria-selected', 'true');
|
||||
|
||||
userEvent.click(allTab);
|
||||
const alert = (await screen.findAllByRole('alert'))[0];
|
||||
const alert = await screen.findByRole('alert');
|
||||
expect(await within(alert).findByText(
|
||||
'5 library components are out of sync. Review updates to accept or ignore changes',
|
||||
)).toBeInTheDocument();
|
||||
@@ -105,7 +105,7 @@ describe('<CourseLibraries />', () => {
|
||||
userEvent.click(allTab);
|
||||
expect(allTab).toHaveAttribute('aria-selected', 'true');
|
||||
|
||||
const alert = (await screen.findAllByRole('alert'))[0];
|
||||
const alert = await screen.findByRole('alert');
|
||||
expect(await within(alert).findByText(
|
||||
'5 library components are out of sync. Review updates to accept or ignore changes',
|
||||
)).toBeInTheDocument();
|
||||
@@ -118,46 +118,6 @@ describe('<CourseLibraries />', () => {
|
||||
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');
|
||||
|
||||
screen.logTestingPlaygroundURL();
|
||||
|
||||
expect(screen.queryAllByRole('alert').length).toEqual(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('<CourseLibraries ReviewTab />', () => {
|
||||
@@ -200,7 +160,7 @@ describe('<CourseLibraries ReviewTab />', () => {
|
||||
|
||||
it('update changes works', async () => {
|
||||
const mockInvalidateQueries = jest.spyOn(queryClient, 'invalidateQueries');
|
||||
const usageKey = mockGetEntityLinks.response[0].downstreamUsageKey;
|
||||
const usageKey = mockGetEntityLinks.response.results[0].downstreamUsageKey;
|
||||
axiosMock.onPost(libraryBlockChangesUrl(usageKey)).reply(200, {});
|
||||
await renderCourseLibrariesReviewPage(mockGetEntityLinksSummaryByDownstreamContext.courseKey);
|
||||
const updateBtns = await screen.findAllByRole('button', { name: 'Update' });
|
||||
@@ -216,7 +176,7 @@ describe('<CourseLibraries ReviewTab />', () => {
|
||||
|
||||
it('update changes works in preview modal', async () => {
|
||||
const mockInvalidateQueries = jest.spyOn(queryClient, 'invalidateQueries');
|
||||
const usageKey = mockGetEntityLinks.response[0].downstreamUsageKey;
|
||||
const usageKey = mockGetEntityLinks.response.results[0].downstreamUsageKey;
|
||||
axiosMock.onPost(libraryBlockChangesUrl(usageKey)).reply(200, {});
|
||||
await renderCourseLibrariesReviewPage(mockGetEntityLinksSummaryByDownstreamContext.courseKey);
|
||||
const previewBtns = await screen.findAllByRole('button', { name: 'Review Updates' });
|
||||
@@ -235,7 +195,7 @@ describe('<CourseLibraries ReviewTab />', () => {
|
||||
|
||||
it('ignore change works', async () => {
|
||||
const mockInvalidateQueries = jest.spyOn(queryClient, 'invalidateQueries');
|
||||
const usageKey = mockGetEntityLinks.response[0].downstreamUsageKey;
|
||||
const usageKey = mockGetEntityLinks.response.results[0].downstreamUsageKey;
|
||||
axiosMock.onDelete(libraryBlockChangesUrl(usageKey)).reply(204, {});
|
||||
await renderCourseLibrariesReviewPage(mockGetEntityLinksSummaryByDownstreamContext.courseKey);
|
||||
const ignoreBtns = await screen.findAllByRole('button', { name: 'Ignore' });
|
||||
@@ -258,7 +218,7 @@ describe('<CourseLibraries ReviewTab />', () => {
|
||||
|
||||
it('ignore change works in preview', async () => {
|
||||
const mockInvalidateQueries = jest.spyOn(queryClient, 'invalidateQueries');
|
||||
const usageKey = mockGetEntityLinks.response[0].downstreamUsageKey;
|
||||
const usageKey = mockGetEntityLinks.response.results[0].downstreamUsageKey;
|
||||
axiosMock.onDelete(libraryBlockChangesUrl(usageKey)).reply(204, {});
|
||||
await renderCourseLibrariesReviewPage(mockGetEntityLinksSummaryByDownstreamContext.courseKey);
|
||||
const previewBtns = await screen.findAllByRole('button', { name: 'Review Updates' });
|
||||
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
Tabs,
|
||||
} from '@openedx/paragon';
|
||||
import {
|
||||
Cached, CheckCircle, Launch, Loop, Info,
|
||||
Cached, CheckCircle, Launch, Loop,
|
||||
} from '@openedx/paragon/icons';
|
||||
|
||||
import sumBy from 'lodash/sumBy';
|
||||
@@ -33,7 +33,6 @@ 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;
|
||||
@@ -165,7 +164,7 @@ export const CourseLibraries: React.FC<Props> = ({ courseId }) => {
|
||||
if (tabKey !== CourseLibraryTabs.review) {
|
||||
return null;
|
||||
}
|
||||
if (!outOfSyncCount) {
|
||||
if (!outOfSyncCount || outOfSyncCount === 0) {
|
||||
return (
|
||||
<Stack direction="horizontal" gap={2}>
|
||||
<Icon src={CheckCircle} size="xs" />
|
||||
@@ -200,12 +199,6 @@ export const CourseLibraries: React.FC<Props> = ({ courseId }) => {
|
||||
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)}
|
||||
|
||||
@@ -18,11 +18,12 @@ interface OutOfSyncAlertProps {
|
||||
* 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
|
||||
* * If the user clicks on dismiss button, the state is stored in localstorage of user
|
||||
* in this format: outOfSyncCountAlert-${courseId} = <number of out of sync components>.
|
||||
* * If the number of sync components don't change for the course and the user opens outline
|
||||
* in the same browser, they don't see the alert again.
|
||||
* * If there is a new published component upstream, the alert is displayed again.
|
||||
* * If the number changes, i.e., if a new component is out of sync or the user updates or ignores
|
||||
* a component, the alert is displayed again.
|
||||
*/
|
||||
export const OutOfSyncAlert: React.FC<OutOfSyncAlertProps> = ({
|
||||
showAlert,
|
||||
@@ -33,9 +34,7 @@ export const OutOfSyncAlert: React.FC<OutOfSyncAlertProps> = ({
|
||||
}) => {
|
||||
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 outOfSyncCount = data?.reduce((count, lib) => count + lib.readyToSyncCount, 0);
|
||||
const alertKey = `outOfSyncCountAlert-${courseId}`;
|
||||
|
||||
useEffect(() => {
|
||||
@@ -47,14 +46,13 @@ export const OutOfSyncAlert: React.FC<OutOfSyncAlertProps> = ({
|
||||
setShowAlert(false);
|
||||
return;
|
||||
}
|
||||
const dismissedAlertDate = parseInt(localStorage.getItem(alertKey) ?? '0', 10);
|
||||
|
||||
setShowAlert((lastPublishedDate ?? 0) > dismissedAlertDate);
|
||||
}, [outOfSyncCount, lastPublishedDate, isLoading, data]);
|
||||
const dismissedAlert = localStorage.getItem(alertKey);
|
||||
setShowAlert(parseInt(dismissedAlert || '', 10) !== outOfSyncCount);
|
||||
}, [outOfSyncCount, isLoading, data]);
|
||||
|
||||
const dismissAlert = () => {
|
||||
setShowAlert(false);
|
||||
localStorage.setItem(alertKey, Date.now().toString());
|
||||
localStorage.setItem(alertKey, String(outOfSyncCount));
|
||||
onDismiss?.();
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, {
|
||||
useCallback, useContext, useMemo, useState,
|
||||
useCallback, useContext, useEffect, useMemo, useState,
|
||||
} from 'react';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
@@ -14,9 +14,11 @@ import {
|
||||
useToggle,
|
||||
} from '@openedx/paragon';
|
||||
|
||||
import { tail, keyBy } from 'lodash';
|
||||
import {
|
||||
tail, keyBy, orderBy, merge, omitBy,
|
||||
} from 'lodash';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { Loop } from '@openedx/paragon/icons';
|
||||
import { Loop, Warning } from '@openedx/paragon/icons';
|
||||
import messages from './messages';
|
||||
import previewChangesMessages from '../course-unit/preview-changes/messages';
|
||||
import { courseLibrariesQueryKeys, useEntityLinks } from './data/apiHooks';
|
||||
@@ -35,6 +37,7 @@ import { useLoadOnScroll } from '../hooks';
|
||||
import DeleteModal from '../generic/delete-modal/DeleteModal';
|
||||
import { PublishableEntityLink } from './data/api';
|
||||
import AlertError from '../generic/alert-error';
|
||||
import AlertMessage from '../generic/alert-message';
|
||||
|
||||
interface Props {
|
||||
courseId: string;
|
||||
@@ -99,8 +102,10 @@ const BlockCard: React.FC<BlockCardProps> = ({ info, actions }) => {
|
||||
|
||||
const ComponentReviewList = ({
|
||||
outOfSyncComponents,
|
||||
onSearchUpdate,
|
||||
}: {
|
||||
outOfSyncComponents: PublishableEntityLink[];
|
||||
onSearchUpdate: () => void;
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const { showToast } = useContext(ToastContext);
|
||||
@@ -108,15 +113,24 @@ const ComponentReviewList = ({
|
||||
// ignore changes confirmation modal toggle.
|
||||
const [isConfirmModalOpen, openConfirmModal, closeConfirmModal] = useToggle(false);
|
||||
const {
|
||||
hits,
|
||||
hits: downstreamInfo,
|
||||
isLoading: isIndexDataLoading,
|
||||
searchKeywords,
|
||||
searchSortOrder,
|
||||
hasError,
|
||||
hasNextPage,
|
||||
isFetchingNextPage,
|
||||
fetchNextPage,
|
||||
} = useSearchContext();
|
||||
|
||||
const downstreamInfo = hits as ContentHit[];
|
||||
} = useSearchContext() as {
|
||||
hits: ContentHit[];
|
||||
isLoading: boolean;
|
||||
searchKeywords: string;
|
||||
searchSortOrder: SearchSortOption;
|
||||
hasError: boolean;
|
||||
hasNextPage: boolean | undefined,
|
||||
isFetchingNextPage: boolean;
|
||||
fetchNextPage: () => void;
|
||||
};
|
||||
|
||||
useLoadOnScroll(
|
||||
hasNextPage,
|
||||
@@ -129,14 +143,24 @@ const ComponentReviewList = ({
|
||||
() => keyBy(outOfSyncComponents, 'downstreamUsageKey'),
|
||||
[outOfSyncComponents],
|
||||
);
|
||||
const downstreamInfoByKey = useMemo(
|
||||
() => keyBy(downstreamInfo, 'usageKey'),
|
||||
[downstreamInfo],
|
||||
);
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
useEffect(() => {
|
||||
if (searchKeywords) {
|
||||
onSearchUpdate();
|
||||
}
|
||||
}, [searchKeywords]);
|
||||
|
||||
// Toggle preview changes modal
|
||||
const [isModalOpen, openModal, closeModal] = useToggle(false);
|
||||
const acceptChangesMutation = useAcceptLibraryBlockChanges();
|
||||
const ignoreChangesMutation = useIgnoreLibraryBlockChanges();
|
||||
|
||||
const setSelectedBlockData = useCallback((info: ContentHit) => {
|
||||
const setSeletecdBlockData = (info: ContentHit) => {
|
||||
setBlockData({
|
||||
displayName: info.displayName,
|
||||
downstreamBlockId: info.usageKey,
|
||||
@@ -144,18 +168,17 @@ const ComponentReviewList = ({
|
||||
upstreamBlockVersionSynced: outOfSyncComponentsByKey[info.usageKey].versionSynced,
|
||||
isVertical: info.blockType === 'vertical',
|
||||
});
|
||||
}, [outOfSyncComponentsByKey]);
|
||||
|
||||
};
|
||||
// Show preview changes on review
|
||||
const onReview = useCallback((info: ContentHit) => {
|
||||
setSelectedBlockData(info);
|
||||
setSeletecdBlockData(info);
|
||||
openModal();
|
||||
}, [setSelectedBlockData, openModal]);
|
||||
}, [setSeletecdBlockData, openModal]);
|
||||
|
||||
const onIgnoreClick = useCallback((info: ContentHit) => {
|
||||
setSelectedBlockData(info);
|
||||
setSeletecdBlockData(info);
|
||||
openConfirmModal();
|
||||
}, [setSelectedBlockData, openConfirmModal]);
|
||||
}, [setSeletecdBlockData, openConfirmModal]);
|
||||
|
||||
const reloadLinks = useCallback((usageKey: string) => {
|
||||
const courseKey = outOfSyncComponentsByKey[usageKey].downstreamContextKey;
|
||||
@@ -213,6 +236,19 @@ const ComponentReviewList = ({
|
||||
}
|
||||
}, [blockData]);
|
||||
|
||||
const orderInfo = useMemo(() => {
|
||||
if (searchSortOrder !== SearchSortOption.RECENTLY_MODIFIED) {
|
||||
return downstreamInfo;
|
||||
}
|
||||
if (isIndexDataLoading) {
|
||||
return [];
|
||||
}
|
||||
let merged = merge(downstreamInfoByKey, outOfSyncComponentsByKey);
|
||||
merged = omitBy(merged, (o) => !o.displayName);
|
||||
const ordered = orderBy(Object.values(merged), 'updated', 'desc');
|
||||
return ordered;
|
||||
}, [downstreamInfoByKey, outOfSyncComponentsByKey]);
|
||||
|
||||
if (isIndexDataLoading) {
|
||||
return <Loading />;
|
||||
}
|
||||
@@ -223,7 +259,7 @@ const ComponentReviewList = ({
|
||||
|
||||
return (
|
||||
<>
|
||||
{downstreamInfo?.map((info) => (
|
||||
{orderInfo?.map((info) => (
|
||||
<BlockCard
|
||||
key={info.usageKey}
|
||||
info={info}
|
||||
@@ -257,14 +293,20 @@ const ComponentReviewList = ({
|
||||
)}
|
||||
/>
|
||||
))}
|
||||
{blockData && (
|
||||
<PreviewLibraryXBlockChanges
|
||||
blockData={blockData}
|
||||
isModalOpen={isModalOpen}
|
||||
closeModal={closeModal}
|
||||
postChange={postChange}
|
||||
/>
|
||||
)}
|
||||
<PreviewLibraryXBlockChanges
|
||||
blockData={blockData}
|
||||
isModalOpen={isModalOpen}
|
||||
closeModal={closeModal}
|
||||
postChange={postChange}
|
||||
alertNode={(
|
||||
<AlertMessage
|
||||
show
|
||||
variant="warning"
|
||||
icon={Warning}
|
||||
title={intl.formatMessage(messages.olderVersionPreviewAlert)}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<DeleteModal
|
||||
isOpen={isConfirmModalOpen}
|
||||
close={closeConfirmModal}
|
||||
@@ -281,17 +323,37 @@ const ComponentReviewList = ({
|
||||
const ReviewTabContent = ({ courseId }: Props) => {
|
||||
const intl = useIntl();
|
||||
const {
|
||||
data: outOfSyncComponents,
|
||||
data: linkPages,
|
||||
isLoading: isSyncComponentsLoading,
|
||||
hasNextPage,
|
||||
isFetchingNextPage,
|
||||
fetchNextPage,
|
||||
isError,
|
||||
error,
|
||||
} = useEntityLinks({ courseId, readyToSync: true });
|
||||
|
||||
const outOfSyncComponents = useMemo(
|
||||
() => linkPages?.pages?.reduce((links, page) => [...links, ...page.results], []) ?? [],
|
||||
[linkPages],
|
||||
);
|
||||
const downstreamKeys = useMemo(
|
||||
() => outOfSyncComponents?.map(link => link.downstreamUsageKey),
|
||||
[outOfSyncComponents],
|
||||
);
|
||||
|
||||
useLoadOnScroll(
|
||||
hasNextPage,
|
||||
isFetchingNextPage,
|
||||
fetchNextPage,
|
||||
true,
|
||||
);
|
||||
|
||||
const onSearchUpdate = () => {
|
||||
if (hasNextPage && !isFetchingNextPage) {
|
||||
fetchNextPage();
|
||||
}
|
||||
};
|
||||
|
||||
const disableSortOptions = [
|
||||
SearchSortOption.RELEVANCE,
|
||||
SearchSortOption.OLDEST,
|
||||
@@ -322,6 +384,7 @@ const ReviewTabContent = ({ courseId }: Props) => {
|
||||
</ActionRow>
|
||||
<ComponentReviewList
|
||||
outOfSyncComponents={outOfSyncComponents}
|
||||
onSearchUpdate={onSearchUpdate}
|
||||
/>
|
||||
</SearchContextProvider>
|
||||
);
|
||||
|
||||
@@ -3,20 +3,18 @@
|
||||
"upstreamContextTitle": "CS problems 3",
|
||||
"upstreamContextKey": "lib:OpenedX:CSPROB3",
|
||||
"readyToSyncCount": 5,
|
||||
"totalCount": 14,
|
||||
"lastPublishedAt": "2025-05-01T20:20:44.989042Z"
|
||||
"totalCount": 14
|
||||
},
|
||||
{
|
||||
"upstreamContextTitle": "CS problems 2",
|
||||
"upstreamContextKey": "lib:OpenedX:CSPROB2",
|
||||
"readyToSyncCount": 0,
|
||||
"totalCount": 21,
|
||||
"lastPublishedAt": "2025-05-01T21:20:44.989042Z"
|
||||
"totalCount": 21
|
||||
},
|
||||
{
|
||||
"upstreamContextTitle": "CS problems",
|
||||
"upstreamContextKey": "lib:OpenedX:CSPROB",
|
||||
"totalCount": 3,
|
||||
"lastPublishedAt": "2025-05-01T22:20:44.989042Z"
|
||||
"readyToSyncCount": 0,
|
||||
"totalCount": 3
|
||||
}
|
||||
]
|
||||
|
||||
@@ -1,72 +1,79 @@
|
||||
[
|
||||
{
|
||||
"id": 875,
|
||||
"upstreamContextTitle": "CS problems 3",
|
||||
"upstreamVersion": 10,
|
||||
"readyToSync": true,
|
||||
"upstreamUsageKey": "lb:OpenedX:CSPROB3:problem:d40264d5-80c4-4be8-bfb6-086391de1cd3",
|
||||
"upstreamContextKey": "lib:OpenedX:CSPROB3",
|
||||
"downstreamUsageKey": "block-v1:OpenEdx+DemoX+CourseX+type@problem+block@problem3",
|
||||
"downstreamContextKey": "course-v1:OpenEdx+DemoX+CourseX",
|
||||
"versionSynced": 2,
|
||||
"versionDeclined": null,
|
||||
"created": "2025-02-08T14:07:05.588484Z",
|
||||
"updated": "2025-02-08T14:07:05.588484Z"
|
||||
},
|
||||
{
|
||||
"id": 876,
|
||||
"upstreamContextTitle": "CS problems 3",
|
||||
"upstreamVersion": 10,
|
||||
"readyToSync": true,
|
||||
"upstreamUsageKey": "lb:OpenedX:CSPROB3:problem:d40264d5-80c4-4be8-bfb6-086391de1cd3",
|
||||
"upstreamContextKey": "lib:OpenedX:CSPROB3",
|
||||
"downstreamUsageKey": "block-v1:OpenEdx+DemoX+CourseX+type@problem+block@problem6",
|
||||
"downstreamContextKey": "course-v1:OpenEdx+DemoX+CourseX",
|
||||
"versionSynced": 2,
|
||||
"versionDeclined": null,
|
||||
"created": "2025-02-08T14:07:05.588484Z",
|
||||
"updated": "2025-02-08T14:07:05.588484Z"
|
||||
},
|
||||
{
|
||||
"id": 884,
|
||||
"upstreamContextTitle": "CS problems 3",
|
||||
"upstreamVersion": 26,
|
||||
"readyToSync": true,
|
||||
"upstreamUsageKey": "lb:OpenedX:CSPROB3:html:ca4d2b1f-0b64-4a2d-88fa-592f7e398477",
|
||||
"upstreamContextKey": "lib:OpenedX:CSPROB3",
|
||||
"downstreamUsageKey": "block-v1:OpenEdx+DemoX+CourseX+type@html+block@257e68e3386d4a8f8739d45b67e76a9b",
|
||||
"downstreamContextKey": "course-v1:OpenEdx+DemoX+CourseX",
|
||||
"versionSynced": 16,
|
||||
"versionDeclined": null,
|
||||
"created": "2025-02-08T14:07:05.588484Z",
|
||||
"updated": "2025-02-08T14:07:05.588484Z"
|
||||
},
|
||||
{
|
||||
"id": 889,
|
||||
"upstreamContextTitle": "CS problems 3",
|
||||
"upstreamVersion": 10,
|
||||
"readyToSync": true,
|
||||
"upstreamUsageKey": "lb:OpenedX:CSPROB3:problem:d40264d5-80c4-4be8-bfb6-086391de1cd3",
|
||||
"upstreamContextKey": "lib:OpenedX:CSPROB3",
|
||||
"downstreamUsageKey": "block-v1:OpenEdx+DemoX+CourseX+type@problem+block@a4455860b03647219ff8b01cde49cf37",
|
||||
"downstreamContextKey": "course-v1:OpenEdx+DemoX+CourseX",
|
||||
"versionSynced": 2,
|
||||
"versionDeclined": null,
|
||||
"created": "2025-02-08T14:07:05.588484Z",
|
||||
"updated": "2025-02-08T14:07:05.588484Z"
|
||||
},
|
||||
{
|
||||
"id": 890,
|
||||
"upstreamContextTitle": "CS problems 3",
|
||||
"upstreamVersion": 10,
|
||||
"readyToSync": true,
|
||||
"upstreamUsageKey": "lb:OpenedX:CSPROB3:problem:d40264d5-80c4-4be8-bfb6-086391de1cd3",
|
||||
"upstreamContextKey": "lib:OpenedX:CSPROB3",
|
||||
"downstreamUsageKey": "block-v1:OpenEdx+DemoX+CourseX+type@problem+block@210e356cfa304b0aac591af53f6a6ae0",
|
||||
"downstreamContextKey": "course-v1:OpenEdx+DemoX+CourseX",
|
||||
"versionSynced": 2,
|
||||
"versionDeclined": null,
|
||||
"created": "2025-02-08T14:07:05.588484Z",
|
||||
"updated": "2025-02-08T14:07:05.588484Z"
|
||||
}
|
||||
]
|
||||
{
|
||||
"count": 7,
|
||||
"next": null,
|
||||
"previous": null,
|
||||
"num_pages": 1,
|
||||
"current_page": 1,
|
||||
"results": [
|
||||
{
|
||||
"id": 875,
|
||||
"upstreamContextTitle": "CS problems 3",
|
||||
"upstreamVersion": 10,
|
||||
"readyToSync": true,
|
||||
"upstreamUsageKey": "lb:OpenedX:CSPROB3:problem:d40264d5-80c4-4be8-bfb6-086391de1cd3",
|
||||
"upstreamContextKey": "lib:OpenedX:CSPROB3",
|
||||
"downstreamUsageKey": "block-v1:OpenEdx+DemoX+CourseX+type@problem+block@problem3",
|
||||
"downstreamContextKey": "course-v1:OpenEdx+DemoX+CourseX",
|
||||
"versionSynced": 2,
|
||||
"versionDeclined": null,
|
||||
"created": "2025-02-08T14:07:05.588484Z",
|
||||
"updated": "2025-02-08T14:07:05.588484Z"
|
||||
},
|
||||
{
|
||||
"id": 876,
|
||||
"upstreamContextTitle": "CS problems 3",
|
||||
"upstreamVersion": 10,
|
||||
"readyToSync": true,
|
||||
"upstreamUsageKey": "lb:OpenedX:CSPROB3:problem:d40264d5-80c4-4be8-bfb6-086391de1cd3",
|
||||
"upstreamContextKey": "lib:OpenedX:CSPROB3",
|
||||
"downstreamUsageKey": "block-v1:OpenEdx+DemoX+CourseX+type@problem+block@problem6",
|
||||
"downstreamContextKey": "course-v1:OpenEdx+DemoX+CourseX",
|
||||
"versionSynced": 2,
|
||||
"versionDeclined": null,
|
||||
"created": "2025-02-08T14:07:05.588484Z",
|
||||
"updated": "2025-02-08T14:07:05.588484Z"
|
||||
},
|
||||
{
|
||||
"id": 884,
|
||||
"upstreamContextTitle": "CS problems 3",
|
||||
"upstreamVersion": 26,
|
||||
"readyToSync": true,
|
||||
"upstreamUsageKey": "lb:OpenedX:CSPROB3:html:ca4d2b1f-0b64-4a2d-88fa-592f7e398477",
|
||||
"upstreamContextKey": "lib:OpenedX:CSPROB3",
|
||||
"downstreamUsageKey": "block-v1:OpenEdx+DemoX+CourseX+type@html+block@257e68e3386d4a8f8739d45b67e76a9b",
|
||||
"downstreamContextKey": "course-v1:OpenEdx+DemoX+CourseX",
|
||||
"versionSynced": 16,
|
||||
"versionDeclined": null,
|
||||
"created": "2025-02-08T14:07:05.588484Z",
|
||||
"updated": "2025-02-08T14:07:05.588484Z"
|
||||
},
|
||||
{
|
||||
"id": 889,
|
||||
"upstreamContextTitle": "CS problems 3",
|
||||
"upstreamVersion": 10,
|
||||
"readyToSync": true,
|
||||
"upstreamUsageKey": "lb:OpenedX:CSPROB3:problem:d40264d5-80c4-4be8-bfb6-086391de1cd3",
|
||||
"upstreamContextKey": "lib:OpenedX:CSPROB3",
|
||||
"downstreamUsageKey": "block-v1:OpenEdx+DemoX+CourseX+type@problem+block@a4455860b03647219ff8b01cde49cf37",
|
||||
"downstreamContextKey": "course-v1:OpenEdx+DemoX+CourseX",
|
||||
"versionSynced": 2,
|
||||
"versionDeclined": null,
|
||||
"created": "2025-02-08T14:07:05.588484Z",
|
||||
"updated": "2025-02-08T14:07:05.588484Z"
|
||||
},
|
||||
{
|
||||
"id": 890,
|
||||
"upstreamContextTitle": "CS problems 3",
|
||||
"upstreamVersion": 10,
|
||||
"readyToSync": true,
|
||||
"upstreamUsageKey": "lb:OpenedX:CSPROB3:problem:d40264d5-80c4-4be8-bfb6-086391de1cd3",
|
||||
"upstreamContextKey": "lib:OpenedX:CSPROB3",
|
||||
"downstreamUsageKey": "block-v1:OpenEdx+DemoX+CourseX+type@problem+block@210e356cfa304b0aac591af53f6a6ae0",
|
||||
"downstreamContextKey": "course-v1:OpenEdx+DemoX+CourseX",
|
||||
"versionSynced": 2,
|
||||
"versionDeclined": null,
|
||||
"created": "2025-02-08T14:07:05.588484Z",
|
||||
"updated": "2025-02-08T14:07:05.588484Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -28,17 +28,27 @@ export async function mockGetEntityLinks(
|
||||
case mockGetEntityLinks.courseKeyLoading:
|
||||
return new Promise(() => {});
|
||||
case mockGetEntityLinks.courseKeyEmpty:
|
||||
return Promise.resolve([]);
|
||||
return Promise.resolve({
|
||||
next: null,
|
||||
previous: null,
|
||||
nextPageNum: null,
|
||||
previousPageNum: null,
|
||||
count: 0,
|
||||
numPages: 0,
|
||||
currentPage: 0,
|
||||
results: [],
|
||||
});
|
||||
default: {
|
||||
let { response } = mockGetEntityLinks;
|
||||
const { response } = mockGetEntityLinks;
|
||||
if (readyToSync !== undefined) {
|
||||
response = response.filter((o) => o.readyToSync === readyToSync);
|
||||
response.results = response.results.filter((o) => o.readyToSync === readyToSync);
|
||||
response.count = response.results.length;
|
||||
}
|
||||
return Promise.resolve(response);
|
||||
}
|
||||
}
|
||||
}
|
||||
mockGetEntityLinks.courseKey = mockLinksResult[0].downstreamContextKey;
|
||||
mockGetEntityLinks.courseKey = mockLinksResult.results[0].downstreamContextKey;
|
||||
mockGetEntityLinks.invalidCourseKey = 'course_key_error';
|
||||
mockGetEntityLinks.courseKeyLoading = 'courseKeyLoading';
|
||||
mockGetEntityLinks.courseKeyEmpty = 'courseKeyEmpty';
|
||||
@@ -75,7 +85,7 @@ export async function mockGetEntityLinksSummaryByDownstreamContext(
|
||||
return Promise.resolve(mockGetEntityLinksSummaryByDownstreamContext.response);
|
||||
}
|
||||
}
|
||||
mockGetEntityLinksSummaryByDownstreamContext.courseKey = mockLinksResult[0].downstreamContextKey;
|
||||
mockGetEntityLinksSummaryByDownstreamContext.courseKey = mockLinksResult.results[0].downstreamContextKey;
|
||||
mockGetEntityLinksSummaryByDownstreamContext.invalidCourseKey = 'course_key_error';
|
||||
mockGetEntityLinksSummaryByDownstreamContext.courseKeyLoading = 'courseKeySummaryLoading';
|
||||
mockGetEntityLinksSummaryByDownstreamContext.courseKeyEmpty = 'courseKeyEmpty';
|
||||
|
||||
@@ -38,13 +38,32 @@ export interface PublishableEntityLinkSummary {
|
||||
upstreamContextTitle: string;
|
||||
readyToSyncCount: number;
|
||||
totalCount: number;
|
||||
lastPublishedAt: string;
|
||||
}
|
||||
|
||||
export const getEntityLinks = async (
|
||||
downstreamContextKey?: string,
|
||||
readyToSync?: boolean,
|
||||
upstreamUsageKey?: string,
|
||||
pageParam?: number,
|
||||
pageSize?: number,
|
||||
): Promise<PaginatedData<PublishableEntityLink[]>> => {
|
||||
const { data } = await getAuthenticatedHttpClient()
|
||||
.get(getEntityLinksByDownstreamContextUrl(), {
|
||||
params: {
|
||||
course_id: downstreamContextKey,
|
||||
ready_to_sync: readyToSync,
|
||||
upstream_usage_key: upstreamUsageKey,
|
||||
page_size: pageSize,
|
||||
page: pageParam,
|
||||
},
|
||||
});
|
||||
return camelCaseObject(data);
|
||||
};
|
||||
|
||||
export const getUnpaginatedEntityLinks = async (
|
||||
downstreamContextKey?: string,
|
||||
readyToSync?: boolean,
|
||||
upstreamUsageKey?: string,
|
||||
): Promise<PublishableEntityLink[]> => {
|
||||
const { data } = await getAuthenticatedHttpClient()
|
||||
.get(getEntityLinksByDownstreamContextUrl(), {
|
||||
|
||||
@@ -4,7 +4,7 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
import { renderHook, waitFor } from '@testing-library/react';
|
||||
import { getEntityLinksByDownstreamContextUrl } from './api';
|
||||
import { useEntityLinks } from './apiHooks';
|
||||
import { useEntityLinks, useUnpaginatedEntityLinks } from './apiHooks';
|
||||
|
||||
let axiosMock: MockAdapter;
|
||||
|
||||
@@ -39,11 +39,26 @@ describe('course libraries api hooks', () => {
|
||||
axiosMock.reset();
|
||||
});
|
||||
|
||||
it('should return paginated links for course', async () => {
|
||||
const courseId = 'course-v1:some+key';
|
||||
const url = getEntityLinksByDownstreamContextUrl();
|
||||
const expectedResult = {
|
||||
next: null, results: [], previous: null, total: 0,
|
||||
};
|
||||
axiosMock.onGet(url).reply(200, expectedResult);
|
||||
const { result } = renderHook(() => useEntityLinks({ courseId }), { wrapper });
|
||||
await waitFor(() => {
|
||||
expect(result.current.isLoading).toBeFalsy();
|
||||
});
|
||||
expect(result.current.data?.pages).toEqual([expectedResult]);
|
||||
expect(axiosMock.history.get[0].url).toEqual(url);
|
||||
});
|
||||
|
||||
it('should return links for course', async () => {
|
||||
const courseId = 'course-v1:some+key';
|
||||
const url = getEntityLinksByDownstreamContextUrl();
|
||||
axiosMock.onGet(url).reply(200, []);
|
||||
const { result } = renderHook(() => useEntityLinks({ courseId }), { wrapper });
|
||||
const { result } = renderHook(() => useUnpaginatedEntityLinks({ courseId }), { wrapper });
|
||||
await waitFor(() => {
|
||||
expect(result.current.isLoading).toBeFalsy();
|
||||
});
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import {
|
||||
useInfiniteQuery,
|
||||
useQuery,
|
||||
} from '@tanstack/react-query';
|
||||
import { getEntityLinks, getEntityLinksSummaryByDownstreamContext } from './api';
|
||||
import { getEntityLinks, getEntityLinksSummaryByDownstreamContext, getUnpaginatedEntityLinks } from './api';
|
||||
|
||||
export const courseLibrariesQueryKeys = {
|
||||
all: ['courseLibraries'],
|
||||
@@ -28,10 +29,39 @@ export const courseLibrariesQueryKeys = {
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook to fetch list of publishable entity links by course key.
|
||||
* Hook to fetch publishable entity links by course key.
|
||||
* (That is, get a list of the library components used in the given course.)
|
||||
*/
|
||||
export const useEntityLinks = ({
|
||||
courseId, readyToSync, upstreamUsageKey, pageSize,
|
||||
}: {
|
||||
courseId?: string,
|
||||
readyToSync?: boolean,
|
||||
upstreamUsageKey?: string,
|
||||
pageSize?: number
|
||||
}) => (
|
||||
useInfiniteQuery({
|
||||
queryKey: courseLibrariesQueryKeys.courseReadyToSyncLibraries({
|
||||
courseId,
|
||||
readyToSync,
|
||||
upstreamUsageKey,
|
||||
}),
|
||||
queryFn: ({ pageParam }) => getEntityLinks(
|
||||
courseId,
|
||||
readyToSync,
|
||||
upstreamUsageKey,
|
||||
pageParam,
|
||||
pageSize,
|
||||
),
|
||||
getNextPageParam: (lastPage) => lastPage.nextPageNum,
|
||||
enabled: courseId !== undefined || upstreamUsageKey !== undefined || readyToSync !== undefined,
|
||||
})
|
||||
);
|
||||
|
||||
/**
|
||||
* Hook to fetch unpaginated list of publishable entity links by course key.
|
||||
*/
|
||||
export const useUnpaginatedEntityLinks = ({
|
||||
courseId, readyToSync, upstreamUsageKey,
|
||||
}: {
|
||||
courseId?: string,
|
||||
@@ -44,7 +74,7 @@ export const useEntityLinks = ({
|
||||
readyToSync,
|
||||
upstreamUsageKey,
|
||||
}),
|
||||
queryFn: () => getEntityLinks(
|
||||
queryFn: () => getUnpaginatedEntityLinks(
|
||||
courseId,
|
||||
readyToSync,
|
||||
upstreamUsageKey,
|
||||
|
||||
@@ -116,10 +116,10 @@ const messages = defineMessages({
|
||||
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.',
|
||||
olderVersionPreviewAlert: {
|
||||
id: 'course-authoring.course-libraries.reviw-tab.preview.old-version-alert',
|
||||
defaultMessage: 'The old version preview is the previous library version',
|
||||
description: 'Alert message stating that older version in preview is of library block',
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -103,7 +103,6 @@ const CourseOutline = ({ courseId }) => {
|
||||
handleNewSectionSubmit,
|
||||
handleNewSubsectionSubmit,
|
||||
handleNewUnitSubmit,
|
||||
handleAddUnitFromLibrary,
|
||||
getUnitUrl,
|
||||
handleVideoSharingOptionChange,
|
||||
handlePasteClipboardClick,
|
||||
@@ -375,7 +374,6 @@ const CourseOutline = ({ courseId }) => {
|
||||
section,
|
||||
section.childInfo.children,
|
||||
)}
|
||||
isSectionsExpanded={isSectionsExpanded}
|
||||
isSelfPaced={statusBarData.isSelfPaced}
|
||||
isCustomRelativeDatesActive={isCustomRelativeDatesActive}
|
||||
savingStatus={savingStatus}
|
||||
@@ -385,7 +383,6 @@ const CourseOutline = ({ courseId }) => {
|
||||
onDuplicateSubmit={handleDuplicateSubsectionSubmit}
|
||||
onOpenConfigureModal={openConfigureModal}
|
||||
onNewUnitSubmit={handleNewUnitSubmit}
|
||||
onAddUnitFromLibrary={handleAddUnitFromLibrary}
|
||||
onOrderChange={updateSubsectionOrderByIndex}
|
||||
onPasteClick={handlePasteClipboardClick}
|
||||
>
|
||||
@@ -456,11 +453,7 @@ const CourseOutline = ({ courseId }) => {
|
||||
</article>
|
||||
</Layout.Element>
|
||||
<Layout.Element>
|
||||
<CourseAuthoringOutlineSidebarSlot
|
||||
courseId={courseId}
|
||||
courseName={courseName}
|
||||
sections={sections}
|
||||
/>
|
||||
<CourseAuthoringOutlineSidebarSlot courseId={courseId} />
|
||||
</Layout.Element>
|
||||
</Layout>
|
||||
<EnableHighlightsModal
|
||||
|
||||
@@ -11,6 +11,7 @@ import { cloneDeep } from 'lodash';
|
||||
import { closestCorners } from '@dnd-kit/core';
|
||||
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import {
|
||||
getCourseBestPracticesApiUrl,
|
||||
getCourseLaunchApiUrl,
|
||||
@@ -59,14 +60,11 @@ import {
|
||||
moveSubsection,
|
||||
moveUnit,
|
||||
} 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();
|
||||
|
||||
@@ -96,24 +94,6 @@ 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', () => ({
|
||||
@@ -153,9 +133,7 @@ describe('<CourseOutline />', () => {
|
||||
pathname: mockPathname,
|
||||
});
|
||||
|
||||
store = initializeStore({
|
||||
studioHome: { studioHomeData: { librariesV2Enabled: true } },
|
||||
});
|
||||
store = initializeStore();
|
||||
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
|
||||
axiosMock
|
||||
.onGet(getCourseOutlineIndexApiUrl(courseId))
|
||||
@@ -174,10 +152,6 @@ describe('<CourseOutline />', () => {
|
||||
await executeThunk(fetchCourseOutlineIndexQuery(courseId), store.dispatch);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('render CourseOutline component correctly', async () => {
|
||||
const { getByText } = render(<RootWrapper />);
|
||||
|
||||
@@ -288,15 +262,13 @@ describe('<CourseOutline />', () => {
|
||||
});
|
||||
|
||||
it('check that new section list is saved when dragged', async () => {
|
||||
const { findAllByRole, findByTestId } = render(<RootWrapper />);
|
||||
const expandAllButton = await findByTestId('expand-collapse-all-button');
|
||||
fireEvent.click(expandAllButton);
|
||||
const [section] = store.getState().courseOutline.sectionsList;
|
||||
const { findAllByRole } = render(<RootWrapper />);
|
||||
const courseBlockId = courseOutlineIndexMock.courseStructure.id;
|
||||
const sectionsDraggers = await findAllByRole('button', { name: 'Drag to reorder' });
|
||||
const draggableButton = sectionsDraggers[1];
|
||||
const draggableButton = sectionsDraggers[6];
|
||||
|
||||
axiosMock
|
||||
.onPut(getCourseBlockApiUrl(section.id))
|
||||
.onPut(getCourseBlockApiUrl(courseBlockId))
|
||||
.reply(200, { dummy: 'value' });
|
||||
|
||||
const section1 = store.getState().courseOutline.sectionsList[0].id;
|
||||
@@ -315,15 +287,13 @@ describe('<CourseOutline />', () => {
|
||||
});
|
||||
|
||||
it('check section list is restored to original order when API call fails', async () => {
|
||||
const { findAllByRole, findByTestId } = render(<RootWrapper />);
|
||||
const expandAllButton = await findByTestId('expand-collapse-all-button');
|
||||
fireEvent.click(expandAllButton);
|
||||
const [section] = store.getState().courseOutline.sectionsList;
|
||||
const { findAllByRole } = render(<RootWrapper />);
|
||||
const courseBlockId = courseOutlineIndexMock.courseStructure.id;
|
||||
const sectionsDraggers = await findAllByRole('button', { name: 'Drag to reorder' });
|
||||
const draggableButton = sectionsDraggers[1];
|
||||
const draggableButton = sectionsDraggers[6];
|
||||
|
||||
axiosMock
|
||||
.onPut(getCourseBlockApiUrl(section.id))
|
||||
.onPut(getCourseBlockApiUrl(courseBlockId))
|
||||
.reply(500);
|
||||
|
||||
const section1 = store.getState().courseOutline.sectionsList[0].id;
|
||||
@@ -398,6 +368,8 @@ 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);
|
||||
|
||||
@@ -418,40 +390,6 @@ describe('<CourseOutline />', () => {
|
||||
}));
|
||||
});
|
||||
|
||||
it('adds a unit from library correctly', async () => {
|
||||
render(<RootWrapper />);
|
||||
const [sectionElement] = await screen.findAllByTestId('section-card');
|
||||
const [subsectionElement] = await within(sectionElement).findAllByTestId('subsection-card');
|
||||
const units = await within(subsectionElement).findAllByTestId('unit-card');
|
||||
expect(units.length).toBe(1);
|
||||
|
||||
axiosMock
|
||||
.onPost(postXBlockBaseApiUrl())
|
||||
.reply(200, {
|
||||
locator: 'some',
|
||||
});
|
||||
|
||||
const addUnitFromLibraryButton = within(subsectionElement).getByRole('button', {
|
||||
name: /use unit from library/i,
|
||||
});
|
||||
fireEvent.click(addUnitFromLibraryButton);
|
||||
|
||||
// click dummy button to execute onComponentSelected prop.
|
||||
const dummyBtn = await screen.findByRole('button', { name: 'Dummy button' });
|
||||
fireEvent.click(dummyBtn);
|
||||
|
||||
waitFor(() => expect(axiosMock.history.post.length).toBe(1));
|
||||
|
||||
const [section] = courseOutlineIndexMock.courseStructure.childInfo.children;
|
||||
const [subsection] = section.childInfo.children;
|
||||
expect(axiosMock.history.post[0].data).toBe(JSON.stringify({
|
||||
type: COMPONENT_TYPES.libraryV2,
|
||||
category: 'vertical',
|
||||
parent_locator: subsection.id,
|
||||
library_content_key: containerKey,
|
||||
}));
|
||||
});
|
||||
|
||||
it('render checklist value correctly', async () => {
|
||||
const { getByText } = render(<RootWrapper />);
|
||||
|
||||
@@ -645,6 +583,8 @@ 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');
|
||||
@@ -657,6 +597,8 @@ 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');
|
||||
|
||||
@@ -695,6 +637,8 @@ 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');
|
||||
|
||||
@@ -764,6 +708,8 @@ 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');
|
||||
|
||||
@@ -1472,6 +1418,8 @@ 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');
|
||||
|
||||
@@ -1831,6 +1779,8 @@ 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');
|
||||
|
||||
@@ -1870,6 +1820,8 @@ 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');
|
||||
|
||||
@@ -1905,6 +1857,8 @@ 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');
|
||||
|
||||
@@ -1949,6 +1903,8 @@ 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];
|
||||
@@ -1986,6 +1942,8 @@ 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];
|
||||
@@ -2030,6 +1988,8 @@ 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');
|
||||
|
||||
@@ -2049,6 +2009,8 @@ 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');
|
||||
|
||||
@@ -2069,9 +2031,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 [section] = store.getState().courseOutline.sectionsList;
|
||||
const subsectionsDraggers = within(sectionElement).getAllByRole('button', { name: 'Drag to reorder' });
|
||||
const draggableButton = subsectionsDraggers[1];
|
||||
@@ -2103,9 +2062,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 [section] = store.getState().courseOutline.sectionsList;
|
||||
const subsectionsDraggers = within(sectionElement).getAllByRole('button', { name: 'Drag to reorder' });
|
||||
const draggableButton = subsectionsDraggers[1];
|
||||
@@ -2135,6 +2091,8 @@ 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;
|
||||
@@ -2169,6 +2127,8 @@ 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;
|
||||
@@ -2206,6 +2166,8 @@ 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 userEvent.click(expandBtn);
|
||||
const [unit] = subsection.childInfo.children;
|
||||
const [unitElement] = await within(subsectionElement).findAllByTestId('unit-card');
|
||||
|
||||
|
||||
@@ -292,11 +292,6 @@ module.exports = {
|
||||
selectedPartitionIndex: -1,
|
||||
selectedGroupsLabel: '',
|
||||
},
|
||||
upstreamInfo: {
|
||||
readyToSync: false,
|
||||
upstreamRef: undefined,
|
||||
versionSynced: undefined,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -680,11 +675,6 @@ module.exports = {
|
||||
selectedPartitionIndex: -1,
|
||||
selectedGroupsLabel: '',
|
||||
},
|
||||
upstreamInfo: {
|
||||
readyToSync: false,
|
||||
upstreamRef: undefined,
|
||||
versionSynced: undefined,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@vertical_2dbb0072785e',
|
||||
@@ -769,11 +759,6 @@ module.exports = {
|
||||
selectedPartitionIndex: -1,
|
||||
selectedGroupsLabel: '',
|
||||
},
|
||||
upstreamInfo: {
|
||||
readyToSync: false,
|
||||
upstreamRef: undefined,
|
||||
versionSynced: undefined,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@vertical_98cf62510471',
|
||||
@@ -858,11 +843,6 @@ module.exports = {
|
||||
selectedPartitionIndex: -1,
|
||||
selectedGroupsLabel: '',
|
||||
},
|
||||
upstreamInfo: {
|
||||
readyToSync: false,
|
||||
upstreamRef: undefined,
|
||||
versionSynced: undefined,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@vertical_d32bf9b2242c',
|
||||
@@ -947,11 +927,6 @@ module.exports = {
|
||||
selectedPartitionIndex: -1,
|
||||
selectedGroupsLabel: '',
|
||||
},
|
||||
upstreamInfo: {
|
||||
readyToSync: false,
|
||||
upstreamRef: undefined,
|
||||
versionSynced: undefined,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@4e592689563243c484af947465eaef0d',
|
||||
@@ -1036,11 +1011,6 @@ module.exports = {
|
||||
selectedPartitionIndex: -1,
|
||||
selectedGroupsLabel: '',
|
||||
},
|
||||
upstreamInfo: {
|
||||
readyToSync: false,
|
||||
upstreamRef: undefined,
|
||||
versionSynced: undefined,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -1226,11 +1196,6 @@ module.exports = {
|
||||
selectedPartitionIndex: -1,
|
||||
selectedGroupsLabel: '',
|
||||
},
|
||||
upstreamInfo: {
|
||||
readyToSync: false,
|
||||
upstreamRef: undefined,
|
||||
versionSynced: undefined,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@vertical_aae927868e55',
|
||||
@@ -1315,11 +1280,6 @@ module.exports = {
|
||||
selectedPartitionIndex: -1,
|
||||
selectedGroupsLabel: '',
|
||||
},
|
||||
upstreamInfo: {
|
||||
readyToSync: false,
|
||||
upstreamRef: undefined,
|
||||
versionSynced: undefined,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@vertical_c037f3757df1',
|
||||
@@ -1404,11 +1364,6 @@ module.exports = {
|
||||
selectedPartitionIndex: -1,
|
||||
selectedGroupsLabel: '',
|
||||
},
|
||||
upstreamInfo: {
|
||||
readyToSync: false,
|
||||
upstreamRef: undefined,
|
||||
versionSynced: undefined,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@vertical_bc69a47c6fae',
|
||||
@@ -1493,11 +1448,6 @@ module.exports = {
|
||||
selectedPartitionIndex: -1,
|
||||
selectedGroupsLabel: '',
|
||||
},
|
||||
upstreamInfo: {
|
||||
readyToSync: false,
|
||||
upstreamRef: undefined,
|
||||
versionSynced: undefined,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@8f89194410954e768bde1764985454a7',
|
||||
@@ -1582,11 +1532,6 @@ module.exports = {
|
||||
selectedPartitionIndex: -1,
|
||||
selectedGroupsLabel: '',
|
||||
},
|
||||
upstreamInfo: {
|
||||
readyToSync: false,
|
||||
upstreamRef: undefined,
|
||||
versionSynced: undefined,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -1772,11 +1717,6 @@ module.exports = {
|
||||
selectedPartitionIndex: -1,
|
||||
selectedGroupsLabel: '',
|
||||
},
|
||||
upstreamInfo: {
|
||||
readyToSync: false,
|
||||
upstreamRef: undefined,
|
||||
versionSynced: undefined,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -2055,11 +1995,6 @@ module.exports = {
|
||||
selectedPartitionIndex: -1,
|
||||
selectedGroupsLabel: '',
|
||||
},
|
||||
upstreamInfo: {
|
||||
readyToSync: false,
|
||||
upstreamRef: undefined,
|
||||
versionSynced: undefined,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@vertical_f04afeac0131',
|
||||
@@ -2144,11 +2079,6 @@ module.exports = {
|
||||
selectedPartitionIndex: -1,
|
||||
selectedGroupsLabel: '',
|
||||
},
|
||||
upstreamInfo: {
|
||||
readyToSync: false,
|
||||
upstreamRef: undefined,
|
||||
versionSynced: undefined,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@b6662b497c094bcc9b870d8270c90c93',
|
||||
@@ -2233,11 +2163,6 @@ module.exports = {
|
||||
selectedPartitionIndex: -1,
|
||||
selectedGroupsLabel: '',
|
||||
},
|
||||
upstreamInfo: {
|
||||
readyToSync: false,
|
||||
upstreamRef: undefined,
|
||||
versionSynced: undefined,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@f91d8d31f7cf48ce990f8d8745ae4cfa',
|
||||
@@ -2322,11 +2247,6 @@ module.exports = {
|
||||
selectedPartitionIndex: -1,
|
||||
selectedGroupsLabel: '',
|
||||
},
|
||||
upstreamInfo: {
|
||||
readyToSync: false,
|
||||
upstreamRef: undefined,
|
||||
versionSynced: undefined,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@vertical_ac391cde8a91',
|
||||
@@ -2411,11 +2331,6 @@ module.exports = {
|
||||
selectedPartitionIndex: -1,
|
||||
selectedGroupsLabel: '',
|
||||
},
|
||||
upstreamInfo: {
|
||||
readyToSync: false,
|
||||
upstreamRef: undefined,
|
||||
versionSynced: undefined,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@vertical_36e0beb03f0a',
|
||||
@@ -2500,11 +2415,6 @@ module.exports = {
|
||||
selectedPartitionIndex: -1,
|
||||
selectedGroupsLabel: '',
|
||||
},
|
||||
upstreamInfo: {
|
||||
readyToSync: false,
|
||||
upstreamRef: undefined,
|
||||
versionSynced: undefined,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@1b0e2c2c84884b95b1c99fb678cc964c',
|
||||
@@ -2589,11 +2499,6 @@ module.exports = {
|
||||
selectedPartitionIndex: -1,
|
||||
selectedGroupsLabel: '',
|
||||
},
|
||||
upstreamInfo: {
|
||||
readyToSync: false,
|
||||
upstreamRef: undefined,
|
||||
versionSynced: undefined,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@c7e98fd39a6944edb6b286c32e1150ff',
|
||||
@@ -2678,11 +2583,6 @@ module.exports = {
|
||||
selectedPartitionIndex: -1,
|
||||
selectedGroupsLabel: '',
|
||||
},
|
||||
upstreamInfo: {
|
||||
readyToSync: false,
|
||||
upstreamRef: undefined,
|
||||
versionSynced: undefined,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@d6eaa391d2be41dea20b8b1bfbcb1c45',
|
||||
@@ -2767,11 +2667,6 @@ module.exports = {
|
||||
selectedPartitionIndex: -1,
|
||||
selectedGroupsLabel: '',
|
||||
},
|
||||
upstreamInfo: {
|
||||
readyToSync: false,
|
||||
upstreamRef: undefined,
|
||||
versionSynced: undefined,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -3050,11 +2945,6 @@ module.exports = {
|
||||
selectedPartitionIndex: -1,
|
||||
selectedGroupsLabel: '',
|
||||
},
|
||||
upstreamInfo: {
|
||||
readyToSync: false,
|
||||
upstreamRef: undefined,
|
||||
versionSynced: undefined,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
// @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';
|
||||
@@ -11,13 +10,11 @@ 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';
|
||||
@@ -58,8 +55,6 @@ const CardHeader = ({
|
||||
discussionsSettings,
|
||||
parentInfo,
|
||||
extraActionsComponent,
|
||||
onClickSync,
|
||||
readyToSync,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const [searchParams] = useSearchParams();
|
||||
@@ -135,28 +130,12 @@ const CardHeader = ({
|
||||
) : (
|
||||
<>
|
||||
{titleComponent}
|
||||
<IconButtonWithTooltip
|
||||
className={classNames(
|
||||
'item-card-button-icon',
|
||||
{
|
||||
'item-card-button-icon-disabled': isDisabledEditField,
|
||||
},
|
||||
)}
|
||||
<IconButton
|
||||
className="item-card-edit-icon"
|
||||
data-testid={`${namePrefix}-edit-button`}
|
||||
alt={intl.formatMessage(
|
||||
isDisabledEditField ? messages.cannotEditTooltip : messages.altButtonRename,
|
||||
)}
|
||||
tooltipContent={(
|
||||
<div>
|
||||
{intl.formatMessage(
|
||||
isDisabledEditField ? messages.cannotEditTooltip : messages.altButtonRename,
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
alt={intl.formatMessage(messages.altButtonEdit)}
|
||||
iconAs={EditIcon}
|
||||
onClick={onClickEdit}
|
||||
// @ts-ignore
|
||||
disabled={isDisabledEditField}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
@@ -168,15 +147,6 @@ const CardHeader = ({
|
||||
<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"
|
||||
@@ -208,7 +178,6 @@ const CardHeader = ({
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item
|
||||
data-testid={`${namePrefix}-card-header__menu-configure-button`}
|
||||
disabled={isDisabledEditField}
|
||||
onClick={onClickConfigure}
|
||||
>
|
||||
{intl.formatMessage(messages.menuConfigure)}
|
||||
@@ -216,7 +185,6 @@ 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)}
|
||||
@@ -287,8 +255,6 @@ CardHeader.defaultProps = {
|
||||
parentInfo: {},
|
||||
cardId: '',
|
||||
extraActionsComponent: null,
|
||||
readyToSync: false,
|
||||
onClickSync: null,
|
||||
};
|
||||
|
||||
CardHeader.propTypes = {
|
||||
@@ -335,8 +301,6 @@ CardHeader.propTypes = {
|
||||
// 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-button-icon {
|
||||
.item-card-edit-icon {
|
||||
opacity: 0;
|
||||
transition: opacity .3s linear;
|
||||
margin-right: .5rem;
|
||||
@@ -23,14 +23,8 @@
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.item-card-button-icon {
|
||||
.item-card-edit-icon {
|
||||
opacity: 1;
|
||||
|
||||
&.item-card-button-icon-disabled {
|
||||
pointer-events: all;
|
||||
opacity: .5;
|
||||
cursor: default;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -240,35 +240,6 @@ 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();
|
||||
|
||||
@@ -368,19 +339,4 @@ 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)',
|
||||
},
|
||||
altButtonRename: {
|
||||
altButtonEdit: {
|
||||
id: 'course-authoring.course-outline.card.button.edit.alt',
|
||||
defaultMessage: 'Rename',
|
||||
defaultMessage: 'Edit',
|
||||
},
|
||||
menuPublish: {
|
||||
id: 'course-authoring.course-outline.card.menu.publish',
|
||||
@@ -77,16 +77,6 @@ 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;
|
||||
|
||||
@@ -53,7 +53,6 @@ import {
|
||||
setPasteFileNotices,
|
||||
updateCourseLaunchQueryStatus,
|
||||
} from './slice';
|
||||
import { createCourseXblock } from '../../course-unit/data/api';
|
||||
|
||||
export function fetchCourseOutlineIndexQuery(courseId) {
|
||||
return async (dispatch) => {
|
||||
@@ -541,26 +540,6 @@ export function addNewUnitQuery(parentLocator, callback) {
|
||||
};
|
||||
}
|
||||
|
||||
export function addUnitFromLibrary(body, callback) {
|
||||
return async (dispatch) => {
|
||||
dispatch(updateSavingStatus({ status: RequestStatus.PENDING }));
|
||||
dispatch(showProcessingNotification(NOTIFICATION_MESSAGES.saving));
|
||||
|
||||
try {
|
||||
await createCourseXblock(body).then(async (result) => {
|
||||
if (result) {
|
||||
dispatch(updateSavingStatus({ status: RequestStatus.SUCCESSFUL }));
|
||||
dispatch(hideProcessingNotification());
|
||||
callback(result.locator);
|
||||
}
|
||||
});
|
||||
} catch (error) /* istanbul ignore next */ {
|
||||
dispatch(hideProcessingNotification());
|
||||
dispatch(updateSavingStatus({ status: RequestStatus.FAILED }));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function setBlockOrderListQuery(
|
||||
parentId,
|
||||
blockIds,
|
||||
|
||||
@@ -66,8 +66,6 @@ const HeaderNavigations = ({
|
||||
{hasSections && (
|
||||
<Button
|
||||
variant="outline-primary"
|
||||
id="expand-collapse-all-button"
|
||||
data-testid="expand-collapse-all-button"
|
||||
iconBefore={isSectionsExpanded ? ArrowUpIcon : ArrowDownIcon}
|
||||
onClick={handleExpandAll}
|
||||
>
|
||||
|
||||
@@ -53,7 +53,6 @@ import {
|
||||
setUnitOrderListQuery,
|
||||
pasteClipboardContent,
|
||||
dismissNotificationQuery,
|
||||
addUnitFromLibrary,
|
||||
} from './data/thunk';
|
||||
|
||||
const useCourseOutline = ({ courseId }) => {
|
||||
@@ -129,10 +128,6 @@ const useCourseOutline = ({ courseId }) => {
|
||||
dispatch(addNewUnitQuery(subsectionId, openUnitPage));
|
||||
};
|
||||
|
||||
const handleAddUnitFromLibrary = (body) => {
|
||||
dispatch(addUnitFromLibrary(body, openUnitPage));
|
||||
};
|
||||
|
||||
const headerNavigationsActions = {
|
||||
handleNewSection: handleNewSectionSubmit,
|
||||
handleReIndex: () => {
|
||||
@@ -341,7 +336,6 @@ const useCourseOutline = ({ courseId }) => {
|
||||
getUnitUrl,
|
||||
openUnitPage,
|
||||
handleNewUnitSubmit,
|
||||
handleAddUnitFromLibrary,
|
||||
handleVideoSharingOptionChange,
|
||||
handlePasteClipboardClick,
|
||||
notificationDismissUrl,
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
// @ts-check
|
||||
import React, {
|
||||
useContext, useEffect, useState, useRef, useCallback,
|
||||
useContext, useEffect, useState, useRef,
|
||||
} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Button, StandardModal, useToggle } from '@openedx/paragon';
|
||||
import { Button, useToggle } from '@openedx/paragon';
|
||||
import { Add as IconAdd } from '@openedx/paragon/icons';
|
||||
import classNames from 'classnames';
|
||||
import { isEmpty } from 'lodash';
|
||||
@@ -22,16 +22,10 @@ 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,
|
||||
@@ -43,7 +37,6 @@ const SubsectionCard = ({
|
||||
onOpenDeleteModal,
|
||||
onDuplicateSubmit,
|
||||
onNewUnitSubmit,
|
||||
onAddUnitFromLibrary,
|
||||
onOrderChange,
|
||||
onOpenConfigureModal,
|
||||
onPasteClick,
|
||||
@@ -58,17 +51,6 @@ const SubsectionCard = ({
|
||||
const [isFormOpen, openForm, closeForm] = useToggle(false);
|
||||
const namePrefix = 'subsection';
|
||||
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,
|
||||
@@ -99,7 +81,7 @@ const SubsectionCard = ({
|
||||
|
||||
return false;
|
||||
};
|
||||
const [isExpanded, setIsExpanded] = useState(containsSearchResult() || !isHeaderVisible || isSectionsExpanded);
|
||||
const [isExpanded, setIsExpanded] = useState(containsSearchResult() || !isHeaderVisible);
|
||||
const subsectionStatus = getItemStatus({
|
||||
published,
|
||||
visibilityState,
|
||||
@@ -107,10 +89,6 @@ const SubsectionCard = ({
|
||||
});
|
||||
const borderStyle = getItemStatusBorder(subsectionStatus);
|
||||
|
||||
useEffect(() => {
|
||||
setIsExpanded(isSectionsExpanded);
|
||||
}, [isSectionsExpanded]);
|
||||
|
||||
const handleExpandContent = () => {
|
||||
setIsExpanded((prevState) => !prevState);
|
||||
};
|
||||
@@ -194,129 +172,90 @@ 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,
|
||||
}}
|
||||
<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}
|
||||
>
|
||||
<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}
|
||||
{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 className="subsection-card__content item-children" data-testid="subsection-card__content">
|
||||
<XBlockStatus
|
||||
isSelfPaced={isSelfPaced}
|
||||
isCustomRelativeDatesActive={isCustomRelativeDatesActive}
|
||||
blockData={subsection}
|
||||
/>
|
||||
<div className="subsection-card__content item-children" data-testid="subsection-card__content">
|
||||
<XBlockStatus
|
||||
isSelfPaced={isSelfPaced}
|
||||
isCustomRelativeDatesActive={isCustomRelativeDatesActive}
|
||||
blockData={subsection}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{(isExpanded) && (
|
||||
<div
|
||||
data-testid="subsection-card__units"
|
||||
className={classNames('subsection-card__units', { 'item-children': isDraggable })}
|
||||
>
|
||||
{children}
|
||||
{actions.childAddable && (
|
||||
<>
|
||||
<Button
|
||||
data-testid="new-unit-button"
|
||||
className="mt-4"
|
||||
variant="outline-primary"
|
||||
iconBefore={IconAdd}
|
||||
block
|
||||
onClick={handleNewButtonClick}
|
||||
>
|
||||
{intl.formatMessage(messages.newUnitButton)}
|
||||
</Button>
|
||||
{enableCopyPasteUnits && showPasteUnit && sharedClipboardData && (
|
||||
<PasteComponent
|
||||
className="mt-4"
|
||||
text={intl.formatMessage(messages.pasteButton)}
|
||||
clipboardData={sharedClipboardData}
|
||||
onClick={handlePasteButtonClick}
|
||||
/>
|
||||
)}
|
||||
{librariesV2Enabled && (
|
||||
<Button
|
||||
data-testid="use-unit-from-library"
|
||||
className="mt-4"
|
||||
variant="outline-primary"
|
||||
iconBefore={IconAdd}
|
||||
block
|
||||
onClick={openAddLibraryUnitModal}
|
||||
>
|
||||
{intl.formatMessage(messages.useUnitFromLibraryButton)}
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</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>
|
||||
</>
|
||||
</>
|
||||
)}
|
||||
{isExpanded && (
|
||||
<div
|
||||
data-testid="subsection-card__units"
|
||||
className={classNames('subsection-card__units', { 'item-children': isDraggable })}
|
||||
>
|
||||
{children}
|
||||
{actions.childAddable && (
|
||||
<>
|
||||
<Button
|
||||
data-testid="new-unit-button"
|
||||
className="mt-4"
|
||||
variant="outline-primary"
|
||||
iconBefore={IconAdd}
|
||||
block
|
||||
onClick={handleNewButtonClick}
|
||||
>
|
||||
{intl.formatMessage(messages.newUnitButton)}
|
||||
</Button>
|
||||
{enableCopyPasteUnits && showPasteUnit && sharedClipboardData && (
|
||||
<PasteComponent
|
||||
className="mt-4"
|
||||
text={intl.formatMessage(messages.pasteButton)}
|
||||
clipboardData={sharedClipboardData}
|
||||
onClick={handlePasteButtonClick}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</SortableItem>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -359,7 +298,6 @@ SubsectionCard.propTypes = {
|
||||
}).isRequired,
|
||||
}).isRequired,
|
||||
children: PropTypes.node,
|
||||
isSectionsExpanded: PropTypes.bool.isRequired,
|
||||
isSelfPaced: PropTypes.bool.isRequired,
|
||||
isCustomRelativeDatesActive: PropTypes.bool.isRequired,
|
||||
onOpenPublishModal: PropTypes.func.isRequired,
|
||||
@@ -368,7 +306,6 @@ 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, screen,
|
||||
act, render, fireEvent, within,
|
||||
} from '@testing-library/react';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { AppProvider } from '@edx/frontend-platform/react';
|
||||
@@ -10,12 +10,9 @@ 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'),
|
||||
@@ -24,31 +21,6 @@ jest.mock('react-router-dom', () => ({
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('react-redux', () => ({
|
||||
...jest.requireActual('react-redux'),
|
||||
useSelector: () => ({
|
||||
librariesV2Enabled: true,
|
||||
}),
|
||||
}));
|
||||
|
||||
// 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',
|
||||
};
|
||||
@@ -108,7 +80,6 @@ const renderComponent = (props, entry = '/') => render(
|
||||
onOpenHighlightsModal={jest.fn()}
|
||||
onOpenDeleteModal={jest.fn()}
|
||||
onNewUnitSubmit={jest.fn()}
|
||||
onAddUnitFromLibrary={handleOnAddUnitFromLibrary}
|
||||
isCustomRelativeDatesActive={false}
|
||||
onEditClick={jest.fn()}
|
||||
savingStatus=""
|
||||
@@ -276,31 +247,4 @@ 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,22 +4,10 @@ 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,10 +1,5 @@
|
||||
// @ts-check
|
||||
import React, {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
} from 'react';
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useToggle } from '@openedx/paragon';
|
||||
@@ -13,16 +8,13 @@ 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 '../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,
|
||||
@@ -48,7 +40,6 @@ 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();
|
||||
@@ -64,24 +55,8 @@ 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.
|
||||
@@ -129,10 +104,6 @@ const UnitCard = ({
|
||||
copyToClipboard(id);
|
||||
};
|
||||
|
||||
const handleOnPostChangeSync = useCallback(async () => {
|
||||
await dispatch(fetchCourseSectionQuery([section.id]));
|
||||
}, [dispatch, section]);
|
||||
|
||||
const titleComponent = (
|
||||
<TitleLink
|
||||
title={displayName}
|
||||
@@ -173,71 +144,59 @@ 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,
|
||||
}}
|
||||
<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}
|
||||
>
|
||||
<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>
|
||||
</SortableItem>
|
||||
{blockSyncData && (
|
||||
<PreviewLibraryXBlockChanges
|
||||
blockData={blockSyncData}
|
||||
isModalOpen={isSyncModalOpen}
|
||||
closeModal={closeSyncModal}
|
||||
postChange={handleOnPostChangeSync}
|
||||
<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}
|
||||
extraActionsComponent={extraActionsComponent}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
<div className="unit-card__content item-children" data-testid="unit-card__content">
|
||||
<XBlockStatus
|
||||
isSelfPaced={isSelfPaced}
|
||||
isCustomRelativeDatesActive={isCustomRelativeDatesActive}
|
||||
blockData={unit}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</SortableItem>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -263,11 +222,6 @@ 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,
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import {
|
||||
act, render, fireEvent, within, screen,
|
||||
waitFor,
|
||||
act, render, fireEvent, within,
|
||||
} from '@testing-library/react';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { AppProvider } from '@edx/frontend-platform/react';
|
||||
@@ -12,17 +11,6 @@ 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',
|
||||
@@ -55,11 +43,6 @@ const unit = {
|
||||
duplicable: true,
|
||||
},
|
||||
isHeaderVisible: true,
|
||||
upstreamInfo: {
|
||||
readyToSync: true,
|
||||
upstreamRef: 'lct:org1:lib1:unit:1',
|
||||
versionSynced: 1,
|
||||
},
|
||||
};
|
||||
|
||||
const queryClient = new QueryClient();
|
||||
@@ -164,51 +147,4 @@ 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());
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
ActionRow,
|
||||
Button,
|
||||
} from '@openedx/paragon';
|
||||
import { StudioFooterSlot } from '@edx/frontend-component-footer';
|
||||
import { StudioFooterSlot } from '@openedx/frontend-slot-footer';
|
||||
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import {
|
||||
Alert, Container, Layout, Button, TransitionReplace,
|
||||
Container, Layout, Stack, Button, TransitionReplace,
|
||||
} from '@openedx/paragon';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
@@ -26,6 +26,8 @@ import AddComponent from './add-component/AddComponent';
|
||||
import HeaderTitle from './header-title/HeaderTitle';
|
||||
import Breadcrumbs from './breadcrumbs/Breadcrumbs';
|
||||
import Sequence from './course-sequence';
|
||||
import Sidebar from './sidebar';
|
||||
import SplitTestSidebarInfo from './sidebar/SplitTestSidebarInfo';
|
||||
import { useCourseUnit, useLayoutGrid, useScrollToLastPosition } from './hooks';
|
||||
import messages from './messages';
|
||||
import { PasteNotificationAlert } from './clipboard';
|
||||
@@ -38,10 +40,8 @@ const CourseUnit = ({ courseId }) => {
|
||||
const { blockId } = useParams();
|
||||
const intl = useIntl();
|
||||
const {
|
||||
courseUnit,
|
||||
isLoading,
|
||||
sequenceId,
|
||||
courseUnitLoadingStatus,
|
||||
unitTitle,
|
||||
unitCategory,
|
||||
errorMessage,
|
||||
@@ -75,8 +75,6 @@ const CourseUnit = ({ courseId }) => {
|
||||
} = useCourseUnit({ courseId, blockId });
|
||||
const layoutGrid = useLayoutGrid(unitCategory, isUnitLibraryType);
|
||||
|
||||
const readOnly = !!courseUnit.readOnly;
|
||||
|
||||
useEffect(() => {
|
||||
document.title = getPageHeadTitle('', unitTitle);
|
||||
}, [unitTitle]);
|
||||
@@ -138,24 +136,6 @@ 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={(
|
||||
@@ -211,21 +191,18 @@ const CourseUnit = ({ courseId }) => {
|
||||
courseId={courseId}
|
||||
blockId={blockId}
|
||||
isUnitVerticalType={isUnitVerticalType}
|
||||
courseUnitLoadingStatus={courseUnitLoadingStatus}
|
||||
unitXBlockActions={unitXBlockActions}
|
||||
courseVerticalChildren={courseVerticalChildren.children}
|
||||
handleConfigureSubmit={handleConfigureSubmit}
|
||||
/>
|
||||
{!readOnly && (
|
||||
<AddComponent
|
||||
parentLocator={blockId}
|
||||
isSplitTestType={isSplitTestType}
|
||||
isUnitVerticalType={isUnitVerticalType}
|
||||
handleCreateNewCourseXBlock={handleCreateNewCourseXBlock}
|
||||
addComponentTemplateData={addComponentTemplateData}
|
||||
/>
|
||||
)}
|
||||
{!readOnly && showPasteXBlock && canPasteComponent && isUnitVerticalType && (
|
||||
<AddComponent
|
||||
parentLocator={blockId}
|
||||
isSplitTestType={isSplitTestType}
|
||||
isUnitVerticalType={isUnitVerticalType}
|
||||
handleCreateNewCourseXBlock={handleCreateNewCourseXBlock}
|
||||
addComponentTemplateData={addComponentTemplateData}
|
||||
/>
|
||||
{showPasteXBlock && canPasteComponent && isUnitVerticalType && (
|
||||
<PasteComponent
|
||||
clipboardData={sharedClipboardData}
|
||||
onClick={
|
||||
@@ -243,15 +220,20 @@ const CourseUnit = ({ courseId }) => {
|
||||
<IframePreviewLibraryXBlockChanges />
|
||||
</Layout.Element>
|
||||
<Layout.Element>
|
||||
<CourseAuthoringUnitSidebarSlot
|
||||
courseId={courseId}
|
||||
blockId={blockId}
|
||||
unitTitle={unitTitle}
|
||||
xBlocks={courseVerticalChildren.children}
|
||||
readOnly={readOnly}
|
||||
isUnitVerticalType={isUnitVerticalType}
|
||||
isSplitTestType={isSplitTestType}
|
||||
/>
|
||||
<Stack gap={3}>
|
||||
{isUnitVerticalType && (
|
||||
<CourseAuthoringUnitSidebarSlot
|
||||
courseId={courseId}
|
||||
blockId={blockId}
|
||||
unitTitle={unitTitle}
|
||||
/>
|
||||
)}
|
||||
{isSplitTestType && (
|
||||
<Sidebar data-testid="course-split-test-sidebar">
|
||||
<SplitTestSidebarInfo />
|
||||
</Sidebar>
|
||||
)}
|
||||
</Stack>
|
||||
</Layout.Element>
|
||||
</Layout>
|
||||
</section>
|
||||
|
||||
@@ -17,6 +17,7 @@ import { cloneDeep, set } from 'lodash';
|
||||
|
||||
import {
|
||||
getCourseSectionVerticalApiUrl,
|
||||
getCourseUnitApiUrl,
|
||||
getCourseVerticalChildrenApiUrl,
|
||||
getCourseOutlineInfoUrl,
|
||||
getXBlockBaseApiUrl,
|
||||
@@ -27,6 +28,7 @@ import {
|
||||
deleteUnitItemQuery,
|
||||
editCourseUnitVisibilityAndData,
|
||||
fetchCourseSectionVerticalData,
|
||||
fetchCourseUnitQuery,
|
||||
fetchCourseVerticalChildrenData,
|
||||
getCourseOutlineInfoQuery,
|
||||
patchUnitItemQuery,
|
||||
@@ -35,12 +37,13 @@ import initializeStore from '../store';
|
||||
import {
|
||||
courseCreateXblockMock,
|
||||
courseSectionVerticalMock,
|
||||
courseUnitIndexMock,
|
||||
courseUnitMock,
|
||||
courseVerticalChildrenMock,
|
||||
clipboardMockResponse,
|
||||
courseOutlineInfoMock,
|
||||
} from './__mocks__';
|
||||
import { clipboardUnit } from '../__mocks__';
|
||||
import { clipboardUnit, clipboardXBlock } from '../__mocks__';
|
||||
import { executeThunk } from '../utils';
|
||||
import { IFRAME_FEATURE_POLICY } from '../constants';
|
||||
import pasteComponentMessages from '../generic/clipboard/paste-component/messages';
|
||||
@@ -62,15 +65,13 @@ 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 sequenceId = 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@19a30717eff543078a5d94ae9d6c18a5';
|
||||
const unitDisplayName = courseSectionVerticalMock.xblock_info.display_name;
|
||||
const unitDisplayName = courseUnitIndexMock.metadata.display_name;
|
||||
const mockedUsedNavigate = jest.fn();
|
||||
const userName = 'openedx';
|
||||
const handleConfigureSubmitMock = jest.fn();
|
||||
@@ -88,7 +89,7 @@ const postXBlockBody = {
|
||||
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
useParams: () => ({ blockId, sequenceId }),
|
||||
useParams: () => ({ blockId }),
|
||||
useNavigate: () => mockedUsedNavigate,
|
||||
}));
|
||||
|
||||
@@ -144,10 +145,14 @@ describe('<CourseUnit />', () => {
|
||||
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, courseId), store.dispatch);
|
||||
await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch);
|
||||
axiosMock
|
||||
.onGet(getCourseVerticalChildrenApiUrl(blockId))
|
||||
.reply(200, courseVerticalChildrenMock);
|
||||
@@ -161,27 +166,27 @@ describe('<CourseUnit />', () => {
|
||||
});
|
||||
|
||||
it('render CourseUnit component 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 { getByText, getByRole, getByTestId } = render(<RootWrapper />);
|
||||
const currentSectionName = courseUnitIndexMock.ancestor_info.ancestors[1].display_name;
|
||||
const currentSubSectionName = courseUnitIndexMock.ancestor_info.ancestors[1].display_name;
|
||||
|
||||
await waitFor(() => {
|
||||
const unitHeaderTitle = screen.getByTestId('unit-header-title');
|
||||
expect(screen.getByText(unitDisplayName)).toBeInTheDocument();
|
||||
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(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();
|
||||
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();
|
||||
});
|
||||
});
|
||||
|
||||
it('renders the course unit iframe with correct attributes', async () => {
|
||||
render(<RootWrapper />);
|
||||
const { getByTitle } = render(<RootWrapper />);
|
||||
|
||||
await waitFor(() => {
|
||||
const iframe = screen.getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
|
||||
const iframe = getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
|
||||
expect(iframe).toHaveAttribute('src', `${getConfig().STUDIO_BASE_URL}/container_embed/${blockId}`);
|
||||
expect(iframe).toHaveAttribute('allow', IFRAME_FEATURE_POLICY);
|
||||
expect(iframe).toHaveAttribute('style', 'height: 0px;');
|
||||
@@ -205,27 +210,27 @@ describe('<CourseUnit />', () => {
|
||||
});
|
||||
|
||||
it('displays an error alert when a studioAjaxError message is received', async () => {
|
||||
render(<RootWrapper />);
|
||||
const { getByTitle, getByTestId } = render(<RootWrapper />);
|
||||
|
||||
await waitFor(() => {
|
||||
const xblocksIframe = screen.getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
|
||||
const xblocksIframe = getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
|
||||
expect(xblocksIframe).toBeInTheDocument();
|
||||
simulatePostMessageEvent(messageTypes.studioAjaxError, {
|
||||
error: 'Some error text...',
|
||||
});
|
||||
});
|
||||
expect(screen.getByTestId('saving-error-alert')).toBeInTheDocument();
|
||||
expect(getByTestId('saving-error-alert')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders XBlock iframe and opens legacy edit modal on editXBlock message', async () => {
|
||||
render(<RootWrapper />);
|
||||
const { getByTitle } = render(<RootWrapper />);
|
||||
|
||||
await waitFor(() => {
|
||||
const xblocksIframe = screen.getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
|
||||
const xblocksIframe = getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
|
||||
expect(xblocksIframe).toBeInTheDocument();
|
||||
simulatePostMessageEvent(messageTypes.editXBlock, { id: blockId });
|
||||
|
||||
const legacyXBlockEditModalIframe = screen.getByTitle(
|
||||
const legacyXBlockEditModalIframe = getByTitle(
|
||||
xblockContainerIframeMessages.legacyEditModalIframeTitle.defaultMessage,
|
||||
);
|
||||
expect(legacyXBlockEditModalIframe).toBeInTheDocument();
|
||||
@@ -243,14 +248,14 @@ describe('<CourseUnit />', () => {
|
||||
});
|
||||
|
||||
it('closes the legacy edit modal when closeXBlockEditorModal message is received', async () => {
|
||||
render(<RootWrapper />);
|
||||
const { getByTitle, queryByTitle } = render(<RootWrapper />);
|
||||
|
||||
await waitFor(() => {
|
||||
const xblocksIframe = screen.getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
|
||||
const xblocksIframe = getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
|
||||
expect(xblocksIframe).toBeInTheDocument();
|
||||
simulatePostMessageEvent(messageTypes.closeXBlockEditorModal, { id: blockId });
|
||||
|
||||
const legacyXBlockEditModalIframe = screen.queryByTitle(
|
||||
const legacyXBlockEditModalIframe = queryByTitle(
|
||||
xblockContainerIframeMessages.legacyEditModalIframeTitle.defaultMessage,
|
||||
);
|
||||
expect(legacyXBlockEditModalIframe).not.toBeInTheDocument();
|
||||
@@ -258,32 +263,29 @@ describe('<CourseUnit />', () => {
|
||||
});
|
||||
|
||||
it('closes legacy edit modal and updates course unit sidebar after saveEditedXBlockData message', async () => {
|
||||
render(<RootWrapper />);
|
||||
const { getByTitle, queryByTitle, getByTestId } = render(<RootWrapper />);
|
||||
|
||||
await waitFor(() => {
|
||||
const xblocksIframe = screen.getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
|
||||
const xblocksIframe = getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
|
||||
expect(xblocksIframe).toBeInTheDocument();
|
||||
simulatePostMessageEvent(messageTypes.saveEditedXBlockData);
|
||||
|
||||
const legacyXBlockEditModalIframe = screen.queryByTitle(
|
||||
const legacyXBlockEditModalIframe = queryByTitle(
|
||||
xblockContainerIframeMessages.legacyEditModalIframeTitle.defaultMessage,
|
||||
);
|
||||
expect(legacyXBlockEditModalIframe).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
axiosMock
|
||||
.onGet(getCourseSectionVerticalApiUrl(blockId))
|
||||
.onGet(getCourseUnitApiUrl(blockId))
|
||||
.reply(200, {
|
||||
...courseSectionVerticalMock,
|
||||
xblock_info: {
|
||||
...courseSectionVerticalMock.xblock_info,
|
||||
has_changes: true,
|
||||
published_by: userName,
|
||||
},
|
||||
...courseUnitIndexMock,
|
||||
has_changes: true,
|
||||
published_by: userName,
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
const courseUnitSidebar = screen.getByTestId('course-unit-sidebar');
|
||||
const courseUnitSidebar = getByTestId('course-unit-sidebar');
|
||||
expect(
|
||||
within(courseUnitSidebar).getByText(sidebarMessages.sidebarTitleDraftUnpublishedChanges.defaultMessage),
|
||||
).toBeInTheDocument();
|
||||
@@ -302,27 +304,24 @@ describe('<CourseUnit />', () => {
|
||||
});
|
||||
|
||||
it('updates course unit sidebar after receiving refreshPositions message', async () => {
|
||||
render(<RootWrapper />);
|
||||
const { getByTitle, getByTestId } = render(<RootWrapper />);
|
||||
|
||||
await waitFor(() => {
|
||||
const xblocksIframe = screen.getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
|
||||
const xblocksIframe = getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
|
||||
expect(xblocksIframe).toBeInTheDocument();
|
||||
simulatePostMessageEvent(messageTypes.refreshPositions);
|
||||
});
|
||||
|
||||
axiosMock
|
||||
.onGet(getCourseSectionVerticalApiUrl(blockId))
|
||||
.onGet(getCourseUnitApiUrl(blockId))
|
||||
.reply(200, {
|
||||
...courseSectionVerticalMock,
|
||||
xblock_info: {
|
||||
...courseSectionVerticalMock.xblock_info,
|
||||
has_changes: true,
|
||||
published_by: userName,
|
||||
},
|
||||
...courseUnitIndexMock,
|
||||
has_changes: true,
|
||||
published_by: userName,
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
const courseUnitSidebar = screen.getByTestId('course-unit-sidebar');
|
||||
const courseUnitSidebar = getByTestId('course-unit-sidebar');
|
||||
expect(
|
||||
within(courseUnitSidebar).getByText(sidebarMessages.sidebarTitleDraftUnpublishedChanges.defaultMessage),
|
||||
).toBeInTheDocument();
|
||||
@@ -341,10 +340,12 @@ describe('<CourseUnit />', () => {
|
||||
});
|
||||
|
||||
it('checks whether xblock is removed when the corresponding delete button is clicked and the sidebar is the updated', async () => {
|
||||
render(<RootWrapper />);
|
||||
const {
|
||||
getByTitle, getByText, queryByRole, getByRole,
|
||||
} = render(<RootWrapper />);
|
||||
|
||||
await waitFor(async () => {
|
||||
const iframe = screen.getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
|
||||
const iframe = getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
|
||||
expect(iframe).toHaveAttribute(
|
||||
'aria-label',
|
||||
xblockContainerIframeMessages.xblockIframeLabel.defaultMessage
|
||||
@@ -355,10 +356,10 @@ describe('<CourseUnit />', () => {
|
||||
usageId: courseVerticalChildrenMock.children[0].block_id,
|
||||
});
|
||||
|
||||
expect(screen.getByText(/Delete this component?/i)).toBeInTheDocument();
|
||||
expect(screen.getByText(/Deleting this component is permanent and cannot be undone./i)).toBeInTheDocument();
|
||||
expect(getByText(/Delete this component?/i)).toBeInTheDocument();
|
||||
expect(getByText(/Deleting this component is permanent and cannot be undone./i)).toBeInTheDocument();
|
||||
|
||||
const dialog = screen.getByRole('dialog');
|
||||
const dialog = getByRole('dialog');
|
||||
expect(dialog).toBeInTheDocument();
|
||||
|
||||
// Find the Cancel and Delete buttons within the iframe by their specific classes
|
||||
@@ -371,7 +372,7 @@ describe('<CourseUnit />', () => {
|
||||
usageId: courseVerticalChildrenMock.children[0].block_id,
|
||||
});
|
||||
|
||||
expect(screen.getByRole('dialog')).toBeInTheDocument();
|
||||
expect(getByRole('dialog')).toBeInTheDocument();
|
||||
userEvent.click(deleteButton);
|
||||
});
|
||||
|
||||
@@ -381,36 +382,30 @@ describe('<CourseUnit />', () => {
|
||||
})
|
||||
.reply(200, { dummy: 'value' });
|
||||
axiosMock
|
||||
.onGet(getCourseSectionVerticalApiUrl(blockId))
|
||||
.onGet(getCourseUnitApiUrl(blockId))
|
||||
.reply(200, {
|
||||
...courseSectionVerticalMock,
|
||||
xblock_info: {
|
||||
...courseSectionVerticalMock.xblock_info,
|
||||
visibility_state: UNIT_VISIBILITY_STATES.live,
|
||||
has_changes: false,
|
||||
published_by: userName,
|
||||
},
|
||||
...courseUnitIndexMock,
|
||||
visibility_state: UNIT_VISIBILITY_STATES.live,
|
||||
has_changes: false,
|
||||
published_by: userName,
|
||||
});
|
||||
await executeThunk(editCourseUnitVisibilityAndData(blockId, PUBLISH_TYPES.makePublic, true), store.dispatch);
|
||||
|
||||
await waitFor(() => {
|
||||
// check if the sidebar status is Published and Live
|
||||
expect(screen.getByText(sidebarMessages.sidebarTitlePublishedAndLive.defaultMessage)).toBeInTheDocument();
|
||||
expect(screen.getByText(
|
||||
expect(getByText(sidebarMessages.sidebarTitlePublishedAndLive.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByText(
|
||||
sidebarMessages.publishLastPublished.defaultMessage
|
||||
.replace('{publishedOn}', courseSectionVerticalMock.xblock_info.published_on)
|
||||
.replace('{publishedOn}', courseUnitIndexMock.published_on)
|
||||
.replace('{publishedBy}', userName),
|
||||
)).toBeInTheDocument();
|
||||
expect(screen.queryByRole('button', { name: sidebarMessages.actionButtonPublishTitle.defaultMessage })).not.toBeInTheDocument();
|
||||
expect(screen.getByText(unitDisplayName)).toBeInTheDocument();
|
||||
expect(queryByRole('button', { name: sidebarMessages.actionButtonPublishTitle.defaultMessage })).not.toBeInTheDocument();
|
||||
expect(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,
|
||||
@@ -431,41 +426,43 @@ describe('<CourseUnit />', () => {
|
||||
await executeThunk(fetchCourseVerticalChildrenData(blockId), store.dispatch);
|
||||
|
||||
axiosMock
|
||||
.onGet(getCourseSectionVerticalApiUrl(blockId))
|
||||
.reply(200, courseSectionVerticalMock);
|
||||
.onGet(getCourseUnitApiUrl(blockId))
|
||||
.reply(200, courseUnitIndexMock);
|
||||
await executeThunk(editCourseUnitVisibilityAndData(blockId, PUBLISH_TYPES.makePublic, true), store.dispatch);
|
||||
|
||||
await waitFor(() => {
|
||||
const iframe = screen.getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
|
||||
const iframe = 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(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(
|
||||
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(
|
||||
sidebarMessages.publishInfoDraftSaved.defaultMessage
|
||||
.replace('{editedOn}', courseSectionVerticalMock.xblock_info.edited_on)
|
||||
.replace('{editedBy}', courseSectionVerticalMock.xblock_info.edited_by),
|
||||
.replace('{editedOn}', courseUnitIndexMock.edited_on)
|
||||
.replace('{editedBy}', courseUnitIndexMock.edited_by),
|
||||
)).toBeInTheDocument();
|
||||
expect(screen.getByText(
|
||||
expect(getByText(
|
||||
sidebarMessages.releaseInfoWithSection.defaultMessage
|
||||
.replace('{sectionName}', courseSectionVerticalMock.xblock_info.release_date_from),
|
||||
.replace('{sectionName}', courseUnitIndexMock.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 () => {
|
||||
render(<RootWrapper />);
|
||||
const {
|
||||
getByTitle, getByRole, getByText, queryByRole,
|
||||
} = render(<RootWrapper />);
|
||||
|
||||
simulatePostMessageEvent(messageTypes.duplicateXBlock, {
|
||||
id: courseVerticalChildrenMock.children[0].block_id,
|
||||
@@ -481,14 +478,8 @@ 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: '',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
@@ -500,9 +491,9 @@ describe('<CourseUnit />', () => {
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
userEvent.click(screen.getByRole('button', { name: sidebarMessages.actionButtonPublishTitle.defaultMessage }));
|
||||
userEvent.click(getByRole('button', { name: sidebarMessages.actionButtonPublishTitle.defaultMessage }));
|
||||
|
||||
const iframe = screen.getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
|
||||
const iframe = getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
|
||||
expect(iframe).toHaveAttribute(
|
||||
'aria-label',
|
||||
xblockContainerIframeMessages.xblockIframeLabel.defaultMessage
|
||||
@@ -520,37 +511,34 @@ describe('<CourseUnit />', () => {
|
||||
})
|
||||
.reply(200, { dummy: 'value' });
|
||||
axiosMock
|
||||
.onGet(getCourseSectionVerticalApiUrl(blockId))
|
||||
.onGet(getCourseUnitApiUrl(blockId))
|
||||
.reply(200, {
|
||||
...courseSectionVerticalMock,
|
||||
xblock_info: {
|
||||
...courseSectionVerticalMock.xblock_info,
|
||||
visibility_state: UNIT_VISIBILITY_STATES.live,
|
||||
has_changes: false,
|
||||
published_by: userName,
|
||||
},
|
||||
...courseUnitIndexMock,
|
||||
visibility_state: UNIT_VISIBILITY_STATES.live,
|
||||
has_changes: false,
|
||||
published_by: userName,
|
||||
});
|
||||
await executeThunk(editCourseUnitVisibilityAndData(blockId, PUBLISH_TYPES.makePublic, true), store.dispatch);
|
||||
|
||||
await waitFor(() => {
|
||||
// check if the sidebar status is Published and Live
|
||||
expect(screen.getByText(sidebarMessages.sidebarTitlePublishedAndLive.defaultMessage)).toBeInTheDocument();
|
||||
expect(screen.getByText(
|
||||
expect(getByText(sidebarMessages.sidebarTitlePublishedAndLive.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByText(
|
||||
sidebarMessages.publishLastPublished.defaultMessage
|
||||
.replace('{publishedOn}', courseSectionVerticalMock.xblock_info.published_on)
|
||||
.replace('{publishedOn}', courseUnitIndexMock.published_on)
|
||||
.replace('{publishedBy}', userName),
|
||||
)).toBeInTheDocument();
|
||||
expect(screen.queryByRole('button', { name: sidebarMessages.actionButtonPublishTitle.defaultMessage })).not.toBeInTheDocument();
|
||||
expect(screen.getByText(unitDisplayName)).toBeInTheDocument();
|
||||
expect(queryByRole('button', { name: sidebarMessages.actionButtonPublishTitle.defaultMessage })).not.toBeInTheDocument();
|
||||
expect(getByText(unitDisplayName)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
axiosMock
|
||||
.onGet(getCourseSectionVerticalApiUrl(blockId))
|
||||
.reply(200, courseSectionVerticalMock);
|
||||
.onGet(getCourseUnitApiUrl(blockId))
|
||||
.reply(200, courseUnitIndexMock);
|
||||
await executeThunk(editCourseUnitVisibilityAndData(blockId, PUBLISH_TYPES.makePublic, true), store.dispatch);
|
||||
|
||||
await waitFor(() => {
|
||||
const iframe = screen.getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
|
||||
const iframe = getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
|
||||
expect(iframe).toHaveAttribute(
|
||||
'aria-label',
|
||||
xblockContainerIframeMessages.xblockIframeLabel.defaultMessage
|
||||
@@ -558,23 +546,23 @@ describe('<CourseUnit />', () => {
|
||||
);
|
||||
|
||||
// after duplicate the 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(
|
||||
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(
|
||||
sidebarMessages.publishInfoDraftSaved.defaultMessage
|
||||
.replace('{editedOn}', courseSectionVerticalMock.xblock_info.edited_on)
|
||||
.replace('{editedBy}', courseSectionVerticalMock.xblock_info.edited_by),
|
||||
.replace('{editedOn}', courseUnitIndexMock.edited_on)
|
||||
.replace('{editedBy}', courseUnitIndexMock.edited_by),
|
||||
)).toBeInTheDocument();
|
||||
expect(screen.getByText(
|
||||
expect(getByText(
|
||||
sidebarMessages.releaseInfoWithSection.defaultMessage
|
||||
.replace('{sectionName}', courseSectionVerticalMock.xblock_info.release_date_from),
|
||||
.replace('{sectionName}', courseUnitIndexMock.release_date_from),
|
||||
)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -582,19 +570,19 @@ describe('<CourseUnit />', () => {
|
||||
it('handles CourseUnit header action buttons', async () => {
|
||||
const { open } = window;
|
||||
window.open = jest.fn();
|
||||
render(<RootWrapper />);
|
||||
const { getByRole } = render(<RootWrapper />);
|
||||
const {
|
||||
draft_preview_link: draftPreviewLink,
|
||||
published_preview_link: publishedPreviewLink,
|
||||
} = courseSectionVerticalMock;
|
||||
|
||||
await waitFor(() => {
|
||||
const viewLiveButton = screen.getByRole('button', { name: headerNavigationsMessages.viewLiveButton.defaultMessage });
|
||||
const viewLiveButton = getByRole('button', { name: headerNavigationsMessages.viewLiveButton.defaultMessage });
|
||||
userEvent.click(viewLiveButton);
|
||||
expect(window.open).toHaveBeenCalled();
|
||||
expect(window.open).toHaveBeenCalledWith(publishedPreviewLink, '_blank');
|
||||
|
||||
const previewButton = screen.getByRole('button', { name: headerNavigationsMessages.previewButton.defaultMessage });
|
||||
const previewButton = getByRole('button', { name: headerNavigationsMessages.previewButton.defaultMessage });
|
||||
userEvent.click(previewButton);
|
||||
expect(window.open).toHaveBeenCalled();
|
||||
expect(window.open).toHaveBeenCalledWith(draftPreviewLink, '_blank');
|
||||
@@ -604,7 +592,12 @@ describe('<CourseUnit />', () => {
|
||||
});
|
||||
|
||||
it('checks courseUnit title changing when edit query is successfully', async () => {
|
||||
render(<RootWrapper />);
|
||||
const {
|
||||
findByText,
|
||||
queryByRole,
|
||||
getByRole,
|
||||
getByTestId,
|
||||
} = render(<RootWrapper />);
|
||||
let editTitleButton = null;
|
||||
let titleEditField = null;
|
||||
const newDisplayName = `${unitDisplayName} new`;
|
||||
@@ -617,15 +610,12 @@ describe('<CourseUnit />', () => {
|
||||
}))
|
||||
.reply(200, { dummy: 'value' });
|
||||
axiosMock
|
||||
.onGet(getCourseSectionVerticalApiUrl(blockId))
|
||||
.onGet(getCourseUnitApiUrl(blockId))
|
||||
.reply(200, {
|
||||
...courseSectionVerticalMock,
|
||||
xblock_info: {
|
||||
...courseSectionVerticalMock.xblock_info,
|
||||
metadata: {
|
||||
...courseSectionVerticalMock.xblock_info.metadata,
|
||||
display_name: newDisplayName,
|
||||
},
|
||||
...courseUnitIndexMock,
|
||||
metadata: {
|
||||
...courseUnitIndexMock.metadata,
|
||||
display_name: newDisplayName,
|
||||
},
|
||||
});
|
||||
axiosMock
|
||||
@@ -643,7 +633,7 @@ describe('<CourseUnit />', () => {
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
const unitHeaderTitle = screen.getByTestId('unit-header-title');
|
||||
const unitHeaderTitle = getByTestId('unit-header-title');
|
||||
editTitleButton = within(unitHeaderTitle)
|
||||
.getByRole('button', { name: headerTitleMessages.altButtonEdit.defaultMessage });
|
||||
titleEditField = within(unitHeaderTitle)
|
||||
@@ -651,7 +641,7 @@ describe('<CourseUnit />', () => {
|
||||
});
|
||||
expect(titleEditField).not.toBeInTheDocument();
|
||||
userEvent.click(editTitleButton);
|
||||
titleEditField = screen.getByRole('textbox', { name: headerTitleMessages.ariaLabelButtonEdit.defaultMessage });
|
||||
titleEditField = getByRole('textbox', { name: headerTitleMessages.ariaLabelButtonEdit.defaultMessage });
|
||||
|
||||
await userEvent.clear(titleEditField);
|
||||
await userEvent.type(titleEditField, newDisplayName);
|
||||
@@ -659,10 +649,9 @@ describe('<CourseUnit />', () => {
|
||||
|
||||
expect(titleEditField).toHaveValue(newDisplayName);
|
||||
|
||||
titleEditField = screen.queryByRole('textbox', { name: headerTitleMessages.ariaLabelButtonEdit.defaultMessage });
|
||||
titleEditField = screen.queryByRole('textbox', { name: headerTitleMessages.ariaLabelButtonEdit.defaultMessage });
|
||||
titleEditField = queryByRole('textbox', { name: headerTitleMessages.ariaLabelButtonEdit.defaultMessage });
|
||||
expect(titleEditField).not.toBeInTheDocument();
|
||||
expect(await screen.findByText(newDisplayName)).toBeInTheDocument();
|
||||
expect(await findByText(newDisplayName)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('doesn\'t handle creating xblock and displays an error message', async () => {
|
||||
@@ -682,14 +671,15 @@ describe('<CourseUnit />', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('handle creating Problem xblock and showing editor modal', async () => {
|
||||
it('handle creating Problem xblock and navigate to editor page', async () => {
|
||||
const { courseKey, locator } = courseCreateXblockMock;
|
||||
axiosMock
|
||||
.onPost(postXBlockBaseApiUrl({ type: 'problem', category: 'problem', parentLocator: blockId }))
|
||||
.reply(200, courseCreateXblockMock);
|
||||
render(<RootWrapper />);
|
||||
const { getByText, getByRole } = render(<RootWrapper />);
|
||||
|
||||
await waitFor(() => {
|
||||
userEvent.click(screen.getByRole('button', { name: sidebarMessages.actionButtonPublishTitle.defaultMessage }));
|
||||
userEvent.click(getByRole('button', { name: sidebarMessages.actionButtonPublishTitle.defaultMessage }));
|
||||
});
|
||||
|
||||
axiosMock
|
||||
@@ -698,57 +688,93 @@ describe('<CourseUnit />', () => {
|
||||
})
|
||||
.reply(200, { dummy: 'value' });
|
||||
axiosMock
|
||||
.onGet(getCourseSectionVerticalApiUrl(blockId))
|
||||
.onGet(getCourseUnitApiUrl(blockId))
|
||||
.reply(200, {
|
||||
...courseSectionVerticalMock,
|
||||
xblock_info: {
|
||||
...courseSectionVerticalMock.xblock_info,
|
||||
visibility_state: UNIT_VISIBILITY_STATES.live,
|
||||
has_changes: false,
|
||||
published_by: userName,
|
||||
},
|
||||
...courseUnitIndexMock,
|
||||
visibility_state: UNIT_VISIBILITY_STATES.live,
|
||||
has_changes: false,
|
||||
published_by: userName,
|
||||
});
|
||||
|
||||
await executeThunk(editCourseUnitVisibilityAndData(blockId, PUBLISH_TYPES.makePublic, true), store.dispatch);
|
||||
|
||||
await waitFor(() => {
|
||||
const problemButton = screen.getByRole('button', {
|
||||
const problemButton = 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(getCourseSectionVerticalApiUrl(blockId))
|
||||
.reply(200, courseSectionVerticalMock);
|
||||
.onGet(getCourseUnitApiUrl(blockId))
|
||||
.reply(200, courseUnitIndexMock);
|
||||
|
||||
await executeThunk(editCourseUnitVisibilityAndData(blockId, PUBLISH_TYPES.makePublic, true), store.dispatch);
|
||||
|
||||
// after creating problem 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(
|
||||
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(
|
||||
sidebarMessages.publishInfoDraftSaved.defaultMessage
|
||||
.replace('{editedOn}', courseSectionVerticalMock.xblock_info.edited_on)
|
||||
.replace('{editedBy}', courseSectionVerticalMock.xblock_info.edited_by),
|
||||
.replace('{editedOn}', courseUnitIndexMock.edited_on)
|
||||
.replace('{editedBy}', courseUnitIndexMock.edited_by),
|
||||
)).toBeInTheDocument();
|
||||
expect(screen.getByText(
|
||||
expect(getByText(
|
||||
sidebarMessages.releaseInfoWithSection.defaultMessage
|
||||
.replace('{sectionName}', courseSectionVerticalMock.xblock_info.release_date_from),
|
||||
.replace('{sectionName}', courseUnitIndexMock.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 () => {
|
||||
render(<RootWrapper />);
|
||||
const { getByRole, getAllByTestId } = render(<RootWrapper />);
|
||||
let units = null;
|
||||
const updatedCourseSectionVerticalData = cloneDeep(courseSectionVerticalMock);
|
||||
const updatedAncestorsChild = updatedCourseSectionVerticalData.xblock_info.ancestor_info.ancestors[0];
|
||||
@@ -758,7 +784,7 @@ describe('<CourseUnit />', () => {
|
||||
]);
|
||||
|
||||
await waitFor(async () => {
|
||||
units = screen.getAllByTestId('course-unit-btn');
|
||||
units = getAllByTestId('course-unit-btn');
|
||||
const courseUnits = courseSectionVerticalMock.xblock_info.ancestor_info.ancestors[0].child_info.children;
|
||||
expect(units).toHaveLength(courseUnits.length);
|
||||
});
|
||||
@@ -775,8 +801,8 @@ describe('<CourseUnit />', () => {
|
||||
|
||||
await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch);
|
||||
|
||||
const addNewUnitBtn = screen.getByRole('button', { name: courseSequenceMessages.newUnitBtnText.defaultMessage });
|
||||
units = screen.getAllByTestId('course-unit-btn');
|
||||
const addNewUnitBtn = getByRole('button', { name: courseSequenceMessages.newUnitBtnText.defaultMessage });
|
||||
units = getAllByTestId('course-unit-btn');
|
||||
const updatedCourseUnits = updatedCourseSectionVerticalData
|
||||
.xblock_info.ancestor_info.ancestors[0].child_info.children;
|
||||
|
||||
@@ -788,7 +814,7 @@ describe('<CourseUnit />', () => {
|
||||
});
|
||||
|
||||
it('the sequence unit is updated after changing the unit header', async () => {
|
||||
render(<RootWrapper />);
|
||||
const { getAllByTestId, getByTestId } = 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', [
|
||||
@@ -805,15 +831,12 @@ describe('<CourseUnit />', () => {
|
||||
},
|
||||
}))
|
||||
.reply(200, { dummy: 'value' })
|
||||
.onGet(getCourseSectionVerticalApiUrl(blockId))
|
||||
.onGet(getCourseUnitApiUrl(blockId))
|
||||
.reply(200, {
|
||||
...courseSectionVerticalMock,
|
||||
xblock_info: {
|
||||
...courseSectionVerticalMock.xblock_info,
|
||||
metadata: {
|
||||
...courseSectionVerticalMock.xblock_info.metadata,
|
||||
display_name: newDisplayName,
|
||||
},
|
||||
...courseUnitIndexMock,
|
||||
metadata: {
|
||||
...courseUnitIndexMock.metadata,
|
||||
display_name: newDisplayName,
|
||||
},
|
||||
})
|
||||
.onGet(getCourseSectionVerticalApiUrl(blockId))
|
||||
@@ -823,7 +846,7 @@ describe('<CourseUnit />', () => {
|
||||
|
||||
await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch);
|
||||
|
||||
const unitHeaderTitle = screen.getByTestId('unit-header-title');
|
||||
const unitHeaderTitle = getByTestId('unit-header-title');
|
||||
|
||||
const editTitleButton = within(unitHeaderTitle).getByRole('button', { name: headerTitleMessages.altButtonEdit.defaultMessage });
|
||||
userEvent.click(editTitleButton);
|
||||
@@ -835,21 +858,20 @@ describe('<CourseUnit />', () => {
|
||||
await userEvent.tab();
|
||||
|
||||
await waitFor(async () => {
|
||||
const units = screen.getAllByTestId('course-unit-btn');
|
||||
const units = getAllByTestId('course-unit-btn');
|
||||
expect(units.some(unit => unit.title === newDisplayName)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
it('handles creating Video xblock and showing editor modal using videogalleryflow', async () => {
|
||||
const waffleSpy = jest.spyOn(selectors, 'getWaffleFlags').mockReturnValue({ useVideoGalleryFlow: true });
|
||||
|
||||
it('handles creating Video xblock and navigates to editor page', async () => {
|
||||
const { courseKey, locator } = courseCreateXblockMock;
|
||||
axiosMock
|
||||
.onPost(postXBlockBaseApiUrl({ type: 'video', category: 'video', parentLocator: blockId }))
|
||||
.reply(200, courseCreateXblockMock);
|
||||
render(<RootWrapper />);
|
||||
const { getByText, queryByRole, getByRole } = render(<RootWrapper />);
|
||||
|
||||
await waitFor(() => {
|
||||
userEvent.click(screen.getByRole('button', { name: sidebarMessages.actionButtonPublishTitle.defaultMessage }));
|
||||
userEvent.click(getByRole('button', { name: sidebarMessages.actionButtonPublishTitle.defaultMessage }));
|
||||
});
|
||||
|
||||
axiosMock
|
||||
@@ -858,181 +880,96 @@ describe('<CourseUnit />', () => {
|
||||
})
|
||||
.reply(200, { dummy: 'value' });
|
||||
axiosMock
|
||||
.onGet(getCourseSectionVerticalApiUrl(blockId))
|
||||
.onGet(getCourseUnitApiUrl(blockId))
|
||||
.reply(200, {
|
||||
...courseSectionVerticalMock,
|
||||
xblock_info: {
|
||||
...courseSectionVerticalMock.xblock_info,
|
||||
visibility_state: UNIT_VISIBILITY_STATES.live,
|
||||
has_changes: false,
|
||||
published_by: userName,
|
||||
},
|
||||
...courseUnitIndexMock,
|
||||
visibility_state: UNIT_VISIBILITY_STATES.live,
|
||||
has_changes: false,
|
||||
published_by: userName,
|
||||
});
|
||||
|
||||
await executeThunk(editCourseUnitVisibilityAndData(blockId, PUBLISH_TYPES.makePublic, true), store.dispatch);
|
||||
|
||||
await waitFor(() => {
|
||||
// check if the sidebar status is Published and Live
|
||||
expect(screen.getByText(sidebarMessages.sidebarTitlePublishedAndLive.defaultMessage)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
expect(screen.getByText(
|
||||
sidebarMessages.publishLastPublished.defaultMessage
|
||||
.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(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();
|
||||
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(
|
||||
expect(getByText(sidebarMessages.sidebarTitlePublishedAndLive.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByText(
|
||||
sidebarMessages.publishLastPublished.defaultMessage
|
||||
.replace('{publishedOn}', courseSectionVerticalMock.xblock_info.published_on)
|
||||
.replace('{publishedOn}', courseUnitIndexMock.published_on)
|
||||
.replace('{publishedBy}', userName),
|
||||
)).toBeInTheDocument();
|
||||
expect(screen.queryByRole('button', { name: sidebarMessages.actionButtonPublishTitle.defaultMessage })).not.toBeInTheDocument();
|
||||
expect(queryByRole('button', { name: sidebarMessages.actionButtonPublishTitle.defaultMessage })).not.toBeInTheDocument();
|
||||
|
||||
const videoButton = screen.getByRole('button', {
|
||||
const videoButton = getByRole('button', {
|
||||
name: new RegExp(`${addComponentMessages.buttonText.defaultMessage} Video`, 'i'),
|
||||
hidden: true,
|
||||
});
|
||||
|
||||
userEvent.click(videoButton);
|
||||
expect(mockedUsedNavigate).toHaveBeenCalled();
|
||||
expect(mockedUsedNavigate).toHaveBeenCalledWith(`/course/${courseKey}/editor/video/${locator}`);
|
||||
});
|
||||
|
||||
/** TODO -- fix this test.
|
||||
await waitFor(() => {
|
||||
expect(getByRole('textbox', { name: /paste your video id or url/i })).toBeInTheDocument();
|
||||
});
|
||||
*/
|
||||
|
||||
axiosMock
|
||||
.onGet(getCourseSectionVerticalApiUrl(blockId))
|
||||
.reply(200, courseSectionVerticalMock);
|
||||
.onGet(getCourseUnitApiUrl(blockId))
|
||||
.reply(200, courseUnitIndexMock);
|
||||
|
||||
await executeThunk(editCourseUnitVisibilityAndData(blockId, PUBLISH_TYPES.makePublic, true), store.dispatch);
|
||||
|
||||
// after creating video xblock, the sidebar status changes to Draft (unpublished changes)
|
||||
expect(screen.getByText(sidebarMessages.sidebarTitleDraftUnpublishedChanges.defaultMessage)).toBeInTheDocument();
|
||||
expect(screen.getByText(sidebarMessages.visibilityStaffAndLearnersTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(screen.getByText(sidebarMessages.releaseStatusTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(screen.getByText(sidebarMessages.sidebarBodyNote.defaultMessage)).toBeInTheDocument();
|
||||
expect(screen.getByText(sidebarMessages.visibilityWillBeVisibleToTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(screen.getByText(sidebarMessages.visibilityCheckboxTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(screen.getByText(sidebarMessages.actionButtonPublishTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(screen.getByText(sidebarMessages.actionButtonDiscardChangesTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(screen.getByText(courseSectionVerticalMock.xblock_info.release_date)).toBeInTheDocument();
|
||||
expect(screen.getByText(
|
||||
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(
|
||||
sidebarMessages.publishInfoDraftSaved.defaultMessage
|
||||
.replace('{editedOn}', courseSectionVerticalMock.xblock_info.edited_on)
|
||||
.replace('{editedBy}', courseSectionVerticalMock.xblock_info.edited_by),
|
||||
.replace('{editedOn}', courseUnitIndexMock.edited_on)
|
||||
.replace('{editedBy}', courseUnitIndexMock.edited_by),
|
||||
)).toBeInTheDocument();
|
||||
expect(screen.getByText(
|
||||
expect(getByText(
|
||||
sidebarMessages.releaseInfoWithSection.defaultMessage
|
||||
.replace('{sectionName}', courseSectionVerticalMock.xblock_info.release_date_from),
|
||||
.replace('{sectionName}', courseUnitIndexMock.release_date_from),
|
||||
)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders course unit details for a draft with unpublished changes', async () => {
|
||||
render(<RootWrapper />);
|
||||
const { getByText } = render(<RootWrapper />);
|
||||
|
||||
await waitFor(() => {
|
||||
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(
|
||||
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(
|
||||
sidebarMessages.publishInfoDraftSaved.defaultMessage
|
||||
.replace('{editedOn}', courseSectionVerticalMock.xblock_info.edited_on)
|
||||
.replace('{editedBy}', courseSectionVerticalMock.xblock_info.edited_by),
|
||||
.replace('{editedOn}', courseUnitIndexMock.edited_on)
|
||||
.replace('{editedBy}', courseUnitIndexMock.edited_by),
|
||||
)).toBeInTheDocument();
|
||||
expect(screen.getByText(
|
||||
expect(getByText(
|
||||
sidebarMessages.releaseInfoWithSection.defaultMessage
|
||||
.replace('{sectionName}', courseSectionVerticalMock.xblock_info.release_date_from),
|
||||
.replace('{sectionName}', courseUnitIndexMock.release_date_from),
|
||||
)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('renders course unit details in the sidebar', async () => {
|
||||
render(<RootWrapper />);
|
||||
const courseUnitLocationId = extractCourseUnitId(courseSectionVerticalMock.xblock_info.id);
|
||||
const { getByText } = render(<RootWrapper />);
|
||||
const courseUnitLocationId = extractCourseUnitId(courseUnitIndexMock.id);
|
||||
|
||||
await waitFor(() => {
|
||||
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
|
||||
expect(getByText(sidebarMessages.sidebarHeaderUnitLocationTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByText(sidebarMessages.unitLocationTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByText(courseUnitLocationId)).toBeInTheDocument();
|
||||
expect(getByText(sidebarMessages.unitLocationDescription.defaultMessage
|
||||
.replace('{id}', courseUnitLocationId))).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -1053,16 +990,13 @@ describe('<CourseUnit />', () => {
|
||||
render(<RootWrapper />);
|
||||
|
||||
axiosMock
|
||||
.onGet(getCourseSectionVerticalApiUrl(courseId))
|
||||
.onGet(getCourseUnitApiUrl(courseId))
|
||||
.reply(200, {
|
||||
...courseSectionVerticalMock,
|
||||
xblock_info: {
|
||||
...courseSectionVerticalMock.xblock_info,
|
||||
currently_visible_to_students: false,
|
||||
},
|
||||
...courseUnitIndexMock,
|
||||
currently_visible_to_students: false,
|
||||
});
|
||||
|
||||
await executeThunk(fetchCourseSectionVerticalData(courseId), store.dispatch);
|
||||
await executeThunk(fetchCourseUnitQuery(courseId), store.dispatch);
|
||||
|
||||
await waitFor(() => {
|
||||
const alert = screen.queryAllByRole('alert').find(
|
||||
@@ -1073,13 +1007,13 @@ describe('<CourseUnit />', () => {
|
||||
});
|
||||
|
||||
it('should toggle visibility from sidebar and update course unit state accordingly', async () => {
|
||||
render(<RootWrapper />);
|
||||
const { getByRole, getByTestId } = render(<RootWrapper />);
|
||||
let courseUnitSidebar;
|
||||
let draftUnpublishedChangesHeading;
|
||||
let visibilityCheckbox;
|
||||
|
||||
await waitFor(() => {
|
||||
courseUnitSidebar = screen.getByTestId('course-unit-sidebar');
|
||||
courseUnitSidebar = getByTestId('course-unit-sidebar');
|
||||
|
||||
draftUnpublishedChangesHeading = within(courseUnitSidebar)
|
||||
.getByText(sidebarMessages.sidebarTitleDraftUnpublishedChanges.defaultMessage);
|
||||
@@ -1099,14 +1033,11 @@ describe('<CourseUnit />', () => {
|
||||
})
|
||||
.reply(200, { dummy: 'value' });
|
||||
axiosMock
|
||||
.onGet(getCourseSectionVerticalApiUrl(blockId))
|
||||
.onGet(getCourseUnitApiUrl(blockId))
|
||||
.reply(200, {
|
||||
...courseSectionVerticalMock,
|
||||
xblock_info: {
|
||||
...courseSectionVerticalMock.xblock_info,
|
||||
visibility_state: UNIT_VISIBILITY_STATES.staffOnly,
|
||||
has_explicit_staff_lock: true,
|
||||
},
|
||||
...courseUnitIndexMock,
|
||||
visibility_state: UNIT_VISIBILITY_STATES.staffOnly,
|
||||
has_explicit_staff_lock: true,
|
||||
});
|
||||
|
||||
await executeThunk(editCourseUnitVisibilityAndData(blockId, PUBLISH_TYPES.republish, true), store.dispatch);
|
||||
@@ -1119,7 +1050,7 @@ describe('<CourseUnit />', () => {
|
||||
|
||||
userEvent.click(visibilityCheckbox);
|
||||
|
||||
const modalNotification = screen.getByRole('dialog');
|
||||
const modalNotification = 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' });
|
||||
@@ -1139,8 +1070,8 @@ describe('<CourseUnit />', () => {
|
||||
})
|
||||
.reply(200, { dummy: 'value' });
|
||||
axiosMock
|
||||
.onGet(getCourseSectionVerticalApiUrl(blockId))
|
||||
.reply(200, courseSectionVerticalMock);
|
||||
.onGet(getCourseUnitApiUrl(blockId))
|
||||
.reply(200, courseUnitIndexMock);
|
||||
|
||||
await executeThunk(editCourseUnitVisibilityAndData(blockId, PUBLISH_TYPES.republish, null), store.dispatch);
|
||||
|
||||
@@ -1151,12 +1082,12 @@ describe('<CourseUnit />', () => {
|
||||
});
|
||||
|
||||
it('should publish course unit after click on the "Publish" button', async () => {
|
||||
render(<RootWrapper />);
|
||||
const { getByTestId } = render(<RootWrapper />);
|
||||
let courseUnitSidebar;
|
||||
let publishBtn;
|
||||
|
||||
await waitFor(() => {
|
||||
courseUnitSidebar = screen.getByTestId('course-unit-sidebar');
|
||||
courseUnitSidebar = getByTestId('course-unit-sidebar');
|
||||
publishBtn = within(courseUnitSidebar).queryByRole('button', { name: sidebarMessages.actionButtonPublishTitle.defaultMessage });
|
||||
expect(publishBtn).toBeInTheDocument();
|
||||
|
||||
@@ -1169,15 +1100,12 @@ describe('<CourseUnit />', () => {
|
||||
})
|
||||
.reply(200, { dummy: 'value' });
|
||||
axiosMock
|
||||
.onGet(getCourseSectionVerticalApiUrl(blockId))
|
||||
.onGet(getCourseUnitApiUrl(blockId))
|
||||
.reply(200, {
|
||||
...courseSectionVerticalMock,
|
||||
xblock_info: {
|
||||
...courseSectionVerticalMock.xblock_info,
|
||||
visibility_state: UNIT_VISIBILITY_STATES.live,
|
||||
has_changes: false,
|
||||
published_by: userName,
|
||||
},
|
||||
...courseUnitIndexMock,
|
||||
visibility_state: UNIT_VISIBILITY_STATES.live,
|
||||
has_changes: false,
|
||||
published_by: userName,
|
||||
});
|
||||
|
||||
await executeThunk(editCourseUnitVisibilityAndData(blockId, PUBLISH_TYPES.makePublic, true), store.dispatch);
|
||||
@@ -1186,19 +1114,19 @@ describe('<CourseUnit />', () => {
|
||||
.getByText(sidebarMessages.sidebarTitlePublishedAndLive.defaultMessage)).toBeInTheDocument();
|
||||
expect(within(courseUnitSidebar).getByText(
|
||||
sidebarMessages.publishLastPublished.defaultMessage
|
||||
.replace('{publishedOn}', courseSectionVerticalMock.xblock_info.published_on)
|
||||
.replace('{publishedOn}', courseUnitIndexMock.published_on)
|
||||
.replace('{publishedBy}', userName),
|
||||
)).toBeInTheDocument();
|
||||
expect(publishBtn).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should discard changes after click on the "Discard changes" button', async () => {
|
||||
render(<RootWrapper />);
|
||||
const { getByTestId, getByRole } = render(<RootWrapper />);
|
||||
let courseUnitSidebar;
|
||||
let discardChangesBtn;
|
||||
|
||||
await waitFor(() => {
|
||||
courseUnitSidebar = screen.getByTestId('course-unit-sidebar');
|
||||
courseUnitSidebar = getByTestId('course-unit-sidebar');
|
||||
|
||||
const draftUnpublishedChangesHeading = within(courseUnitSidebar)
|
||||
.getByText(sidebarMessages.sidebarTitleDraftUnpublishedChanges.defaultMessage);
|
||||
@@ -1208,7 +1136,7 @@ describe('<CourseUnit />', () => {
|
||||
|
||||
userEvent.click(discardChangesBtn);
|
||||
|
||||
const modalNotification = screen.getByRole('dialog');
|
||||
const modalNotification = getByRole('dialog');
|
||||
expect(modalNotification).toBeInTheDocument();
|
||||
expect(within(modalNotification)
|
||||
.getByText(sidebarMessages.modalDiscardUnitChangesDescription.defaultMessage)).toBeInTheDocument();
|
||||
@@ -1228,14 +1156,9 @@ describe('<CourseUnit />', () => {
|
||||
})
|
||||
.reply(200, { dummy: 'value' });
|
||||
axiosMock
|
||||
.onGet(getCourseSectionVerticalApiUrl(blockId))
|
||||
.onGet(getCourseUnitApiUrl(blockId))
|
||||
.reply(200, {
|
||||
...courseSectionVerticalMock,
|
||||
xblock_info: {
|
||||
...courseSectionVerticalMock.xblock_info,
|
||||
published: true,
|
||||
has_changes: false,
|
||||
},
|
||||
...courseUnitIndexMock, published: true, has_changes: false,
|
||||
});
|
||||
|
||||
await executeThunk(editCourseUnitVisibilityAndData(
|
||||
@@ -1250,7 +1173,7 @@ describe('<CourseUnit />', () => {
|
||||
});
|
||||
|
||||
it('should toggle visibility from header configure modal and update course unit state accordingly', async () => {
|
||||
render(<RootWrapper />);
|
||||
const { getByRole, getByTestId } = render(<RootWrapper />);
|
||||
let courseUnitSidebar;
|
||||
let sidebarVisibilityCheckbox;
|
||||
let modalVisibilityCheckbox;
|
||||
@@ -1258,16 +1181,16 @@ describe('<CourseUnit />', () => {
|
||||
let restrictAccessSelect;
|
||||
|
||||
await waitFor(() => {
|
||||
courseUnitSidebar = screen.getByTestId('course-unit-sidebar');
|
||||
courseUnitSidebar = getByTestId('course-unit-sidebar');
|
||||
sidebarVisibilityCheckbox = within(courseUnitSidebar)
|
||||
.getByLabelText(sidebarMessages.visibilityCheckboxTitle.defaultMessage);
|
||||
expect(sidebarVisibilityCheckbox).not.toBeChecked();
|
||||
|
||||
const headerConfigureBtn = screen.getByRole('button', { name: /settings/i });
|
||||
const headerConfigureBtn = getByRole('button', { name: /settings/i });
|
||||
expect(headerConfigureBtn).toBeInTheDocument();
|
||||
|
||||
userEvent.click(headerConfigureBtn);
|
||||
configureModal = screen.getByTestId('configure-modal');
|
||||
configureModal = getByTestId('configure-modal');
|
||||
restrictAccessSelect = within(configureModal)
|
||||
.getByRole('combobox', { name: configureModalMessages.restrictAccessTo.defaultMessage });
|
||||
expect(within(configureModal)
|
||||
@@ -1292,20 +1215,17 @@ describe('<CourseUnit />', () => {
|
||||
});
|
||||
|
||||
axiosMock
|
||||
.onPost(getXBlockBaseApiUrl(courseSectionVerticalMock.xblock_info.id), {
|
||||
.onPost(getXBlockBaseApiUrl(courseUnitIndexMock.id), {
|
||||
publish: null,
|
||||
metadata: { visible_to_staff_only: true, group_access: { 50: [2] }, discussion_enabled: true },
|
||||
})
|
||||
.reply(200, { dummy: 'value' });
|
||||
axiosMock
|
||||
.onGet(getCourseSectionVerticalApiUrl(blockId))
|
||||
.reply(200, {
|
||||
...courseSectionVerticalMock,
|
||||
xblock_info: {
|
||||
...courseSectionVerticalMock.xblock_info,
|
||||
visibility_state: UNIT_VISIBILITY_STATES.staffOnly,
|
||||
has_explicit_staff_lock: true,
|
||||
},
|
||||
.onGet(getCourseUnitApiUrl(blockId))
|
||||
.replyOnce(200, {
|
||||
...courseUnitIndexMock,
|
||||
visibility_state: UNIT_VISIBILITY_STATES.staffOnly,
|
||||
has_explicit_staff_lock: true,
|
||||
});
|
||||
|
||||
const modalSaveBtn = within(configureModal)
|
||||
@@ -1326,8 +1246,8 @@ describe('<CourseUnit />', () => {
|
||||
...getConfig(),
|
||||
ENABLE_TAGGING_TAXONOMY_PAGES: 'true',
|
||||
});
|
||||
render(<RootWrapper />);
|
||||
await waitFor(() => { expect(screen.getByText('Unit tags')).toBeInTheDocument(); });
|
||||
const { getByText } = render(<RootWrapper />);
|
||||
await waitFor(() => { expect(getByText('Unit tags')).toBeInTheDocument(); });
|
||||
});
|
||||
|
||||
it('hides the Tags sidebar when not enabled', async () => {
|
||||
@@ -1335,28 +1255,28 @@ describe('<CourseUnit />', () => {
|
||||
...getConfig(),
|
||||
ENABLE_TAGGING_TAXONOMY_PAGES: 'false',
|
||||
});
|
||||
render(<RootWrapper />);
|
||||
await waitFor(() => { expect(screen.queryByText('Unit tags')).not.toBeInTheDocument(); });
|
||||
const { queryByText } = render(<RootWrapper />);
|
||||
await waitFor(() => { expect(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 () => {
|
||||
render(<RootWrapper />);
|
||||
const {
|
||||
getAllByTestId, getByRole,
|
||||
} = render(<RootWrapper />);
|
||||
|
||||
axiosMock
|
||||
.onGet(getCourseSectionVerticalApiUrl(blockId))
|
||||
.onGet(getCourseUnitApiUrl(courseId))
|
||||
.reply(200, {
|
||||
...courseSectionVerticalMock,
|
||||
xblock_info: {
|
||||
...courseSectionVerticalMock.xblock_info,
|
||||
enable_copy_paste_units: true,
|
||||
},
|
||||
...courseUnitIndexMock,
|
||||
enable_copy_paste_units: true,
|
||||
});
|
||||
|
||||
await executeThunk(fetchCourseUnitQuery(courseId), store.dispatch);
|
||||
await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch);
|
||||
|
||||
userEvent.click(screen.getByRole('button', { name: sidebarMessages.actionButtonCopyUnitTitle.defaultMessage }));
|
||||
userEvent.click(screen.getByRole('button', { name: courseSequenceMessages.pasteAsNewUnitLink.defaultMessage }));
|
||||
userEvent.click(getByRole('button', { name: sidebarMessages.actionButtonCopyUnitTitle.defaultMessage }));
|
||||
userEvent.click(getByRole('button', { name: courseSequenceMessages.pasteAsNewUnitLink.defaultMessage }));
|
||||
|
||||
let units = null;
|
||||
const updatedCourseSectionVerticalData = cloneDeep(courseSectionVerticalMock);
|
||||
@@ -1367,7 +1287,7 @@ describe('<CourseUnit />', () => {
|
||||
]);
|
||||
|
||||
await waitFor(() => {
|
||||
units = screen.getAllByTestId('course-unit-btn');
|
||||
units = getAllByTestId('course-unit-btn');
|
||||
const courseUnits = courseSectionVerticalMock.xblock_info.ancestor_info.ancestors[0].child_info.children;
|
||||
expect(units).toHaveLength(courseUnits.length);
|
||||
});
|
||||
@@ -1383,7 +1303,7 @@ describe('<CourseUnit />', () => {
|
||||
|
||||
await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch);
|
||||
|
||||
units = screen.getAllByTestId('course-unit-btn');
|
||||
units = getAllByTestId('course-unit-btn');
|
||||
const updatedCourseUnits = updatedCourseSectionVerticalData
|
||||
.xblock_info.ancestor_info.ancestors[0].child_info.children;
|
||||
|
||||
@@ -1394,27 +1314,30 @@ describe('<CourseUnit />', () => {
|
||||
});
|
||||
|
||||
it('should increase the number of course XBlocks after copying and pasting a block', async () => {
|
||||
render(<RootWrapper />);
|
||||
const { getByRole, getByTitle } = render(<RootWrapper />);
|
||||
|
||||
simulatePostMessageEvent(messageTypes.copyXBlock, {
|
||||
id: courseVerticalChildrenMock.children[0].block_id,
|
||||
});
|
||||
|
||||
axiosMock
|
||||
.onGet(getCourseSectionVerticalApiUrl(blockId))
|
||||
.onGet(getClipboardUrl())
|
||||
.reply(200, clipboardXBlock);
|
||||
|
||||
axiosMock
|
||||
.onGet(getCourseUnitApiUrl(courseId))
|
||||
.reply(200, {
|
||||
...courseSectionVerticalMock,
|
||||
xblock_info: {
|
||||
...courseSectionVerticalMock.xblock_info,
|
||||
enable_copy_paste_units: true,
|
||||
},
|
||||
...courseUnitIndexMock,
|
||||
enable_copy_paste_units: true,
|
||||
});
|
||||
await executeThunk(fetchCourseUnitQuery(courseId), store.dispatch);
|
||||
await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch);
|
||||
|
||||
userEvent.click(screen.getByRole('button', { name: sidebarMessages.actionButtonCopyUnitTitle.defaultMessage }));
|
||||
userEvent.click(getByRole('button', { name: sidebarMessages.actionButtonCopyUnitTitle.defaultMessage }));
|
||||
userEvent.click(getByRole('button', { name: messages.pasteButtonText.defaultMessage }));
|
||||
|
||||
await waitFor(() => {
|
||||
const iframe = screen.getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
|
||||
const iframe = getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
|
||||
expect(iframe).toHaveAttribute(
|
||||
'aria-label',
|
||||
xblockContainerIframeMessages.xblockIframeLabel.defaultMessage
|
||||
@@ -1450,7 +1373,7 @@ describe('<CourseUnit />', () => {
|
||||
await executeThunk(fetchCourseVerticalChildrenData(blockId), store.dispatch);
|
||||
|
||||
await waitFor(() => {
|
||||
const iframe = screen.getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
|
||||
const iframe = getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
|
||||
expect(iframe).toHaveAttribute(
|
||||
'aria-label',
|
||||
xblockContainerIframeMessages.xblockIframeLabel.defaultMessage
|
||||
@@ -1460,22 +1383,22 @@ describe('<CourseUnit />', () => {
|
||||
});
|
||||
|
||||
it('displays a notification about new files after pasting a component', async () => {
|
||||
render(<RootWrapper />);
|
||||
const {
|
||||
queryByTestId, getByTestId, getByRole,
|
||||
} = render(<RootWrapper />);
|
||||
|
||||
axiosMock
|
||||
.onGet(getCourseSectionVerticalApiUrl(blockId))
|
||||
.onGet(getCourseUnitApiUrl(courseId))
|
||||
.reply(200, {
|
||||
...courseSectionVerticalMock,
|
||||
xblock_info: {
|
||||
...courseSectionVerticalMock.xblock_info,
|
||||
enable_copy_paste_units: true,
|
||||
},
|
||||
...courseUnitIndexMock,
|
||||
enable_copy_paste_units: true,
|
||||
});
|
||||
|
||||
await executeThunk(fetchCourseUnitQuery(courseId), store.dispatch);
|
||||
await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch);
|
||||
|
||||
userEvent.click(screen.getByRole('button', { name: sidebarMessages.actionButtonCopyUnitTitle.defaultMessage }));
|
||||
userEvent.click(screen.getByRole('button', { name: courseSequenceMessages.pasteAsNewUnitLink.defaultMessage }));
|
||||
userEvent.click(getByRole('button', { name: sidebarMessages.actionButtonCopyUnitTitle.defaultMessage }));
|
||||
userEvent.click(getByRole('button', { name: courseSequenceMessages.pasteAsNewUnitLink.defaultMessage }));
|
||||
|
||||
const updatedCourseSectionVerticalData = cloneDeep(courseSectionVerticalMock);
|
||||
const updatedAncestorsChild = updatedCourseSectionVerticalData.xblock_info.ancestor_info.ancestors[0];
|
||||
@@ -1494,7 +1417,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 = screen.getByTestId('has-new-files-alert');
|
||||
const newFilesAlert = getByTestId('has-new-files-alert');
|
||||
|
||||
expect(within(newFilesAlert)
|
||||
.getByText(pasteNotificationsMessages.hasNewFilesTitle.defaultMessage)).toBeInTheDocument();
|
||||
@@ -1508,26 +1431,26 @@ describe('<CourseUnit />', () => {
|
||||
|
||||
userEvent.click(within(newFilesAlert).getByText(/Dismiss/i));
|
||||
|
||||
expect(screen.queryByTestId('has-new-files-alert')).toBeNull();
|
||||
expect(queryByTestId('has-new-files-alert')).toBeNull();
|
||||
});
|
||||
|
||||
it('displays a notification about conflicting errors after pasting a component', async () => {
|
||||
render(<RootWrapper />);
|
||||
const {
|
||||
queryByTestId, getByTestId, getByRole,
|
||||
} = render(<RootWrapper />);
|
||||
|
||||
axiosMock
|
||||
.onGet(getCourseSectionVerticalApiUrl(blockId))
|
||||
.onGet(getCourseUnitApiUrl(courseId))
|
||||
.reply(200, {
|
||||
...courseSectionVerticalMock,
|
||||
xblock_info: {
|
||||
...courseSectionVerticalMock.xblock_info,
|
||||
enable_copy_paste_units: true,
|
||||
},
|
||||
...courseUnitIndexMock,
|
||||
enable_copy_paste_units: true,
|
||||
});
|
||||
|
||||
await executeThunk(fetchCourseUnitQuery(courseId), store.dispatch);
|
||||
await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch);
|
||||
|
||||
userEvent.click(screen.getByRole('button', { name: sidebarMessages.actionButtonCopyUnitTitle.defaultMessage }));
|
||||
userEvent.click(screen.getByRole('button', { name: courseSequenceMessages.pasteAsNewUnitLink.defaultMessage }));
|
||||
userEvent.click(getByRole('button', { name: sidebarMessages.actionButtonCopyUnitTitle.defaultMessage }));
|
||||
userEvent.click(getByRole('button', { name: courseSequenceMessages.pasteAsNewUnitLink.defaultMessage }));
|
||||
|
||||
const updatedCourseSectionVerticalData = cloneDeep(courseSectionVerticalMock);
|
||||
const updatedAncestorsChild = updatedCourseSectionVerticalData.xblock_info.ancestor_info.ancestors[0];
|
||||
@@ -1548,7 +1471,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 = screen.getByTestId('has-conflicting-errors-alert');
|
||||
const conflictingErrorsAlert = getByTestId('has-conflicting-errors-alert');
|
||||
|
||||
expect(within(conflictingErrorsAlert)
|
||||
.getByText(pasteNotificationsMessages.hasConflictingErrorsTitle.defaultMessage)).toBeInTheDocument();
|
||||
@@ -1562,26 +1485,26 @@ describe('<CourseUnit />', () => {
|
||||
|
||||
userEvent.click(within(conflictingErrorsAlert).getByText(/Dismiss/i));
|
||||
|
||||
expect(screen.queryByTestId('has-conflicting-errors-alert')).toBeNull();
|
||||
expect(queryByTestId('has-conflicting-errors-alert')).toBeNull();
|
||||
});
|
||||
|
||||
it('displays a notification about error files after pasting a component', async () => {
|
||||
render(<RootWrapper />);
|
||||
const {
|
||||
queryByTestId, getByTestId, getByRole,
|
||||
} = render(<RootWrapper />);
|
||||
|
||||
axiosMock
|
||||
.onGet(getCourseSectionVerticalApiUrl(blockId))
|
||||
.onGet(getCourseUnitApiUrl(courseId))
|
||||
.reply(200, {
|
||||
...courseSectionVerticalMock,
|
||||
xblock_info: {
|
||||
...courseSectionVerticalMock.xblock_info,
|
||||
enable_copy_paste_units: true,
|
||||
},
|
||||
...courseUnitIndexMock,
|
||||
enable_copy_paste_units: true,
|
||||
});
|
||||
|
||||
await executeThunk(fetchCourseUnitQuery(courseId), store.dispatch);
|
||||
await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch);
|
||||
|
||||
userEvent.click(screen.getByRole('button', { name: sidebarMessages.actionButtonCopyUnitTitle.defaultMessage }));
|
||||
userEvent.click(screen.getByRole('button', { name: courseSequenceMessages.pasteAsNewUnitLink.defaultMessage }));
|
||||
userEvent.click(getByRole('button', { name: sidebarMessages.actionButtonCopyUnitTitle.defaultMessage }));
|
||||
userEvent.click(getByRole('button', { name: courseSequenceMessages.pasteAsNewUnitLink.defaultMessage }));
|
||||
|
||||
const updatedCourseSectionVerticalData = cloneDeep(courseSectionVerticalMock);
|
||||
const updatedAncestorsChild = updatedCourseSectionVerticalData.xblock_info.ancestor_info.ancestors[0];
|
||||
@@ -1602,7 +1525,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 = screen.getByTestId('has-error-files-alert');
|
||||
const errorFilesAlert = getByTestId('has-error-files-alert');
|
||||
|
||||
expect(within(errorFilesAlert)
|
||||
.getByText(pasteNotificationsMessages.hasErrorsTitle.defaultMessage)).toBeInTheDocument();
|
||||
@@ -1611,11 +1534,11 @@ describe('<CourseUnit />', () => {
|
||||
|
||||
userEvent.click(within(errorFilesAlert).getByText(/Dismiss/i));
|
||||
|
||||
expect(screen.queryByTestId('has-error-files')).toBeNull();
|
||||
expect(queryByTestId('has-error-files')).toBeNull();
|
||||
});
|
||||
|
||||
it('should hide the "Paste component" block if canPasteComponent is false', async () => {
|
||||
render(<RootWrapper />);
|
||||
const { queryByText, queryByRole } = render(<RootWrapper />);
|
||||
|
||||
axiosMock
|
||||
.onGet(getCourseVerticalChildrenApiUrl(blockId))
|
||||
@@ -1626,10 +1549,10 @@ describe('<CourseUnit />', () => {
|
||||
|
||||
await executeThunk(fetchCourseVerticalChildrenData(blockId), store.dispatch);
|
||||
|
||||
expect(screen.queryByRole('button', {
|
||||
expect(queryByRole('button', {
|
||||
name: messages.pasteButtonText.defaultMessage,
|
||||
})).not.toBeInTheDocument();
|
||||
expect(screen.queryByText(
|
||||
expect(queryByText(
|
||||
pasteComponentMessages.pasteButtonWhatsInClipboardText.defaultMessage,
|
||||
)).not.toBeInTheDocument();
|
||||
});
|
||||
@@ -1663,7 +1586,9 @@ describe('<CourseUnit />', () => {
|
||||
});
|
||||
|
||||
it('should display "Move Modal" on receive trigger message', async () => {
|
||||
render(<RootWrapper />);
|
||||
const {
|
||||
getByRole,
|
||||
} = render(<RootWrapper />);
|
||||
|
||||
await screen.findByText(unitDisplayName);
|
||||
|
||||
@@ -1677,12 +1602,15 @@ describe('<CourseUnit />', () => {
|
||||
await screen.findByText(
|
||||
moveModalMessages.moveModalTitle.defaultMessage.replace('{displayName}', requestData.title),
|
||||
);
|
||||
expect(screen.getByRole('button', { name: moveModalMessages.moveModalSubmitButton.defaultMessage })).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: moveModalMessages.moveModalCancelButton.defaultMessage })).toBeInTheDocument();
|
||||
expect(getByRole('button', { name: moveModalMessages.moveModalSubmitButton.defaultMessage })).toBeInTheDocument();
|
||||
expect(getByRole('button', { name: moveModalMessages.moveModalCancelButton.defaultMessage })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should navigates to xBlock current unit', async () => {
|
||||
render(<RootWrapper />);
|
||||
const {
|
||||
getByText,
|
||||
getByRole,
|
||||
} = render(<RootWrapper />);
|
||||
|
||||
await screen.findByText(unitDisplayName);
|
||||
|
||||
@@ -1698,7 +1626,7 @@ describe('<CourseUnit />', () => {
|
||||
);
|
||||
|
||||
const currentSection = courseOutlineInfoMock.child_info.children[1];
|
||||
const currentSectionItemBtn = screen.getByRole('button', {
|
||||
const currentSectionItemBtn = getByRole('button', {
|
||||
name: `${currentSection.display_name} ${moveModalMessages.moveModalOutlineItemCurrentLocationText.defaultMessage} ${moveModalMessages.moveModalOutlineItemViewText.defaultMessage}`,
|
||||
});
|
||||
expect(currentSectionItemBtn).toBeInTheDocument();
|
||||
@@ -1706,7 +1634,7 @@ describe('<CourseUnit />', () => {
|
||||
|
||||
await waitFor(() => {
|
||||
const currentSubsection = currentSection.child_info.children[0];
|
||||
const currentSubsectionItemBtn = screen.getByRole('button', {
|
||||
const currentSubsectionItemBtn = getByRole('button', {
|
||||
name: `${currentSubsection.display_name} ${moveModalMessages.moveModalOutlineItemCurrentLocationText.defaultMessage} ${moveModalMessages.moveModalOutlineItemViewText.defaultMessage}`,
|
||||
});
|
||||
expect(currentSubsectionItemBtn).toBeInTheDocument();
|
||||
@@ -1714,7 +1642,7 @@ describe('<CourseUnit />', () => {
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
const currentComponentLocationText = screen.getByText(
|
||||
const currentComponentLocationText = getByText(
|
||||
moveModalMessages.moveModalOutlineItemCurrentComponentLocationText.defaultMessage,
|
||||
);
|
||||
expect(currentComponentLocationText).toBeInTheDocument();
|
||||
@@ -1722,15 +1650,17 @@ describe('<CourseUnit />', () => {
|
||||
});
|
||||
|
||||
it('should allow move operation and handles it successfully', async () => {
|
||||
render(<RootWrapper />);
|
||||
const {
|
||||
getByRole,
|
||||
} = render(<RootWrapper />);
|
||||
|
||||
axiosMock
|
||||
.onPatch(postXBlockBaseApiUrl())
|
||||
.reply(200, {});
|
||||
|
||||
axiosMock
|
||||
.onGet(getCourseSectionVerticalApiUrl(blockId))
|
||||
.reply(200, courseSectionVerticalMock);
|
||||
.onGet(getCourseUnitApiUrl(blockId))
|
||||
.reply(200, courseUnitIndexMock);
|
||||
|
||||
await screen.findByText(unitDisplayName);
|
||||
|
||||
@@ -1746,7 +1676,7 @@ describe('<CourseUnit />', () => {
|
||||
);
|
||||
|
||||
const currentSection = courseOutlineInfoMock.child_info.children[1];
|
||||
const currentSectionItemBtn = screen.getByRole('button', {
|
||||
const currentSectionItemBtn = getByRole('button', {
|
||||
name: `${currentSection.display_name} ${moveModalMessages.moveModalOutlineItemCurrentLocationText.defaultMessage} ${moveModalMessages.moveModalOutlineItemViewText.defaultMessage}`,
|
||||
});
|
||||
expect(currentSectionItemBtn).toBeInTheDocument();
|
||||
@@ -1754,7 +1684,7 @@ describe('<CourseUnit />', () => {
|
||||
|
||||
const currentSubsection = currentSection.child_info.children[1];
|
||||
await waitFor(() => {
|
||||
const currentSubsectionItemBtn = screen.getByRole('button', {
|
||||
const currentSubsectionItemBtn = getByRole('button', {
|
||||
name: `${currentSubsection.display_name} ${moveModalMessages.moveModalOutlineItemViewText.defaultMessage}`,
|
||||
});
|
||||
expect(currentSubsectionItemBtn).toBeInTheDocument();
|
||||
@@ -1763,14 +1693,14 @@ describe('<CourseUnit />', () => {
|
||||
|
||||
await waitFor(() => {
|
||||
const currentUnit = currentSubsection.child_info.children[0];
|
||||
const currentUnitItemBtn = screen.getByRole('button', {
|
||||
const currentUnitItemBtn = getByRole('button', {
|
||||
name: `${currentUnit.display_name} ${moveModalMessages.moveModalOutlineItemViewText.defaultMessage}`,
|
||||
});
|
||||
expect(currentUnitItemBtn).toBeInTheDocument();
|
||||
userEvent.click(currentUnitItemBtn);
|
||||
});
|
||||
|
||||
const moveModalBtn = screen.getByRole('button', {
|
||||
const moveModalBtn = getByRole('button', {
|
||||
name: moveModalMessages.moveModalSubmitButton.defaultMessage,
|
||||
});
|
||||
expect(moveModalBtn).toBeInTheDocument();
|
||||
@@ -1784,7 +1714,10 @@ describe('<CourseUnit />', () => {
|
||||
});
|
||||
|
||||
it('should display "Move Confirmation" alert after moving and undo operations', async () => {
|
||||
render(<RootWrapper />);
|
||||
const {
|
||||
queryByRole,
|
||||
getByText,
|
||||
} = render(<RootWrapper />);
|
||||
|
||||
axiosMock
|
||||
.onPatch(postXBlockBaseApiUrl())
|
||||
@@ -1801,18 +1734,18 @@ describe('<CourseUnit />', () => {
|
||||
|
||||
simulatePostMessageEvent(messageTypes.rollbackMovedXBlock, { locator: requestData.sourceLocator });
|
||||
|
||||
const dismissButton = screen.queryByRole('button', {
|
||||
const dismissButton = queryByRole('button', {
|
||||
name: /dismiss/i, hidden: true,
|
||||
});
|
||||
const undoButton = screen.queryByRole('button', {
|
||||
const undoButton = queryByRole('button', {
|
||||
name: messages.undoMoveButton.defaultMessage, hidden: true,
|
||||
});
|
||||
const newLocationButton = screen.queryByRole('button', {
|
||||
const newLocationButton = queryByRole('button', {
|
||||
name: messages.newLocationButton.defaultMessage, hidden: true,
|
||||
});
|
||||
|
||||
expect(screen.getByText(messages.alertMoveSuccessTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(screen.getByText(`${requestData.title} has been moved`)).toBeInTheDocument();
|
||||
expect(getByText(messages.alertMoveSuccessTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByText(`${requestData.title} has been moved`)).toBeInTheDocument();
|
||||
expect(dismissButton).toBeInTheDocument();
|
||||
expect(undoButton).toBeInTheDocument();
|
||||
expect(newLocationButton).toBeInTheDocument();
|
||||
@@ -1820,9 +1753,9 @@ describe('<CourseUnit />', () => {
|
||||
userEvent.click(undoButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(messages.alertMoveCancelTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByText(messages.alertMoveCancelTitle.defaultMessage)).toBeInTheDocument();
|
||||
});
|
||||
expect(screen.getByText(
|
||||
expect(getByText(
|
||||
messages.alertMoveCancelDescription.defaultMessage.replace('{title}', requestData.title),
|
||||
)).toBeInTheDocument();
|
||||
expect(dismissButton).toBeInTheDocument();
|
||||
@@ -1831,7 +1764,9 @@ describe('<CourseUnit />', () => {
|
||||
});
|
||||
|
||||
it('should navigate to new location by button click', async () => {
|
||||
render(<RootWrapper />);
|
||||
const {
|
||||
queryByRole,
|
||||
} = render(<RootWrapper />);
|
||||
|
||||
axiosMock
|
||||
.onPatch(postXBlockBaseApiUrl())
|
||||
@@ -1846,7 +1781,7 @@ describe('<CourseUnit />', () => {
|
||||
callbackFn: requestData.callbackFn,
|
||||
}), store.dispatch);
|
||||
|
||||
const newLocationButton = screen.queryByRole('button', {
|
||||
const newLocationButton = queryByRole('button', {
|
||||
name: messages.newLocationButton.defaultMessage, hidden: true,
|
||||
});
|
||||
userEvent.click(newLocationButton);
|
||||
@@ -1859,14 +1794,16 @@ describe('<CourseUnit />', () => {
|
||||
|
||||
describe('XBlock restrict access', () => {
|
||||
it('opens xblock restrict access modal successfully', async () => {
|
||||
render(<RootWrapper />);
|
||||
const {
|
||||
getByTitle, getByTestId,
|
||||
} = render(<RootWrapper />);
|
||||
|
||||
const modalSubtitleText = configureModalMessages.restrictAccessTo.defaultMessage;
|
||||
const modalCancelBtnText = configureModalMessages.cancelButton.defaultMessage;
|
||||
const modalSaveBtnText = configureModalMessages.saveButton.defaultMessage;
|
||||
|
||||
await waitFor(() => {
|
||||
const iframe = screen.getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
|
||||
const iframe = getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
|
||||
const usageId = courseVerticalChildrenMock.children[0].block_id;
|
||||
expect(iframe).toBeInTheDocument();
|
||||
|
||||
@@ -1876,7 +1813,7 @@ describe('<CourseUnit />', () => {
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
const configureModal = screen.getByTestId('configure-modal');
|
||||
const configureModal = getByTestId('configure-modal');
|
||||
|
||||
expect(within(configureModal).getByText(modalSubtitleText)).toBeInTheDocument();
|
||||
expect(within(configureModal).getByRole('button', { name: modalCancelBtnText })).toBeInTheDocument();
|
||||
@@ -1885,10 +1822,12 @@ describe('<CourseUnit />', () => {
|
||||
});
|
||||
|
||||
it('closes xblock restrict access modal when cancel button is clicked', async () => {
|
||||
render(<RootWrapper />);
|
||||
const {
|
||||
getByTitle, queryByTestId, getByTestId,
|
||||
} = render(<RootWrapper />);
|
||||
|
||||
await waitFor(() => {
|
||||
const iframe = screen.getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
|
||||
const iframe = getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
|
||||
expect(iframe).toBeInTheDocument();
|
||||
simulatePostMessageEvent(messageTypes.manageXBlockAccess, {
|
||||
usageId: courseVerticalChildrenMock.children[0].block_id,
|
||||
@@ -1896,7 +1835,7 @@ describe('<CourseUnit />', () => {
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
const configureModal = screen.getByTestId('configure-modal');
|
||||
const configureModal = getByTestId('configure-modal');
|
||||
expect(configureModal).toBeInTheDocument();
|
||||
userEvent.click(within(configureModal).getByRole('button', {
|
||||
name: configureModalMessages.cancelButton.defaultMessage,
|
||||
@@ -1904,7 +1843,7 @@ describe('<CourseUnit />', () => {
|
||||
expect(handleConfigureSubmitMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
expect(screen.queryByTestId('configure-modal')).not.toBeInTheDocument();
|
||||
expect(queryByTestId('configure-modal')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('handles submit xblock restrict access data when save button is clicked', async () => {
|
||||
@@ -1915,13 +1854,15 @@ describe('<CourseUnit />', () => {
|
||||
})
|
||||
.reply(200, { dummy: 'value' });
|
||||
|
||||
render(<RootWrapper />);
|
||||
const {
|
||||
getByTitle, getByRole, getByTestId, queryByTestId,
|
||||
} = render(<RootWrapper />);
|
||||
|
||||
const accessGroupName1 = userPartitionInfoFormatted.selectablePartitions[0].groups[0].name;
|
||||
const accessGroupName2 = userPartitionInfoFormatted.selectablePartitions[0].groups[1].name;
|
||||
|
||||
await waitFor(() => {
|
||||
const iframe = screen.getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
|
||||
const iframe = getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
|
||||
expect(iframe).toBeInTheDocument();
|
||||
});
|
||||
|
||||
@@ -1931,13 +1872,13 @@ describe('<CourseUnit />', () => {
|
||||
});
|
||||
});
|
||||
|
||||
const configureModal = await waitFor(() => screen.getByTestId('configure-modal'));
|
||||
const configureModal = await waitFor(() => getByTestId('configure-modal'));
|
||||
expect(configureModal).toBeInTheDocument();
|
||||
|
||||
expect(within(configureModal).queryByText(accessGroupName1)).not.toBeInTheDocument();
|
||||
expect(within(configureModal).queryByText(accessGroupName2)).not.toBeInTheDocument();
|
||||
|
||||
const restrictAccessSelect = screen.getByRole('combobox', {
|
||||
const restrictAccessSelect = getByRole('combobox', {
|
||||
name: configureModalMessages.restrictAccessTo.defaultMessage,
|
||||
});
|
||||
|
||||
@@ -1967,17 +1908,17 @@ describe('<CourseUnit />', () => {
|
||||
expect(axiosMock.history.post[0].url).toBe(getXBlockBaseApiUrl(id));
|
||||
});
|
||||
|
||||
expect(screen.queryByTestId('configure-modal')).not.toBeInTheDocument();
|
||||
expect(queryByTestId('configure-modal')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
const checkLegacyEditModalOnEditMessage = async () => {
|
||||
render(<RootWrapper />);
|
||||
const { getByTitle, getByTestId } = render(<RootWrapper />);
|
||||
|
||||
await waitFor(() => {
|
||||
const editButton = screen.getByTestId('header-edit-button');
|
||||
const editButton = getByTestId('header-edit-button');
|
||||
expect(editButton).toBeInTheDocument();
|
||||
const xblocksIframe = screen.getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
|
||||
const xblocksIframe = getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
|
||||
expect(xblocksIframe).toBeInTheDocument();
|
||||
userEvent.click(editButton);
|
||||
});
|
||||
@@ -2012,6 +1953,7 @@ describe('<CourseUnit />', () => {
|
||||
|
||||
describe('Library Content page', () => {
|
||||
const newUnitId = '12345';
|
||||
const sequenceId = courseSectionVerticalMock.subsection_location;
|
||||
|
||||
beforeEach(async () => {
|
||||
axiosMock
|
||||
@@ -2028,6 +1970,20 @@ describe('<CourseUnit />', () => {
|
||||
},
|
||||
});
|
||||
await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch);
|
||||
axiosMock
|
||||
.onGet(getCourseUnitApiUrl(courseId))
|
||||
.reply(200, {
|
||||
...courseUnitIndexMock,
|
||||
category: 'library_content',
|
||||
ancestor_info: {
|
||||
...courseUnitIndexMock.ancestor_info,
|
||||
child_info: {
|
||||
...courseUnitIndexMock.ancestor_info.child_info,
|
||||
category: 'library_content',
|
||||
},
|
||||
},
|
||||
});
|
||||
await executeThunk(fetchCourseUnitQuery(courseId), store.dispatch);
|
||||
});
|
||||
|
||||
it('navigates to library content page on receive window event', async () => {
|
||||
@@ -2047,8 +2003,8 @@ describe('<CourseUnit />', () => {
|
||||
findByTestId,
|
||||
} = 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 currentSectionName = courseUnitIndexMock.ancestor_info.ancestors[1].display_name;
|
||||
const currentSubSectionName = courseUnitIndexMock.ancestor_info.ancestors[1].display_name;
|
||||
|
||||
const unitHeaderTitle = await findByTestId('unit-header-title');
|
||||
await findByText(unitDisplayName);
|
||||
@@ -2076,6 +2032,7 @@ describe('<CourseUnit />', () => {
|
||||
|
||||
describe('Split Test Content page', () => {
|
||||
const newUnitId = '12345';
|
||||
const sequenceId = courseSectionVerticalMock.subsection_location;
|
||||
|
||||
beforeEach(async () => {
|
||||
axiosMock
|
||||
@@ -2092,6 +2049,20 @@ describe('<CourseUnit />', () => {
|
||||
},
|
||||
});
|
||||
await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch);
|
||||
axiosMock
|
||||
.onGet(getCourseUnitApiUrl(courseId))
|
||||
.reply(200, {
|
||||
...courseUnitIndexMock,
|
||||
category: 'split_test',
|
||||
ancestor_info: {
|
||||
...courseUnitIndexMock.ancestor_info,
|
||||
child_info: {
|
||||
...courseUnitIndexMock.ancestor_info.child_info,
|
||||
category: 'split_test',
|
||||
},
|
||||
},
|
||||
});
|
||||
await executeThunk(fetchCourseUnitQuery(courseId), store.dispatch);
|
||||
});
|
||||
|
||||
it('navigates to split test content page on receive window event', async () => {
|
||||
@@ -2134,65 +2105,46 @@ describe('<CourseUnit />', () => {
|
||||
});
|
||||
|
||||
it('should render split test content page correctly', async () => {
|
||||
render(<RootWrapper />);
|
||||
const {
|
||||
getByText,
|
||||
getByRole,
|
||||
queryByRole,
|
||||
getByTestId,
|
||||
queryByText,
|
||||
} = 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 currentSectionName = courseUnitIndexMock.ancestor_info.ancestors[1].display_name;
|
||||
const currentSubSectionName = courseUnitIndexMock.ancestor_info.ancestors[1].display_name;
|
||||
const helpLinkUrl = 'https://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/latest/developing_course/course_components.html#components-that-contain-other-components';
|
||||
|
||||
waitFor(() => {
|
||||
const unitHeaderTitle = screen.getByTestId('unit-header-title');
|
||||
expect(screen.getByText(unitDisplayName)).toBeInTheDocument();
|
||||
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(screen.getByRole('button', { name: currentSectionName })).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: currentSubSectionName })).toBeInTheDocument();
|
||||
expect(getByRole('button', { name: currentSectionName })).toBeInTheDocument();
|
||||
expect(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(queryByRole('heading', { name: addComponentMessages.title.defaultMessage })).not.toBeInTheDocument();
|
||||
expect(queryByRole('button', { name: headerNavigationsMessages.viewLiveButton.defaultMessage })).not.toBeInTheDocument();
|
||||
expect(queryByRole('button', { name: headerNavigationsMessages.previewButton.defaultMessage })).not.toBeInTheDocument();
|
||||
|
||||
expect(screen.queryByRole('heading', { name: /unit tags/i })).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole('heading', { name: /unit location/i })).not.toBeInTheDocument();
|
||||
expect(queryByRole('heading', { name: /unit tags/i })).not.toBeInTheDocument();
|
||||
expect(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,
|
||||
},
|
||||
{ query: queryByRole, type: 'heading', name: sidebarMessages.sidebarSplitTestAddComponentTitle.defaultMessage },
|
||||
{ query: queryByText, name: sidebarMessages.sidebarSplitTestSelectComponentType.defaultMessage.replaceAll('{bold_tag}', '') },
|
||||
{ query: queryByText, name: sidebarMessages.sidebarSplitTestComponentAdded.defaultMessage },
|
||||
{ query: queryByRole, type: 'heading', name: sidebarMessages.sidebarSplitTestEditComponentTitle.defaultMessage },
|
||||
{ query: queryByText, name: sidebarMessages.sidebarSplitTestEditComponentInstruction.defaultMessage.replaceAll('{bold_tag}', '') },
|
||||
{ query: queryByRole, type: 'heading', name: sidebarMessages.sidebarSplitTestReorganizeComponentTitle.defaultMessage },
|
||||
{ query: queryByText, name: sidebarMessages.sidebarSplitTestReorganizeComponentInstruction.defaultMessage },
|
||||
{ query: queryByText, name: sidebarMessages.sidebarSplitTestReorganizeGroupsInstruction.defaultMessage },
|
||||
{ query: queryByRole, type: 'heading', name: sidebarMessages.sidebarSplitTestExperimentComponentTitle.defaultMessage },
|
||||
{ query: queryByText, name: sidebarMessages.sidebarSplitTestExperimentComponentInstruction.defaultMessage },
|
||||
{ query: queryByRole, type: 'link', name: sidebarMessages.sidebarSplitTestLearnMoreLinkLabel.defaultMessage },
|
||||
];
|
||||
|
||||
sidebarContent.forEach(({ query, type, name }) => {
|
||||
@@ -2200,7 +2152,7 @@ describe('<CourseUnit />', () => {
|
||||
});
|
||||
|
||||
expect(
|
||||
screen.queryByRole('link', { name: sidebarMessages.sidebarSplitTestLearnMoreLinkLabel.defaultMessage }),
|
||||
queryByRole('link', { name: sidebarMessages.sidebarSplitTestLearnMoreLinkLabel.defaultMessage }),
|
||||
).toHaveAttribute('href', helpLinkUrl);
|
||||
});
|
||||
});
|
||||
@@ -2213,7 +2165,7 @@ describe('<CourseUnit />', () => {
|
||||
});
|
||||
|
||||
it('renders and navigates to the new HTML XBlock editor after xblock duplicating', async () => {
|
||||
render(<RootWrapper />);
|
||||
const { getByTitle } = render(<RootWrapper />);
|
||||
const updatedCourseVerticalChildrenMock = JSON.parse(JSON.stringify(courseVerticalChildrenMock));
|
||||
const targetBlockId = updatedCourseVerticalChildrenMock.children[1].block_id;
|
||||
|
||||
@@ -2222,13 +2174,6 @@ describe('<CourseUnit />', () => {
|
||||
? { ...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);
|
||||
@@ -2236,7 +2181,7 @@ describe('<CourseUnit />', () => {
|
||||
await executeThunk(fetchCourseVerticalChildrenData(blockId), store.dispatch);
|
||||
|
||||
await waitFor(() => {
|
||||
const iframe = screen.getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
|
||||
const iframe = getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
|
||||
expect(iframe).toBeInTheDocument();
|
||||
simulatePostMessageEvent(messageTypes.currentXBlockId, {
|
||||
id: targetBlockId,
|
||||
@@ -2250,58 +2195,4 @@ describe('<CourseUnit />', () => {
|
||||
.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();
|
||||
});
|
||||
});
|
||||
|
||||
1123
src/course-unit/__mocks__/courseUnitIndex.js
Normal file
1123
src/course-unit/__mocks__/courseUnitIndex.js
Normal file
@@ -0,0 +1,1123 @@
|
||||
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: '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: [
|
||||
{
|
||||
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: '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,
|
||||
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,3 +1,4 @@
|
||||
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,14 +1,13 @@
|
||||
import { useCallback, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useIntl, FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
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';
|
||||
@@ -17,8 +16,6 @@ import { ComponentPicker } from '../../library-authoring/component-picker';
|
||||
import { messageTypes } from '../constants';
|
||||
import { useIframe } from '../../generic/hooks/context/hooks';
|
||||
import { useEventListener } from '../../generic/hooks';
|
||||
import VideoSelectorPage from '../../editors/VideoSelectorPage';
|
||||
import EditorPage from '../../editors/EditorPage';
|
||||
|
||||
const AddComponent = ({
|
||||
parentLocator,
|
||||
@@ -27,6 +24,7 @@ const AddComponent = ({
|
||||
addComponentTemplateData,
|
||||
handleCreateNewCourseXBlock,
|
||||
}) => {
|
||||
const navigate = useNavigate();
|
||||
const intl = useIntl();
|
||||
const [isOpenAdvanced, openAdvanced, closeAdvanced] = useToggle(false);
|
||||
const [isOpenHtml, openHtml, closeHtml] = useToggle(false);
|
||||
@@ -34,17 +32,10 @@ const AddComponent = ({
|
||||
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, payload } }) => {
|
||||
if (type === messageTypes.showMultipleComponentPicker) {
|
||||
@@ -63,12 +54,6 @@ const AddComponent = ({
|
||||
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,
|
||||
@@ -86,27 +71,11 @@ const AddComponent = ({
|
||||
handleCreateNewCourseXBlock({ type, parentLocator: blockId });
|
||||
break;
|
||||
case COMPONENT_TYPES.problem:
|
||||
handleCreateNewCourseXBlock({ type, parentLocator: blockId }, ({ courseKey, 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();
|
||||
}
|
||||
},
|
||||
);
|
||||
handleCreateNewCourseXBlock({ type, parentLocator: blockId }, ({ courseKey, locator }) => {
|
||||
localStorage.setItem('createXBlockLastYPosition', window.scrollY);
|
||||
navigate(`/course/${courseKey}/editor/${type}/${locator}`);
|
||||
});
|
||||
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).
|
||||
@@ -130,11 +99,9 @@ const AddComponent = ({
|
||||
type,
|
||||
boilerplate: moduleName,
|
||||
parentLocator: blockId,
|
||||
}, /* istanbul ignore next */ ({ courseKey, locator }) => {
|
||||
setCourseId(courseKey);
|
||||
setBlockType(type);
|
||||
setNewBlockId(locator);
|
||||
showXBlockEditorModal();
|
||||
}, ({ courseKey, locator }) => {
|
||||
localStorage.setItem('createXBlockLastYPosition', window.scrollY);
|
||||
navigate(`/course/${courseKey}/editor/html/${locator}`);
|
||||
});
|
||||
break;
|
||||
default:
|
||||
@@ -234,38 +201,6 @@ const AddComponent = ({
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -273,6 +208,10 @@ const AddComponent = ({
|
||||
return null;
|
||||
};
|
||||
|
||||
AddComponent.defaultProps = {
|
||||
addComponentTemplateData: {},
|
||||
};
|
||||
|
||||
AddComponent.propTypes = {
|
||||
isSplitTestType: PropTypes.bool.isRequired,
|
||||
isUnitVerticalType: PropTypes.bool.isRequired,
|
||||
|
||||
@@ -31,11 +31,6 @@ 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',
|
||||
|
||||
@@ -5,11 +5,11 @@ import {
|
||||
} from '../../testUtils';
|
||||
|
||||
import { executeThunk } from '../../utils';
|
||||
import { getCourseSectionVerticalApiUrl } from '../data/api';
|
||||
import { getCourseSectionVerticalApiUrl, getCourseUnitApiUrl } from '../data/api';
|
||||
import { getApiWaffleFlagsUrl } from '../../data/api';
|
||||
import { fetchWaffleFlags } from '../../data/thunks';
|
||||
import { fetchCourseSectionVerticalData } from '../data/thunk';
|
||||
import { courseSectionVerticalMock } from '../__mocks__';
|
||||
import { fetchCourseSectionVerticalData, fetchCourseUnitQuery } from '../data/thunk';
|
||||
import { courseSectionVerticalMock, courseUnitIndexMock } from '../__mocks__';
|
||||
import Breadcrumbs from './Breadcrumbs';
|
||||
|
||||
let axiosMock;
|
||||
@@ -43,9 +43,9 @@ describe('<Breadcrumbs />', () => {
|
||||
reduxStore = mocks.reduxStore;
|
||||
|
||||
axiosMock
|
||||
.onGet(getCourseSectionVerticalApiUrl(courseId))
|
||||
.reply(200, courseSectionVerticalMock);
|
||||
await executeThunk(fetchCourseSectionVerticalData(courseId), reduxStore.dispatch);
|
||||
.onGet(getCourseUnitApiUrl(courseId))
|
||||
.reply(200, courseUnitIndexMock);
|
||||
await executeThunk(fetchCourseUnitQuery(courseId), reduxStore.dispatch);
|
||||
axiosMock
|
||||
.onGet(getCourseSectionVerticalApiUrl(courseId))
|
||||
.reply(200, courseSectionVerticalMock);
|
||||
|
||||
@@ -12,19 +12,16 @@ export function useSequenceNavigationMetadata(courseId, currentSequenceId, curre
|
||||
const isLastUnit = !nextUrl;
|
||||
const sequenceIds = useSelector(getSequenceIds);
|
||||
const sequenceIndex = sequenceIds.indexOf(currentSequenceId);
|
||||
let unitIndex = sequence?.unitIds.indexOf(currentUnitId);
|
||||
const unitIndex = sequence.unitIds.indexOf(currentUnitId);
|
||||
|
||||
const nextSequenceId = sequenceIndex < sequenceIds.length - 1 ? sequenceIds[sequenceIndex + 1] : null;
|
||||
const previousSequenceId = sequenceIndex > 0 ? sequenceIds[sequenceIndex - 1] : null;
|
||||
if (!unitIndex) {
|
||||
// Handle case where unitIndex is not found
|
||||
unitIndex = 0;
|
||||
}
|
||||
|
||||
let nextLink;
|
||||
const nextIndex = unitIndex + 1;
|
||||
|
||||
if (nextIndex < sequence?.unitIds.length) {
|
||||
const nextUnitId = sequence?.unitIds[nextIndex];
|
||||
if (nextIndex < sequence.unitIds.length) {
|
||||
const nextUnitId = sequence.unitIds[nextIndex];
|
||||
nextLink = `/course/${courseId}/container/${nextUnitId}/${currentSequenceId}`;
|
||||
} else if (nextSequenceId) {
|
||||
const pathToNextUnit = decodeURIComponent(nextUrl);
|
||||
@@ -35,7 +32,7 @@ export function useSequenceNavigationMetadata(courseId, currentSequenceId, curre
|
||||
const previousIndex = unitIndex - 1;
|
||||
|
||||
if (previousIndex >= 0) {
|
||||
const previousUnitId = sequence?.unitIds[previousIndex];
|
||||
const previousUnitId = sequence.unitIds[previousIndex];
|
||||
previousLink = `/course/${courseId}/container/${previousUnitId}/${currentSequenceId}`;
|
||||
} else if (previousSequenceId) {
|
||||
const pathToPreviousUnit = decodeURIComponent(prevUrl);
|
||||
|
||||
@@ -35,7 +35,7 @@ const SequenceNavigation = ({
|
||||
|
||||
const shouldDisplayNotificationTriggerInSequence = useWindowSize().width < breakpoints.small.minWidth;
|
||||
const renderUnitButtons = () => {
|
||||
if (sequence.unitIds.length === 0 || unitId === null) {
|
||||
if (sequence.unitIds?.length === 0 || unitId === null) {
|
||||
return (
|
||||
<div style={{ flexBasis: '100%', minWidth: 0, borderBottom: 'solid 1px #EAEAEA' }} />
|
||||
);
|
||||
@@ -43,7 +43,7 @@ const SequenceNavigation = ({
|
||||
|
||||
return (
|
||||
<SequenceNavigationTabs
|
||||
unitIds={sequence?.unitIds || []}
|
||||
unitIds={sequence.unitIds || []}
|
||||
unitId={unitId}
|
||||
handleCreateNewCourseXBlock={handleCreateNewCourseXBlock}
|
||||
showPasteUnit={showPasteUnit}
|
||||
|
||||
@@ -3,10 +3,11 @@ import { camelCaseObject, getConfig } from '@edx/frontend-platform';
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
|
||||
import { PUBLISH_TYPES } from '../constants';
|
||||
import { isUnitReadOnly, normalizeCourseSectionVerticalData, updateXBlockBlockIdToId } from './utils';
|
||||
import { normalizeCourseSectionVerticalData, updateXBlockBlockIdToId } from './utils';
|
||||
|
||||
const getStudioBaseUrl = () => getConfig().STUDIO_BASE_URL;
|
||||
|
||||
export const getCourseUnitApiUrl = (itemId) => `${getStudioBaseUrl()}/xblock/container/${itemId}`;
|
||||
export const getXBlockBaseApiUrl = (itemId) => `${getStudioBaseUrl()}/xblock/${itemId}`;
|
||||
export const getCourseSectionVerticalApiUrl = (itemId) => `${getStudioBaseUrl()}/api/contentstore/v1/container_handler/${itemId}`;
|
||||
export const getCourseVerticalChildrenApiUrl = (itemId) => `${getStudioBaseUrl()}/api/contentstore/v1/container/vertical/${itemId}/children`;
|
||||
@@ -14,6 +15,18 @@ export const getCourseOutlineInfoUrl = (courseId) => `${getStudioBaseUrl()}/cour
|
||||
export const postXBlockBaseApiUrl = () => `${getStudioBaseUrl()}/xblock/`;
|
||||
export const libraryBlockChangesUrl = (blockId) => `${getStudioBaseUrl()}/api/contentstore/v2/downstreams/${blockId}/sync`;
|
||||
|
||||
/**
|
||||
* Get course unit.
|
||||
* @param {string} unitId
|
||||
* @returns {Promise<Object>}
|
||||
*/
|
||||
export async function getCourseUnitData(unitId) {
|
||||
const { data } = await getAuthenticatedHttpClient()
|
||||
.get(getCourseUnitApiUrl(unitId));
|
||||
|
||||
return camelCaseObject(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Edit course unit display name.
|
||||
* @param {string} unitId
|
||||
@@ -32,18 +45,15 @@ export async function editUnitDisplayName(unitId, displayName) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch vertical block data from the container_handler endpoint.
|
||||
* Get an object containing course section vertical data.
|
||||
* @param {string} unitId
|
||||
* @returns {Promise<Object>}
|
||||
*/
|
||||
export async function getVerticalData(unitId) {
|
||||
export async function getCourseSectionVerticalData(unitId) {
|
||||
const { data } = await getAuthenticatedHttpClient()
|
||||
.get(getCourseSectionVerticalApiUrl(unitId));
|
||||
|
||||
const courseSectionVerticalData = normalizeCourseSectionVerticalData(data);
|
||||
courseSectionVerticalData.xblockInfo.readOnly = isUnitReadOnly(courseSectionVerticalData.xblockInfo);
|
||||
|
||||
return courseSectionVerticalData;
|
||||
return normalizeCourseSectionVerticalData(data);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { RequestStatus } from 'CourseAuthoring/data/constants';
|
||||
|
||||
export const getCourseUnitData = (state) => state.courseUnit.courseSectionVertical.xblockInfo ?? {};
|
||||
export const getCourseUnitData = (state) => state.courseUnit.unit;
|
||||
export const getCanEdit = (state) => state.courseUnit.canEdit;
|
||||
export const getStaticFileNotices = (state) => state.courseUnit.staticFileNotices;
|
||||
export const getCourseUnit = (state) => state.courseUnit;
|
||||
@@ -16,7 +16,7 @@ export const getCourseVerticalChildren = (state) => state.courseUnit.courseVerti
|
||||
export const getCourseOutlineInfo = (state) => state.courseUnit.courseOutlineInfo;
|
||||
export const getCourseOutlineInfoLoadingStatus = (state) => state.courseUnit.courseOutlineInfoLoadingStatus;
|
||||
export const getMovedXBlockParams = (state) => state.courseUnit.movedXBlockParams;
|
||||
export const getLoadingStatuses = (state) => state.courseUnit.loadingStatus;
|
||||
const getLoadingStatuses = (state) => state.courseUnit.loadingStatus;
|
||||
export const getIsLoading = createSelector(
|
||||
[getLoadingStatuses],
|
||||
loadingStatus => Object.values(loadingStatus)
|
||||
|
||||
@@ -12,9 +12,11 @@ const slice = createSlice({
|
||||
isTitleEditFormOpen: false,
|
||||
canEdit: true,
|
||||
loadingStatus: {
|
||||
fetchUnitLoadingStatus: RequestStatus.IN_PROGRESS,
|
||||
courseSectionVerticalLoadingStatus: RequestStatus.IN_PROGRESS,
|
||||
courseVerticalChildrenLoadingStatus: RequestStatus.IN_PROGRESS,
|
||||
},
|
||||
unit: {},
|
||||
courseSectionVertical: {},
|
||||
courseVerticalChildren: { children: [], isPublished: true },
|
||||
staticFileNotices: {},
|
||||
@@ -29,6 +31,15 @@ const slice = createSlice({
|
||||
},
|
||||
},
|
||||
reducers: {
|
||||
fetchCourseItemSuccess: (state, { payload }) => {
|
||||
state.unit = payload;
|
||||
},
|
||||
updateLoadingCourseUnitStatus: (state, { payload }) => {
|
||||
state.loadingStatus = {
|
||||
...state.loadingStatus,
|
||||
fetchUnitLoadingStatus: payload.status,
|
||||
};
|
||||
},
|
||||
updateQueryPendingStatus: (state, { payload }) => {
|
||||
state.isQueryPending = payload;
|
||||
},
|
||||
@@ -70,6 +81,12 @@ const slice = createSlice({
|
||||
createUnitXblockLoadingStatus: payload.status,
|
||||
};
|
||||
},
|
||||
addNewUnitStatus: (state, { payload }) => {
|
||||
state.loadingStatus = {
|
||||
...state.loadingStatus,
|
||||
fetchUnitLoadingStatus: payload.status,
|
||||
};
|
||||
},
|
||||
updateCourseVerticalChildren: (state, { payload }) => {
|
||||
state.courseVerticalChildren = payload;
|
||||
},
|
||||
@@ -92,6 +109,8 @@ const slice = createSlice({
|
||||
});
|
||||
|
||||
export const {
|
||||
fetchCourseItemSuccess,
|
||||
updateLoadingCourseUnitStatus,
|
||||
updateSavingStatus,
|
||||
updateModel,
|
||||
fetchSequenceRequest,
|
||||
|
||||
@@ -10,8 +10,9 @@ import { NOTIFICATION_MESSAGES } from '../../constants';
|
||||
import { updateModel, updateModels } from '../../generic/model-store';
|
||||
import { messageTypes } from '../constants';
|
||||
import {
|
||||
getCourseUnitData,
|
||||
editUnitDisplayName,
|
||||
getVerticalData,
|
||||
getCourseSectionVerticalData,
|
||||
createCourseXblock,
|
||||
getCourseVerticalChildren,
|
||||
handleCourseUnitVisibilityAndData,
|
||||
@@ -21,6 +22,8 @@ import {
|
||||
patchUnitItem,
|
||||
} from './api';
|
||||
import {
|
||||
updateLoadingCourseUnitStatus,
|
||||
fetchCourseItemSuccess,
|
||||
updateSavingStatus,
|
||||
fetchSequenceRequest,
|
||||
fetchSequenceFailure,
|
||||
@@ -37,13 +40,29 @@ import {
|
||||
} from './slice';
|
||||
import { getNotificationMessage } from './utils';
|
||||
|
||||
export function fetchCourseUnitQuery(courseId) {
|
||||
return async (dispatch) => {
|
||||
dispatch(updateLoadingCourseUnitStatus({ status: RequestStatus.IN_PROGRESS }));
|
||||
|
||||
try {
|
||||
const courseUnit = await getCourseUnitData(courseId);
|
||||
dispatch(fetchCourseItemSuccess(courseUnit));
|
||||
dispatch(updateLoadingCourseUnitStatus({ status: RequestStatus.SUCCESSFUL }));
|
||||
return true;
|
||||
} catch (error) {
|
||||
dispatch(updateLoadingCourseUnitStatus({ status: RequestStatus.FAILED }));
|
||||
return false;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function fetchCourseSectionVerticalData(courseId, sequenceId) {
|
||||
return async (dispatch) => {
|
||||
dispatch(updateLoadingCourseSectionVerticalDataStatus({ status: RequestStatus.IN_PROGRESS }));
|
||||
dispatch(fetchSequenceRequest({ sequenceId }));
|
||||
|
||||
try {
|
||||
const courseSectionVerticalData = await getVerticalData(courseId);
|
||||
const courseSectionVerticalData = await getCourseSectionVerticalData(courseId);
|
||||
dispatch(fetchCourseSectionVerticalDataSuccess(courseSectionVerticalData));
|
||||
dispatch(updateLoadingCourseSectionVerticalDataStatus({ status: RequestStatus.SUCCESSFUL }));
|
||||
dispatch(updateModel({
|
||||
@@ -74,7 +93,8 @@ export function editCourseItemQuery(itemId, displayName, sequenceId) {
|
||||
try {
|
||||
await editUnitDisplayName(itemId, displayName).then(async (result) => {
|
||||
if (result) {
|
||||
const courseSectionVerticalData = await getVerticalData(itemId);
|
||||
const courseUnit = await getCourseUnitData(itemId);
|
||||
const courseSectionVerticalData = await getCourseSectionVerticalData(itemId);
|
||||
dispatch(fetchCourseSectionVerticalDataSuccess(courseSectionVerticalData));
|
||||
dispatch(updateLoadingCourseSectionVerticalDataStatus({ status: RequestStatus.SUCCESSFUL }));
|
||||
dispatch(updateModel({
|
||||
@@ -86,6 +106,7 @@ export function editCourseItemQuery(itemId, displayName, sequenceId) {
|
||||
models: courseSectionVerticalData.units || [],
|
||||
}));
|
||||
dispatch(fetchSequenceSuccess({ sequenceId }));
|
||||
dispatch(fetchCourseItemSuccess(courseUnit));
|
||||
dispatch(hideProcessingNotification());
|
||||
dispatch(updateSavingStatus({ status: RequestStatus.SUCCESSFUL }));
|
||||
}
|
||||
@@ -124,8 +145,8 @@ export function editCourseUnitVisibilityAndData(
|
||||
if (callback) {
|
||||
callback();
|
||||
}
|
||||
const courseSectionVerticalData = await getVerticalData(blockId);
|
||||
dispatch(fetchCourseSectionVerticalDataSuccess(courseSectionVerticalData));
|
||||
const courseUnit = await getCourseUnitData(blockId);
|
||||
dispatch(fetchCourseItemSuccess(courseUnit));
|
||||
const courseVerticalChildrenData = await getCourseVerticalChildren(blockId);
|
||||
dispatch(updateCourseVerticalChildren(courseVerticalChildrenData));
|
||||
dispatch(hideProcessingNotification());
|
||||
@@ -152,7 +173,7 @@ export function createNewCourseXBlock(body, callback, blockId, sendMessageToIfra
|
||||
if (result) {
|
||||
const formattedResult = camelCaseObject(result);
|
||||
if (body.category === 'vertical') {
|
||||
const courseSectionVerticalData = await getVerticalData(formattedResult.locator);
|
||||
const courseSectionVerticalData = await getCourseSectionVerticalData(formattedResult.locator);
|
||||
dispatch(fetchCourseSectionVerticalDataSuccess(courseSectionVerticalData));
|
||||
}
|
||||
if (body.stagedContent) {
|
||||
@@ -172,8 +193,8 @@ export function createNewCourseXBlock(body, callback, blockId, sendMessageToIfra
|
||||
sendMessageToIframe(messageTypes.addXBlock, { data: result });
|
||||
}
|
||||
const currentBlockId = body.category === 'vertical' ? formattedResult.locator : blockId;
|
||||
const courseSectionVerticalData = await getVerticalData(currentBlockId);
|
||||
dispatch(fetchCourseSectionVerticalDataSuccess(courseSectionVerticalData));
|
||||
const courseUnit = await getCourseUnitData(currentBlockId);
|
||||
dispatch(fetchCourseItemSuccess(courseUnit));
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
@@ -218,8 +239,8 @@ export function deleteUnitItemQuery(itemId, xblockId, sendMessageToIframe) {
|
||||
try {
|
||||
await deleteUnitItem(xblockId);
|
||||
sendMessageToIframe(messageTypes.completeXBlockDeleting, { locator: xblockId });
|
||||
const courseSectionVerticalData = await getVerticalData(itemId);
|
||||
dispatch(fetchCourseSectionVerticalDataSuccess(courseSectionVerticalData));
|
||||
const courseUnit = await getCourseUnitData(itemId);
|
||||
dispatch(fetchCourseItemSuccess(courseUnit));
|
||||
dispatch(hideProcessingNotification());
|
||||
dispatch(updateSavingStatus({ status: RequestStatus.SUCCESSFUL }));
|
||||
} catch (error) {
|
||||
@@ -237,10 +258,8 @@ export function duplicateUnitItemQuery(itemId, xblockId, callback) {
|
||||
try {
|
||||
const { courseKey, locator } = await duplicateUnitItem(itemId, xblockId);
|
||||
callback(courseKey, locator);
|
||||
const courseSectionVerticalData = await getVerticalData(itemId);
|
||||
dispatch(fetchCourseSectionVerticalDataSuccess(courseSectionVerticalData));
|
||||
const courseVerticalChildrenData = await getCourseVerticalChildren(itemId);
|
||||
dispatch(updateCourseVerticalChildren(courseVerticalChildrenData));
|
||||
const courseUnit = await getCourseUnitData(itemId);
|
||||
dispatch(fetchCourseItemSuccess(courseUnit));
|
||||
dispatch(hideProcessingNotification());
|
||||
dispatch(updateSavingStatus({ status: RequestStatus.SUCCESSFUL }));
|
||||
} catch (error) {
|
||||
@@ -294,8 +313,8 @@ export function patchUnitItemQuery({
|
||||
dispatch(updateCourseOutlineInfoLoadingStatus({ status: RequestStatus.IN_PROGRESS }));
|
||||
callbackFn(sourceLocator);
|
||||
try {
|
||||
const courseSectionVerticalData = await getVerticalData(currentParentLocator);
|
||||
dispatch(fetchCourseSectionVerticalDataSuccess(courseSectionVerticalData));
|
||||
const courseUnit = await getCourseUnitData(currentParentLocator);
|
||||
dispatch(fetchCourseItemSuccess(courseUnit));
|
||||
} catch (error) {
|
||||
handleResponseErrors(error, dispatch, updateSavingStatus);
|
||||
}
|
||||
@@ -313,8 +332,8 @@ export function updateCourseUnitSidebar(itemId) {
|
||||
dispatch(showProcessingNotification(NOTIFICATION_MESSAGES.saving));
|
||||
|
||||
try {
|
||||
const courseSectionVerticalData = await getVerticalData(itemId);
|
||||
dispatch(fetchCourseSectionVerticalDataSuccess(courseSectionVerticalData));
|
||||
const courseUnit = await getCourseUnitData(itemId);
|
||||
dispatch(fetchCourseItemSuccess(courseUnit));
|
||||
dispatch(hideProcessingNotification());
|
||||
dispatch(updateSavingStatus({ status: RequestStatus.SUCCESSFUL }));
|
||||
} catch (error) {
|
||||
|
||||
@@ -84,15 +84,3 @@ export const updateXBlockBlockIdToId = (data) => {
|
||||
|
||||
return updatedData;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns whether the given Unit should be read-only.
|
||||
*
|
||||
* Units sourced from libraries are read-only (temporary, for Teak).
|
||||
*
|
||||
* @param {object} unit - uses the 'upstreamInfo' object if found.
|
||||
* @returns {boolean} True if readOnly, False if editable.
|
||||
*/
|
||||
export const isUnitReadOnly = ({ upstreamInfo }) => (
|
||||
upstreamInfo && upstreamInfo.upstreamRef && upstreamInfo.upstreamRef.startsWith('lct:')
|
||||
);
|
||||
|
||||
@@ -34,8 +34,6 @@ const HeaderTitle = ({
|
||||
COURSE_BLOCK_NAMES.component.id,
|
||||
].includes(currentItemData.category);
|
||||
|
||||
const readOnly = !!currentItemData.readOnly;
|
||||
|
||||
const onConfigureSubmit = (...arg) => {
|
||||
handleConfigureSubmit(currentItemData.id, ...arg, closeConfigureModal);
|
||||
};
|
||||
@@ -82,7 +80,6 @@ const HeaderTitle = ({
|
||||
className="ml-1 flex-shrink-0"
|
||||
iconAs={EditIcon}
|
||||
onClick={handleTitleEdit}
|
||||
disabled={readOnly}
|
||||
/>
|
||||
<IconButton
|
||||
alt={intl.formatMessage(messages.altButtonSettings)}
|
||||
@@ -105,8 +102,6 @@ const HeaderTitle = ({
|
||||
);
|
||||
};
|
||||
|
||||
export default HeaderTitle;
|
||||
|
||||
HeaderTitle.propTypes = {
|
||||
unitTitle: PropTypes.string.isRequired,
|
||||
isTitleEditFormOpen: PropTypes.bool.isRequired,
|
||||
@@ -114,3 +109,5 @@ HeaderTitle.propTypes = {
|
||||
handleTitleEditSubmit: PropTypes.func.isRequired,
|
||||
handleConfigureSubmit: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default HeaderTitle;
|
||||
|
||||
@@ -8,9 +8,9 @@ import { initializeMockApp } from '@edx/frontend-platform';
|
||||
|
||||
import initializeStore from '../../store';
|
||||
import { executeThunk } from '../../utils';
|
||||
import { getCourseSectionVerticalApiUrl } from '../data/api';
|
||||
import { fetchCourseSectionVerticalData } from '../data/thunk';
|
||||
import { courseSectionVerticalMock } from '../__mocks__';
|
||||
import { getCourseUnitApiUrl } from '../data/api';
|
||||
import { fetchCourseUnitQuery } from '../data/thunk';
|
||||
import { courseUnitIndexMock } from '../__mocks__';
|
||||
import HeaderTitle from './HeaderTitle';
|
||||
import messages from './messages';
|
||||
|
||||
@@ -52,9 +52,9 @@ describe('<HeaderTitle />', () => {
|
||||
store = initializeStore();
|
||||
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
|
||||
axiosMock
|
||||
.onGet(getCourseSectionVerticalApiUrl(blockId))
|
||||
.reply(200, courseSectionVerticalMock);
|
||||
await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch);
|
||||
.onGet(getCourseUnitApiUrl(blockId))
|
||||
.reply(200, courseUnitIndexMock);
|
||||
await executeThunk(fetchCourseUnitQuery(blockId), store.dispatch);
|
||||
});
|
||||
|
||||
it('render HeaderTitle component correctly', () => {
|
||||
@@ -72,31 +72,8 @@ describe('<HeaderTitle />', () => {
|
||||
|
||||
expect(getByRole('textbox', { name: messages.ariaLabelButtonEdit.defaultMessage })).toBeInTheDocument();
|
||||
expect(getByRole('textbox', { name: messages.ariaLabelButtonEdit.defaultMessage })).toHaveValue(unitTitle);
|
||||
expect(getByRole('button', { name: messages.altButtonEdit.defaultMessage })).toBeEnabled();
|
||||
expect(getByRole('button', { name: messages.altButtonSettings.defaultMessage })).toBeEnabled();
|
||||
});
|
||||
|
||||
it('Units sourced from upstream show a disabled edit button', async () => {
|
||||
// Override mock unit with one sourced from an upstream library
|
||||
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
|
||||
axiosMock
|
||||
.onGet(getCourseSectionVerticalApiUrl(blockId))
|
||||
.reply(200, {
|
||||
...courseSectionVerticalMock,
|
||||
xblock_info: {
|
||||
...courseSectionVerticalMock.xblock_info,
|
||||
upstreamInfo: {
|
||||
...courseSectionVerticalMock.xblock_info.upstreamInfo,
|
||||
upstreamRef: 'lct:org:lib:unit:unit-1',
|
||||
},
|
||||
},
|
||||
});
|
||||
await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch);
|
||||
|
||||
const { getByRole } = renderComponent();
|
||||
|
||||
expect(getByRole('button', { name: messages.altButtonEdit.defaultMessage })).toBeDisabled();
|
||||
expect(getByRole('button', { name: messages.altButtonSettings.defaultMessage })).toBeEnabled();
|
||||
expect(getByRole('button', { name: messages.altButtonEdit.defaultMessage })).toBeInTheDocument();
|
||||
expect(getByRole('button', { name: messages.altButtonSettings.defaultMessage })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('calls toggle edit title form by clicking on Edit button', () => {
|
||||
@@ -126,19 +103,16 @@ describe('<HeaderTitle />', () => {
|
||||
|
||||
it('displays a visibility message with the selected groups for the unit', async () => {
|
||||
axiosMock
|
||||
.onGet(getCourseSectionVerticalApiUrl(blockId))
|
||||
.onGet(getCourseUnitApiUrl(blockId))
|
||||
.reply(200, {
|
||||
...courseSectionVerticalMock,
|
||||
xblock_info: {
|
||||
...courseSectionVerticalMock.xblock_info,
|
||||
user_partition_info: {
|
||||
...courseSectionVerticalMock.xblock_info.user_partition_info,
|
||||
selected_partition_index: 1,
|
||||
selected_groups_label: 'Visibility group 1',
|
||||
},
|
||||
...courseUnitIndexMock,
|
||||
user_partition_info: {
|
||||
...courseUnitIndexMock.user_partition_info,
|
||||
selected_partition_index: 1,
|
||||
selected_groups_label: 'Visibility group 1',
|
||||
},
|
||||
});
|
||||
await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch);
|
||||
await executeThunk(fetchCourseUnitQuery(blockId), store.dispatch);
|
||||
const { getByText } = renderComponent();
|
||||
const visibilityMessage = messages.definedVisibilityMessage.defaultMessage
|
||||
.replace('{selectedGroupsLabel}', 'Visibility group 1');
|
||||
@@ -150,15 +124,12 @@ describe('<HeaderTitle />', () => {
|
||||
|
||||
it('displays a visibility message with the selected groups for some of xblock', async () => {
|
||||
axiosMock
|
||||
.onGet(getCourseSectionVerticalApiUrl(blockId))
|
||||
.onGet(getCourseUnitApiUrl(blockId))
|
||||
.reply(200, {
|
||||
...courseSectionVerticalMock,
|
||||
xblock_info: {
|
||||
...courseSectionVerticalMock.xblock_info,
|
||||
has_partition_group_components: true,
|
||||
},
|
||||
...courseUnitIndexMock,
|
||||
has_partition_group_components: true,
|
||||
});
|
||||
await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch);
|
||||
await executeThunk(fetchCourseUnitQuery(blockId), store.dispatch);
|
||||
const { getByText } = renderComponent();
|
||||
|
||||
await waitFor(() => {
|
||||
|
||||
@@ -18,10 +18,10 @@ import {
|
||||
editCourseItemQuery,
|
||||
editCourseUnitVisibilityAndData,
|
||||
fetchCourseSectionVerticalData,
|
||||
fetchCourseUnitQuery,
|
||||
fetchCourseVerticalChildrenData,
|
||||
getCourseOutlineInfoQuery,
|
||||
patchUnitItemQuery,
|
||||
updateCourseUnitSidebar,
|
||||
} from './data/thunk';
|
||||
import {
|
||||
getCanEdit,
|
||||
@@ -35,7 +35,6 @@ import {
|
||||
getSavingStatus,
|
||||
getSequenceStatus,
|
||||
getStaticFileNotices,
|
||||
getLoadingStatuses,
|
||||
} from './data/selectors';
|
||||
import {
|
||||
changeEditTitleFormOpen,
|
||||
@@ -52,7 +51,6 @@ export const useCourseUnit = ({ courseId, blockId }) => {
|
||||
const [isMoveModalOpen, openMoveModal, closeMoveModal] = useToggle(false);
|
||||
|
||||
const courseUnit = useSelector(getCourseUnitData);
|
||||
const courseUnitLoadingStatus = useSelector(getLoadingStatuses);
|
||||
const savingStatus = useSelector(getSavingStatus);
|
||||
const isLoading = useSelector(getIsLoading);
|
||||
const errorMessage = useSelector(getErrorMessage);
|
||||
@@ -198,6 +196,7 @@ export const useCourseUnit = ({ courseId, blockId }) => {
|
||||
}, [savingStatus]);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(fetchCourseUnitQuery(blockId));
|
||||
dispatch(fetchCourseSectionVerticalData(blockId, sequenceId));
|
||||
dispatch(fetchCourseVerticalChildrenData(blockId, isSplitTestType));
|
||||
handleNavigate(sequenceId);
|
||||
@@ -216,27 +215,9 @@ export const useCourseUnit = ({ courseId, blockId }) => {
|
||||
}
|
||||
}, [isMoveModalOpen]);
|
||||
|
||||
useEffect(() => {
|
||||
const handlePageRefreshUsingStorage = (event) => {
|
||||
// ignoring tests for if block, because it triggers when someone
|
||||
// edits the component using editor which has a separate store
|
||||
/* istanbul ignore next */
|
||||
if (event.key === 'courseRefreshTriggerOnComponentEditSave') {
|
||||
dispatch(updateCourseUnitSidebar(blockId));
|
||||
localStorage.removeItem(event.key);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('storage', handlePageRefreshUsingStorage);
|
||||
return () => {
|
||||
window.removeEventListener('storage', handlePageRefreshUsingStorage);
|
||||
};
|
||||
}, [blockId, sequenceId, isSplitTestType]);
|
||||
|
||||
return {
|
||||
sequenceId,
|
||||
courseUnit,
|
||||
courseUnitLoadingStatus,
|
||||
unitTitle,
|
||||
unitCategory,
|
||||
errorMessage,
|
||||
|
||||
@@ -43,16 +43,6 @@ const messages = defineMessages({
|
||||
defaultMessage: 'Take me to the new location',
|
||||
description: 'Text for the button allowing users to navigate to the new location after an XBlock has been moved',
|
||||
},
|
||||
alertLibraryUnitReadOnlyText: {
|
||||
id: 'course-authoring.course-unit.alert.read-only.text',
|
||||
defaultMessage: 'This unit can only be edited from the {link}.',
|
||||
description: 'Text of the alert when the unit is read only because is a library unit',
|
||||
},
|
||||
alertLibraryUnitReadOnlyLinkText: {
|
||||
id: 'course-authoring.course-unit.alert.read-only.link.text',
|
||||
defaultMessage: 'library',
|
||||
description: 'Text of the link in the alert when the unit is read only because is a library unit',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
|
||||
@@ -12,12 +12,13 @@ import IframePreviewLibraryXBlockChanges, { LibraryChangesMessageData } from '.'
|
||||
import { messageTypes } from '../constants';
|
||||
import { libraryBlockChangesUrl } from '../data/api';
|
||||
import { ToastActionData } from '../../generic/toast-context';
|
||||
import { getLibraryBlockMetadataUrl } from '../../library-authoring/data/api';
|
||||
|
||||
const usageKey = 'some-id';
|
||||
const defaultEventData: LibraryChangesMessageData = {
|
||||
displayName: 'Test block',
|
||||
downstreamBlockId: usageKey,
|
||||
upstreamBlockId: 'lct:org:lib1:unit:1',
|
||||
upstreamBlockId: 'some-lib-id',
|
||||
upstreamBlockVersionSynced: 1,
|
||||
isVertical: false,
|
||||
};
|
||||
@@ -65,7 +66,7 @@ describe('<IframePreviewLibraryXBlockChanges />', () => {
|
||||
expect(await screen.findByRole('tab', { name: 'Old version' })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders default displayName for units with no displayName', async () => {
|
||||
it('renders displayName for units', async () => {
|
||||
render({ ...defaultEventData, isVertical: true, displayName: '' });
|
||||
|
||||
expect(await screen.findByText('Preview changes: Unit')).toBeInTheDocument();
|
||||
@@ -77,6 +78,15 @@ describe('<IframePreviewLibraryXBlockChanges />', () => {
|
||||
expect(await screen.findByText('Preview changes: Component')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders both new and old title if they are different', async () => {
|
||||
axiosMock.onGet(getLibraryBlockMetadataUrl(defaultEventData.upstreamBlockId)).reply(200, {
|
||||
displayName: 'New test block',
|
||||
});
|
||||
render();
|
||||
|
||||
expect(await screen.findByText('Preview changes: Test block -> New test block')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('accept changes works', async () => {
|
||||
axiosMock.onPost(libraryBlockChangesUrl(usageKey)).reply(200, {});
|
||||
render();
|
||||
@@ -85,10 +95,7 @@ describe('<IframePreviewLibraryXBlockChanges />', () => {
|
||||
const acceptBtn = await screen.findByRole('button', { name: 'Accept changes' });
|
||||
userEvent.click(acceptBtn);
|
||||
await waitFor(() => {
|
||||
expect(mockSendMessageToIframe).toHaveBeenCalledWith(
|
||||
messageTypes.completeXBlockEditing,
|
||||
{ locator: usageKey },
|
||||
);
|
||||
expect(mockSendMessageToIframe).toHaveBeenCalledWith(messageTypes.refreshXBlock, null);
|
||||
expect(axiosMock.history.post.length).toEqual(1);
|
||||
expect(axiosMock.history.post[0].url).toEqual(libraryBlockChangesUrl(usageKey));
|
||||
});
|
||||
@@ -103,6 +110,7 @@ describe('<IframePreviewLibraryXBlockChanges />', () => {
|
||||
const acceptBtn = await screen.findByRole('button', { name: 'Accept changes' });
|
||||
userEvent.click(acceptBtn);
|
||||
await waitFor(() => {
|
||||
expect(mockSendMessageToIframe).not.toHaveBeenCalledWith(messageTypes.refreshXBlock, null);
|
||||
expect(axiosMock.history.post.length).toEqual(1);
|
||||
expect(axiosMock.history.post[0].url).toEqual(libraryBlockChangesUrl(usageKey));
|
||||
});
|
||||
@@ -120,10 +128,7 @@ describe('<IframePreviewLibraryXBlockChanges />', () => {
|
||||
const ignoreConfirmBtn = await screen.findByRole('button', { name: 'Ignore' });
|
||||
userEvent.click(ignoreConfirmBtn);
|
||||
await waitFor(() => {
|
||||
expect(mockSendMessageToIframe).toHaveBeenCalledWith(
|
||||
messageTypes.completeXBlockEditing,
|
||||
{ locator: usageKey },
|
||||
);
|
||||
expect(mockSendMessageToIframe).toHaveBeenCalledWith(messageTypes.refreshXBlock, null);
|
||||
expect(axiosMock.history.delete.length).toEqual(1);
|
||||
expect(axiosMock.history.delete[0].url).toEqual(libraryBlockChangesUrl(usageKey));
|
||||
});
|
||||
|
||||
@@ -1,21 +1,20 @@
|
||||
import { useCallback, useContext, useState } from 'react';
|
||||
import React, { useCallback, useContext, useState } from 'react';
|
||||
import {
|
||||
ActionRow, Button, ModalDialog, useToggle,
|
||||
} from '@openedx/paragon';
|
||||
import { Warning } from '@openedx/paragon/icons';
|
||||
import { useIntl, FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { useEventListener } from '../../generic/hooks';
|
||||
import { messageTypes } from '../constants';
|
||||
import CompareChangesWidget from '../../library-authoring/component-comparison/CompareChangesWidget';
|
||||
import { useAcceptLibraryBlockChanges, useIgnoreLibraryBlockChanges } from '../data/apiHooks';
|
||||
import AlertMessage from '../../generic/alert-message';
|
||||
import { useIframe } from '../../generic/hooks/context/hooks';
|
||||
import DeleteModal from '../../generic/delete-modal/DeleteModal';
|
||||
import messages from './messages';
|
||||
import { ToastContext } from '../../generic/toast-context';
|
||||
import LoadingButton from '../../generic/loading-button';
|
||||
import Loading from '../../generic/Loading';
|
||||
import { useLibraryBlockMetadata } from '../../library-authoring/data/apiHooks';
|
||||
|
||||
export interface LibraryChangesMessageData {
|
||||
displayName: string,
|
||||
@@ -26,10 +25,11 @@ export interface LibraryChangesMessageData {
|
||||
}
|
||||
|
||||
export interface PreviewLibraryXBlockChangesProps {
|
||||
blockData: LibraryChangesMessageData,
|
||||
blockData?: LibraryChangesMessageData,
|
||||
isModalOpen: boolean,
|
||||
closeModal: () => void,
|
||||
postChange: (accept: boolean) => void,
|
||||
alertNode?: React.ReactNode,
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -41,16 +41,34 @@ export const PreviewLibraryXBlockChanges = ({
|
||||
isModalOpen,
|
||||
closeModal,
|
||||
postChange,
|
||||
alertNode,
|
||||
}: PreviewLibraryXBlockChangesProps) => {
|
||||
const { showToast } = useContext(ToastContext);
|
||||
const intl = useIntl();
|
||||
|
||||
// ignore changes confirmation modal toggle.
|
||||
const [isConfirmModalOpen, openConfirmModal, closeConfirmModal] = useToggle(false);
|
||||
const { data: componentMetadata } = useLibraryBlockMetadata(blockData?.upstreamBlockId);
|
||||
|
||||
const acceptChangesMutation = useAcceptLibraryBlockChanges();
|
||||
const ignoreChangesMutation = useIgnoreLibraryBlockChanges();
|
||||
|
||||
const getTitle = useCallback(() => {
|
||||
const oldName = blockData?.displayName;
|
||||
const newName = componentMetadata?.displayName;
|
||||
|
||||
if (!oldName) {
|
||||
if (blockData?.isVertical) {
|
||||
return intl.formatMessage(messages.defaultUnitTitle);
|
||||
}
|
||||
return intl.formatMessage(messages.defaultComponentTitle);
|
||||
}
|
||||
if (oldName === newName || !newName) {
|
||||
return intl.formatMessage(messages.title, { blockTitle: oldName });
|
||||
}
|
||||
return intl.formatMessage(messages.diffTitle, { oldName, newName });
|
||||
}, [blockData, componentMetadata]);
|
||||
|
||||
const getBody = useCallback(() => {
|
||||
if (!blockData) {
|
||||
return <Loading />;
|
||||
@@ -60,7 +78,6 @@ export const PreviewLibraryXBlockChanges = ({
|
||||
usageKey={blockData.upstreamBlockId}
|
||||
oldVersion={blockData.upstreamBlockVersionSynced || 'published'}
|
||||
newVersion="published"
|
||||
isContainer={blockData.isVertical}
|
||||
/>
|
||||
);
|
||||
}, [blockData]);
|
||||
@@ -84,21 +101,12 @@ export const PreviewLibraryXBlockChanges = ({
|
||||
}
|
||||
}, [blockData]);
|
||||
|
||||
const defaultTitle = intl.formatMessage(
|
||||
blockData.isVertical
|
||||
? messages.defaultUnitTitle
|
||||
: messages.defaultComponentTitle,
|
||||
);
|
||||
const title = blockData.displayName
|
||||
? intl.formatMessage(messages.title, { blockTitle: blockData?.displayName })
|
||||
: defaultTitle;
|
||||
|
||||
return (
|
||||
<ModalDialog
|
||||
isOpen={isModalOpen}
|
||||
onClose={closeModal}
|
||||
size="xl"
|
||||
title={title}
|
||||
title={getTitle()}
|
||||
className="lib-preview-xblock-changes-modal"
|
||||
hasCloseButton
|
||||
isFullscreenOnMobile
|
||||
@@ -106,16 +114,11 @@ export const PreviewLibraryXBlockChanges = ({
|
||||
>
|
||||
<ModalDialog.Header>
|
||||
<ModalDialog.Title>
|
||||
{title}
|
||||
{getTitle()}
|
||||
</ModalDialog.Title>
|
||||
</ModalDialog.Header>
|
||||
<ModalDialog.Body>
|
||||
<AlertMessage
|
||||
show
|
||||
variant="warning"
|
||||
icon={Warning}
|
||||
title={intl.formatMessage(messages.olderVersionPreviewAlert)}
|
||||
/>
|
||||
{alertNode}
|
||||
{getBody()}
|
||||
</ModalDialog.Body>
|
||||
<ModalDialog.Footer>
|
||||
@@ -176,18 +179,12 @@ const IframePreviewLibraryXBlockChanges = () => {
|
||||
|
||||
useEventListener('message', receiveMessage);
|
||||
|
||||
if (!blockData) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const blockPayload = { locator: blockData.downstreamBlockId };
|
||||
|
||||
return (
|
||||
<PreviewLibraryXBlockChanges
|
||||
blockData={blockData}
|
||||
isModalOpen={isModalOpen}
|
||||
closeModal={closeModal}
|
||||
postChange={() => sendMessageToIframe(messageTypes.completeXBlockEditing, blockPayload)}
|
||||
postChange={() => sendMessageToIframe(messageTypes.refreshXBlock, null)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -6,6 +6,11 @@ const messages = defineMessages({
|
||||
defaultMessage: 'Preview changes: {blockTitle}',
|
||||
description: 'Preview changes modal title text',
|
||||
},
|
||||
diffTitle: {
|
||||
id: 'authoring.course-unit.preview-changes.modal-diff-title',
|
||||
defaultMessage: 'Preview changes: {oldName} -> {newName}',
|
||||
description: 'Preview changes modal title text',
|
||||
},
|
||||
defaultUnitTitle: {
|
||||
id: 'authoring.course-unit.preview-changes.modal-default-unit-title',
|
||||
defaultMessage: 'Preview changes: Unit',
|
||||
@@ -56,11 +61,6 @@ const messages = defineMessages({
|
||||
defaultMessage: 'Ignore',
|
||||
description: 'Preview changes confirmation dialog confirm button text when user clicks on ignore changes.',
|
||||
},
|
||||
olderVersionPreviewAlert: {
|
||||
id: 'course-authoring.review-tab.preview.old-version-alert',
|
||||
defaultMessage: 'The old version preview is the previous library version',
|
||||
description: 'Alert message stating that older version in preview is of library block',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useToggle } from '@openedx/paragon';
|
||||
import { InfoOutline as InfoOutlineIcon } from '@openedx/paragon/icons';
|
||||
@@ -11,19 +12,14 @@ import { getCourseUnitData } from '../data/selectors';
|
||||
import messages from './messages';
|
||||
import ModalNotification from '../../generic/modal-notification';
|
||||
|
||||
interface PublishControlsProps {
|
||||
blockId?: string,
|
||||
}
|
||||
|
||||
const PublishControls = ({ blockId }: PublishControlsProps) => {
|
||||
const unitData = useSelector(getCourseUnitData);
|
||||
const PublishControls = ({ blockId }) => {
|
||||
const {
|
||||
title,
|
||||
locationId,
|
||||
releaseLabel,
|
||||
visibilityState,
|
||||
visibleToStaffOnly,
|
||||
} = useCourseUnitData(unitData);
|
||||
} = useCourseUnitData(useSelector(getCourseUnitData));
|
||||
const intl = useIntl();
|
||||
const { sendMessageToIframe } = useIframe();
|
||||
|
||||
@@ -94,4 +90,12 @@ const PublishControls = ({ blockId }: PublishControlsProps) => {
|
||||
);
|
||||
};
|
||||
|
||||
PublishControls.propTypes = {
|
||||
blockId: PropTypes.string,
|
||||
};
|
||||
|
||||
PublishControls.defaultProps = {
|
||||
blockId: null,
|
||||
};
|
||||
|
||||
export default PublishControls;
|
||||
@@ -1,3 +1,4 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { Button } from '@openedx/paragon';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
@@ -7,15 +8,7 @@ import { getCanEdit, getCourseUnitData } from '../../../data/selectors';
|
||||
import { useClipboard } from '../../../../generic/clipboard';
|
||||
import messages from '../../messages';
|
||||
|
||||
interface ActionButtonsProps {
|
||||
openDiscardModal: () => void,
|
||||
handlePublishing: () => void,
|
||||
}
|
||||
|
||||
const ActionButtons = ({
|
||||
openDiscardModal,
|
||||
handlePublishing,
|
||||
}: ActionButtonsProps) => {
|
||||
const ActionButtons = ({ openDiscardModal, handlePublishing }) => {
|
||||
const intl = useIntl();
|
||||
const {
|
||||
id,
|
||||
@@ -29,12 +22,7 @@ const ActionButtons = ({
|
||||
return (
|
||||
<>
|
||||
{(!published || hasChanges) && (
|
||||
<Button
|
||||
size="sm"
|
||||
className="mt-3.5"
|
||||
variant="outline-primary"
|
||||
onClick={handlePublishing}
|
||||
>
|
||||
<Button size="sm" className="mt-3.5" variant="outline-primary" onClick={handlePublishing}>
|
||||
{intl.formatMessage(messages.actionButtonPublishTitle)}
|
||||
</Button>
|
||||
)}
|
||||
@@ -64,4 +52,9 @@ const ActionButtons = ({
|
||||
);
|
||||
};
|
||||
|
||||
ActionButtons.propTypes = {
|
||||
openDiscardModal: PropTypes.func.isRequired,
|
||||
handlePublishing: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default ActionButtons;
|
||||
@@ -10,10 +10,10 @@ import userEvent from '@testing-library/user-event';
|
||||
import initializeStore from '../../../../store';
|
||||
import { executeThunk } from '../../../../utils';
|
||||
import { clipboardUnit } from '../../../../__mocks__';
|
||||
import { getCourseSectionVerticalApiUrl } from '../../../data/api';
|
||||
import { getCourseUnitApiUrl } from '../../../data/api';
|
||||
import { getClipboardUrl } from '../../../../generic/data/api';
|
||||
import { fetchCourseSectionVerticalData } from '../../../data/thunk';
|
||||
import { courseSectionVerticalMock } from '../../../__mocks__';
|
||||
import { fetchCourseUnitQuery } from '../../../data/thunk';
|
||||
import { courseUnitIndexMock } from '../../../__mocks__';
|
||||
import messages from '../../messages';
|
||||
import ActionButtons from './ActionButtons';
|
||||
|
||||
@@ -46,14 +46,8 @@ describe('<ActionButtons />', () => {
|
||||
store = initializeStore();
|
||||
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
|
||||
axiosMock
|
||||
.onGet(getCourseSectionVerticalApiUrl(courseId))
|
||||
.reply(200, {
|
||||
...courseSectionVerticalMock,
|
||||
xblock_info: {
|
||||
...courseSectionVerticalMock.xblock_info,
|
||||
enable_copy_paste_units: true,
|
||||
},
|
||||
});
|
||||
.onGet(getCourseUnitApiUrl(courseId))
|
||||
.reply(200, { ...courseUnitIndexMock, enable_copy_paste_units: true });
|
||||
axiosMock
|
||||
.onPost(getClipboardUrl())
|
||||
.reply(200, clipboardUnit);
|
||||
@@ -63,7 +57,7 @@ describe('<ActionButtons />', () => {
|
||||
|
||||
queryClient = new QueryClient();
|
||||
|
||||
await executeThunk(fetchCourseSectionVerticalData(courseId), store.dispatch);
|
||||
await executeThunk(fetchCourseUnitQuery(courseId), store.dispatch);
|
||||
});
|
||||
|
||||
it('render ActionButtons component with Copy to clipboard', () => {
|
||||
@@ -80,9 +74,7 @@ describe('<ActionButtons />', () => {
|
||||
|
||||
userEvent.click(copyXBlockBtn);
|
||||
expect(axiosMock.history.post.length).toBe(1);
|
||||
expect(axiosMock.history.post[0].data).toBe(
|
||||
JSON.stringify({ usage_key: courseSectionVerticalMock.xblock_info.id }),
|
||||
);
|
||||
expect(axiosMock.history.post[0].data).toBe(JSON.stringify({ usage_key: courseUnitIndexMock.id }));
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { Form } from '@openedx/paragon';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
@@ -9,15 +10,7 @@ import { PUBLISH_TYPES } from '../../../constants';
|
||||
import { getVisibilityTitle } from '../../utils';
|
||||
import messages from '../../messages';
|
||||
|
||||
interface UnitVisibilityInfoProps {
|
||||
openVisibleModal: () => void,
|
||||
visibleToStaffOnly: boolean,
|
||||
}
|
||||
|
||||
const UnitVisibilityInfo = ({
|
||||
openVisibleModal,
|
||||
visibleToStaffOnly,
|
||||
}: UnitVisibilityInfoProps) => {
|
||||
const UnitVisibilityInfo = ({ openVisibleModal, visibleToStaffOnly }) => {
|
||||
const intl = useIntl();
|
||||
const { blockId } = useParams();
|
||||
const dispatch = useDispatch();
|
||||
@@ -66,4 +59,9 @@ const UnitVisibilityInfo = ({
|
||||
);
|
||||
};
|
||||
|
||||
UnitVisibilityInfo.propTypes = {
|
||||
openVisibleModal: PropTypes.func.isRequired,
|
||||
visibleToStaffOnly: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
export default UnitVisibilityInfo;
|
||||
@@ -1,3 +1,4 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import { Card, Stack } from '@openedx/paragon';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
@@ -5,23 +6,14 @@ import messages from '../../messages';
|
||||
import UnitVisibilityInfo from './UnitVisibilityInfo';
|
||||
import ActionButtons from './ActionButtons';
|
||||
|
||||
interface SidebarFooterProps {
|
||||
locationId?: string,
|
||||
displayUnitLocation?: boolean,
|
||||
openDiscardModal: () => void,
|
||||
openVisibleModal: () => void,
|
||||
handlePublishing: () => void,
|
||||
visibleToStaffOnly: boolean,
|
||||
}
|
||||
|
||||
const SidebarFooter = ({
|
||||
locationId,
|
||||
openVisibleModal,
|
||||
handlePublishing,
|
||||
openDiscardModal,
|
||||
visibleToStaffOnly,
|
||||
displayUnitLocation = false,
|
||||
}: SidebarFooterProps) => {
|
||||
displayUnitLocation,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
|
||||
return (
|
||||
@@ -48,4 +40,18 @@ const SidebarFooter = ({
|
||||
);
|
||||
};
|
||||
|
||||
SidebarFooter.propTypes = {
|
||||
locationId: PropTypes.string,
|
||||
displayUnitLocation: PropTypes.bool,
|
||||
openDiscardModal: PropTypes.func,
|
||||
openVisibleModal: PropTypes.func,
|
||||
handlePublishing: PropTypes.func,
|
||||
visibleToStaffOnly: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
SidebarFooter.defaultProps = {
|
||||
displayUnitLocation: false,
|
||||
locationId: null,
|
||||
};
|
||||
|
||||
export default SidebarFooter;
|
||||
@@ -99,4 +99,4 @@ export const getIconVariant = (visibilityState, published, hasChanges) => {
|
||||
* @param {string} id - The course unit ID.
|
||||
* @returns {string} The clear course unit ID extracted from the provided data.
|
||||
*/
|
||||
export const extractCourseUnitId = (id) => id?.match(/block@(.+)$/)[1];
|
||||
export const extractCourseUnitId = (id) => id.match(/block@(.+)$/)[1];
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
export type UseMessageHandlersTypes = {
|
||||
courseId: string;
|
||||
navigate: (path: string) => void;
|
||||
dispatch: (action: any) => void;
|
||||
setIframeOffset: (height: number) => void;
|
||||
handleDeleteXBlock: (usageId: string) => void;
|
||||
handleScrollToXBlock: (scrollOffset: number) => void;
|
||||
handleDuplicateXBlock: (usageId: string) => void;
|
||||
handleEditXBlock: (blockType: string, usageId: string) => void;
|
||||
handleDuplicateXBlock: (blockType: string, usageId: string) => void;
|
||||
handleManageXBlockAccess: (usageId: string) => void;
|
||||
handleShowLegacyEditXBlockModal: (id: string) => void;
|
||||
handleCloseLegacyEditorXBlockModal: () => void;
|
||||
@@ -14,6 +14,7 @@ export type UseMessageHandlersTypes = {
|
||||
handleOpenManageTagsModal: (id: string) => void;
|
||||
handleShowProcessingNotification: (variant: string) => void;
|
||||
handleHideProcessingNotification: () => void;
|
||||
handleRedirectToXBlockEditPage: (payload: { type: string, locator: string }) => void;
|
||||
};
|
||||
|
||||
export type MessageHandlersTypes = Record<string, (payload: any) => void>;
|
||||
|
||||
@@ -16,6 +16,7 @@ import { MessageHandlersTypes, UseMessageHandlersTypes } from './types';
|
||||
*/
|
||||
export const useMessageHandlers = ({
|
||||
courseId,
|
||||
navigate,
|
||||
dispatch,
|
||||
setIframeOffset,
|
||||
handleDeleteXBlock,
|
||||
@@ -29,15 +30,15 @@ export const useMessageHandlers = ({
|
||||
handleOpenManageTagsModal,
|
||||
handleShowProcessingNotification,
|
||||
handleHideProcessingNotification,
|
||||
handleEditXBlock,
|
||||
handleRedirectToXBlockEditPage,
|
||||
}: UseMessageHandlersTypes): MessageHandlersTypes => {
|
||||
const { copyToClipboard } = useClipboard();
|
||||
|
||||
return useMemo(() => ({
|
||||
[messageTypes.copyXBlock]: ({ usageId }) => copyToClipboard(usageId),
|
||||
[messageTypes.deleteXBlock]: ({ usageId }) => handleDeleteXBlock(usageId),
|
||||
[messageTypes.newXBlockEditor]: ({ blockType, usageId }) => handleEditXBlock(blockType, usageId),
|
||||
[messageTypes.duplicateXBlock]: ({ usageId }) => handleDuplicateXBlock(usageId),
|
||||
[messageTypes.newXBlockEditor]: ({ blockType, usageId }) => navigate(`/course/${courseId}/editor/${blockType}/${usageId}`),
|
||||
[messageTypes.duplicateXBlock]: ({ blockType, usageId }) => handleDuplicateXBlock(blockType, usageId),
|
||||
[messageTypes.manageXBlockAccess]: ({ usageId }) => handleManageXBlockAccess(usageId),
|
||||
[messageTypes.scrollToXBlock]: debounce(({ scrollOffset }) => handleScrollToXBlock(scrollOffset), 1000),
|
||||
[messageTypes.toggleCourseXBlockDropdown]: ({
|
||||
@@ -51,14 +52,9 @@ export const useMessageHandlers = ({
|
||||
[messageTypes.openManageTags]: (payload) => handleOpenManageTagsModal(payload.contentId),
|
||||
[messageTypes.addNewComponent]: () => handleShowProcessingNotification(NOTIFICATION_MESSAGES.adding),
|
||||
[messageTypes.pasteNewComponent]: () => handleShowProcessingNotification(NOTIFICATION_MESSAGES.pasting),
|
||||
[messageTypes.copyXBlockLegacy]: /* istanbul ignore next */ () => handleShowProcessingNotification(
|
||||
NOTIFICATION_MESSAGES.copying,
|
||||
),
|
||||
[messageTypes.copyXBlockLegacy]: () => handleShowProcessingNotification(NOTIFICATION_MESSAGES.copying),
|
||||
[messageTypes.hideProcessingNotification]: handleHideProcessingNotification,
|
||||
[messageTypes.handleRedirectToXBlockEditPage]: /* istanbul ignore next */ (payload) => handleEditXBlock(
|
||||
payload.type,
|
||||
payload.locator,
|
||||
),
|
||||
[messageTypes.handleRedirectToXBlockEditPage]: (payload) => handleRedirectToXBlockEditPage(payload),
|
||||
}), [
|
||||
courseId,
|
||||
handleDeleteXBlock,
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import {
|
||||
FC, useEffect, useState, useMemo, useCallback,
|
||||
} from 'react';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { useToggle, Sheet, StandardModal } from '@openedx/paragon';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useToggle, Sheet } from '@openedx/paragon';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import {
|
||||
hideProcessingNotification,
|
||||
@@ -13,9 +13,9 @@ import {
|
||||
import DeleteModal from '../../generic/delete-modal/DeleteModal';
|
||||
import ConfigureModal from '../../generic/configure-modal/ConfigureModal';
|
||||
import ModalIframe from '../../generic/modal-iframe';
|
||||
import { getWaffleFlags } from '../../data/selectors';
|
||||
import { IFRAME_FEATURE_POLICY } from '../../constants';
|
||||
import ContentTagsDrawer from '../../content-tags-drawer/ContentTagsDrawer';
|
||||
import supportedEditors from '../../editors/supportedEditors';
|
||||
import { useIframe } from '../../generic/hooks/context/hooks';
|
||||
import {
|
||||
fetchCourseSectionVerticalData,
|
||||
@@ -35,29 +35,16 @@ import messages from './messages';
|
||||
import { useIframeBehavior } from '../../generic/hooks/useIframeBehavior';
|
||||
import { useIframeContent } from '../../generic/hooks/useIframeContent';
|
||||
import { useIframeMessages } from '../../generic/hooks/useIframeMessages';
|
||||
import VideoSelectorPage from '../../editors/VideoSelectorPage';
|
||||
import EditorPage from '../../editors/EditorPage';
|
||||
import { RequestStatus } from '../../data/constants';
|
||||
|
||||
const XBlockContainerIframe: FC<XBlockContainerIframeProps> = ({
|
||||
courseId,
|
||||
blockId,
|
||||
unitXBlockActions,
|
||||
courseVerticalChildren,
|
||||
handleConfigureSubmit,
|
||||
isUnitVerticalType,
|
||||
courseUnitLoadingStatus,
|
||||
courseId, blockId, unitXBlockActions, courseVerticalChildren, handleConfigureSubmit, isUnitVerticalType,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const dispatch = useDispatch();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [isDeleteModalOpen, openDeleteModal, closeDeleteModal] = useToggle(false);
|
||||
const [isConfigureModalOpen, openConfigureModal, closeConfigureModal] = useToggle(false);
|
||||
const [isVideoSelectorModalOpen, showVideoSelectorModal, closeVideoSelectorModal] = useToggle();
|
||||
const [isXBlockEditorModalOpen, showXBlockEditorModal, closeXBlockEditorModal] = useToggle();
|
||||
const [blockType, setBlockType] = useState<string>('');
|
||||
const { useVideoGalleryFlow, useReactMarkdownEditor } = useSelector(getWaffleFlags);
|
||||
const [newBlockId, setNewBlockId] = useState<string>('');
|
||||
const [accessManagedXBlockData, setAccessManagedXBlockData] = useState<AccessManagedXBlockDataTypes | {}>({});
|
||||
const [iframeOffset, setIframeOffset] = useState(0);
|
||||
const [deleteXBlockId, setDeleteXBlockId] = useState<string | null>(null);
|
||||
@@ -77,44 +64,14 @@ const XBlockContainerIframe: FC<XBlockContainerIframeProps> = ({
|
||||
setIframeRef(iframeRef);
|
||||
}, [setIframeRef]);
|
||||
|
||||
useEffect(() => {
|
||||
const iframe = iframeRef?.current;
|
||||
if (!iframe) { return undefined; }
|
||||
|
||||
const handleIframeLoad = () => {
|
||||
if (courseUnitLoadingStatus.fetchUnitLoadingStatus === RequestStatus.FAILED) {
|
||||
window.location.reload();
|
||||
}
|
||||
};
|
||||
|
||||
iframe.addEventListener('load', handleIframeLoad);
|
||||
|
||||
return () => {
|
||||
iframe.removeEventListener('load', handleIframeLoad);
|
||||
};
|
||||
}, [iframeRef]);
|
||||
|
||||
const onXBlockSave = useCallback(/* istanbul ignore next */ () => {
|
||||
closeXBlockEditorModal();
|
||||
closeVideoSelectorModal();
|
||||
sendMessageToIframe(messageTypes.refreshXBlock, null);
|
||||
}, [closeXBlockEditorModal, closeVideoSelectorModal, sendMessageToIframe]);
|
||||
|
||||
const handleEditXBlock = useCallback((type: string, id: string) => {
|
||||
setBlockType(type);
|
||||
setNewBlockId(id);
|
||||
if (type === 'video' && useVideoGalleryFlow) {
|
||||
showVideoSelectorModal();
|
||||
} else {
|
||||
showXBlockEditorModal();
|
||||
}
|
||||
}, [showVideoSelectorModal, showXBlockEditorModal]);
|
||||
|
||||
const handleDuplicateXBlock = useCallback(
|
||||
(usageId: string) => {
|
||||
(blockType: string, usageId: string) => {
|
||||
unitXBlockActions.handleDuplicate(usageId);
|
||||
if (supportedEditors[blockType]) {
|
||||
navigate(`/course/${courseId}/editor/${blockType}/${usageId}`);
|
||||
}
|
||||
},
|
||||
[unitXBlockActions, courseId],
|
||||
[unitXBlockActions, courseId, navigate],
|
||||
);
|
||||
|
||||
const handleDeleteXBlock = (usageId: string) => {
|
||||
@@ -190,8 +147,13 @@ const XBlockContainerIframe: FC<XBlockContainerIframeProps> = ({
|
||||
dispatch(hideProcessingNotification());
|
||||
};
|
||||
|
||||
const handleRedirectToXBlockEditPage = (payload: { type: string, locator: string }) => {
|
||||
navigate(`/course/${courseId}/editor/${payload.type}/${payload.locator}`);
|
||||
};
|
||||
|
||||
const messageHandlers = useMessageHandlers({
|
||||
courseId,
|
||||
navigate,
|
||||
dispatch,
|
||||
setIframeOffset,
|
||||
handleDeleteXBlock,
|
||||
@@ -205,7 +167,7 @@ const XBlockContainerIframe: FC<XBlockContainerIframeProps> = ({
|
||||
handleOpenManageTagsModal,
|
||||
handleShowProcessingNotification,
|
||||
handleHideProcessingNotification,
|
||||
handleEditXBlock,
|
||||
handleRedirectToXBlockEditPage,
|
||||
});
|
||||
|
||||
useIframeMessages(messageHandlers);
|
||||
@@ -224,38 +186,6 @@ const XBlockContainerIframe: FC<XBlockContainerIframeProps> = ({
|
||||
close={closeDeleteModal}
|
||||
onDeleteSubmit={onDeleteSubmit}
|
||||
/>
|
||||
<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>
|
||||
)}
|
||||
{Object.keys(accessManagedXBlockData).length ? (
|
||||
<ConfigureModal
|
||||
isXBlockComponent
|
||||
|
||||
@@ -15,10 +15,6 @@ const messages = defineMessages({
|
||||
id: 'course-authoring.course-unit.xblock.iframe.label',
|
||||
defaultMessage: '{xblockCount} xBlocks inside the frame',
|
||||
},
|
||||
videoPickerModalTitle: {
|
||||
id: 'course-authoring.course-unit.xblock.video-editor.title',
|
||||
defaultMessage: 'Select video',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
|
||||
@@ -42,11 +42,6 @@ export interface XBlockContainerIframeProps {
|
||||
courseId: string;
|
||||
blockId: string;
|
||||
isUnitVerticalType: boolean,
|
||||
courseUnitLoadingStatus: {
|
||||
fetchUnitLoadingStatus: string;
|
||||
fetchVerticalChildrenLoadingStatus: string;
|
||||
fetchXBlockDataLoadingStatus: string;
|
||||
};
|
||||
unitXBlockActions: {
|
||||
handleDelete: (XBlockId: string | null) => void;
|
||||
handleDuplicate: (XBlockId: string | null) => void;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useContext } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { intlShape, injectIntl } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
ActionRow,
|
||||
IconButtonWithTooltip,
|
||||
@@ -27,8 +27,9 @@ const CustomPageCard = ({
|
||||
dispatch,
|
||||
deletePageStatus,
|
||||
setCurrentPage,
|
||||
// injected
|
||||
intl,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const [isDeleteConfirmationOpen, openDeleteConfirmation, closeDeleteConfirmation] = useToggle(false);
|
||||
const { path: customPagesPath } = useContext(CustomPagesContext);
|
||||
const navigate = useNavigate();
|
||||
@@ -128,6 +129,8 @@ CustomPageCard.propTypes = {
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
deletePageStatus: PropTypes.string.isRequired,
|
||||
setCurrentPage: PropTypes.func.isRequired,
|
||||
// injected
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
export default CustomPageCard;
|
||||
export default injectIntl(CustomPageCard);
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
} from 'react-router-dom';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { AppContext, PageWrap } from '@edx/frontend-platform/react';
|
||||
import { useIntl, FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
import { injectIntl, FormattedMessage, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
ActionRow,
|
||||
Breadcrumb,
|
||||
@@ -45,8 +45,9 @@ import { getPagePath } from '../utils';
|
||||
|
||||
const CustomPages = ({
|
||||
courseId,
|
||||
// injected
|
||||
intl,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const navigate = useNavigate();
|
||||
const dispatch = useDispatch();
|
||||
const [orderedPages, setOrderedPages] = useState([]);
|
||||
@@ -277,6 +278,8 @@ const CustomPages = ({
|
||||
|
||||
CustomPages.propTypes = {
|
||||
courseId: PropTypes.string.isRequired,
|
||||
// injected
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
export default CustomPages;
|
||||
export default injectIntl(CustomPages);
|
||||
|
||||
@@ -26,7 +26,6 @@ const slice = createSlice({
|
||||
useNewCertificatesPage: true,
|
||||
useNewTextbooksPage: true,
|
||||
useNewGroupConfigurationsPage: true,
|
||||
useVideoGalleryFlow: false,
|
||||
},
|
||||
},
|
||||
reducers: {
|
||||
|
||||
@@ -7,22 +7,22 @@ import * as hooks from './hooks';
|
||||
|
||||
import supportedEditors from './supportedEditors';
|
||||
import type { EditorComponent } from './EditorComponent';
|
||||
import { useEditorContext } from './EditorContext';
|
||||
import AdvancedEditor from './AdvancedEditor';
|
||||
|
||||
export interface Props extends EditorComponent {
|
||||
blockType: string;
|
||||
blockId: string | null;
|
||||
isMarkdownEditorEnabledForCourse: boolean;
|
||||
learningContextId: string | null;
|
||||
lmsEndpointUrl: string | null;
|
||||
studioEndpointUrl: string | null;
|
||||
fullScreen?: boolean; // eslint-disable-line react/no-unused-prop-types
|
||||
}
|
||||
|
||||
const Editor: React.FC<Props> = ({
|
||||
learningContextId,
|
||||
blockType,
|
||||
blockId,
|
||||
isMarkdownEditorEnabledForCourse,
|
||||
lmsEndpointUrl,
|
||||
studioEndpointUrl,
|
||||
onClose = null,
|
||||
@@ -34,12 +34,12 @@ const Editor: React.FC<Props> = ({
|
||||
data: {
|
||||
blockId,
|
||||
blockType,
|
||||
isMarkdownEditorEnabledForCourse,
|
||||
learningContextId,
|
||||
lmsEndpointUrl,
|
||||
studioEndpointUrl,
|
||||
},
|
||||
});
|
||||
const { fullScreen } = useEditorContext();
|
||||
|
||||
const EditorComponent = supportedEditors[blockType];
|
||||
|
||||
@@ -57,7 +57,24 @@ const Editor: React.FC<Props> = ({
|
||||
);
|
||||
}
|
||||
|
||||
return <EditorComponent {...{ onClose, returnFunction }} />;
|
||||
const innerEditor = <EditorComponent {...{ onClose, returnFunction }} />;
|
||||
|
||||
if (fullScreen) {
|
||||
return (
|
||||
<div
|
||||
className="d-flex flex-column"
|
||||
>
|
||||
<div
|
||||
className="pgn__modal-fullscreen h-100"
|
||||
role="dialog"
|
||||
aria-label={blockType}
|
||||
>
|
||||
{innerEditor}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return innerEditor;
|
||||
};
|
||||
|
||||
export default Editor;
|
||||
|
||||
@@ -24,13 +24,6 @@ jest.mock('@edx/frontend-platform/i18n', () => ({
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('react-redux', () => ({
|
||||
...jest.requireActual('react-redux'),
|
||||
useSelector: () => ({
|
||||
useReactMarkdownEditor: true, // or false depending on the test
|
||||
}),
|
||||
}));
|
||||
|
||||
const props = { learningContextId: 'cOuRsEId' };
|
||||
|
||||
describe('Editor Container', () => {
|
||||
|
||||
@@ -5,13 +5,11 @@ import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Button, Hyperlink } from '@openedx/paragon';
|
||||
import { Warning as WarningIcon } from '@openedx/paragon/icons';
|
||||
|
||||
import { useSelector } from 'react-redux';
|
||||
import EditorPage from './EditorPage';
|
||||
import AlertMessage from '../generic/alert-message';
|
||||
import messages from './messages';
|
||||
import { getLibraryId } from '../generic/key-utils';
|
||||
import { createCorrectInternalRoute } from '../utils';
|
||||
import { getWaffleFlags } from '../data/selectors';
|
||||
|
||||
interface Props {
|
||||
/** Course ID or Library ID */
|
||||
@@ -39,8 +37,6 @@ const EditorContainer: React.FC<Props> = ({
|
||||
const location = useLocation();
|
||||
const [searchParams] = useSearchParams();
|
||||
const upstreamLibRef = searchParams.get('upstreamLibRef');
|
||||
const waffleFlags = useSelector(getWaffleFlags);
|
||||
const isMarkdownEditorEnabledForCourse = waffleFlags?.useReactMarkdownEditor;
|
||||
|
||||
if (blockType === undefined || blockId === undefined) {
|
||||
// istanbul ignore next - This shouldn't be possible; it's just here to satisfy the type checker.
|
||||
@@ -80,7 +76,6 @@ const EditorContainer: React.FC<Props> = ({
|
||||
courseId={learningContextId}
|
||||
blockType={blockType}
|
||||
blockId={blockId}
|
||||
isMarkdownEditorEnabledForCourse={isMarkdownEditorEnabledForCourse}
|
||||
studioEndpointUrl={getConfig().STUDIO_BASE_URL}
|
||||
lmsEndpointUrl={getConfig().LMS_BASE_URL}
|
||||
onClose={onClose ? () => onClose(location.state?.from) : null}
|
||||
|
||||
@@ -7,6 +7,14 @@ import React from 'react';
|
||||
*/
|
||||
export interface EditorContext {
|
||||
learningContextId: string;
|
||||
/**
|
||||
* When editing components in the libraries part of the Authoring MFE, we show
|
||||
* the editors in a modal (fullScreen = false). This is the preferred approach
|
||||
* so that authors can see context behind the modal.
|
||||
* However, when making edits from the legacy course view, we display the
|
||||
* editors in a fullscreen view. This approach is deprecated.
|
||||
*/
|
||||
fullScreen: boolean;
|
||||
}
|
||||
|
||||
const context = React.createContext<EditorContext | undefined>(undefined);
|
||||
@@ -24,6 +32,7 @@ export function useEditorContext() {
|
||||
export const EditorContextProvider: React.FC<{
|
||||
children: React.ReactNode,
|
||||
learningContextId: string;
|
||||
fullScreen: boolean;
|
||||
}> = ({ children, ...contextData }) => {
|
||||
const ctx: EditorContext = React.useMemo(() => ({ ...contextData }), []);
|
||||
return <context.Provider value={ctx}>{children}</context.Provider>;
|
||||
|
||||
@@ -37,6 +37,7 @@ const defaultPropsHtml = {
|
||||
lmsEndpointUrl: 'http://lms.test.none/',
|
||||
studioEndpointUrl: 'http://cms.test.none/',
|
||||
onClose: jest.fn(),
|
||||
fullScreen: false,
|
||||
};
|
||||
const fieldsHtml = {
|
||||
displayName: 'Introduction to Testing',
|
||||
@@ -65,6 +66,22 @@ describe('EditorPage', () => {
|
||||
expect(modalElement.classList).not.toContain('pgn__modal-fullscreen');
|
||||
});
|
||||
|
||||
test('it can display the Text (html) editor as a full page (when coming from the legacy UI)', async () => {
|
||||
jest.spyOn(editorCmsApi, 'fetchBlockById').mockImplementationOnce(async () => (
|
||||
{ status: 200, data: snakeCaseObject(fieldsHtml) }
|
||||
));
|
||||
|
||||
render(<EditorPage {...defaultPropsHtml} fullScreen />);
|
||||
|
||||
// Then the editor should open
|
||||
expect(await screen.findByRole('heading', { name: /Introduction to Testing/ })).toBeInTheDocument();
|
||||
|
||||
const modalElement = screen.getByRole('dialog');
|
||||
expect(modalElement.classList).toContain('pgn__modal-fullscreen');
|
||||
expect(modalElement.classList).not.toContain('pgn__modal');
|
||||
expect(modalElement.classList).not.toContain('pgn__modal-xl');
|
||||
});
|
||||
|
||||
test('it shows the Advanced Editor if there is no corresponding editor', async () => {
|
||||
jest.spyOn(editorCmsApi, 'fetchBlockById').mockImplementationOnce(async () => ( // eslint-disable-next-line
|
||||
{ status: 200, data: { display_name: 'Fake Un-editable Block', category: 'fake', metadata: {}, data: '' } }
|
||||
|
||||
@@ -11,9 +11,9 @@ interface Props extends EditorComponent {
|
||||
blockId?: string;
|
||||
blockType: string;
|
||||
courseId: string;
|
||||
isMarkdownEditorEnabledForCourse?: boolean;
|
||||
lmsEndpointUrl?: string;
|
||||
studioEndpointUrl?: string;
|
||||
fullScreen?: boolean;
|
||||
children?: never;
|
||||
}
|
||||
|
||||
@@ -25,11 +25,11 @@ const EditorPage: React.FC<Props> = ({
|
||||
courseId,
|
||||
blockType,
|
||||
blockId = null,
|
||||
isMarkdownEditorEnabledForCourse = false,
|
||||
lmsEndpointUrl = null,
|
||||
studioEndpointUrl = null,
|
||||
onClose = null,
|
||||
returnFunction = null,
|
||||
fullScreen = true,
|
||||
}) => (
|
||||
<Provider store={store}>
|
||||
<ErrorBoundary
|
||||
@@ -38,14 +38,13 @@ const EditorPage: React.FC<Props> = ({
|
||||
studioEndpointUrl,
|
||||
}}
|
||||
>
|
||||
<EditorContextProvider learningContextId={courseId}>
|
||||
<EditorContextProvider fullScreen={fullScreen} learningContextId={courseId}>
|
||||
<Editor
|
||||
{...{
|
||||
onClose,
|
||||
learningContextId: courseId,
|
||||
blockType,
|
||||
blockId,
|
||||
isMarkdownEditorEnabledForCourse,
|
||||
lmsEndpointUrl,
|
||||
studioEndpointUrl,
|
||||
returnFunction,
|
||||
|
||||
@@ -9,8 +9,6 @@ const VideoSelector = ({
|
||||
learningContextId,
|
||||
lmsEndpointUrl,
|
||||
studioEndpointUrl,
|
||||
returnFunction,
|
||||
onCancel,
|
||||
}) => {
|
||||
const dispatch = useDispatch();
|
||||
const loading = hooks.useInitializeApp({
|
||||
@@ -28,7 +26,7 @@ const VideoSelector = ({
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<VideoGallery returnFunction={returnFunction} onCancel={onCancel} />
|
||||
<VideoGallery />
|
||||
);
|
||||
};
|
||||
|
||||
@@ -37,8 +35,6 @@ VideoSelector.propTypes = {
|
||||
learningContextId: PropTypes.string.isRequired,
|
||||
lmsEndpointUrl: PropTypes.string.isRequired,
|
||||
studioEndpointUrl: PropTypes.string.isRequired,
|
||||
returnFunction: PropTypes.func,
|
||||
onCancel: PropTypes.func,
|
||||
};
|
||||
|
||||
export default VideoSelector;
|
||||
|
||||
@@ -10,8 +10,6 @@ const VideoSelectorPage = ({
|
||||
courseId,
|
||||
lmsEndpointUrl,
|
||||
studioEndpointUrl,
|
||||
returnFunction,
|
||||
onCancel,
|
||||
}) => (
|
||||
<Provider store={store}>
|
||||
<ErrorBoundary
|
||||
@@ -26,8 +24,6 @@ const VideoSelectorPage = ({
|
||||
learningContextId: courseId,
|
||||
lmsEndpointUrl,
|
||||
studioEndpointUrl,
|
||||
returnFunction,
|
||||
onCancel,
|
||||
}}
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
@@ -46,8 +42,6 @@ VideoSelectorPage.propTypes = {
|
||||
courseId: PropTypes.string,
|
||||
lmsEndpointUrl: PropTypes.string,
|
||||
studioEndpointUrl: PropTypes.string,
|
||||
returnFunction: PropTypes.func,
|
||||
onCancel: PropTypes.func,
|
||||
};
|
||||
|
||||
export default VideoSelectorPage;
|
||||
|
||||
@@ -60,7 +60,6 @@ exports[`Editor Container snapshots rendering correctly with expected Input 1`]
|
||||
blockId="company-id1"
|
||||
blockType="html"
|
||||
courseId="cOuRsEId"
|
||||
isMarkdownEditorEnabledForCourse={true}
|
||||
lmsEndpointUrl="http://localhost:18000"
|
||||
onClose={null}
|
||||
returnFunction={null}
|
||||
|
||||
@@ -32,6 +32,7 @@ const defaultPropsHtml = {
|
||||
lmsEndpointUrl: 'http://lms.test.none/',
|
||||
studioEndpointUrl: 'http://cms.test.none/',
|
||||
onClose: jest.fn(),
|
||||
fullScreen: false,
|
||||
};
|
||||
const fieldsHtml = {
|
||||
displayName: 'Introduction to Testing',
|
||||
|
||||
@@ -14,6 +14,7 @@ import { Close } from '@openedx/paragon/icons';
|
||||
import { useIntl, FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { EditorComponent } from '../../EditorComponent';
|
||||
import { useEditorContext } from '../../EditorContext';
|
||||
import TitleHeader from './components/TitleHeader';
|
||||
import * as hooks from './hooks';
|
||||
import messages from './messages';
|
||||
@@ -29,18 +30,37 @@ interface WrapperProps {
|
||||
}
|
||||
|
||||
export const EditorModalWrapper: React.FC<WrapperProps & { onClose: () => void }> = ({ children, onClose }) => {
|
||||
const { fullScreen } = useEditorContext();
|
||||
const intl = useIntl();
|
||||
|
||||
if (fullScreen) {
|
||||
return (
|
||||
<div
|
||||
className="editor-container d-flex flex-column position-relative zindex-0"
|
||||
style={{ minHeight: '100%' }}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
const title = intl.formatMessage(messages.modalTitle);
|
||||
return (
|
||||
<ModalDialog isOpen size="xl" isOverflowVisible={false} onClose={onClose} title={title}>{children}</ModalDialog>
|
||||
);
|
||||
};
|
||||
|
||||
export const EditorModalBody: React.FC<WrapperProps> = ({ children }) => <ModalDialog.Body className="pb-0">{ children }</ModalDialog.Body>;
|
||||
export const EditorModalBody: React.FC<WrapperProps> = ({ children }) => {
|
||||
const { fullScreen } = useEditorContext();
|
||||
return <ModalDialog.Body className={fullScreen ? 'pb-6' : 'pb-0'}>{ children }</ModalDialog.Body>;
|
||||
};
|
||||
|
||||
// eslint-disable-next-line react/jsx-no-useless-fragment
|
||||
export const FooterWrapper: React.FC<WrapperProps> = ({ children }) => <>{ children }</>;
|
||||
export const FooterWrapper: React.FC<WrapperProps> = ({ children }) => {
|
||||
const { fullScreen } = useEditorContext();
|
||||
if (fullScreen) {
|
||||
return <div className="editor-footer fixed-bottom">{children}</div>;
|
||||
}
|
||||
// eslint-disable-next-line react/jsx-no-useless-fragment
|
||||
return <>{ children }</>;
|
||||
};
|
||||
|
||||
interface Props extends EditorComponent {
|
||||
children: React.ReactNode;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user