Compare commits

..

1 Commits

Author SHA1 Message Date
Muhammad Faraz Maqsood
f16ccfe9cf fix: use hyperlink instead of Link 2025-04-21 11:10:42 +05:00
304 changed files with 2344 additions and 6344 deletions

3
.env
View File

@@ -45,4 +45,5 @@ ENABLE_HOME_PAGE_COURSE_API_V2=true
ENABLE_CHECKLIST_QUALITY='' ENABLE_CHECKLIST_QUALITY=''
ENABLE_GRADING_METHOD_IN_PROBLEMS=false ENABLE_GRADING_METHOD_IN_PROBLEMS=false
# "Multi-level" blocks are unsupported in libraries # "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"

View File

@@ -40,4 +40,5 @@ ENABLE_HOME_PAGE_COURSE_API_V2=true
ENABLE_CHECKLIST_QUALITY=true ENABLE_CHECKLIST_QUALITY=true
ENABLE_GRADING_METHOD_IN_PROBLEMS=false ENABLE_GRADING_METHOD_IN_PROBLEMS=false
# "Multi-level" blocks are unsupported in libraries # "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"

View File

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

66
package-lock.json generated
View File

@@ -10,7 +10,6 @@
"license": "AGPL-3.0", "license": "AGPL-3.0",
"dependencies": { "dependencies": {
"@codemirror/lang-html": "^6.0.0", "@codemirror/lang-html": "^6.0.0",
"@codemirror/lang-markdown": "^6.0.0",
"@codemirror/lang-xml": "^6.0.0", "@codemirror/lang-xml": "^6.0.0",
"@codemirror/lint": "^6.2.1", "@codemirror/lint": "^6.2.1",
"@codemirror/state": "^6.0.0", "@codemirror/state": "^6.0.0",
@@ -21,7 +20,7 @@
"@dnd-kit/utilities": "^3.2.2", "@dnd-kit/utilities": "^3.2.2",
"@edx/brand": "npm:@openedx/brand-openedx@^1.2.3", "@edx/brand": "npm:@openedx/brand-openedx@^1.2.3",
"@edx/browserslist-config": "1.2.0", "@edx/browserslist-config": "1.2.0",
"@edx/frontend-component-footer": "^14.6.0", "@edx/frontend-component-footer": "^14.3.0",
"@edx/frontend-component-header": "^6.2.0", "@edx/frontend-component-header": "^6.2.0",
"@edx/frontend-enterprise-hotjar": "^7.2.0", "@edx/frontend-enterprise-hotjar": "^7.2.0",
"@edx/frontend-platform": "^8.3.1", "@edx/frontend-platform": "^8.3.1",
@@ -37,7 +36,8 @@
"@openedx-plugins/course-app-wiki": "file:plugins/course-apps/wiki", "@openedx-plugins/course-app-wiki": "file:plugins/course-apps/wiki",
"@openedx-plugins/course-app-xpert_unit_summary": "file:plugins/course-apps/xpert_unit_summary", "@openedx-plugins/course-app-xpert_unit_summary": "file:plugins/course-apps/xpert_unit_summary",
"@openedx/frontend-build": "^14.3.3", "@openedx/frontend-build": "^14.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", "@openedx/paragon": "^22.16.0",
"@redux-devtools/extension": "^3.3.0", "@redux-devtools/extension": "^3.3.0",
"@reduxjs/toolkit": "1.9.7", "@reduxjs/toolkit": "1.9.7",
@@ -2033,21 +2033,6 @@
"@lezer/javascript": "^1.0.0" "@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": { "node_modules/@codemirror/lang-xml": {
"version": "6.1.0", "version": "6.1.0",
"resolved": "https://registry.npmjs.org/@codemirror/lang-xml/-/lang-xml-6.1.0.tgz", "resolved": "https://registry.npmjs.org/@codemirror/lang-xml/-/lang-xml-6.1.0.tgz",
@@ -2344,9 +2329,9 @@
} }
}, },
"node_modules/@edx/frontend-component-footer": { "node_modules/@edx/frontend-component-footer": {
"version": "14.6.0", "version": "14.3.0",
"resolved": "https://registry.npmjs.org/@edx/frontend-component-footer/-/frontend-component-footer-14.6.0.tgz", "resolved": "https://registry.npmjs.org/@edx/frontend-component-footer/-/frontend-component-footer-14.3.0.tgz",
"integrity": "sha512-cgRhom6W/WErQ9yvLmfgB6ANBs+rBDLOH73NcvJIhfwWgAg67q+MLUscIbcX9N/9Yykk+kb7Ytr3CDefiKS7HA==", "integrity": "sha512-domQOIsAf+b1YiQvpt245Cfz6OgrKKw3TJrDIFS+J70Mn98MpCGGg55mBraOzTfopsouzp5bN03F1PLkXyjnEQ==",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"dependencies": { "dependencies": {
"@fortawesome/fontawesome-svg-core": "6.7.2", "@fortawesome/fontawesome-svg-core": "6.7.2",
@@ -2354,7 +2339,6 @@
"@fortawesome/free-regular-svg-icons": "6.7.2", "@fortawesome/free-regular-svg-icons": "6.7.2",
"@fortawesome/free-solid-svg-icons": "6.7.2", "@fortawesome/free-solid-svg-icons": "6.7.2",
"@fortawesome/react-fontawesome": "0.2.2", "@fortawesome/react-fontawesome": "0.2.2",
"@openedx/frontend-plugin-framework": "^1.7.0",
"classnames": "^2.5.1", "classnames": "^2.5.1",
"jest-environment-jsdom": "^29.7.0", "jest-environment-jsdom": "^29.7.0",
"lodash": "^4.17.21", "lodash": "^4.17.21",
@@ -2369,9 +2353,9 @@
} }
}, },
"node_modules/@edx/frontend-component-header": { "node_modules/@edx/frontend-component-header": {
"version": "6.4.0", "version": "6.2.0",
"resolved": "https://registry.npmjs.org/@edx/frontend-component-header/-/frontend-component-header-6.4.0.tgz", "resolved": "https://registry.npmjs.org/@edx/frontend-component-header/-/frontend-component-header-6.2.0.tgz",
"integrity": "sha512-RNV3XRXhhN9QlhAoP26CjzoRIPlLSYDp3PZCnK6g6kIHgxC9dCpu2PTZdxV2AVChqVuxtZK5zLbk9yeAtf4U/A==", "integrity": "sha512-rM/+NtvPAQk+RmAA/fhXnsneeta/CGi319Wei/or6aW7ETpSmMkfoYM4MKv+JPhF/vLMxqBzz6lwzefF9D62Lw==",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"dependencies": { "dependencies": {
"@fortawesome/fontawesome-svg-core": "6.6.0", "@fortawesome/fontawesome-svg-core": "6.6.0",
@@ -2379,7 +2363,7 @@
"@fortawesome/free-regular-svg-icons": "6.6.0", "@fortawesome/free-regular-svg-icons": "6.6.0",
"@fortawesome/free-solid-svg-icons": "6.6.0", "@fortawesome/free-solid-svg-icons": "6.6.0",
"@fortawesome/react-fontawesome": "^0.2.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", "axios-mock-adapter": "1.22.0",
"babel-polyfill": "6.26.0", "babel-polyfill": "6.26.0",
"classnames": "^2.5.1", "classnames": "^2.5.1",
@@ -3885,16 +3869,6 @@
"@lezer/common": "^1.0.0" "@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": { "node_modules/@lezer/xml": {
"version": "1.0.6", "version": "1.0.6",
"resolved": "https://registry.npmjs.org/@lezer/xml/-/xml-1.0.6.tgz", "resolved": "https://registry.npmjs.org/@lezer/xml/-/xml-1.0.6.tgz",
@@ -4183,9 +4157,9 @@
} }
}, },
"node_modules/@openedx/frontend-plugin-framework": { "node_modules/@openedx/frontend-plugin-framework": {
"version": "1.7.0", "version": "1.6.0",
"resolved": "https://registry.npmjs.org/@openedx/frontend-plugin-framework/-/frontend-plugin-framework-1.7.0.tgz", "resolved": "https://registry.npmjs.org/@openedx/frontend-plugin-framework/-/frontend-plugin-framework-1.6.0.tgz",
"integrity": "sha512-8tGkuHvtzhbqb9dU4sXUtR0K44+Hjh1uGR6DvhZAt9wSKQC1v4RBk34ef8DFzQhoNQa/Jtn6BJuta4Un6MmHmw==", "integrity": "sha512-zgP+/hs/cvcPmFOgVm2xt/qgX1nheNsfipzCO7I3bON4hHyOhmOyzwFZJ7pz7GzCJwKlMVguh3HcJgf4p/BPKQ==",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"dependencies": { "dependencies": {
"@edx/brand": "npm:@openedx/brand-openedx@^1.2.2", "@edx/brand": "npm:@openedx/brand-openedx@^1.2.2",
@@ -4268,6 +4242,20 @@
"@babel/runtime": "^7.9.2" "@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": { "node_modules/@openedx/paragon": {
"version": "22.17.0", "version": "22.17.0",
"resolved": "https://registry.npmjs.org/@openedx/paragon/-/paragon-22.17.0.tgz", "resolved": "https://registry.npmjs.org/@openedx/paragon/-/paragon-22.17.0.tgz",

View File

@@ -35,7 +35,6 @@
"dependencies": { "dependencies": {
"@codemirror/lang-html": "^6.0.0", "@codemirror/lang-html": "^6.0.0",
"@codemirror/lang-xml": "^6.0.0", "@codemirror/lang-xml": "^6.0.0",
"@codemirror/lang-markdown": "^6.0.0",
"@codemirror/lint": "^6.2.1", "@codemirror/lint": "^6.2.1",
"@codemirror/state": "^6.0.0", "@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.0.0", "@codemirror/view": "^6.0.0",
@@ -45,7 +44,7 @@
"@dnd-kit/utilities": "^3.2.2", "@dnd-kit/utilities": "^3.2.2",
"@edx/brand": "npm:@openedx/brand-openedx@^1.2.3", "@edx/brand": "npm:@openedx/brand-openedx@^1.2.3",
"@edx/browserslist-config": "1.2.0", "@edx/browserslist-config": "1.2.0",
"@edx/frontend-component-footer": "^14.6.0", "@edx/frontend-component-footer": "^14.3.0",
"@edx/frontend-component-header": "^6.2.0", "@edx/frontend-component-header": "^6.2.0",
"@edx/frontend-enterprise-hotjar": "^7.2.0", "@edx/frontend-enterprise-hotjar": "^7.2.0",
"@edx/frontend-platform": "^8.3.1", "@edx/frontend-platform": "^8.3.1",
@@ -61,7 +60,8 @@
"@openedx-plugins/course-app-wiki": "file:plugins/course-apps/wiki", "@openedx-plugins/course-app-wiki": "file:plugins/course-apps/wiki",
"@openedx-plugins/course-app-xpert_unit_summary": "file:plugins/course-apps/xpert_unit_summary", "@openedx-plugins/course-app-xpert_unit_summary": "file:plugins/course-apps/xpert_unit_summary",
"@openedx/frontend-build": "^14.3.3", "@openedx/frontend-build": "^14.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", "@openedx/paragon": "^22.16.0",
"@redux-devtools/extension": "^3.3.0", "@redux-devtools/extension": "^3.3.0",
"@reduxjs/toolkit": "1.9.7", "@reduxjs/toolkit": "1.9.7",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,4 @@
import { useIntl } from '@edx/frontend-platform/i18n'; import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { Button, Form } from '@openedx/paragon'; import { Button, Form } from '@openedx/paragon';
import { Add } from '@openedx/paragon/icons'; import { Add } from '@openedx/paragon/icons';
@@ -17,9 +17,9 @@ import messages from './messages';
setupYupExtensions(); setupYupExtensions();
const TeamSettings = ({ const TeamSettings = ({
intl,
onClose, onClose,
}) => { }) => {
const intl = useIntl();
const [teamsConfiguration, saveSettings] = useAppSetting('teamsConfiguration'); const [teamsConfiguration, saveSettings] = useAppSetting('teamsConfiguration');
const blankNewGroup = { const blankNewGroup = {
name: '', name: '',
@@ -166,7 +166,8 @@ const TeamSettings = ({
}; };
TeamSettings.propTypes = { TeamSettings.propTypes = {
intl: intlShape.isRequired,
onClose: PropTypes.func.isRequired, onClose: PropTypes.func.isRequired,
}; };
export default TeamSettings; export default injectIntl(TeamSettings);

View File

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

View File

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

View File

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

View File

@@ -5,7 +5,7 @@ import { useDispatch, useSelector } from 'react-redux';
import { import {
useLocation, useLocation,
} from 'react-router-dom'; } from 'react-router-dom';
import { StudioFooterSlot } from '@edx/frontend-component-footer'; import { StudioFooterSlot } from '@openedx/frontend-slot-footer';
import Header from './header'; import Header from './header';
import { fetchCourseDetail, fetchWaffleFlags } from './data/thunks'; import { fetchCourseDetail, fetchWaffleFlags } from './data/thunks';
import { useModel } from './generic/model-store'; import { useModel } from './generic/model-store';

View File

@@ -4,7 +4,7 @@ import {
} from 'react-router-dom'; } from 'react-router-dom';
import { getConfig } from '@edx/frontend-platform'; import { getConfig } from '@edx/frontend-platform';
import { PageWrap } from '@edx/frontend-platform/react'; import { PageWrap } from '@edx/frontend-platform/react';
import { Textbooks } from './textbooks'; import { Textbooks } from 'CourseAuthoring/textbooks';
import CourseAuthoringPage from './CourseAuthoringPage'; import CourseAuthoringPage from './CourseAuthoringPage';
import { PagesAndResources } from './pages-and-resources'; import { PagesAndResources } from './pages-and-resources';
import EditorContainer from './editors/EditorContainer'; import EditorContainer from './editors/EditorContainer';

View File

@@ -2,7 +2,7 @@ import React from 'react';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { Helmet } from 'react-helmet'; import { Helmet } from 'react-helmet';
import { Container } from '@openedx/paragon'; import { Container } from '@openedx/paragon';
import { StudioFooterSlot } from '@edx/frontend-component-footer'; import { StudioFooterSlot } from '@openedx/frontend-slot-footer';
import Header from '../header'; import Header from '../header';
import messages from './messages'; import messages from './messages';

View File

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

View File

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

View File

@@ -21,7 +21,6 @@ const path = '/content/:contentId?/*';
const mockOnClose = jest.fn(); const mockOnClose = jest.fn();
const mockSetBlockingSheet = jest.fn(); const mockSetBlockingSheet = jest.fn();
const mockNavigate = jest.fn(); const mockNavigate = jest.fn();
const mockSidebarAction = jest.fn();
mockContentTaxonomyTagsData.applyMock(); mockContentTaxonomyTagsData.applyMock();
mockTaxonomyListData.applyMock(); mockTaxonomyListData.applyMock();
mockTaxonomyTagsData.applyMock(); mockTaxonomyTagsData.applyMock();
@@ -41,11 +40,6 @@ jest.mock('react-router-dom', () => ({
useNavigate: () => mockNavigate, useNavigate: () => mockNavigate,
})); }));
jest.mock('../library-authoring/common/context/SidebarContext', () => ({
...jest.requireActual('../library-authoring/common/context/SidebarContext'),
useSidebarContext: () => ({ sidebarAction: mockSidebarAction() }),
}));
const renderDrawer = (contentId, drawerParams = {}) => ( const renderDrawer = (contentId, drawerParams = {}) => (
render( render(
<ContentTagsDrawerSheetContext.Provider value={drawerParams}> <ContentTagsDrawerSheetContext.Provider value={drawerParams}>
@@ -190,26 +184,6 @@ describe('<ContentTagsDrawer />', () => {
expect(screen.getByRole('button', { name: /save/i })).toBeInTheDocument(); expect(screen.getByRole('button', { name: /save/i })).toBeInTheDocument();
}); });
it('should change to edit mode sidebar action is set to JumpToManageTags', async () => {
mockSidebarAction.mockReturnValueOnce('jump-to-manage-tags');
renderDrawer(stagedTagsId, { variant: 'component' });
expect(await screen.findByText('Taxonomy 1')).toBeInTheDocument();
// Show delete tag buttons
expect(screen.getAllByRole('button', {
name: /delete/i,
}).length).toBe(2);
// Show add a tag select
expect(screen.getByText(/add a tag/i)).toBeInTheDocument();
// Show cancel button
expect(screen.getByRole('button', { name: /cancel/i })).toBeInTheDocument();
// Show save button
expect(screen.getByRole('button', { name: /save/i })).toBeInTheDocument();
});
it('should change to read mode when click on `Cancel` on drawer variant', async () => { it('should change to read mode when click on `Cancel` on drawer variant', async () => {
renderDrawer(stagedTagsId); renderDrawer(stagedTagsId);
expect(await screen.findByText('Taxonomy 1')).toBeInTheDocument(); expect(await screen.findByText('Taxonomy 1')).toBeInTheDocument();

View File

@@ -14,7 +14,6 @@ import ContentTagsCollapsible from './ContentTagsCollapsible';
import Loading from '../generic/Loading'; import Loading from '../generic/Loading';
import { useCreateContentTagsDrawerContext } from './ContentTagsDrawerHelper'; import { useCreateContentTagsDrawerContext } from './ContentTagsDrawerHelper';
import { ContentTagsDrawerContext, ContentTagsDrawerSheetContext } from './common/context'; import { ContentTagsDrawerContext, ContentTagsDrawerSheetContext } from './common/context';
import { SidebarActions, useSidebarContext } from '../library-authoring/common/context/SidebarContext';
interface TaxonomyListProps { interface TaxonomyListProps {
contentId: string; contentId: string;
@@ -245,7 +244,6 @@ const ContentTagsDrawer = ({
if (contentId === undefined) { if (contentId === undefined) {
throw new Error('Error: contentId cannot be null.'); throw new Error('Error: contentId cannot be null.');
} }
const { sidebarAction } = useSidebarContext();
const context = useCreateContentTagsDrawerContext(contentId, !readOnly, variant === 'drawer'); const context = useCreateContentTagsDrawerContext(contentId, !readOnly, variant === 'drawer');
const { blockingSheet } = useContext(ContentTagsDrawerSheetContext); const { blockingSheet } = useContext(ContentTagsDrawerSheetContext);
@@ -262,7 +260,6 @@ const ContentTagsDrawer = ({
closeToast, closeToast,
setCollapsibleToInitalState, setCollapsibleToInitalState,
otherTaxonomies, otherTaxonomies,
toEditMode,
} = context; } = context;
let onCloseDrawer: () => void; let onCloseDrawer: () => void;
@@ -305,13 +302,8 @@ const ContentTagsDrawer = ({
// First call of the initial collapsible states // First call of the initial collapsible states
React.useEffect(() => { React.useEffect(() => {
// Open tag edit mode when sidebarAction is JumpToManageTags setCollapsibleToInitalState();
if (sidebarAction === SidebarActions.JumpToManageTags) { }, [isTaxonomyListLoaded, isContentTaxonomyTagsLoaded]);
toEditMode();
} else {
setCollapsibleToInitalState();
}
}, [isTaxonomyListLoaded, isContentTaxonomyTagsLoaded, sidebarAction, toEditMode]);
const renderFooter = () => { const renderFooter = () => {
if (isTaxonomyListLoaded && isContentTaxonomyTagsLoaded) { if (isTaxonomyListLoaded && isContentTaxonomyTagsLoaded) {

View File

@@ -7,7 +7,6 @@ import {
useMutation, useMutation,
useQueryClient, useQueryClient,
} from '@tanstack/react-query'; } from '@tanstack/react-query';
import { useParams } from 'react-router';
import { import {
getTaxonomyTagsData, getTaxonomyTagsData,
getContentTaxonomyTagsData, getContentTaxonomyTagsData,
@@ -15,7 +14,7 @@ import {
updateContentTaxonomyTags, updateContentTaxonomyTags,
getContentTaxonomyTagsCount, getContentTaxonomyTagsCount,
} from './api'; } from './api';
import { libraryAuthoringQueryKeys, libraryQueryPredicate, xblockQueryKeys } from '../../library-authoring/data/apiHooks'; import { libraryQueryPredicate, xblockQueryKeys } from '../../library-authoring/data/apiHooks';
import { getLibraryId } from '../../generic/key-utils'; import { getLibraryId } from '../../generic/key-utils';
/** @typedef {import("../../taxonomy/data/types.js").TagListData} TagListData */ /** @typedef {import("../../taxonomy/data/types.js").TagListData} TagListData */
@@ -130,7 +129,6 @@ export const useContentData = (contentId, enabled) => (
export const useContentTaxonomyTagsUpdater = (contentId) => { export const useContentTaxonomyTagsUpdater = (contentId) => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const unitIframe = window.frames['xblock-iframe']; const unitIframe = window.frames['xblock-iframe'];
const { unitId } = useParams();
return useMutation({ return useMutation({
/** /**
@@ -160,10 +158,6 @@ export const useContentTaxonomyTagsUpdater = (contentId) => {
queryClient.invalidateQueries(xblockQueryKeys.componentMetadata(contentId)); queryClient.invalidateQueries(xblockQueryKeys.componentMetadata(contentId));
// Invalidate content search to update tags count // Invalidate content search to update tags count
queryClient.invalidateQueries(['content_search'], { predicate: (query) => libraryQueryPredicate(query, libraryId) }); queryClient.invalidateQueries(['content_search'], { predicate: (query) => libraryQueryPredicate(query, libraryId) });
// If the tags for a compoent were edited from Unit page, invalidate children query to fetch count again.
if (unitId) {
queryClient.invalidateQueries(libraryAuthoringQueryKeys.containerChildren(unitId));
}
} }
}, },
onSuccess: /* istanbul ignore next */ () => { onSuccess: /* istanbul ignore next */ () => {

View File

@@ -157,7 +157,7 @@ describe('useContentTaxonomyTagsUpdater', () => {
const contentId = 'testerContent'; const contentId = 'testerContent';
const taxonomyId = 123; const taxonomyId = 123;
const mutation = renderHook(() => useContentTaxonomyTagsUpdater(contentId)).result.current; const mutation = useContentTaxonomyTagsUpdater(contentId);
const tagsData = [{ const tagsData = [{
taxonomy: taxonomyId, taxonomy: taxonomyId,
tags: ['tag1', 'tag2'], tags: ['tag1', 'tag2'],

View File

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

View File

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

View File

@@ -118,46 +118,6 @@ describe('<CourseLibraries />', () => {
userEvent.click(reviewActionBtn); userEvent.click(reviewActionBtn);
expect(await screen.findByRole('tab', { name: 'Review Content Updates 5' })).toHaveAttribute('aria-selected', 'true'); expect(await screen.findByRole('tab', { name: 'Review Content Updates 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.findByRole('alert');
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.queryByRole('alert')).not.toBeInTheDocument();
});
}); });
describe('<CourseLibraries ReviewTab />', () => { describe('<CourseLibraries ReviewTab />', () => {
@@ -200,7 +160,7 @@ describe('<CourseLibraries ReviewTab />', () => {
it('update changes works', async () => { it('update changes works', async () => {
const mockInvalidateQueries = jest.spyOn(queryClient, 'invalidateQueries'); 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, {}); axiosMock.onPost(libraryBlockChangesUrl(usageKey)).reply(200, {});
await renderCourseLibrariesReviewPage(mockGetEntityLinksSummaryByDownstreamContext.courseKey); await renderCourseLibrariesReviewPage(mockGetEntityLinksSummaryByDownstreamContext.courseKey);
const updateBtns = await screen.findAllByRole('button', { name: 'Update' }); const updateBtns = await screen.findAllByRole('button', { name: 'Update' });
@@ -216,7 +176,7 @@ describe('<CourseLibraries ReviewTab />', () => {
it('update changes works in preview modal', async () => { it('update changes works in preview modal', async () => {
const mockInvalidateQueries = jest.spyOn(queryClient, 'invalidateQueries'); 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, {}); axiosMock.onPost(libraryBlockChangesUrl(usageKey)).reply(200, {});
await renderCourseLibrariesReviewPage(mockGetEntityLinksSummaryByDownstreamContext.courseKey); await renderCourseLibrariesReviewPage(mockGetEntityLinksSummaryByDownstreamContext.courseKey);
const previewBtns = await screen.findAllByRole('button', { name: 'Review Updates' }); const previewBtns = await screen.findAllByRole('button', { name: 'Review Updates' });
@@ -235,7 +195,7 @@ describe('<CourseLibraries ReviewTab />', () => {
it('ignore change works', async () => { it('ignore change works', async () => {
const mockInvalidateQueries = jest.spyOn(queryClient, 'invalidateQueries'); 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, {}); axiosMock.onDelete(libraryBlockChangesUrl(usageKey)).reply(204, {});
await renderCourseLibrariesReviewPage(mockGetEntityLinksSummaryByDownstreamContext.courseKey); await renderCourseLibrariesReviewPage(mockGetEntityLinksSummaryByDownstreamContext.courseKey);
const ignoreBtns = await screen.findAllByRole('button', { name: 'Ignore' }); const ignoreBtns = await screen.findAllByRole('button', { name: 'Ignore' });
@@ -258,7 +218,7 @@ describe('<CourseLibraries ReviewTab />', () => {
it('ignore change works in preview', async () => { it('ignore change works in preview', async () => {
const mockInvalidateQueries = jest.spyOn(queryClient, 'invalidateQueries'); 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, {}); axiosMock.onDelete(libraryBlockChangesUrl(usageKey)).reply(204, {});
await renderCourseLibrariesReviewPage(mockGetEntityLinksSummaryByDownstreamContext.courseKey); await renderCourseLibrariesReviewPage(mockGetEntityLinksSummaryByDownstreamContext.courseKey);
const previewBtns = await screen.findAllByRole('button', { name: 'Review Updates' }); const previewBtns = await screen.findAllByRole('button', { name: 'Review Updates' });

View File

@@ -164,7 +164,7 @@ export const CourseLibraries: React.FC<Props> = ({ courseId }) => {
if (tabKey !== CourseLibraryTabs.review) { if (tabKey !== CourseLibraryTabs.review) {
return null; return null;
} }
if (!outOfSyncCount) { if (!outOfSyncCount || outOfSyncCount === 0) {
return ( return (
<Stack direction="horizontal" gap={2}> <Stack direction="horizontal" gap={2}>
<Icon src={CheckCircle} size="xs" /> <Icon src={CheckCircle} size="xs" />

View File

@@ -18,11 +18,12 @@ interface OutOfSyncAlertProps {
* in course can be updated. Following are the conditions for displaying the alert. * in course can be updated. Following are the conditions for displaying the alert.
* *
* * The alert is displayed if components are out of sync. * * 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 * * If the user clicks on dismiss button, the state is stored in localstorage of user
* in this format: outOfSyncCountAlert-${courseId} = <datetime value in milliseconds>. * in this format: outOfSyncCountAlert-${courseId} = <number of out of sync components>.
* * If there are not new published components for the course and the user opens outline * * 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. * 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> = ({ export const OutOfSyncAlert: React.FC<OutOfSyncAlertProps> = ({
showAlert, showAlert,
@@ -33,9 +34,7 @@ export const OutOfSyncAlert: React.FC<OutOfSyncAlertProps> = ({
}) => { }) => {
const intl = useIntl(); const intl = useIntl();
const { data, isLoading } = useEntityLinksSummaryByDownstreamContext(courseId); const { data, isLoading } = useEntityLinksSummaryByDownstreamContext(courseId);
const outOfSyncCount = data?.reduce((count, lib) => count + (lib.readyToSyncCount || 0), 0); const outOfSyncCount = data?.reduce((count, lib) => count + lib.readyToSyncCount, 0);
const lastPublishedDate = data?.map(lib => new Date(lib.lastPublishedAt || 0).getTime())
.reduce((acc, lastPublished) => Math.max(lastPublished, acc), 0);
const alertKey = `outOfSyncCountAlert-${courseId}`; const alertKey = `outOfSyncCountAlert-${courseId}`;
useEffect(() => { useEffect(() => {
@@ -47,14 +46,13 @@ export const OutOfSyncAlert: React.FC<OutOfSyncAlertProps> = ({
setShowAlert(false); setShowAlert(false);
return; return;
} }
const dismissedAlertDate = parseInt(localStorage.getItem(alertKey) ?? '0', 10); const dismissedAlert = localStorage.getItem(alertKey);
setShowAlert(parseInt(dismissedAlert || '', 10) !== outOfSyncCount);
setShowAlert((lastPublishedDate ?? 0) > dismissedAlertDate); }, [outOfSyncCount, isLoading, data]);
}, [outOfSyncCount, lastPublishedDate, isLoading, data]);
const dismissAlert = () => { const dismissAlert = () => {
setShowAlert(false); setShowAlert(false);
localStorage.setItem(alertKey, Date.now().toString()); localStorage.setItem(alertKey, String(outOfSyncCount));
onDismiss?.(); onDismiss?.();
}; };

View File

@@ -1,5 +1,5 @@
import React, { import React, {
useCallback, useContext, useMemo, useState, useCallback, useContext, useEffect, useMemo, useState,
} from 'react'; } from 'react';
import { getConfig } from '@edx/frontend-platform'; import { getConfig } from '@edx/frontend-platform';
import { useIntl } from '@edx/frontend-platform/i18n'; import { useIntl } from '@edx/frontend-platform/i18n';
@@ -14,9 +14,11 @@ import {
useToggle, useToggle,
} from '@openedx/paragon'; } from '@openedx/paragon';
import { tail, keyBy } from 'lodash'; import {
tail, keyBy, orderBy, merge, omitBy,
} from 'lodash';
import { useQueryClient } from '@tanstack/react-query'; import { useQueryClient } from '@tanstack/react-query';
import { Loop } from '@openedx/paragon/icons'; import { Loop, Warning } from '@openedx/paragon/icons';
import messages from './messages'; import messages from './messages';
import previewChangesMessages from '../course-unit/preview-changes/messages'; import previewChangesMessages from '../course-unit/preview-changes/messages';
import { courseLibrariesQueryKeys, useEntityLinks } from './data/apiHooks'; import { courseLibrariesQueryKeys, useEntityLinks } from './data/apiHooks';
@@ -35,6 +37,7 @@ import { useLoadOnScroll } from '../hooks';
import DeleteModal from '../generic/delete-modal/DeleteModal'; import DeleteModal from '../generic/delete-modal/DeleteModal';
import { PublishableEntityLink } from './data/api'; import { PublishableEntityLink } from './data/api';
import AlertError from '../generic/alert-error'; import AlertError from '../generic/alert-error';
import AlertMessage from '../generic/alert-message';
interface Props { interface Props {
courseId: string; courseId: string;
@@ -99,8 +102,10 @@ const BlockCard: React.FC<BlockCardProps> = ({ info, actions }) => {
const ComponentReviewList = ({ const ComponentReviewList = ({
outOfSyncComponents, outOfSyncComponents,
onSearchUpdate,
}: { }: {
outOfSyncComponents: PublishableEntityLink[]; outOfSyncComponents: PublishableEntityLink[];
onSearchUpdate: () => void;
}) => { }) => {
const intl = useIntl(); const intl = useIntl();
const { showToast } = useContext(ToastContext); const { showToast } = useContext(ToastContext);
@@ -108,15 +113,24 @@ const ComponentReviewList = ({
// ignore changes confirmation modal toggle. // ignore changes confirmation modal toggle.
const [isConfirmModalOpen, openConfirmModal, closeConfirmModal] = useToggle(false); const [isConfirmModalOpen, openConfirmModal, closeConfirmModal] = useToggle(false);
const { const {
hits, hits: downstreamInfo,
isLoading: isIndexDataLoading, isLoading: isIndexDataLoading,
searchKeywords,
searchSortOrder,
hasError, hasError,
hasNextPage, hasNextPage,
isFetchingNextPage, isFetchingNextPage,
fetchNextPage, fetchNextPage,
} = useSearchContext(); } = useSearchContext() as {
hits: ContentHit[];
const downstreamInfo = hits as ContentHit[]; isLoading: boolean;
searchKeywords: string;
searchSortOrder: SearchSortOption;
hasError: boolean;
hasNextPage: boolean | undefined,
isFetchingNextPage: boolean;
fetchNextPage: () => void;
};
useLoadOnScroll( useLoadOnScroll(
hasNextPage, hasNextPage,
@@ -129,14 +143,24 @@ const ComponentReviewList = ({
() => keyBy(outOfSyncComponents, 'downstreamUsageKey'), () => keyBy(outOfSyncComponents, 'downstreamUsageKey'),
[outOfSyncComponents], [outOfSyncComponents],
); );
const downstreamInfoByKey = useMemo(
() => keyBy(downstreamInfo, 'usageKey'),
[downstreamInfo],
);
const queryClient = useQueryClient(); const queryClient = useQueryClient();
useEffect(() => {
if (searchKeywords) {
onSearchUpdate();
}
}, [searchKeywords]);
// Toggle preview changes modal // Toggle preview changes modal
const [isModalOpen, openModal, closeModal] = useToggle(false); const [isModalOpen, openModal, closeModal] = useToggle(false);
const acceptChangesMutation = useAcceptLibraryBlockChanges(); const acceptChangesMutation = useAcceptLibraryBlockChanges();
const ignoreChangesMutation = useIgnoreLibraryBlockChanges(); const ignoreChangesMutation = useIgnoreLibraryBlockChanges();
const setSelectedBlockData = useCallback((info: ContentHit) => { const setSeletecdBlockData = (info: ContentHit) => {
setBlockData({ setBlockData({
displayName: info.displayName, displayName: info.displayName,
downstreamBlockId: info.usageKey, downstreamBlockId: info.usageKey,
@@ -144,18 +168,17 @@ const ComponentReviewList = ({
upstreamBlockVersionSynced: outOfSyncComponentsByKey[info.usageKey].versionSynced, upstreamBlockVersionSynced: outOfSyncComponentsByKey[info.usageKey].versionSynced,
isVertical: info.blockType === 'vertical', isVertical: info.blockType === 'vertical',
}); });
}, [outOfSyncComponentsByKey]); };
// Show preview changes on review // Show preview changes on review
const onReview = useCallback((info: ContentHit) => { const onReview = useCallback((info: ContentHit) => {
setSelectedBlockData(info); setSeletecdBlockData(info);
openModal(); openModal();
}, [setSelectedBlockData, openModal]); }, [setSeletecdBlockData, openModal]);
const onIgnoreClick = useCallback((info: ContentHit) => { const onIgnoreClick = useCallback((info: ContentHit) => {
setSelectedBlockData(info); setSeletecdBlockData(info);
openConfirmModal(); openConfirmModal();
}, [setSelectedBlockData, openConfirmModal]); }, [setSeletecdBlockData, openConfirmModal]);
const reloadLinks = useCallback((usageKey: string) => { const reloadLinks = useCallback((usageKey: string) => {
const courseKey = outOfSyncComponentsByKey[usageKey].downstreamContextKey; const courseKey = outOfSyncComponentsByKey[usageKey].downstreamContextKey;
@@ -213,6 +236,19 @@ const ComponentReviewList = ({
} }
}, [blockData]); }, [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) { if (isIndexDataLoading) {
return <Loading />; return <Loading />;
} }
@@ -223,7 +259,7 @@ const ComponentReviewList = ({
return ( return (
<> <>
{downstreamInfo?.map((info) => ( {orderInfo?.map((info) => (
<BlockCard <BlockCard
key={info.usageKey} key={info.usageKey}
info={info} info={info}
@@ -257,14 +293,20 @@ const ComponentReviewList = ({
)} )}
/> />
))} ))}
{blockData && ( <PreviewLibraryXBlockChanges
<PreviewLibraryXBlockChanges blockData={blockData}
blockData={blockData} isModalOpen={isModalOpen}
isModalOpen={isModalOpen} closeModal={closeModal}
closeModal={closeModal} postChange={postChange}
postChange={postChange} alertNode={(
/> <AlertMessage
)} show
variant="warning"
icon={Warning}
title={intl.formatMessage(messages.olderVersionPreviewAlert)}
/>
)}
/>
<DeleteModal <DeleteModal
isOpen={isConfirmModalOpen} isOpen={isConfirmModalOpen}
close={closeConfirmModal} close={closeConfirmModal}
@@ -281,17 +323,37 @@ const ComponentReviewList = ({
const ReviewTabContent = ({ courseId }: Props) => { const ReviewTabContent = ({ courseId }: Props) => {
const intl = useIntl(); const intl = useIntl();
const { const {
data: outOfSyncComponents, data: linkPages,
isLoading: isSyncComponentsLoading, isLoading: isSyncComponentsLoading,
hasNextPage,
isFetchingNextPage,
fetchNextPage,
isError, isError,
error, error,
} = useEntityLinks({ courseId, readyToSync: true }); } = useEntityLinks({ courseId, readyToSync: true });
const outOfSyncComponents = useMemo(
() => linkPages?.pages?.reduce((links, page) => [...links, ...page.results], []) ?? [],
[linkPages],
);
const downstreamKeys = useMemo( const downstreamKeys = useMemo(
() => outOfSyncComponents?.map(link => link.downstreamUsageKey), () => outOfSyncComponents?.map(link => link.downstreamUsageKey),
[outOfSyncComponents], [outOfSyncComponents],
); );
useLoadOnScroll(
hasNextPage,
isFetchingNextPage,
fetchNextPage,
true,
);
const onSearchUpdate = () => {
if (hasNextPage && !isFetchingNextPage) {
fetchNextPage();
}
};
const disableSortOptions = [ const disableSortOptions = [
SearchSortOption.RELEVANCE, SearchSortOption.RELEVANCE,
SearchSortOption.OLDEST, SearchSortOption.OLDEST,
@@ -322,6 +384,7 @@ const ReviewTabContent = ({ courseId }: Props) => {
</ActionRow> </ActionRow>
<ComponentReviewList <ComponentReviewList
outOfSyncComponents={outOfSyncComponents} outOfSyncComponents={outOfSyncComponents}
onSearchUpdate={onSearchUpdate}
/> />
</SearchContextProvider> </SearchContextProvider>
); );

View File

@@ -3,20 +3,18 @@
"upstreamContextTitle": "CS problems 3", "upstreamContextTitle": "CS problems 3",
"upstreamContextKey": "lib:OpenedX:CSPROB3", "upstreamContextKey": "lib:OpenedX:CSPROB3",
"readyToSyncCount": 5, "readyToSyncCount": 5,
"totalCount": 14, "totalCount": 14
"lastPublishedAt": "2025-05-01T20:20:44.989042Z"
}, },
{ {
"upstreamContextTitle": "CS problems 2", "upstreamContextTitle": "CS problems 2",
"upstreamContextKey": "lib:OpenedX:CSPROB2", "upstreamContextKey": "lib:OpenedX:CSPROB2",
"readyToSyncCount": 0, "readyToSyncCount": 0,
"totalCount": 21, "totalCount": 21
"lastPublishedAt": "2025-05-01T21:20:44.989042Z"
}, },
{ {
"upstreamContextTitle": "CS problems", "upstreamContextTitle": "CS problems",
"upstreamContextKey": "lib:OpenedX:CSPROB", "upstreamContextKey": "lib:OpenedX:CSPROB",
"totalCount": 3, "readyToSyncCount": 0,
"lastPublishedAt": "2025-05-01T22:20:44.989042Z" "totalCount": 3
} }
] ]

View File

@@ -1,72 +1,79 @@
[ {
{ "count": 7,
"id": 875, "next": null,
"upstreamContextTitle": "CS problems 3", "previous": null,
"upstreamVersion": 10, "num_pages": 1,
"readyToSync": true, "current_page": 1,
"upstreamUsageKey": "lb:OpenedX:CSPROB3:problem:d40264d5-80c4-4be8-bfb6-086391de1cd3", "results": [
"upstreamContextKey": "lib:OpenedX:CSPROB3", {
"downstreamUsageKey": "block-v1:OpenEdx+DemoX+CourseX+type@problem+block@problem3", "id": 875,
"downstreamContextKey": "course-v1:OpenEdx+DemoX+CourseX", "upstreamContextTitle": "CS problems 3",
"versionSynced": 2, "upstreamVersion": 10,
"versionDeclined": null, "readyToSync": true,
"created": "2025-02-08T14:07:05.588484Z", "upstreamUsageKey": "lb:OpenedX:CSPROB3:problem:d40264d5-80c4-4be8-bfb6-086391de1cd3",
"updated": "2025-02-08T14:07:05.588484Z" "upstreamContextKey": "lib:OpenedX:CSPROB3",
}, "downstreamUsageKey": "block-v1:OpenEdx+DemoX+CourseX+type@problem+block@problem3",
{ "downstreamContextKey": "course-v1:OpenEdx+DemoX+CourseX",
"id": 876, "versionSynced": 2,
"upstreamContextTitle": "CS problems 3", "versionDeclined": null,
"upstreamVersion": 10, "created": "2025-02-08T14:07:05.588484Z",
"readyToSync": true, "updated": "2025-02-08T14:07:05.588484Z"
"upstreamUsageKey": "lb:OpenedX:CSPROB3:problem:d40264d5-80c4-4be8-bfb6-086391de1cd3", },
"upstreamContextKey": "lib:OpenedX:CSPROB3", {
"downstreamUsageKey": "block-v1:OpenEdx+DemoX+CourseX+type@problem+block@problem6", "id": 876,
"downstreamContextKey": "course-v1:OpenEdx+DemoX+CourseX", "upstreamContextTitle": "CS problems 3",
"versionSynced": 2, "upstreamVersion": 10,
"versionDeclined": null, "readyToSync": true,
"created": "2025-02-08T14:07:05.588484Z", "upstreamUsageKey": "lb:OpenedX:CSPROB3:problem:d40264d5-80c4-4be8-bfb6-086391de1cd3",
"updated": "2025-02-08T14:07:05.588484Z" "upstreamContextKey": "lib:OpenedX:CSPROB3",
}, "downstreamUsageKey": "block-v1:OpenEdx+DemoX+CourseX+type@problem+block@problem6",
{ "downstreamContextKey": "course-v1:OpenEdx+DemoX+CourseX",
"id": 884, "versionSynced": 2,
"upstreamContextTitle": "CS problems 3", "versionDeclined": null,
"upstreamVersion": 26, "created": "2025-02-08T14:07:05.588484Z",
"readyToSync": true, "updated": "2025-02-08T14:07:05.588484Z"
"upstreamUsageKey": "lb:OpenedX:CSPROB3:html:ca4d2b1f-0b64-4a2d-88fa-592f7e398477", },
"upstreamContextKey": "lib:OpenedX:CSPROB3", {
"downstreamUsageKey": "block-v1:OpenEdx+DemoX+CourseX+type@html+block@257e68e3386d4a8f8739d45b67e76a9b", "id": 884,
"downstreamContextKey": "course-v1:OpenEdx+DemoX+CourseX", "upstreamContextTitle": "CS problems 3",
"versionSynced": 16, "upstreamVersion": 26,
"versionDeclined": null, "readyToSync": true,
"created": "2025-02-08T14:07:05.588484Z", "upstreamUsageKey": "lb:OpenedX:CSPROB3:html:ca4d2b1f-0b64-4a2d-88fa-592f7e398477",
"updated": "2025-02-08T14:07:05.588484Z" "upstreamContextKey": "lib:OpenedX:CSPROB3",
}, "downstreamUsageKey": "block-v1:OpenEdx+DemoX+CourseX+type@html+block@257e68e3386d4a8f8739d45b67e76a9b",
{ "downstreamContextKey": "course-v1:OpenEdx+DemoX+CourseX",
"id": 889, "versionSynced": 16,
"upstreamContextTitle": "CS problems 3", "versionDeclined": null,
"upstreamVersion": 10, "created": "2025-02-08T14:07:05.588484Z",
"readyToSync": true, "updated": "2025-02-08T14:07:05.588484Z"
"upstreamUsageKey": "lb:OpenedX:CSPROB3:problem:d40264d5-80c4-4be8-bfb6-086391de1cd3", },
"upstreamContextKey": "lib:OpenedX:CSPROB3", {
"downstreamUsageKey": "block-v1:OpenEdx+DemoX+CourseX+type@problem+block@a4455860b03647219ff8b01cde49cf37", "id": 889,
"downstreamContextKey": "course-v1:OpenEdx+DemoX+CourseX", "upstreamContextTitle": "CS problems 3",
"versionSynced": 2, "upstreamVersion": 10,
"versionDeclined": null, "readyToSync": true,
"created": "2025-02-08T14:07:05.588484Z", "upstreamUsageKey": "lb:OpenedX:CSPROB3:problem:d40264d5-80c4-4be8-bfb6-086391de1cd3",
"updated": "2025-02-08T14:07:05.588484Z" "upstreamContextKey": "lib:OpenedX:CSPROB3",
}, "downstreamUsageKey": "block-v1:OpenEdx+DemoX+CourseX+type@problem+block@a4455860b03647219ff8b01cde49cf37",
{ "downstreamContextKey": "course-v1:OpenEdx+DemoX+CourseX",
"id": 890, "versionSynced": 2,
"upstreamContextTitle": "CS problems 3", "versionDeclined": null,
"upstreamVersion": 10, "created": "2025-02-08T14:07:05.588484Z",
"readyToSync": true, "updated": "2025-02-08T14:07:05.588484Z"
"upstreamUsageKey": "lb:OpenedX:CSPROB3:problem:d40264d5-80c4-4be8-bfb6-086391de1cd3", },
"upstreamContextKey": "lib:OpenedX:CSPROB3", {
"downstreamUsageKey": "block-v1:OpenEdx+DemoX+CourseX+type@problem+block@210e356cfa304b0aac591af53f6a6ae0", "id": 890,
"downstreamContextKey": "course-v1:OpenEdx+DemoX+CourseX", "upstreamContextTitle": "CS problems 3",
"versionSynced": 2, "upstreamVersion": 10,
"versionDeclined": null, "readyToSync": true,
"created": "2025-02-08T14:07:05.588484Z", "upstreamUsageKey": "lb:OpenedX:CSPROB3:problem:d40264d5-80c4-4be8-bfb6-086391de1cd3",
"updated": "2025-02-08T14:07:05.588484Z" "upstreamContextKey": "lib:OpenedX:CSPROB3",
} "downstreamUsageKey": "block-v1:OpenEdx+DemoX+CourseX+type@problem+block@210e356cfa304b0aac591af53f6a6ae0",
] "downstreamContextKey": "course-v1:OpenEdx+DemoX+CourseX",
"versionSynced": 2,
"versionDeclined": null,
"created": "2025-02-08T14:07:05.588484Z",
"updated": "2025-02-08T14:07:05.588484Z"
}
]
}

View File

@@ -28,17 +28,27 @@ export async function mockGetEntityLinks(
case mockGetEntityLinks.courseKeyLoading: case mockGetEntityLinks.courseKeyLoading:
return new Promise(() => {}); return new Promise(() => {});
case mockGetEntityLinks.courseKeyEmpty: 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: { default: {
let { response } = mockGetEntityLinks; const { response } = mockGetEntityLinks;
if (readyToSync !== undefined) { 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); return Promise.resolve(response);
} }
} }
} }
mockGetEntityLinks.courseKey = mockLinksResult[0].downstreamContextKey; mockGetEntityLinks.courseKey = mockLinksResult.results[0].downstreamContextKey;
mockGetEntityLinks.invalidCourseKey = 'course_key_error'; mockGetEntityLinks.invalidCourseKey = 'course_key_error';
mockGetEntityLinks.courseKeyLoading = 'courseKeyLoading'; mockGetEntityLinks.courseKeyLoading = 'courseKeyLoading';
mockGetEntityLinks.courseKeyEmpty = 'courseKeyEmpty'; mockGetEntityLinks.courseKeyEmpty = 'courseKeyEmpty';
@@ -75,7 +85,7 @@ export async function mockGetEntityLinksSummaryByDownstreamContext(
return Promise.resolve(mockGetEntityLinksSummaryByDownstreamContext.response); return Promise.resolve(mockGetEntityLinksSummaryByDownstreamContext.response);
} }
} }
mockGetEntityLinksSummaryByDownstreamContext.courseKey = mockLinksResult[0].downstreamContextKey; mockGetEntityLinksSummaryByDownstreamContext.courseKey = mockLinksResult.results[0].downstreamContextKey;
mockGetEntityLinksSummaryByDownstreamContext.invalidCourseKey = 'course_key_error'; mockGetEntityLinksSummaryByDownstreamContext.invalidCourseKey = 'course_key_error';
mockGetEntityLinksSummaryByDownstreamContext.courseKeyLoading = 'courseKeySummaryLoading'; mockGetEntityLinksSummaryByDownstreamContext.courseKeyLoading = 'courseKeySummaryLoading';
mockGetEntityLinksSummaryByDownstreamContext.courseKeyEmpty = 'courseKeyEmpty'; mockGetEntityLinksSummaryByDownstreamContext.courseKeyEmpty = 'courseKeyEmpty';

View File

@@ -38,13 +38,32 @@ export interface PublishableEntityLinkSummary {
upstreamContextTitle: string; upstreamContextTitle: string;
readyToSyncCount: number; readyToSyncCount: number;
totalCount: number; totalCount: number;
lastPublishedAt: string;
} }
export const getEntityLinks = async ( export const getEntityLinks = async (
downstreamContextKey?: string, downstreamContextKey?: string,
readyToSync?: boolean, readyToSync?: boolean,
upstreamUsageKey?: string, 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[]> => { ): Promise<PublishableEntityLink[]> => {
const { data } = await getAuthenticatedHttpClient() const { data } = await getAuthenticatedHttpClient()
.get(getEntityLinksByDownstreamContextUrl(), { .get(getEntityLinksByDownstreamContextUrl(), {

View File

@@ -4,7 +4,7 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import MockAdapter from 'axios-mock-adapter'; import MockAdapter from 'axios-mock-adapter';
import { renderHook, waitFor } from '@testing-library/react'; import { renderHook, waitFor } from '@testing-library/react';
import { getEntityLinksByDownstreamContextUrl } from './api'; import { getEntityLinksByDownstreamContextUrl } from './api';
import { useEntityLinks } from './apiHooks'; import { useEntityLinks, useUnpaginatedEntityLinks } from './apiHooks';
let axiosMock: MockAdapter; let axiosMock: MockAdapter;
@@ -39,11 +39,26 @@ describe('course libraries api hooks', () => {
axiosMock.reset(); 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 () => { it('should return links for course', async () => {
const courseId = 'course-v1:some+key'; const courseId = 'course-v1:some+key';
const url = getEntityLinksByDownstreamContextUrl(); const url = getEntityLinksByDownstreamContextUrl();
axiosMock.onGet(url).reply(200, []); axiosMock.onGet(url).reply(200, []);
const { result } = renderHook(() => useEntityLinks({ courseId }), { wrapper }); const { result } = renderHook(() => useUnpaginatedEntityLinks({ courseId }), { wrapper });
await waitFor(() => { await waitFor(() => {
expect(result.current.isLoading).toBeFalsy(); expect(result.current.isLoading).toBeFalsy();
}); });

View File

@@ -1,7 +1,8 @@
import { import {
useInfiniteQuery,
useQuery, useQuery,
} from '@tanstack/react-query'; } from '@tanstack/react-query';
import { getEntityLinks, getEntityLinksSummaryByDownstreamContext } from './api'; import { getEntityLinks, getEntityLinksSummaryByDownstreamContext, getUnpaginatedEntityLinks } from './api';
export const courseLibrariesQueryKeys = { export const courseLibrariesQueryKeys = {
all: ['courseLibraries'], 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.) * (That is, get a list of the library components used in the given course.)
*/ */
export const useEntityLinks = ({ 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, readyToSync, upstreamUsageKey,
}: { }: {
courseId?: string, courseId?: string,
@@ -44,7 +74,7 @@ export const useEntityLinks = ({
readyToSync, readyToSync,
upstreamUsageKey, upstreamUsageKey,
}), }),
queryFn: () => getEntityLinks( queryFn: () => getUnpaginatedEntityLinks(
courseId, courseId,
readyToSync, readyToSync,
upstreamUsageKey, upstreamUsageKey,

View File

@@ -116,6 +116,11 @@ const messages = defineMessages({
defaultMessage: 'Something went wrong! Could not fetch results.', defaultMessage: 'Something went wrong! Could not fetch results.',
description: 'Generic error message displayed when fetching link data fails.', description: 'Generic error message displayed when fetching link data fails.',
}, },
olderVersionPreviewAlert: {
id: 'course-authoring.course-libraries.reviw-tab.preview.old-version-alert',
defaultMessage: 'The old version preview is the previous library version',
description: 'Alert message stating that older version in preview is of library block',
},
}); });
export default messages; export default messages;

View File

@@ -103,7 +103,6 @@ const CourseOutline = ({ courseId }) => {
handleNewSectionSubmit, handleNewSectionSubmit,
handleNewSubsectionSubmit, handleNewSubsectionSubmit,
handleNewUnitSubmit, handleNewUnitSubmit,
handleAddUnitFromLibrary,
getUnitUrl, getUnitUrl,
handleVideoSharingOptionChange, handleVideoSharingOptionChange,
handlePasteClipboardClick, handlePasteClipboardClick,
@@ -375,7 +374,6 @@ const CourseOutline = ({ courseId }) => {
section, section,
section.childInfo.children, section.childInfo.children,
)} )}
isSectionsExpanded={isSectionsExpanded}
isSelfPaced={statusBarData.isSelfPaced} isSelfPaced={statusBarData.isSelfPaced}
isCustomRelativeDatesActive={isCustomRelativeDatesActive} isCustomRelativeDatesActive={isCustomRelativeDatesActive}
savingStatus={savingStatus} savingStatus={savingStatus}
@@ -385,7 +383,6 @@ const CourseOutline = ({ courseId }) => {
onDuplicateSubmit={handleDuplicateSubsectionSubmit} onDuplicateSubmit={handleDuplicateSubsectionSubmit}
onOpenConfigureModal={openConfigureModal} onOpenConfigureModal={openConfigureModal}
onNewUnitSubmit={handleNewUnitSubmit} onNewUnitSubmit={handleNewUnitSubmit}
onAddUnitFromLibrary={handleAddUnitFromLibrary}
onOrderChange={updateSubsectionOrderByIndex} onOrderChange={updateSubsectionOrderByIndex}
onPasteClick={handlePasteClipboardClick} onPasteClick={handlePasteClipboardClick}
> >
@@ -456,11 +453,7 @@ const CourseOutline = ({ courseId }) => {
</article> </article>
</Layout.Element> </Layout.Element>
<Layout.Element> <Layout.Element>
<CourseAuthoringOutlineSidebarSlot <CourseAuthoringOutlineSidebarSlot courseId={courseId} />
courseId={courseId}
courseName={courseName}
sections={sections}
/>
</Layout.Element> </Layout.Element>
</Layout> </Layout>
<EnableHighlightsModal <EnableHighlightsModal

View File

@@ -11,6 +11,7 @@ import { cloneDeep } from 'lodash';
import { closestCorners } from '@dnd-kit/core'; import { closestCorners } from '@dnd-kit/core';
import { useLocation } from 'react-router-dom'; import { useLocation } from 'react-router-dom';
import userEvent from '@testing-library/user-event';
import { import {
getCourseBestPracticesApiUrl, getCourseBestPracticesApiUrl,
getCourseLaunchApiUrl, getCourseLaunchApiUrl,
@@ -59,14 +60,11 @@ import {
moveSubsection, moveSubsection,
moveUnit, moveUnit,
} from './drag-helper/utils'; } from './drag-helper/utils';
import { postXBlockBaseApiUrl } from '../course-unit/data/api';
import { COMPONENT_TYPES } from '../generic/block-type-utils/constants';
let axiosMock; let axiosMock;
let store; let store;
const mockPathname = '/foo-bar'; const mockPathname = '/foo-bar';
const courseId = '123'; const courseId = '123';
const containerKey = 'lct:org:lib:unit:1';
window.HTMLElement.prototype.scrollIntoView = jest.fn(); window.HTMLElement.prototype.scrollIntoView = jest.fn();
@@ -96,24 +94,6 @@ jest.mock('./data/api', () => ({
getTagsCount: () => jest.fn().mockResolvedValue({}), getTagsCount: () => jest.fn().mockResolvedValue({}),
})); }));
// Mock ComponentPicker to call onComponentSelected on click
jest.mock('../library-authoring/component-picker', () => ({
ComponentPicker: (props) => {
const onClick = () => {
// eslint-disable-next-line react/prop-types
props.onComponentSelected({
usageKey: containerKey,
blockType: 'unti',
});
};
return (
<button type="submit" onClick={onClick}>
Dummy button
</button>
);
},
}));
const queryClient = new QueryClient(); const queryClient = new QueryClient();
jest.mock('@dnd-kit/core', () => ({ jest.mock('@dnd-kit/core', () => ({
@@ -153,9 +133,7 @@ describe('<CourseOutline />', () => {
pathname: mockPathname, pathname: mockPathname,
}); });
store = initializeStore({ store = initializeStore();
studioHome: { studioHomeData: { librariesV2Enabled: true } },
});
axiosMock = new MockAdapter(getAuthenticatedHttpClient()); axiosMock = new MockAdapter(getAuthenticatedHttpClient());
axiosMock axiosMock
.onGet(getCourseOutlineIndexApiUrl(courseId)) .onGet(getCourseOutlineIndexApiUrl(courseId))
@@ -174,10 +152,6 @@ describe('<CourseOutline />', () => {
await executeThunk(fetchCourseOutlineIndexQuery(courseId), store.dispatch); await executeThunk(fetchCourseOutlineIndexQuery(courseId), store.dispatch);
}); });
afterEach(() => {
jest.restoreAllMocks();
});
it('render CourseOutline component correctly', async () => { it('render CourseOutline component correctly', async () => {
const { getByText } = render(<RootWrapper />); const { getByText } = render(<RootWrapper />);
@@ -288,15 +262,13 @@ describe('<CourseOutline />', () => {
}); });
it('check that new section list is saved when dragged', async () => { it('check that new section list is saved when dragged', async () => {
const { findAllByRole, findByTestId } = render(<RootWrapper />); const { findAllByRole } = render(<RootWrapper />);
const expandAllButton = await findByTestId('expand-collapse-all-button'); const courseBlockId = courseOutlineIndexMock.courseStructure.id;
fireEvent.click(expandAllButton);
const [section] = store.getState().courseOutline.sectionsList;
const sectionsDraggers = await findAllByRole('button', { name: 'Drag to reorder' }); const sectionsDraggers = await findAllByRole('button', { name: 'Drag to reorder' });
const draggableButton = sectionsDraggers[1]; const draggableButton = sectionsDraggers[6];
axiosMock axiosMock
.onPut(getCourseBlockApiUrl(section.id)) .onPut(getCourseBlockApiUrl(courseBlockId))
.reply(200, { dummy: 'value' }); .reply(200, { dummy: 'value' });
const section1 = store.getState().courseOutline.sectionsList[0].id; const section1 = store.getState().courseOutline.sectionsList[0].id;
@@ -315,15 +287,13 @@ describe('<CourseOutline />', () => {
}); });
it('check section list is restored to original order when API call fails', async () => { it('check section list is restored to original order when API call fails', async () => {
const { findAllByRole, findByTestId } = render(<RootWrapper />); const { findAllByRole } = render(<RootWrapper />);
const expandAllButton = await findByTestId('expand-collapse-all-button'); const courseBlockId = courseOutlineIndexMock.courseStructure.id;
fireEvent.click(expandAllButton);
const [section] = store.getState().courseOutline.sectionsList;
const sectionsDraggers = await findAllByRole('button', { name: 'Drag to reorder' }); const sectionsDraggers = await findAllByRole('button', { name: 'Drag to reorder' });
const draggableButton = sectionsDraggers[1]; const draggableButton = sectionsDraggers[6];
axiosMock axiosMock
.onPut(getCourseBlockApiUrl(section.id)) .onPut(getCourseBlockApiUrl(courseBlockId))
.reply(500); .reply(500);
const section1 = store.getState().courseOutline.sectionsList[0].id; const section1 = store.getState().courseOutline.sectionsList[0].id;
@@ -398,6 +368,8 @@ describe('<CourseOutline />', () => {
const { findAllByTestId } = render(<RootWrapper />); const { findAllByTestId } = render(<RootWrapper />);
const [sectionElement] = await findAllByTestId('section-card'); const [sectionElement] = await findAllByTestId('section-card');
const [subsectionElement] = await within(sectionElement).findAllByTestId('subsection-card'); const [subsectionElement] = await within(sectionElement).findAllByTestId('subsection-card');
const expandBtn = await within(subsectionElement).findByTestId('subsection-card-header__expanded-btn');
fireEvent.click(expandBtn);
const units = await within(subsectionElement).findAllByTestId('unit-card'); const units = await within(subsectionElement).findAllByTestId('unit-card');
expect(units.length).toBe(1); expect(units.length).toBe(1);
@@ -418,40 +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 () => { it('render checklist value correctly', async () => {
const { getByText } = render(<RootWrapper />); const { getByText } = render(<RootWrapper />);
@@ -645,6 +583,8 @@ describe('<CourseOutline />', () => {
await checkEditTitle(section, subsectionElement, subsection, 'New subsection name', 'subsection'); await checkEditTitle(section, subsectionElement, subsection, 'New subsection name', 'subsection');
// check unit // check unit
const expandBtn = await within(subsectionElement).findByTestId('subsection-card-header__expanded-btn');
fireEvent.click(expandBtn);
const [unit] = subsection.childInfo.children; const [unit] = subsection.childInfo.children;
const [unitElement] = await within(subsectionElement).findAllByTestId('unit-card'); const [unitElement] = await within(subsectionElement).findAllByTestId('unit-card');
await checkEditTitle(section, unitElement, unit, 'New unit name', 'unit'); await checkEditTitle(section, unitElement, unit, 'New unit name', 'unit');
@@ -657,6 +597,8 @@ describe('<CourseOutline />', () => {
const [sectionElement] = await screen.findAllByTestId('section-card'); const [sectionElement] = await screen.findAllByTestId('section-card');
const [subsection] = section.childInfo.children; const [subsection] = section.childInfo.children;
const [subsectionElement] = await within(sectionElement).findAllByTestId('subsection-card'); const [subsectionElement] = await within(sectionElement).findAllByTestId('subsection-card');
const expandBtn = await within(subsectionElement).findByTestId('subsection-card-header__expanded-btn');
fireEvent.click(expandBtn);
const [unit] = subsection.childInfo.children; const [unit] = subsection.childInfo.children;
const [unitElement] = await within(subsectionElement).findAllByTestId('unit-card'); const [unitElement] = await within(subsectionElement).findAllByTestId('unit-card');
@@ -695,6 +637,8 @@ describe('<CourseOutline />', () => {
const [sectionElement] = await findAllByTestId('section-card'); const [sectionElement] = await findAllByTestId('section-card');
const [subsection] = section.childInfo.children; const [subsection] = section.childInfo.children;
const [subsectionElement] = await within(sectionElement).findAllByTestId('subsection-card'); const [subsectionElement] = await within(sectionElement).findAllByTestId('subsection-card');
const expandBtn = await within(subsectionElement).findByTestId('subsection-card-header__expanded-btn');
fireEvent.click(expandBtn);
const [unit] = subsection.childInfo.children; const [unit] = subsection.childInfo.children;
const [unitElement] = await within(subsectionElement).findAllByTestId('unit-card'); const [unitElement] = await within(subsectionElement).findAllByTestId('unit-card');
@@ -764,6 +708,8 @@ describe('<CourseOutline />', () => {
const [sectionElement] = await findAllByTestId('section-card'); const [sectionElement] = await findAllByTestId('section-card');
const [subsection] = section.childInfo.children; const [subsection] = section.childInfo.children;
const [subsectionElement] = await within(sectionElement).findAllByTestId('subsection-card'); const [subsectionElement] = await within(sectionElement).findAllByTestId('subsection-card');
const expandBtn = await within(subsectionElement).findByTestId('subsection-card-header__expanded-btn');
fireEvent.click(expandBtn);
const [unit] = subsection.childInfo.children; const [unit] = subsection.childInfo.children;
const [unitElement] = await within(subsectionElement).findAllByTestId('unit-card'); const [unitElement] = await within(subsectionElement).findAllByTestId('unit-card');
@@ -1472,6 +1418,8 @@ describe('<CourseOutline />', () => {
const [firstSection] = await findAllByTestId('section-card'); const [firstSection] = await findAllByTestId('section-card');
const [firstSubsection] = await within(firstSection).findAllByTestId('subsection-card'); const [firstSubsection] = await within(firstSection).findAllByTestId('subsection-card');
const subsectionExpandButton = await within(firstSubsection).getByTestId('subsection-card-header__expanded-btn');
fireEvent.click(subsectionExpandButton);
const [firstUnit] = await within(firstSubsection).findAllByTestId('unit-card'); const [firstUnit] = await within(firstSubsection).findAllByTestId('unit-card');
const unitDropdownButton = await within(firstUnit).findByTestId('unit-card-header__menu-button'); const unitDropdownButton = await within(firstUnit).findByTestId('unit-card-header__menu-button');
@@ -1831,6 +1779,8 @@ describe('<CourseOutline />', () => {
const [, sectionElement] = await findAllByTestId('section-card'); const [, sectionElement] = await findAllByTestId('section-card');
const [, subsection] = section.childInfo.children; const [, subsection] = section.childInfo.children;
const [, subsectionElement] = await within(sectionElement).findAllByTestId('subsection-card'); const [, subsectionElement] = await within(sectionElement).findAllByTestId('subsection-card');
const expandBtn = await within(subsectionElement).findByTestId('subsection-card-header__expanded-btn');
await act(async () => fireEvent.click(expandBtn));
const [, secondUnit] = subsection.childInfo.children; const [, secondUnit] = subsection.childInfo.children;
const [, unitElement] = await within(subsectionElement).findAllByTestId('unit-card'); const [, unitElement] = await within(subsectionElement).findAllByTestId('unit-card');
@@ -1870,6 +1820,8 @@ describe('<CourseOutline />', () => {
const [, sectionElement] = await findAllByTestId('section-card'); const [, sectionElement] = await findAllByTestId('section-card');
const [firstSubsection, subsection] = section.childInfo.children; const [firstSubsection, subsection] = section.childInfo.children;
const [, subsectionElement] = await within(sectionElement).findAllByTestId('subsection-card'); const [, subsectionElement] = await within(sectionElement).findAllByTestId('subsection-card');
const expandBtn = await within(subsectionElement).findByTestId('subsection-card-header__expanded-btn');
await act(async () => fireEvent.click(expandBtn));
const [unit] = subsection.childInfo.children; const [unit] = subsection.childInfo.children;
const [unitElement] = await within(subsectionElement).findAllByTestId('unit-card'); const [unitElement] = await within(subsectionElement).findAllByTestId('unit-card');
@@ -1905,6 +1857,8 @@ describe('<CourseOutline />', () => {
const [subsection] = secondSection.childInfo.children; const [subsection] = secondSection.childInfo.children;
const firstSectionLastSubsection = firstSection.childInfo.children[firstSection.childInfo.children.length - 1]; const firstSectionLastSubsection = firstSection.childInfo.children[firstSection.childInfo.children.length - 1];
const [subsectionElement] = await within(sectionElement).findAllByTestId('subsection-card'); const [subsectionElement] = await within(sectionElement).findAllByTestId('subsection-card');
const expandBtn = await within(subsectionElement).findByTestId('subsection-card-header__expanded-btn');
await act(async () => fireEvent.click(expandBtn));
const [unit] = subsection.childInfo.children; const [unit] = subsection.childInfo.children;
const [unitElement] = await within(subsectionElement).findAllByTestId('unit-card'); const [unitElement] = await within(subsectionElement).findAllByTestId('unit-card');
@@ -1949,6 +1903,8 @@ describe('<CourseOutline />', () => {
const [, sectionElement] = await findAllByTestId('section-card'); const [, sectionElement] = await findAllByTestId('section-card');
const [firstSubsection, subsection] = section.childInfo.children; const [firstSubsection, subsection] = section.childInfo.children;
const [subsectionElement] = await within(sectionElement).findAllByTestId('subsection-card'); const [subsectionElement] = await within(sectionElement).findAllByTestId('subsection-card');
const expandBtn = await within(subsectionElement).findByTestId('subsection-card-header__expanded-btn');
await act(async () => fireEvent.click(expandBtn));
const lastUnitIdx = firstSubsection.childInfo.children.length - 1; const lastUnitIdx = firstSubsection.childInfo.children.length - 1;
const unit = firstSubsection.childInfo.children[lastUnitIdx]; const unit = firstSubsection.childInfo.children[lastUnitIdx];
const unitElement = (await within(subsectionElement).findAllByTestId('unit-card'))[lastUnitIdx]; const unitElement = (await within(subsectionElement).findAllByTestId('unit-card'))[lastUnitIdx];
@@ -1986,6 +1942,8 @@ describe('<CourseOutline />', () => {
const secondSectionLastSubsection = secondSection.childInfo.children[lastSubIndex]; const secondSectionLastSubsection = secondSection.childInfo.children[lastSubIndex];
const thirdSectionFirstSubsection = thirdSection.childInfo.children[0]; const thirdSectionFirstSubsection = thirdSection.childInfo.children[0];
const subsectionElement = (await within(sectionElement).findAllByTestId('subsection-card'))[lastSubIndex]; const subsectionElement = (await within(sectionElement).findAllByTestId('subsection-card'))[lastSubIndex];
const expandBtn = await within(subsectionElement).findByTestId('subsection-card-header__expanded-btn');
await act(async () => fireEvent.click(expandBtn));
const lastUnitIdx = secondSectionLastSubsection.childInfo.children.length - 1; const lastUnitIdx = secondSectionLastSubsection.childInfo.children.length - 1;
const unit = secondSectionLastSubsection.childInfo.children[lastUnitIdx]; const unit = secondSectionLastSubsection.childInfo.children[lastUnitIdx];
const unitElement = (await within(subsectionElement).findAllByTestId('unit-card'))[lastUnitIdx]; const unitElement = (await within(subsectionElement).findAllByTestId('unit-card'))[lastUnitIdx];
@@ -2030,6 +1988,8 @@ describe('<CourseOutline />', () => {
const sections = await findAllByTestId('section-card'); const sections = await findAllByTestId('section-card');
const [sectionElement] = sections; const [sectionElement] = sections;
const [subsectionElement] = await within(sectionElement).findAllByTestId('subsection-card'); const [subsectionElement] = await within(sectionElement).findAllByTestId('subsection-card');
const expandBtn = await within(subsectionElement).findByTestId('subsection-card-header__expanded-btn');
await act(async () => fireEvent.click(expandBtn));
// get first and only unit in the subsection // get first and only unit in the subsection
const [firstUnit] = await within(subsectionElement).findAllByTestId('unit-card'); const [firstUnit] = await within(subsectionElement).findAllByTestId('unit-card');
@@ -2049,6 +2009,8 @@ describe('<CourseOutline />', () => {
const lastSection = sections[sections.length - 1]; const lastSection = sections[sections.length - 1];
// it has only one subsection // it has only one subsection
const [lastSubsectionElement] = await within(lastSection).findAllByTestId('subsection-card'); const [lastSubsectionElement] = await within(lastSection).findAllByTestId('subsection-card');
const lastExpandBtn = await within(lastSubsectionElement).findByTestId('subsection-card-header__expanded-btn');
await act(async () => fireEvent.click(lastExpandBtn));
// get last and the only unit in the subsection // get last and the only unit in the subsection
const [lastUnit] = await within(lastSubsectionElement).findAllByTestId('unit-card'); const [lastUnit] = await within(lastSubsectionElement).findAllByTestId('unit-card');
@@ -2069,9 +2031,6 @@ describe('<CourseOutline />', () => {
const { findAllByTestId } = render(<RootWrapper />); const { findAllByTestId } = render(<RootWrapper />);
const [sectionElement] = await findAllByTestId('section-card'); const [sectionElement] = await findAllByTestId('section-card');
const [subsectionElement] = await within(sectionElement).findAllByTestId('subsection-card');
const expandBtn = await within(subsectionElement).findByTestId('subsection-card-header__expanded-btn');
fireEvent.click(expandBtn);
const [section] = store.getState().courseOutline.sectionsList; const [section] = store.getState().courseOutline.sectionsList;
const subsectionsDraggers = within(sectionElement).getAllByRole('button', { name: 'Drag to reorder' }); const subsectionsDraggers = within(sectionElement).getAllByRole('button', { name: 'Drag to reorder' });
const draggableButton = subsectionsDraggers[1]; const draggableButton = subsectionsDraggers[1];
@@ -2103,9 +2062,6 @@ describe('<CourseOutline />', () => {
const { findAllByTestId } = render(<RootWrapper />); const { findAllByTestId } = render(<RootWrapper />);
const [sectionElement] = await findAllByTestId('section-card'); const [sectionElement] = await findAllByTestId('section-card');
const [subsectionElement] = await within(sectionElement).findAllByTestId('subsection-card');
const expandBtn = await within(subsectionElement).findByTestId('subsection-card-header__expanded-btn');
fireEvent.click(expandBtn);
const [section] = store.getState().courseOutline.sectionsList; const [section] = store.getState().courseOutline.sectionsList;
const subsectionsDraggers = within(sectionElement).getAllByRole('button', { name: 'Drag to reorder' }); const subsectionsDraggers = within(sectionElement).getAllByRole('button', { name: 'Drag to reorder' });
const draggableButton = subsectionsDraggers[1]; const draggableButton = subsectionsDraggers[1];
@@ -2135,6 +2091,8 @@ describe('<CourseOutline />', () => {
const [subsectionElement] = await within(sectionElement).findAllByTestId('subsection-card'); const [subsectionElement] = await within(sectionElement).findAllByTestId('subsection-card');
const section = store.getState().courseOutline.sectionsList[2]; const section = store.getState().courseOutline.sectionsList[2];
const [subsection] = section.childInfo.children; const [subsection] = section.childInfo.children;
const expandBtn = within(subsectionElement).getByTestId('subsection-card-header__expanded-btn');
fireEvent.click(expandBtn);
const unitDraggers = await within(subsectionElement).findAllByRole('button', { name: 'Drag to reorder' }); const unitDraggers = await within(subsectionElement).findAllByRole('button', { name: 'Drag to reorder' });
const draggableButton = unitDraggers[1]; const draggableButton = unitDraggers[1];
const sections = courseOutlineIndexMock.courseStructure.childInfo.children; const sections = courseOutlineIndexMock.courseStructure.childInfo.children;
@@ -2169,6 +2127,8 @@ describe('<CourseOutline />', () => {
const [subsectionElement] = await within(sectionElement).findAllByTestId('subsection-card'); const [subsectionElement] = await within(sectionElement).findAllByTestId('subsection-card');
const section = store.getState().courseOutline.sectionsList[2]; const section = store.getState().courseOutline.sectionsList[2];
const [subsection] = section.childInfo.children; const [subsection] = section.childInfo.children;
const expandBtn = within(subsectionElement).getByTestId('subsection-card-header__expanded-btn');
fireEvent.click(expandBtn);
const unitDraggers = await within(subsectionElement).findAllByRole('button', { name: 'Drag to reorder' }); const unitDraggers = await within(subsectionElement).findAllByRole('button', { name: 'Drag to reorder' });
const draggableButton = unitDraggers[1]; const draggableButton = unitDraggers[1];
const sections = courseOutlineIndexMock.courseStructure.childInfo.children; const sections = courseOutlineIndexMock.courseStructure.childInfo.children;
@@ -2206,6 +2166,8 @@ describe('<CourseOutline />', () => {
.onGet(getXBlockApiUrl(section.id)) .onGet(getXBlockApiUrl(section.id))
.reply(200, courseSectionMock); .reply(200, courseSectionMock);
let [subsectionElement] = await within(sectionElement).findAllByTestId('subsection-card'); let [subsectionElement] = await within(sectionElement).findAllByTestId('subsection-card');
const expandBtn = await within(subsectionElement).findByTestId('subsection-card-header__expanded-btn');
await userEvent.click(expandBtn);
const [unit] = subsection.childInfo.children; const [unit] = subsection.childInfo.children;
const [unitElement] = await within(subsectionElement).findAllByTestId('unit-card'); const [unitElement] = await within(subsectionElement).findAllByTestId('unit-card');

View File

@@ -292,11 +292,6 @@ module.exports = {
selectedPartitionIndex: -1, selectedPartitionIndex: -1,
selectedGroupsLabel: '', selectedGroupsLabel: '',
}, },
upstreamInfo: {
readyToSync: false,
upstreamRef: undefined,
versionSynced: undefined,
},
}, },
], ],
}, },
@@ -680,11 +675,6 @@ module.exports = {
selectedPartitionIndex: -1, selectedPartitionIndex: -1,
selectedGroupsLabel: '', selectedGroupsLabel: '',
}, },
upstreamInfo: {
readyToSync: false,
upstreamRef: undefined,
versionSynced: undefined,
},
}, },
{ {
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@vertical_2dbb0072785e', id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@vertical_2dbb0072785e',
@@ -769,11 +759,6 @@ module.exports = {
selectedPartitionIndex: -1, selectedPartitionIndex: -1,
selectedGroupsLabel: '', selectedGroupsLabel: '',
}, },
upstreamInfo: {
readyToSync: false,
upstreamRef: undefined,
versionSynced: undefined,
},
}, },
{ {
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@vertical_98cf62510471', id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@vertical_98cf62510471',
@@ -858,11 +843,6 @@ module.exports = {
selectedPartitionIndex: -1, selectedPartitionIndex: -1,
selectedGroupsLabel: '', selectedGroupsLabel: '',
}, },
upstreamInfo: {
readyToSync: false,
upstreamRef: undefined,
versionSynced: undefined,
},
}, },
{ {
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@vertical_d32bf9b2242c', id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@vertical_d32bf9b2242c',
@@ -947,11 +927,6 @@ module.exports = {
selectedPartitionIndex: -1, selectedPartitionIndex: -1,
selectedGroupsLabel: '', selectedGroupsLabel: '',
}, },
upstreamInfo: {
readyToSync: false,
upstreamRef: undefined,
versionSynced: undefined,
},
}, },
{ {
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@4e592689563243c484af947465eaef0d', id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@4e592689563243c484af947465eaef0d',
@@ -1036,11 +1011,6 @@ module.exports = {
selectedPartitionIndex: -1, selectedPartitionIndex: -1,
selectedGroupsLabel: '', selectedGroupsLabel: '',
}, },
upstreamInfo: {
readyToSync: false,
upstreamRef: undefined,
versionSynced: undefined,
},
}, },
], ],
}, },
@@ -1226,11 +1196,6 @@ module.exports = {
selectedPartitionIndex: -1, selectedPartitionIndex: -1,
selectedGroupsLabel: '', selectedGroupsLabel: '',
}, },
upstreamInfo: {
readyToSync: false,
upstreamRef: undefined,
versionSynced: undefined,
},
}, },
{ {
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@vertical_aae927868e55', id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@vertical_aae927868e55',
@@ -1315,11 +1280,6 @@ module.exports = {
selectedPartitionIndex: -1, selectedPartitionIndex: -1,
selectedGroupsLabel: '', selectedGroupsLabel: '',
}, },
upstreamInfo: {
readyToSync: false,
upstreamRef: undefined,
versionSynced: undefined,
},
}, },
{ {
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@vertical_c037f3757df1', id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@vertical_c037f3757df1',
@@ -1404,11 +1364,6 @@ module.exports = {
selectedPartitionIndex: -1, selectedPartitionIndex: -1,
selectedGroupsLabel: '', selectedGroupsLabel: '',
}, },
upstreamInfo: {
readyToSync: false,
upstreamRef: undefined,
versionSynced: undefined,
},
}, },
{ {
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@vertical_bc69a47c6fae', id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@vertical_bc69a47c6fae',
@@ -1493,11 +1448,6 @@ module.exports = {
selectedPartitionIndex: -1, selectedPartitionIndex: -1,
selectedGroupsLabel: '', selectedGroupsLabel: '',
}, },
upstreamInfo: {
readyToSync: false,
upstreamRef: undefined,
versionSynced: undefined,
},
}, },
{ {
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@8f89194410954e768bde1764985454a7', id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@8f89194410954e768bde1764985454a7',
@@ -1582,11 +1532,6 @@ module.exports = {
selectedPartitionIndex: -1, selectedPartitionIndex: -1,
selectedGroupsLabel: '', selectedGroupsLabel: '',
}, },
upstreamInfo: {
readyToSync: false,
upstreamRef: undefined,
versionSynced: undefined,
},
}, },
], ],
}, },
@@ -1772,11 +1717,6 @@ module.exports = {
selectedPartitionIndex: -1, selectedPartitionIndex: -1,
selectedGroupsLabel: '', selectedGroupsLabel: '',
}, },
upstreamInfo: {
readyToSync: false,
upstreamRef: undefined,
versionSynced: undefined,
},
}, },
], ],
}, },
@@ -2055,11 +1995,6 @@ module.exports = {
selectedPartitionIndex: -1, selectedPartitionIndex: -1,
selectedGroupsLabel: '', selectedGroupsLabel: '',
}, },
upstreamInfo: {
readyToSync: false,
upstreamRef: undefined,
versionSynced: undefined,
},
}, },
{ {
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@vertical_f04afeac0131', id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@vertical_f04afeac0131',
@@ -2144,11 +2079,6 @@ module.exports = {
selectedPartitionIndex: -1, selectedPartitionIndex: -1,
selectedGroupsLabel: '', selectedGroupsLabel: '',
}, },
upstreamInfo: {
readyToSync: false,
upstreamRef: undefined,
versionSynced: undefined,
},
}, },
{ {
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@b6662b497c094bcc9b870d8270c90c93', id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@b6662b497c094bcc9b870d8270c90c93',
@@ -2233,11 +2163,6 @@ module.exports = {
selectedPartitionIndex: -1, selectedPartitionIndex: -1,
selectedGroupsLabel: '', selectedGroupsLabel: '',
}, },
upstreamInfo: {
readyToSync: false,
upstreamRef: undefined,
versionSynced: undefined,
},
}, },
{ {
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@f91d8d31f7cf48ce990f8d8745ae4cfa', id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@f91d8d31f7cf48ce990f8d8745ae4cfa',
@@ -2322,11 +2247,6 @@ module.exports = {
selectedPartitionIndex: -1, selectedPartitionIndex: -1,
selectedGroupsLabel: '', selectedGroupsLabel: '',
}, },
upstreamInfo: {
readyToSync: false,
upstreamRef: undefined,
versionSynced: undefined,
},
}, },
{ {
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@vertical_ac391cde8a91', id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@vertical_ac391cde8a91',
@@ -2411,11 +2331,6 @@ module.exports = {
selectedPartitionIndex: -1, selectedPartitionIndex: -1,
selectedGroupsLabel: '', selectedGroupsLabel: '',
}, },
upstreamInfo: {
readyToSync: false,
upstreamRef: undefined,
versionSynced: undefined,
},
}, },
{ {
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@vertical_36e0beb03f0a', id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@vertical_36e0beb03f0a',
@@ -2500,11 +2415,6 @@ module.exports = {
selectedPartitionIndex: -1, selectedPartitionIndex: -1,
selectedGroupsLabel: '', selectedGroupsLabel: '',
}, },
upstreamInfo: {
readyToSync: false,
upstreamRef: undefined,
versionSynced: undefined,
},
}, },
{ {
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@1b0e2c2c84884b95b1c99fb678cc964c', id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@1b0e2c2c84884b95b1c99fb678cc964c',
@@ -2589,11 +2499,6 @@ module.exports = {
selectedPartitionIndex: -1, selectedPartitionIndex: -1,
selectedGroupsLabel: '', selectedGroupsLabel: '',
}, },
upstreamInfo: {
readyToSync: false,
upstreamRef: undefined,
versionSynced: undefined,
},
}, },
{ {
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@c7e98fd39a6944edb6b286c32e1150ff', id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@c7e98fd39a6944edb6b286c32e1150ff',
@@ -2678,11 +2583,6 @@ module.exports = {
selectedPartitionIndex: -1, selectedPartitionIndex: -1,
selectedGroupsLabel: '', selectedGroupsLabel: '',
}, },
upstreamInfo: {
readyToSync: false,
upstreamRef: undefined,
versionSynced: undefined,
},
}, },
{ {
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@d6eaa391d2be41dea20b8b1bfbcb1c45', id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@d6eaa391d2be41dea20b8b1bfbcb1c45',
@@ -2767,11 +2667,6 @@ module.exports = {
selectedPartitionIndex: -1, selectedPartitionIndex: -1,
selectedGroupsLabel: '', selectedGroupsLabel: '',
}, },
upstreamInfo: {
readyToSync: false,
upstreamRef: undefined,
versionSynced: undefined,
},
}, },
], ],
}, },
@@ -3050,11 +2945,6 @@ module.exports = {
selectedPartitionIndex: -1, selectedPartitionIndex: -1,
selectedGroupsLabel: '', selectedGroupsLabel: '',
}, },
upstreamInfo: {
readyToSync: false,
upstreamRef: undefined,
versionSynced: undefined,
},
}, },
], ],
}, },

View File

@@ -15,7 +15,6 @@ import {
import { import {
MoreVert as MoveVertIcon, MoreVert as MoveVertIcon,
EditOutline as EditIcon, EditOutline as EditIcon,
Sync as SyncIcon,
} from '@openedx/paragon/icons'; } from '@openedx/paragon/icons';
import { useContentTagsCount } from '../../generic/data/apiHooks'; import { useContentTagsCount } from '../../generic/data/apiHooks';
@@ -56,8 +55,6 @@ const CardHeader = ({
discussionsSettings, discussionsSettings,
parentInfo, parentInfo,
extraActionsComponent, extraActionsComponent,
onClickSync,
readyToSync,
}) => { }) => {
const intl = useIntl(); const intl = useIntl();
const [searchParams] = useSearchParams(); const [searchParams] = useSearchParams();
@@ -133,23 +130,12 @@ const CardHeader = ({
) : ( ) : (
<> <>
{titleComponent} {titleComponent}
{readyToSync && (
<IconButton
className="item-card-button-icon"
data-testid={`${namePrefix}-sync-button`}
alt={intl.formatMessage(messages.readyToSyncButtonAlt)}
iconAs={SyncIcon}
onClick={onClickSync}
/>
)}
<IconButton <IconButton
className="item-card-button-icon" className="item-card-edit-icon"
data-testid={`${namePrefix}-edit-button`} data-testid={`${namePrefix}-edit-button`}
alt={intl.formatMessage(messages.altButtonEdit)} alt={intl.formatMessage(messages.altButtonEdit)}
iconAs={EditIcon} iconAs={EditIcon}
onClick={onClickEdit} onClick={onClickEdit}
// @ts-ignore
disabled={isDisabledEditField}
/> />
</> </>
)} )}
@@ -192,7 +178,6 @@ const CardHeader = ({
</Dropdown.Item> </Dropdown.Item>
<Dropdown.Item <Dropdown.Item
data-testid={`${namePrefix}-card-header__menu-configure-button`} data-testid={`${namePrefix}-card-header__menu-configure-button`}
disabled={isDisabledEditField}
onClick={onClickConfigure} onClick={onClickConfigure}
> >
{intl.formatMessage(messages.menuConfigure)} {intl.formatMessage(messages.menuConfigure)}
@@ -200,7 +185,6 @@ const CardHeader = ({
{getConfig().ENABLE_TAGGING_TAXONOMY_PAGES === 'true' && ( {getConfig().ENABLE_TAGGING_TAXONOMY_PAGES === 'true' && (
<Dropdown.Item <Dropdown.Item
data-testid={`${namePrefix}-card-header__menu-manage-tags-button`} data-testid={`${namePrefix}-card-header__menu-manage-tags-button`}
disabled={isDisabledEditField}
onClick={openManageTagsDrawer} onClick={openManageTagsDrawer}
> >
{intl.formatMessage(messages.menuManageTags)} {intl.formatMessage(messages.menuManageTags)}
@@ -271,8 +255,6 @@ CardHeader.defaultProps = {
parentInfo: {}, parentInfo: {},
cardId: '', cardId: '',
extraActionsComponent: null, extraActionsComponent: null,
readyToSync: false,
onClickSync: null,
}; };
CardHeader.propTypes = { CardHeader.propTypes = {
@@ -319,8 +301,6 @@ CardHeader.propTypes = {
// An optional component that is rendered before the dropdown. This is used by the Subsection // An optional component that is rendered before the dropdown. This is used by the Subsection
// and Unit card components to render their plugin slots. // and Unit card components to render their plugin slots.
extraActionsComponent: PropTypes.node, extraActionsComponent: PropTypes.node,
onClickSync: PropTypes.func,
readyToSync: PropTypes.bool,
}; };
export default CardHeader; export default CardHeader;

View File

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

View File

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

View File

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

View File

@@ -53,7 +53,6 @@ import {
setPasteFileNotices, setPasteFileNotices,
updateCourseLaunchQueryStatus, updateCourseLaunchQueryStatus,
} from './slice'; } from './slice';
import { createCourseXblock } from '../../course-unit/data/api';
export function fetchCourseOutlineIndexQuery(courseId) { export function fetchCourseOutlineIndexQuery(courseId) {
return async (dispatch) => { return async (dispatch) => {
@@ -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( function setBlockOrderListQuery(
parentId, parentId,
blockIds, blockIds,

View File

@@ -66,8 +66,6 @@ const HeaderNavigations = ({
{hasSections && ( {hasSections && (
<Button <Button
variant="outline-primary" variant="outline-primary"
id="expand-collapse-all-button"
data-testid="expand-collapse-all-button"
iconBefore={isSectionsExpanded ? ArrowUpIcon : ArrowDownIcon} iconBefore={isSectionsExpanded ? ArrowUpIcon : ArrowDownIcon}
onClick={handleExpandAll} onClick={handleExpandAll}
> >

View File

@@ -53,7 +53,6 @@ import {
setUnitOrderListQuery, setUnitOrderListQuery,
pasteClipboardContent, pasteClipboardContent,
dismissNotificationQuery, dismissNotificationQuery,
addUnitFromLibrary,
} from './data/thunk'; } from './data/thunk';
const useCourseOutline = ({ courseId }) => { const useCourseOutline = ({ courseId }) => {
@@ -129,10 +128,6 @@ const useCourseOutline = ({ courseId }) => {
dispatch(addNewUnitQuery(subsectionId, openUnitPage)); dispatch(addNewUnitQuery(subsectionId, openUnitPage));
}; };
const handleAddUnitFromLibrary = (body) => {
dispatch(addUnitFromLibrary(body, openUnitPage));
};
const headerNavigationsActions = { const headerNavigationsActions = {
handleNewSection: handleNewSectionSubmit, handleNewSection: handleNewSectionSubmit,
handleReIndex: () => { handleReIndex: () => {
@@ -341,7 +336,6 @@ const useCourseOutline = ({ courseId }) => {
getUnitUrl, getUnitUrl,
openUnitPage, openUnitPage,
handleNewUnitSubmit, handleNewUnitSubmit,
handleAddUnitFromLibrary,
handleVideoSharingOptionChange, handleVideoSharingOptionChange,
handlePasteClipboardClick, handlePasteClipboardClick,
notificationDismissUrl, notificationDismissUrl,

View File

@@ -1,12 +1,12 @@
// @ts-check // @ts-check
import React, { import React, {
useContext, useEffect, useState, useRef, useCallback, useContext, useEffect, useState, useRef,
} from 'react'; } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { useDispatch, useSelector } from 'react-redux'; import { useDispatch } from 'react-redux';
import { useSearchParams } from 'react-router-dom'; import { useSearchParams } from 'react-router-dom';
import { useIntl } from '@edx/frontend-platform/i18n'; import { useIntl } from '@edx/frontend-platform/i18n';
import { Button, StandardModal, useToggle } from '@openedx/paragon'; import { Button, useToggle } from '@openedx/paragon';
import { Add as IconAdd } from '@openedx/paragon/icons'; import { Add as IconAdd } from '@openedx/paragon/icons';
import classNames from 'classnames'; import classNames from 'classnames';
import { isEmpty } from 'lodash'; import { isEmpty } from 'lodash';
@@ -22,16 +22,10 @@ import TitleButton from '../card-header/TitleButton';
import XBlockStatus from '../xblock-status/XBlockStatus'; import XBlockStatus from '../xblock-status/XBlockStatus';
import { getItemStatus, getItemStatusBorder, scrollToElement } from '../utils'; import { getItemStatus, getItemStatusBorder, scrollToElement } from '../utils';
import messages from './messages'; import messages from './messages';
import { ComponentPicker } from '../../library-authoring';
import { COMPONENT_TYPES } from '../../generic/block-type-utils/constants';
import { ContainerType } from '../../generic/key-utils';
import { ContentType } from '../../library-authoring/routes';
import { getStudioHomeData } from '../../studio-home/data/selectors';
const SubsectionCard = ({ const SubsectionCard = ({
section, section,
subsection, subsection,
isSectionsExpanded,
isSelfPaced, isSelfPaced,
isCustomRelativeDatesActive, isCustomRelativeDatesActive,
children, children,
@@ -43,7 +37,6 @@ const SubsectionCard = ({
onOpenDeleteModal, onOpenDeleteModal,
onDuplicateSubmit, onDuplicateSubmit,
onNewUnitSubmit, onNewUnitSubmit,
onAddUnitFromLibrary,
onOrderChange, onOrderChange,
onOpenConfigureModal, onOpenConfigureModal,
onPasteClick, onPasteClick,
@@ -58,17 +51,6 @@ const SubsectionCard = ({
const [isFormOpen, openForm, closeForm] = useToggle(false); const [isFormOpen, openForm, closeForm] = useToggle(false);
const namePrefix = 'subsection'; const namePrefix = 'subsection';
const { sharedClipboardData, showPasteUnit } = useClipboard(); const { sharedClipboardData, showPasteUnit } = 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 { const {
id, id,
@@ -99,7 +81,7 @@ const SubsectionCard = ({
return false; return false;
}; };
const [isExpanded, setIsExpanded] = useState(containsSearchResult() || !isHeaderVisible || isSectionsExpanded); const [isExpanded, setIsExpanded] = useState(containsSearchResult() || !isHeaderVisible);
const subsectionStatus = getItemStatus({ const subsectionStatus = getItemStatus({
published, published,
visibilityState, visibilityState,
@@ -107,10 +89,6 @@ const SubsectionCard = ({
}); });
const borderStyle = getItemStatusBorder(subsectionStatus); const borderStyle = getItemStatusBorder(subsectionStatus);
useEffect(() => {
setIsExpanded(isSectionsExpanded);
}, [isSectionsExpanded]);
const handleExpandContent = () => { const handleExpandContent = () => {
setIsExpanded((prevState) => !prevState); setIsExpanded((prevState) => !prevState);
}; };
@@ -194,129 +172,90 @@ const SubsectionCard = ({
&& !(isHeaderVisible === false) && !(isHeaderVisible === false)
); );
const handleSelectLibraryUnit = useCallback((selectedUnit) => {
onAddUnitFromLibrary({
type: COMPONENT_TYPES.libraryV2,
category: ContainerType.Vertical,
parentLocator: id,
libraryContentKey: selectedUnit.usageKey,
});
closeAddLibraryUnitModal();
}, []);
return ( return (
<> <SortableItem
<SortableItem id={id}
id={id} category={category}
category={category} key={id}
key={id} isDraggable={isDraggable}
isDraggable={isDraggable} isDroppable={actions.childAddable}
isDroppable={actions.childAddable} componentStyle={{
componentStyle={{ background: '#f8f7f6',
background: '#f8f7f6', ...borderStyle,
...borderStyle, }}
}} >
<div
className={`subsection-card ${isScrolledToElement ? 'highlight' : ''}`}
data-testid="subsection-card"
ref={currentRef}
> >
<div {isHeaderVisible && (
className={`subsection-card ${isScrolledToElement ? 'highlight' : ''}`} <>
data-testid="subsection-card" <CardHeader
ref={currentRef} title={displayName}
> status={subsectionStatus}
{isHeaderVisible && ( cardId={id}
<> hasChanges={hasChanges}
<CardHeader onClickMenuButton={handleClickMenuButton}
title={displayName} onClickPublish={onOpenPublishModal}
status={subsectionStatus} onClickEdit={openForm}
cardId={id} onClickDelete={onOpenDeleteModal}
hasChanges={hasChanges} onClickMoveUp={handleSubsectionMoveUp}
onClickMenuButton={handleClickMenuButton} onClickMoveDown={handleSubsectionMoveDown}
onClickPublish={onOpenPublishModal} onClickConfigure={onOpenConfigureModal}
onClickEdit={openForm} isFormOpen={isFormOpen}
onClickDelete={onOpenDeleteModal} closeForm={closeForm}
onClickMoveUp={handleSubsectionMoveUp} onEditSubmit={handleEditSubmit}
onClickMoveDown={handleSubsectionMoveDown} isDisabledEditField={savingStatus === RequestStatus.IN_PROGRESS}
onClickConfigure={onOpenConfigureModal} onClickDuplicate={onDuplicateSubmit}
isFormOpen={isFormOpen} titleComponent={titleComponent}
closeForm={closeForm} namePrefix={namePrefix}
onEditSubmit={handleEditSubmit} actions={actions}
isDisabledEditField={savingStatus === RequestStatus.IN_PROGRESS} proctoringExamConfigurationLink={proctoringExamConfigurationLink}
onClickDuplicate={onDuplicateSubmit} isSequential
titleComponent={titleComponent} extraActionsComponent={extraActionsComponent}
namePrefix={namePrefix} />
actions={actions} <div className="subsection-card__content item-children" data-testid="subsection-card__content">
proctoringExamConfigurationLink={proctoringExamConfigurationLink} <XBlockStatus
isSequential isSelfPaced={isSelfPaced}
extraActionsComponent={extraActionsComponent} 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>
)} </>
</div> )}
</SortableItem> {isExpanded && (
<StandardModal <div
title={intl.formatMessage(messages.unitPickerModalTitle)} data-testid="subsection-card__units"
isOpen={isAddLibraryUnitModalOpen} className={classNames('subsection-card__units', { 'item-children': isDraggable })}
onClose={closeAddLibraryUnitModal} >
isOverflowVisible={false} {children}
size="xl" {actions.childAddable && (
> <>
<ComponentPicker <Button
showOnlyPublished data-testid="new-unit-button"
extraFilter={['block_type = "unit"']} className="mt-4"
componentPickerMode="single" variant="outline-primary"
onComponentSelected={handleSelectLibraryUnit} iconBefore={IconAdd}
visibleTabs={[ContentType.units]} block
/> onClick={handleNewButtonClick}
</StandardModal> >
</> {intl.formatMessage(messages.newUnitButton)}
</Button>
{enableCopyPasteUnits && showPasteUnit && 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,
}).isRequired, }).isRequired,
children: PropTypes.node, children: PropTypes.node,
isSectionsExpanded: PropTypes.bool.isRequired,
isSelfPaced: PropTypes.bool.isRequired, isSelfPaced: PropTypes.bool.isRequired,
isCustomRelativeDatesActive: PropTypes.bool.isRequired, isCustomRelativeDatesActive: PropTypes.bool.isRequired,
onOpenPublishModal: PropTypes.func.isRequired, onOpenPublishModal: PropTypes.func.isRequired,
@@ -368,7 +306,6 @@ SubsectionCard.propTypes = {
onOpenDeleteModal: PropTypes.func.isRequired, onOpenDeleteModal: PropTypes.func.isRequired,
onDuplicateSubmit: PropTypes.func.isRequired, onDuplicateSubmit: PropTypes.func.isRequired,
onNewUnitSubmit: PropTypes.func.isRequired, onNewUnitSubmit: PropTypes.func.isRequired,
onAddUnitFromLibrary: PropTypes.func.isRequired,
index: PropTypes.number.isRequired, index: PropTypes.number.isRequired,
getPossibleMoves: PropTypes.func.isRequired, getPossibleMoves: PropTypes.func.isRequired,
onOrderChange: PropTypes.func.isRequired, onOrderChange: PropTypes.func.isRequired,

View File

@@ -1,6 +1,6 @@
import { MemoryRouter } from 'react-router-dom'; import { MemoryRouter } from 'react-router-dom';
import { import {
act, render, fireEvent, within, screen, act, render, fireEvent, within,
} from '@testing-library/react'; } from '@testing-library/react';
import { IntlProvider } from '@edx/frontend-platform/i18n'; import { IntlProvider } from '@edx/frontend-platform/i18n';
import { AppProvider } from '@edx/frontend-platform/react'; import { AppProvider } from '@edx/frontend-platform/react';
@@ -10,12 +10,9 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import initializeStore from '../../store'; import initializeStore from '../../store';
import SubsectionCard from './SubsectionCard'; import SubsectionCard from './SubsectionCard';
import cardHeaderMessages from '../card-header/messages'; import cardHeaderMessages from '../card-header/messages';
import { COMPONENT_TYPES } from '../../generic/block-type-utils/constants';
let store; let store;
const mockPathname = '/foo-bar'; const mockPathname = '/foo-bar';
const containerKey = 'lct:org:lib:unit:1';
const handleOnAddUnitFromLibrary = jest.fn();
jest.mock('react-router-dom', () => ({ jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'), ...jest.requireActual('react-router-dom'),
@@ -24,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 = { const unit = {
id: 'unit-1', id: 'unit-1',
}; };
@@ -108,7 +80,6 @@ const renderComponent = (props, entry = '/') => render(
onOpenHighlightsModal={jest.fn()} onOpenHighlightsModal={jest.fn()}
onOpenDeleteModal={jest.fn()} onOpenDeleteModal={jest.fn()}
onNewUnitSubmit={jest.fn()} onNewUnitSubmit={jest.fn()}
onAddUnitFromLibrary={handleOnAddUnitFromLibrary}
isCustomRelativeDatesActive={false} isCustomRelativeDatesActive={false}
onEditClick={jest.fn()} onEditClick={jest.fn()}
savingStatus="" savingStatus=""
@@ -276,31 +247,4 @@ describe('<SubsectionCard />', () => {
expect(cardUnits).toBeNull(); expect(cardUnits).toBeNull();
expect(newUnitButton).toBeNull(); expect(newUnitButton).toBeNull();
}); });
it('should add unit from library', async () => {
renderComponent();
const expandButton = await screen.findByTestId('subsection-card-header__expanded-btn');
fireEvent.click(expandButton);
const useUnitFromLibraryButton = screen.getByRole('button', {
name: /use unit from library/i,
});
expect(useUnitFromLibraryButton).toBeInTheDocument();
fireEvent.click(useUnitFromLibraryButton);
expect(await screen.findByText('Select unit'));
// click dummy button to execute onComponentSelected prop.
const dummyBtn = await screen.findByRole('button', { name: 'Dummy button' });
fireEvent.click(dummyBtn);
expect(handleOnAddUnitFromLibrary).toHaveBeenCalled();
expect(handleOnAddUnitFromLibrary).toHaveBeenCalledWith({
type: COMPONENT_TYPES.libraryV2,
parentLocator: '123',
category: 'vertical',
libraryContentKey: containerKey,
});
});
}); });

View File

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

View File

@@ -1,10 +1,5 @@
// @ts-check // @ts-check
import React, { import React, { useEffect, useRef } from 'react';
useCallback,
useEffect,
useMemo,
useRef,
} from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { useDispatch } from 'react-redux'; import { useDispatch } from 'react-redux';
import { useToggle } from '@openedx/paragon'; import { useToggle } from '@openedx/paragon';
@@ -13,16 +8,13 @@ import { useSearchParams } from 'react-router-dom';
import CourseOutlineUnitCardExtraActionsSlot from '../../plugin-slots/CourseOutlineUnitCardExtraActionsSlot'; import CourseOutlineUnitCardExtraActionsSlot from '../../plugin-slots/CourseOutlineUnitCardExtraActionsSlot';
import { setCurrentItem, setCurrentSection, setCurrentSubsection } from '../data/slice'; import { setCurrentItem, setCurrentSection, setCurrentSubsection } from '../data/slice';
import { fetchCourseSectionQuery } from '../data/thunk';
import { RequestStatus } from '../../data/constants'; import { RequestStatus } from '../../data/constants';
import { isUnitReadOnly } from '../../course-unit/data/utils';
import CardHeader from '../card-header/CardHeader'; import CardHeader from '../card-header/CardHeader';
import SortableItem from '../drag-helper/SortableItem'; import SortableItem from '../drag-helper/SortableItem';
import TitleLink from '../card-header/TitleLink'; import TitleLink from '../card-header/TitleLink';
import XBlockStatus from '../xblock-status/XBlockStatus'; import XBlockStatus from '../xblock-status/XBlockStatus';
import { getItemStatus, getItemStatusBorder, scrollToElement } from '../utils'; import { getItemStatus, getItemStatusBorder, scrollToElement } from '../utils';
import { useClipboard } from '../../generic/clipboard'; import { useClipboard } from '../../generic/clipboard';
import { PreviewLibraryXBlockChanges } from '../../course-unit/preview-changes';
const UnitCard = ({ const UnitCard = ({
unit, unit,
@@ -48,7 +40,6 @@ const UnitCard = ({
const locatorId = searchParams.get('show'); const locatorId = searchParams.get('show');
const isScrolledToElement = locatorId === unit.id; const isScrolledToElement = locatorId === unit.id;
const [isFormOpen, openForm, closeForm] = useToggle(false); const [isFormOpen, openForm, closeForm] = useToggle(false);
const [isSyncModalOpen, openSyncModal, closeSyncModal] = useToggle(false);
const namePrefix = 'unit'; const namePrefix = 'unit';
const { copyToClipboard } = useClipboard(); const { copyToClipboard } = useClipboard();
@@ -64,24 +55,8 @@ const UnitCard = ({
isHeaderVisible = true, isHeaderVisible = true,
enableCopyPasteUnits = false, enableCopyPasteUnits = false,
discussionEnabled, discussionEnabled,
upstreamInfo,
} = unit; } = unit;
const blockSyncData = useMemo(() => {
if (!upstreamInfo.readyToSync) {
return undefined;
}
return {
displayName,
downstreamBlockId: id,
upstreamBlockId: upstreamInfo.upstreamRef,
upstreamBlockVersionSynced: upstreamInfo.versionSynced,
isVertical: true,
};
}, [upstreamInfo]);
const readOnly = isUnitReadOnly(unit);
// re-create actions object for customizations // re-create actions object for customizations
const actions = { ...unitActions }; const actions = { ...unitActions };
// add actions to control display of move up & down menu buton. // add actions to control display of move up & down menu buton.
@@ -129,10 +104,6 @@ const UnitCard = ({
copyToClipboard(id); copyToClipboard(id);
}; };
const handleOnPostChangeSync = useCallback(async () => {
await dispatch(fetchCourseSectionQuery([section.id]));
}, [dispatch, section]);
const titleComponent = ( const titleComponent = (
<TitleLink <TitleLink
title={displayName} title={displayName}
@@ -173,71 +144,59 @@ const UnitCard = ({
const isDraggable = actions.draggable && (actions.allowMoveUp || actions.allowMoveDown); const isDraggable = actions.draggable && (actions.allowMoveUp || actions.allowMoveDown);
return ( return (
<> <SortableItem
<SortableItem id={id}
id={id} category={category}
category={category} key={id}
key={id} isDraggable={isDraggable}
isDraggable={isDraggable} isDroppable={actions.childAddable}
isDroppable={actions.childAddable} componentStyle={{
componentStyle={{ background: '#fdfdfd',
background: '#fdfdfd', ...borderStyle,
...borderStyle, }}
}} >
<div
className={`unit-card ${isScrolledToElement ? 'highlight' : ''}`}
data-testid="unit-card"
ref={currentRef}
> >
<div <CardHeader
className={`unit-card ${isScrolledToElement ? 'highlight' : ''}`} title={displayName}
data-testid="unit-card" status={unitStatus}
ref={currentRef} hasChanges={hasChanges}
> cardId={id}
<CardHeader onClickMenuButton={handleClickMenuButton}
title={displayName} onClickPublish={onOpenPublishModal}
status={unitStatus} onClickConfigure={onOpenConfigureModal}
hasChanges={hasChanges} onClickEdit={openForm}
cardId={id} onClickDelete={onOpenDeleteModal}
onClickMenuButton={handleClickMenuButton} onClickMoveUp={handleUnitMoveUp}
onClickPublish={onOpenPublishModal} onClickMoveDown={handleUnitMoveDown}
onClickConfigure={onOpenConfigureModal} isFormOpen={isFormOpen}
onClickEdit={openForm} closeForm={closeForm}
onClickDelete={onOpenDeleteModal} onEditSubmit={handleEditSubmit}
onClickMoveUp={handleUnitMoveUp} isDisabledEditField={savingStatus === RequestStatus.IN_PROGRESS}
onClickMoveDown={handleUnitMoveDown} onClickDuplicate={onDuplicateSubmit}
onClickSync={openSyncModal} titleComponent={titleComponent}
isFormOpen={isFormOpen} namePrefix={namePrefix}
closeForm={closeForm} actions={actions}
onEditSubmit={handleEditSubmit} isVertical
isDisabledEditField={readOnly || savingStatus === RequestStatus.IN_PROGRESS} enableCopyPasteUnits={enableCopyPasteUnits}
onClickDuplicate={onDuplicateSubmit} onClickCopy={handleCopyClick}
titleComponent={titleComponent} discussionEnabled={discussionEnabled}
namePrefix={namePrefix} discussionsSettings={discussionsSettings}
actions={actions} parentInfo={parentInfo}
isVertical extraActionsComponent={extraActionsComponent}
enableCopyPasteUnits={enableCopyPasteUnits}
onClickCopy={handleCopyClick}
discussionEnabled={discussionEnabled}
discussionsSettings={discussionsSettings}
parentInfo={parentInfo}
extraActionsComponent={extraActionsComponent}
readyToSync={upstreamInfo.readyToSync}
/>
<div className="unit-card__content item-children" data-testid="unit-card__content">
<XBlockStatus
isSelfPaced={isSelfPaced}
isCustomRelativeDatesActive={isCustomRelativeDatesActive}
blockData={unit}
/>
</div>
</div>
</SortableItem>
{blockSyncData && (
<PreviewLibraryXBlockChanges
blockData={blockSyncData}
isModalOpen={isSyncModalOpen}
closeModal={closeSyncModal}
postChange={handleOnPostChangeSync}
/> />
)} <div className="unit-card__content item-children" data-testid="unit-card__content">
</> <XBlockStatus
isSelfPaced={isSelfPaced}
isCustomRelativeDatesActive={isCustomRelativeDatesActive}
blockData={unit}
/>
</div>
</div>
</SortableItem>
); );
}; };
@@ -263,11 +222,6 @@ UnitCard.propTypes = {
isHeaderVisible: PropTypes.bool, isHeaderVisible: PropTypes.bool,
enableCopyPasteUnits: PropTypes.bool, enableCopyPasteUnits: PropTypes.bool,
discussionEnabled: PropTypes.bool, discussionEnabled: PropTypes.bool,
upstreamInfo: PropTypes.shape({
readyToSync: PropTypes.bool.isRequired,
upstreamRef: PropTypes.string.isRequired,
versionSynced: PropTypes.number.isRequired,
}).isRequired,
}).isRequired, }).isRequired,
subsection: PropTypes.shape({ subsection: PropTypes.shape({
id: PropTypes.string.isRequired, id: PropTypes.string.isRequired,

View File

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

View File

@@ -7,7 +7,7 @@ import {
ActionRow, ActionRow,
Button, Button,
} from '@openedx/paragon'; } from '@openedx/paragon';
import { StudioFooterSlot } from '@edx/frontend-component-footer'; import { StudioFooterSlot } from '@openedx/frontend-slot-footer';
import { useNavigate, useParams } from 'react-router-dom'; import { useNavigate, useParams } from 'react-router-dom';

View File

@@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import { useParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
import { import {
Alert, Container, Layout, Button, TransitionReplace, Container, Layout, Stack, Button, TransitionReplace,
} from '@openedx/paragon'; } from '@openedx/paragon';
import { useIntl } from '@edx/frontend-platform/i18n'; import { useIntl } from '@edx/frontend-platform/i18n';
import { import {
@@ -26,6 +26,8 @@ import AddComponent from './add-component/AddComponent';
import HeaderTitle from './header-title/HeaderTitle'; import HeaderTitle from './header-title/HeaderTitle';
import Breadcrumbs from './breadcrumbs/Breadcrumbs'; import Breadcrumbs from './breadcrumbs/Breadcrumbs';
import Sequence from './course-sequence'; import Sequence from './course-sequence';
import Sidebar from './sidebar';
import SplitTestSidebarInfo from './sidebar/SplitTestSidebarInfo';
import { useCourseUnit, useLayoutGrid, useScrollToLastPosition } from './hooks'; import { useCourseUnit, useLayoutGrid, useScrollToLastPosition } from './hooks';
import messages from './messages'; import messages from './messages';
import { PasteNotificationAlert } from './clipboard'; import { PasteNotificationAlert } from './clipboard';
@@ -38,10 +40,8 @@ const CourseUnit = ({ courseId }) => {
const { blockId } = useParams(); const { blockId } = useParams();
const intl = useIntl(); const intl = useIntl();
const { const {
courseUnit,
isLoading, isLoading,
sequenceId, sequenceId,
courseUnitLoadingStatus,
unitTitle, unitTitle,
unitCategory, unitCategory,
errorMessage, errorMessage,
@@ -75,8 +75,6 @@ const CourseUnit = ({ courseId }) => {
} = useCourseUnit({ courseId, blockId }); } = useCourseUnit({ courseId, blockId });
const layoutGrid = useLayoutGrid(unitCategory, isUnitLibraryType); const layoutGrid = useLayoutGrid(unitCategory, isUnitLibraryType);
const readOnly = !!courseUnit.readOnly;
useEffect(() => { useEffect(() => {
document.title = getPageHeadTitle('', unitTitle); document.title = getPageHeadTitle('', unitTitle);
}, [unitTitle]); }, [unitTitle]);
@@ -138,24 +136,6 @@ const CourseUnit = ({ courseId }) => {
/> />
) : null} ) : null}
</TransitionReplace> </TransitionReplace>
{courseUnit.upstreamInfo?.upstreamLink && (
<AlertMessage
title={intl.formatMessage(
messages.alertLibraryUnitReadOnlyText,
{
link: (
<Alert.Link
className="ml-1"
href={courseUnit.upstreamInfo.upstreamLink}
>
{intl.formatMessage(messages.alertLibraryUnitReadOnlyLinkText)}
</Alert.Link>
),
},
)}
variant="info"
/>
)}
<SubHeader <SubHeader
hideBorder hideBorder
title={( title={(
@@ -211,21 +191,18 @@ const CourseUnit = ({ courseId }) => {
courseId={courseId} courseId={courseId}
blockId={blockId} blockId={blockId}
isUnitVerticalType={isUnitVerticalType} isUnitVerticalType={isUnitVerticalType}
courseUnitLoadingStatus={courseUnitLoadingStatus}
unitXBlockActions={unitXBlockActions} unitXBlockActions={unitXBlockActions}
courseVerticalChildren={courseVerticalChildren.children} courseVerticalChildren={courseVerticalChildren.children}
handleConfigureSubmit={handleConfigureSubmit} handleConfigureSubmit={handleConfigureSubmit}
/> />
{!readOnly && ( <AddComponent
<AddComponent parentLocator={blockId}
parentLocator={blockId} isSplitTestType={isSplitTestType}
isSplitTestType={isSplitTestType} isUnitVerticalType={isUnitVerticalType}
isUnitVerticalType={isUnitVerticalType} handleCreateNewCourseXBlock={handleCreateNewCourseXBlock}
handleCreateNewCourseXBlock={handleCreateNewCourseXBlock} addComponentTemplateData={addComponentTemplateData}
addComponentTemplateData={addComponentTemplateData} />
/> {showPasteXBlock && canPasteComponent && isUnitVerticalType && (
)}
{!readOnly && showPasteXBlock && canPasteComponent && isUnitVerticalType && (
<PasteComponent <PasteComponent
clipboardData={sharedClipboardData} clipboardData={sharedClipboardData}
onClick={ onClick={
@@ -243,15 +220,20 @@ const CourseUnit = ({ courseId }) => {
<IframePreviewLibraryXBlockChanges /> <IframePreviewLibraryXBlockChanges />
</Layout.Element> </Layout.Element>
<Layout.Element> <Layout.Element>
<CourseAuthoringUnitSidebarSlot <Stack gap={3}>
courseId={courseId} {isUnitVerticalType && (
blockId={blockId} <CourseAuthoringUnitSidebarSlot
unitTitle={unitTitle} courseId={courseId}
xBlocks={courseVerticalChildren.children} blockId={blockId}
readOnly={readOnly} unitTitle={unitTitle}
isUnitVerticalType={isUnitVerticalType} />
isSplitTestType={isSplitTestType} )}
/> {isSplitTestType && (
<Sidebar data-testid="course-split-test-sidebar">
<SplitTestSidebarInfo />
</Sidebar>
)}
</Stack>
</Layout.Element> </Layout.Element>
</Layout> </Layout>
</section> </section>

View File

@@ -65,7 +65,6 @@ import xblockContainerIframeMessages from './xblock-container-iframe/messages';
import headerNavigationsMessages from './header-navigations/messages'; import headerNavigationsMessages from './header-navigations/messages';
import sidebarMessages from './sidebar/messages'; import sidebarMessages from './sidebar/messages';
import messages from './messages'; import messages from './messages';
import * as selectors from '../data/selectors';
let axiosMock; let axiosMock;
let store; let store;
@@ -167,27 +166,27 @@ describe('<CourseUnit />', () => {
}); });
it('render CourseUnit component correctly', async () => { it('render CourseUnit component correctly', async () => {
render(<RootWrapper />); const { getByText, getByRole, getByTestId } = render(<RootWrapper />);
const currentSectionName = courseUnitIndexMock.ancestor_info.ancestors[1].display_name; const currentSectionName = courseUnitIndexMock.ancestor_info.ancestors[1].display_name;
const currentSubSectionName = courseUnitIndexMock.ancestor_info.ancestors[1].display_name; const currentSubSectionName = courseUnitIndexMock.ancestor_info.ancestors[1].display_name;
await waitFor(() => { await waitFor(() => {
const unitHeaderTitle = screen.getByTestId('unit-header-title'); const unitHeaderTitle = getByTestId('unit-header-title');
expect(screen.getByText(unitDisplayName)).toBeInTheDocument(); expect(getByText(unitDisplayName)).toBeInTheDocument();
expect(within(unitHeaderTitle).getByRole('button', { name: headerTitleMessages.altButtonEdit.defaultMessage })).toBeInTheDocument(); expect(within(unitHeaderTitle).getByRole('button', { name: headerTitleMessages.altButtonEdit.defaultMessage })).toBeInTheDocument();
expect(within(unitHeaderTitle).getByRole('button', { name: headerTitleMessages.altButtonSettings.defaultMessage })).toBeInTheDocument(); expect(within(unitHeaderTitle).getByRole('button', { name: headerTitleMessages.altButtonSettings.defaultMessage })).toBeInTheDocument();
expect(screen.getByRole('button', { name: headerNavigationsMessages.viewLiveButton.defaultMessage })).toBeInTheDocument(); expect(getByRole('button', { name: headerNavigationsMessages.viewLiveButton.defaultMessage })).toBeInTheDocument();
expect(screen.getByRole('button', { name: headerNavigationsMessages.previewButton.defaultMessage })).toBeInTheDocument(); expect(getByRole('button', { name: headerNavigationsMessages.previewButton.defaultMessage })).toBeInTheDocument();
expect(screen.getByRole('button', { name: currentSectionName })).toBeInTheDocument(); expect(getByRole('button', { name: currentSectionName })).toBeInTheDocument();
expect(screen.getByRole('button', { name: currentSubSectionName })).toBeInTheDocument(); expect(getByRole('button', { name: currentSubSectionName })).toBeInTheDocument();
}); });
}); });
it('renders the course unit iframe with correct attributes', async () => { it('renders the course unit iframe with correct attributes', async () => {
render(<RootWrapper />); const { getByTitle } = render(<RootWrapper />);
await waitFor(() => { await waitFor(() => {
const iframe = screen.getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage); const iframe = getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
expect(iframe).toHaveAttribute('src', `${getConfig().STUDIO_BASE_URL}/container_embed/${blockId}`); expect(iframe).toHaveAttribute('src', `${getConfig().STUDIO_BASE_URL}/container_embed/${blockId}`);
expect(iframe).toHaveAttribute('allow', IFRAME_FEATURE_POLICY); expect(iframe).toHaveAttribute('allow', IFRAME_FEATURE_POLICY);
expect(iframe).toHaveAttribute('style', 'height: 0px;'); expect(iframe).toHaveAttribute('style', 'height: 0px;');
@@ -211,27 +210,27 @@ describe('<CourseUnit />', () => {
}); });
it('displays an error alert when a studioAjaxError message is received', async () => { it('displays an error alert when a studioAjaxError message is received', async () => {
render(<RootWrapper />); const { getByTitle, getByTestId } = render(<RootWrapper />);
await waitFor(() => { await waitFor(() => {
const xblocksIframe = screen.getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage); const xblocksIframe = getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
expect(xblocksIframe).toBeInTheDocument(); expect(xblocksIframe).toBeInTheDocument();
simulatePostMessageEvent(messageTypes.studioAjaxError, { simulatePostMessageEvent(messageTypes.studioAjaxError, {
error: 'Some error text...', error: 'Some error text...',
}); });
}); });
expect(screen.getByTestId('saving-error-alert')).toBeInTheDocument(); expect(getByTestId('saving-error-alert')).toBeInTheDocument();
}); });
it('renders XBlock iframe and opens legacy edit modal on editXBlock message', async () => { it('renders XBlock iframe and opens legacy edit modal on editXBlock message', async () => {
render(<RootWrapper />); const { getByTitle } = render(<RootWrapper />);
await waitFor(() => { await waitFor(() => {
const xblocksIframe = screen.getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage); const xblocksIframe = getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
expect(xblocksIframe).toBeInTheDocument(); expect(xblocksIframe).toBeInTheDocument();
simulatePostMessageEvent(messageTypes.editXBlock, { id: blockId }); simulatePostMessageEvent(messageTypes.editXBlock, { id: blockId });
const legacyXBlockEditModalIframe = screen.getByTitle( const legacyXBlockEditModalIframe = getByTitle(
xblockContainerIframeMessages.legacyEditModalIframeTitle.defaultMessage, xblockContainerIframeMessages.legacyEditModalIframeTitle.defaultMessage,
); );
expect(legacyXBlockEditModalIframe).toBeInTheDocument(); expect(legacyXBlockEditModalIframe).toBeInTheDocument();
@@ -249,14 +248,14 @@ describe('<CourseUnit />', () => {
}); });
it('closes the legacy edit modal when closeXBlockEditorModal message is received', async () => { it('closes the legacy edit modal when closeXBlockEditorModal message is received', async () => {
render(<RootWrapper />); const { getByTitle, queryByTitle } = render(<RootWrapper />);
await waitFor(() => { await waitFor(() => {
const xblocksIframe = screen.getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage); const xblocksIframe = getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
expect(xblocksIframe).toBeInTheDocument(); expect(xblocksIframe).toBeInTheDocument();
simulatePostMessageEvent(messageTypes.closeXBlockEditorModal, { id: blockId }); simulatePostMessageEvent(messageTypes.closeXBlockEditorModal, { id: blockId });
const legacyXBlockEditModalIframe = screen.queryByTitle( const legacyXBlockEditModalIframe = queryByTitle(
xblockContainerIframeMessages.legacyEditModalIframeTitle.defaultMessage, xblockContainerIframeMessages.legacyEditModalIframeTitle.defaultMessage,
); );
expect(legacyXBlockEditModalIframe).not.toBeInTheDocument(); expect(legacyXBlockEditModalIframe).not.toBeInTheDocument();
@@ -264,14 +263,14 @@ describe('<CourseUnit />', () => {
}); });
it('closes legacy edit modal and updates course unit sidebar after saveEditedXBlockData message', async () => { it('closes legacy edit modal and updates course unit sidebar after saveEditedXBlockData message', async () => {
render(<RootWrapper />); const { getByTitle, queryByTitle, getByTestId } = render(<RootWrapper />);
await waitFor(() => { await waitFor(() => {
const xblocksIframe = screen.getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage); const xblocksIframe = getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
expect(xblocksIframe).toBeInTheDocument(); expect(xblocksIframe).toBeInTheDocument();
simulatePostMessageEvent(messageTypes.saveEditedXBlockData); simulatePostMessageEvent(messageTypes.saveEditedXBlockData);
const legacyXBlockEditModalIframe = screen.queryByTitle( const legacyXBlockEditModalIframe = queryByTitle(
xblockContainerIframeMessages.legacyEditModalIframeTitle.defaultMessage, xblockContainerIframeMessages.legacyEditModalIframeTitle.defaultMessage,
); );
expect(legacyXBlockEditModalIframe).not.toBeInTheDocument(); expect(legacyXBlockEditModalIframe).not.toBeInTheDocument();
@@ -286,7 +285,7 @@ describe('<CourseUnit />', () => {
}); });
await waitFor(() => { await waitFor(() => {
const courseUnitSidebar = screen.getByTestId('course-unit-sidebar'); const courseUnitSidebar = getByTestId('course-unit-sidebar');
expect( expect(
within(courseUnitSidebar).getByText(sidebarMessages.sidebarTitleDraftUnpublishedChanges.defaultMessage), within(courseUnitSidebar).getByText(sidebarMessages.sidebarTitleDraftUnpublishedChanges.defaultMessage),
).toBeInTheDocument(); ).toBeInTheDocument();
@@ -305,10 +304,10 @@ describe('<CourseUnit />', () => {
}); });
it('updates course unit sidebar after receiving refreshPositions message', async () => { it('updates course unit sidebar after receiving refreshPositions message', async () => {
render(<RootWrapper />); const { getByTitle, getByTestId } = render(<RootWrapper />);
await waitFor(() => { await waitFor(() => {
const xblocksIframe = screen.getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage); const xblocksIframe = getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
expect(xblocksIframe).toBeInTheDocument(); expect(xblocksIframe).toBeInTheDocument();
simulatePostMessageEvent(messageTypes.refreshPositions); simulatePostMessageEvent(messageTypes.refreshPositions);
}); });
@@ -322,7 +321,7 @@ describe('<CourseUnit />', () => {
}); });
await waitFor(() => { await waitFor(() => {
const courseUnitSidebar = screen.getByTestId('course-unit-sidebar'); const courseUnitSidebar = getByTestId('course-unit-sidebar');
expect( expect(
within(courseUnitSidebar).getByText(sidebarMessages.sidebarTitleDraftUnpublishedChanges.defaultMessage), within(courseUnitSidebar).getByText(sidebarMessages.sidebarTitleDraftUnpublishedChanges.defaultMessage),
).toBeInTheDocument(); ).toBeInTheDocument();
@@ -341,10 +340,12 @@ describe('<CourseUnit />', () => {
}); });
it('checks whether xblock is removed when the corresponding delete button is clicked and the sidebar is the updated', async () => { it('checks whether xblock is removed when the corresponding delete button is clicked and the sidebar is the updated', async () => {
render(<RootWrapper />); const {
getByTitle, getByText, queryByRole, getByRole,
} = render(<RootWrapper />);
await waitFor(async () => { await waitFor(async () => {
const iframe = screen.getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage); const iframe = getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
expect(iframe).toHaveAttribute( expect(iframe).toHaveAttribute(
'aria-label', 'aria-label',
xblockContainerIframeMessages.xblockIframeLabel.defaultMessage xblockContainerIframeMessages.xblockIframeLabel.defaultMessage
@@ -355,10 +356,10 @@ describe('<CourseUnit />', () => {
usageId: courseVerticalChildrenMock.children[0].block_id, usageId: courseVerticalChildrenMock.children[0].block_id,
}); });
expect(screen.getByText(/Delete this component?/i)).toBeInTheDocument(); expect(getByText(/Delete this component?/i)).toBeInTheDocument();
expect(screen.getByText(/Deleting this component is permanent and cannot be undone./i)).toBeInTheDocument(); expect(getByText(/Deleting this component is permanent and cannot be undone./i)).toBeInTheDocument();
const dialog = screen.getByRole('dialog'); const dialog = getByRole('dialog');
expect(dialog).toBeInTheDocument(); expect(dialog).toBeInTheDocument();
// Find the Cancel and Delete buttons within the iframe by their specific classes // Find the Cancel and Delete buttons within the iframe by their specific classes
@@ -371,7 +372,7 @@ describe('<CourseUnit />', () => {
usageId: courseVerticalChildrenMock.children[0].block_id, usageId: courseVerticalChildrenMock.children[0].block_id,
}); });
expect(screen.getByRole('dialog')).toBeInTheDocument(); expect(getByRole('dialog')).toBeInTheDocument();
userEvent.click(deleteButton); userEvent.click(deleteButton);
}); });
@@ -392,14 +393,14 @@ describe('<CourseUnit />', () => {
await waitFor(() => { await waitFor(() => {
// check if the sidebar status is Published and Live // check if the sidebar status is Published and Live
expect(screen.getByText(sidebarMessages.sidebarTitlePublishedAndLive.defaultMessage)).toBeInTheDocument(); expect(getByText(sidebarMessages.sidebarTitlePublishedAndLive.defaultMessage)).toBeInTheDocument();
expect(screen.getByText( expect(getByText(
sidebarMessages.publishLastPublished.defaultMessage sidebarMessages.publishLastPublished.defaultMessage
.replace('{publishedOn}', courseUnitIndexMock.published_on) .replace('{publishedOn}', courseUnitIndexMock.published_on)
.replace('{publishedBy}', userName), .replace('{publishedBy}', userName),
)).toBeInTheDocument(); )).toBeInTheDocument();
expect(screen.queryByRole('button', { name: sidebarMessages.actionButtonPublishTitle.defaultMessage })).not.toBeInTheDocument(); expect(queryByRole('button', { name: sidebarMessages.actionButtonPublishTitle.defaultMessage })).not.toBeInTheDocument();
expect(screen.getByText(unitDisplayName)).toBeInTheDocument(); expect(getByText(unitDisplayName)).toBeInTheDocument();
}); });
axiosMock axiosMock
@@ -430,28 +431,28 @@ describe('<CourseUnit />', () => {
await executeThunk(editCourseUnitVisibilityAndData(blockId, PUBLISH_TYPES.makePublic, true), store.dispatch); await executeThunk(editCourseUnitVisibilityAndData(blockId, PUBLISH_TYPES.makePublic, true), store.dispatch);
await waitFor(() => { await waitFor(() => {
const iframe = screen.getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage); const iframe = getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
expect(iframe).toHaveAttribute( expect(iframe).toHaveAttribute(
'aria-label', 'aria-label',
xblockContainerIframeMessages.xblockIframeLabel.defaultMessage xblockContainerIframeMessages.xblockIframeLabel.defaultMessage
.replace('{xblockCount}', updatedCourseVerticalChildren.length), .replace('{xblockCount}', updatedCourseVerticalChildren.length),
); );
// after removing the xblock, the sidebar status changes to Draft (unpublished changes) // after removing the xblock, the sidebar status changes to Draft (unpublished changes)
expect(screen.getByText(sidebarMessages.sidebarTitleDraftUnpublishedChanges.defaultMessage)).toBeInTheDocument(); expect(getByText(sidebarMessages.sidebarTitleDraftUnpublishedChanges.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.visibilityStaffAndLearnersTitle.defaultMessage)).toBeInTheDocument(); expect(getByText(sidebarMessages.visibilityStaffAndLearnersTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.releaseStatusTitle.defaultMessage)).toBeInTheDocument(); expect(getByText(sidebarMessages.releaseStatusTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.sidebarBodyNote.defaultMessage)).toBeInTheDocument(); expect(getByText(sidebarMessages.sidebarBodyNote.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.visibilityWillBeVisibleToTitle.defaultMessage)).toBeInTheDocument(); expect(getByText(sidebarMessages.visibilityWillBeVisibleToTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.visibilityCheckboxTitle.defaultMessage)).toBeInTheDocument(); expect(getByText(sidebarMessages.visibilityCheckboxTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.actionButtonPublishTitle.defaultMessage)).toBeInTheDocument(); expect(getByText(sidebarMessages.actionButtonPublishTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.actionButtonDiscardChangesTitle.defaultMessage)).toBeInTheDocument(); expect(getByText(sidebarMessages.actionButtonDiscardChangesTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(courseUnitIndexMock.release_date)).toBeInTheDocument(); expect(getByText(courseUnitIndexMock.release_date)).toBeInTheDocument();
expect(screen.getByText( expect(getByText(
sidebarMessages.publishInfoDraftSaved.defaultMessage sidebarMessages.publishInfoDraftSaved.defaultMessage
.replace('{editedOn}', courseUnitIndexMock.edited_on) .replace('{editedOn}', courseUnitIndexMock.edited_on)
.replace('{editedBy}', courseUnitIndexMock.edited_by), .replace('{editedBy}', courseUnitIndexMock.edited_by),
)).toBeInTheDocument(); )).toBeInTheDocument();
expect(screen.getByText( expect(getByText(
sidebarMessages.releaseInfoWithSection.defaultMessage sidebarMessages.releaseInfoWithSection.defaultMessage
.replace('{sectionName}', courseUnitIndexMock.release_date_from), .replace('{sectionName}', courseUnitIndexMock.release_date_from),
)).toBeInTheDocument(); )).toBeInTheDocument();
@@ -459,16 +460,14 @@ describe('<CourseUnit />', () => {
}); });
it('checks if xblock is a duplicate when the corresponding duplicate button is clicked and if the sidebar status is updated', async () => { it('checks if xblock is a duplicate when the corresponding duplicate button is clicked and if the sidebar status is updated', async () => {
render(<RootWrapper />); const {
getByTitle, getByRole, getByText, queryByRole,
} = render(<RootWrapper />);
simulatePostMessageEvent(messageTypes.duplicateXBlock, { simulatePostMessageEvent(messageTypes.duplicateXBlock, {
id: courseVerticalChildrenMock.children[0].block_id, id: courseVerticalChildrenMock.children[0].block_id,
}); });
axiosMock
.onGet(getCourseUnitApiUrl(blockId))
.reply(200, courseUnitIndexMock);
axiosMock axiosMock
.onPost(postXBlockBaseApiUrl({ .onPost(postXBlockBaseApiUrl({
parent_locator: blockId, parent_locator: blockId,
@@ -479,14 +478,8 @@ describe('<CourseUnit />', () => {
const updatedCourseVerticalChildren = [ const updatedCourseVerticalChildren = [
...courseVerticalChildrenMock.children, ...courseVerticalChildrenMock.children,
{ {
...courseVerticalChildrenMock.children[0],
name: 'New Cloned XBlock', name: 'New Cloned XBlock',
block_id: '1234567890',
block_type: 'drag-and-drop-v2',
user_partition_info: {
selectable_partitions: [],
selected_partition_index: -1,
selected_groups_label: '',
},
}, },
]; ];
@@ -498,9 +491,9 @@ describe('<CourseUnit />', () => {
}); });
await waitFor(() => { await waitFor(() => {
userEvent.click(screen.getByRole('button', { name: sidebarMessages.actionButtonPublishTitle.defaultMessage })); userEvent.click(getByRole('button', { name: sidebarMessages.actionButtonPublishTitle.defaultMessage }));
const iframe = screen.getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage); const iframe = getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
expect(iframe).toHaveAttribute( expect(iframe).toHaveAttribute(
'aria-label', 'aria-label',
xblockContainerIframeMessages.xblockIframeLabel.defaultMessage xblockContainerIframeMessages.xblockIframeLabel.defaultMessage
@@ -529,14 +522,14 @@ describe('<CourseUnit />', () => {
await waitFor(() => { await waitFor(() => {
// check if the sidebar status is Published and Live // check if the sidebar status is Published and Live
expect(screen.getByText(sidebarMessages.sidebarTitlePublishedAndLive.defaultMessage)).toBeInTheDocument(); expect(getByText(sidebarMessages.sidebarTitlePublishedAndLive.defaultMessage)).toBeInTheDocument();
expect(screen.getByText( expect(getByText(
sidebarMessages.publishLastPublished.defaultMessage sidebarMessages.publishLastPublished.defaultMessage
.replace('{publishedOn}', courseUnitIndexMock.published_on) .replace('{publishedOn}', courseUnitIndexMock.published_on)
.replace('{publishedBy}', userName), .replace('{publishedBy}', userName),
)).toBeInTheDocument(); )).toBeInTheDocument();
expect(screen.queryByRole('button', { name: sidebarMessages.actionButtonPublishTitle.defaultMessage })).not.toBeInTheDocument(); expect(queryByRole('button', { name: sidebarMessages.actionButtonPublishTitle.defaultMessage })).not.toBeInTheDocument();
expect(screen.getByText(unitDisplayName)).toBeInTheDocument(); expect(getByText(unitDisplayName)).toBeInTheDocument();
}); });
axiosMock axiosMock
@@ -545,7 +538,7 @@ describe('<CourseUnit />', () => {
await executeThunk(editCourseUnitVisibilityAndData(blockId, PUBLISH_TYPES.makePublic, true), store.dispatch); await executeThunk(editCourseUnitVisibilityAndData(blockId, PUBLISH_TYPES.makePublic, true), store.dispatch);
await waitFor(() => { await waitFor(() => {
const iframe = screen.getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage); const iframe = getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
expect(iframe).toHaveAttribute( expect(iframe).toHaveAttribute(
'aria-label', 'aria-label',
xblockContainerIframeMessages.xblockIframeLabel.defaultMessage xblockContainerIframeMessages.xblockIframeLabel.defaultMessage
@@ -553,21 +546,21 @@ describe('<CourseUnit />', () => {
); );
// after duplicate the xblock, the sidebar status changes to Draft (unpublished changes) // after duplicate the xblock, the sidebar status changes to Draft (unpublished changes)
expect(screen.getByText(sidebarMessages.sidebarTitleDraftUnpublishedChanges.defaultMessage)).toBeInTheDocument(); expect(getByText(sidebarMessages.sidebarTitleDraftUnpublishedChanges.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.visibilityStaffAndLearnersTitle.defaultMessage)).toBeInTheDocument(); expect(getByText(sidebarMessages.visibilityStaffAndLearnersTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.releaseStatusTitle.defaultMessage)).toBeInTheDocument(); expect(getByText(sidebarMessages.releaseStatusTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.sidebarBodyNote.defaultMessage)).toBeInTheDocument(); expect(getByText(sidebarMessages.sidebarBodyNote.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.visibilityWillBeVisibleToTitle.defaultMessage)).toBeInTheDocument(); expect(getByText(sidebarMessages.visibilityWillBeVisibleToTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.visibilityCheckboxTitle.defaultMessage)).toBeInTheDocument(); expect(getByText(sidebarMessages.visibilityCheckboxTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.actionButtonPublishTitle.defaultMessage)).toBeInTheDocument(); expect(getByText(sidebarMessages.actionButtonPublishTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.actionButtonDiscardChangesTitle.defaultMessage)).toBeInTheDocument(); expect(getByText(sidebarMessages.actionButtonDiscardChangesTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(courseUnitIndexMock.release_date)).toBeInTheDocument(); expect(getByText(courseUnitIndexMock.release_date)).toBeInTheDocument();
expect(screen.getByText( expect(getByText(
sidebarMessages.publishInfoDraftSaved.defaultMessage sidebarMessages.publishInfoDraftSaved.defaultMessage
.replace('{editedOn}', courseUnitIndexMock.edited_on) .replace('{editedOn}', courseUnitIndexMock.edited_on)
.replace('{editedBy}', courseUnitIndexMock.edited_by), .replace('{editedBy}', courseUnitIndexMock.edited_by),
)).toBeInTheDocument(); )).toBeInTheDocument();
expect(screen.getByText( expect(getByText(
sidebarMessages.releaseInfoWithSection.defaultMessage sidebarMessages.releaseInfoWithSection.defaultMessage
.replace('{sectionName}', courseUnitIndexMock.release_date_from), .replace('{sectionName}', courseUnitIndexMock.release_date_from),
)).toBeInTheDocument(); )).toBeInTheDocument();
@@ -577,19 +570,19 @@ describe('<CourseUnit />', () => {
it('handles CourseUnit header action buttons', async () => { it('handles CourseUnit header action buttons', async () => {
const { open } = window; const { open } = window;
window.open = jest.fn(); window.open = jest.fn();
render(<RootWrapper />); const { getByRole } = render(<RootWrapper />);
const { const {
draft_preview_link: draftPreviewLink, draft_preview_link: draftPreviewLink,
published_preview_link: publishedPreviewLink, published_preview_link: publishedPreviewLink,
} = courseSectionVerticalMock; } = courseSectionVerticalMock;
await waitFor(() => { await waitFor(() => {
const viewLiveButton = screen.getByRole('button', { name: headerNavigationsMessages.viewLiveButton.defaultMessage }); const viewLiveButton = getByRole('button', { name: headerNavigationsMessages.viewLiveButton.defaultMessage });
userEvent.click(viewLiveButton); userEvent.click(viewLiveButton);
expect(window.open).toHaveBeenCalled(); expect(window.open).toHaveBeenCalled();
expect(window.open).toHaveBeenCalledWith(publishedPreviewLink, '_blank'); expect(window.open).toHaveBeenCalledWith(publishedPreviewLink, '_blank');
const previewButton = screen.getByRole('button', { name: headerNavigationsMessages.previewButton.defaultMessage }); const previewButton = getByRole('button', { name: headerNavigationsMessages.previewButton.defaultMessage });
userEvent.click(previewButton); userEvent.click(previewButton);
expect(window.open).toHaveBeenCalled(); expect(window.open).toHaveBeenCalled();
expect(window.open).toHaveBeenCalledWith(draftPreviewLink, '_blank'); expect(window.open).toHaveBeenCalledWith(draftPreviewLink, '_blank');
@@ -599,7 +592,12 @@ describe('<CourseUnit />', () => {
}); });
it('checks courseUnit title changing when edit query is successfully', async () => { it('checks courseUnit title changing when edit query is successfully', async () => {
render(<RootWrapper />); const {
findByText,
queryByRole,
getByRole,
getByTestId,
} = render(<RootWrapper />);
let editTitleButton = null; let editTitleButton = null;
let titleEditField = null; let titleEditField = null;
const newDisplayName = `${unitDisplayName} new`; const newDisplayName = `${unitDisplayName} new`;
@@ -635,7 +633,7 @@ describe('<CourseUnit />', () => {
}); });
await waitFor(() => { await waitFor(() => {
const unitHeaderTitle = screen.getByTestId('unit-header-title'); const unitHeaderTitle = getByTestId('unit-header-title');
editTitleButton = within(unitHeaderTitle) editTitleButton = within(unitHeaderTitle)
.getByRole('button', { name: headerTitleMessages.altButtonEdit.defaultMessage }); .getByRole('button', { name: headerTitleMessages.altButtonEdit.defaultMessage });
titleEditField = within(unitHeaderTitle) titleEditField = within(unitHeaderTitle)
@@ -643,7 +641,7 @@ describe('<CourseUnit />', () => {
}); });
expect(titleEditField).not.toBeInTheDocument(); expect(titleEditField).not.toBeInTheDocument();
userEvent.click(editTitleButton); userEvent.click(editTitleButton);
titleEditField = screen.getByRole('textbox', { name: headerTitleMessages.ariaLabelButtonEdit.defaultMessage }); titleEditField = getByRole('textbox', { name: headerTitleMessages.ariaLabelButtonEdit.defaultMessage });
await userEvent.clear(titleEditField); await userEvent.clear(titleEditField);
await userEvent.type(titleEditField, newDisplayName); await userEvent.type(titleEditField, newDisplayName);
@@ -651,10 +649,9 @@ describe('<CourseUnit />', () => {
expect(titleEditField).toHaveValue(newDisplayName); expect(titleEditField).toHaveValue(newDisplayName);
titleEditField = screen.queryByRole('textbox', { name: headerTitleMessages.ariaLabelButtonEdit.defaultMessage }); titleEditField = queryByRole('textbox', { name: headerTitleMessages.ariaLabelButtonEdit.defaultMessage });
titleEditField = screen.queryByRole('textbox', { name: headerTitleMessages.ariaLabelButtonEdit.defaultMessage });
expect(titleEditField).not.toBeInTheDocument(); expect(titleEditField).not.toBeInTheDocument();
expect(await screen.findByText(newDisplayName)).toBeInTheDocument(); expect(await findByText(newDisplayName)).toBeInTheDocument();
}); });
it('doesn\'t handle creating xblock and displays an error message', async () => { it('doesn\'t handle creating xblock and displays an error message', async () => {
@@ -674,14 +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 axiosMock
.onPost(postXBlockBaseApiUrl({ type: 'problem', category: 'problem', parentLocator: blockId })) .onPost(postXBlockBaseApiUrl({ type: 'problem', category: 'problem', parentLocator: blockId }))
.reply(200, courseCreateXblockMock); .reply(200, courseCreateXblockMock);
render(<RootWrapper />); const { getByText, getByRole } = render(<RootWrapper />);
await waitFor(() => { await waitFor(() => {
userEvent.click(screen.getByRole('button', { name: sidebarMessages.actionButtonPublishTitle.defaultMessage })); userEvent.click(getByRole('button', { name: sidebarMessages.actionButtonPublishTitle.defaultMessage }));
}); });
axiosMock axiosMock
@@ -701,12 +699,13 @@ describe('<CourseUnit />', () => {
await executeThunk(editCourseUnitVisibilityAndData(blockId, PUBLISH_TYPES.makePublic, true), store.dispatch); await executeThunk(editCourseUnitVisibilityAndData(blockId, PUBLISH_TYPES.makePublic, true), store.dispatch);
await waitFor(() => { await waitFor(() => {
const problemButton = screen.getByRole('button', { const problemButton = getByRole('button', {
name: new RegExp(`problem ${addComponentMessages.buttonText.defaultMessage} Problem`, 'i'), name: new RegExp(`problem ${addComponentMessages.buttonText.defaultMessage} Problem`, 'i'),
hidden: true,
}); });
userEvent.click(problemButton); userEvent.click(problemButton);
expect(mockedUsedNavigate).toHaveBeenCalled();
expect(mockedUsedNavigate).toHaveBeenCalledWith(`/course/${courseKey}/editor/problem/${locator}`);
}); });
axiosMock axiosMock
@@ -716,28 +715,66 @@ describe('<CourseUnit />', () => {
await executeThunk(editCourseUnitVisibilityAndData(blockId, PUBLISH_TYPES.makePublic, true), store.dispatch); await executeThunk(editCourseUnitVisibilityAndData(blockId, PUBLISH_TYPES.makePublic, true), store.dispatch);
// after creating problem xblock, the sidebar status changes to Draft (unpublished changes) // after creating problem xblock, the sidebar status changes to Draft (unpublished changes)
expect(screen.getByText(sidebarMessages.sidebarTitleDraftUnpublishedChanges.defaultMessage)).toBeInTheDocument(); expect(getByText(sidebarMessages.sidebarTitleDraftUnpublishedChanges.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.visibilityStaffAndLearnersTitle.defaultMessage)).toBeInTheDocument(); expect(getByText(sidebarMessages.visibilityStaffAndLearnersTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.releaseStatusTitle.defaultMessage)).toBeInTheDocument(); expect(getByText(sidebarMessages.releaseStatusTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.sidebarBodyNote.defaultMessage)).toBeInTheDocument(); expect(getByText(sidebarMessages.sidebarBodyNote.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.visibilityWillBeVisibleToTitle.defaultMessage)).toBeInTheDocument(); expect(getByText(sidebarMessages.visibilityWillBeVisibleToTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.visibilityCheckboxTitle.defaultMessage)).toBeInTheDocument(); expect(getByText(sidebarMessages.visibilityCheckboxTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.actionButtonPublishTitle.defaultMessage)).toBeInTheDocument(); expect(getByText(sidebarMessages.actionButtonPublishTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.actionButtonDiscardChangesTitle.defaultMessage)).toBeInTheDocument(); expect(getByText(sidebarMessages.actionButtonDiscardChangesTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(courseUnitIndexMock.release_date)).toBeInTheDocument(); expect(getByText(courseUnitIndexMock.release_date)).toBeInTheDocument();
expect(screen.getByText( expect(getByText(
sidebarMessages.publishInfoDraftSaved.defaultMessage sidebarMessages.publishInfoDraftSaved.defaultMessage
.replace('{editedOn}', courseUnitIndexMock.edited_on) .replace('{editedOn}', courseUnitIndexMock.edited_on)
.replace('{editedBy}', courseUnitIndexMock.edited_by), .replace('{editedBy}', courseUnitIndexMock.edited_by),
)).toBeInTheDocument(); )).toBeInTheDocument();
expect(screen.getByText( expect(getByText(
sidebarMessages.releaseInfoWithSection.defaultMessage sidebarMessages.releaseInfoWithSection.defaultMessage
.replace('{sectionName}', courseUnitIndexMock.release_date_from), .replace('{sectionName}', courseUnitIndexMock.release_date_from),
)).toBeInTheDocument(); )).toBeInTheDocument();
}); });
it('handle creating Text xblock and saves scroll position in localStorage', async () => {
const { getByText, getByRole } = render(<RootWrapper />);
const xblockType = 'text';
axiosMock
.onPost(postXBlockBaseApiUrl({ type: xblockType, category: 'html', parentLocator: blockId }))
.reply(200, courseCreateXblockMock);
window.scrollTo(0, 250);
Object.defineProperty(window, 'scrollY', { value: 250, configurable: true });
await waitFor(() => {
const textButton = screen.getByRole('button', { name: /Text/i });
expect(getByText(addComponentMessages.title.defaultMessage)).toBeInTheDocument();
userEvent.click(textButton);
const addXBlockDialog = getByRole('dialog');
expect(addXBlockDialog).toBeInTheDocument();
expect(getByText(
addComponentMessages.modalContainerTitle.defaultMessage.replace('{componentTitle}', xblockType),
)).toBeInTheDocument();
const textRadio = screen.getByRole('radio', { name: /Text/i });
userEvent.click(textRadio);
expect(textRadio).toBeChecked();
const selectBtn = getByRole('button', { name: addComponentMessages.modalBtnText.defaultMessage });
expect(selectBtn).toBeInTheDocument();
userEvent.click(selectBtn);
});
expect(localStorage.getItem('createXBlockLastYPosition')).toBe('250');
});
it('correct addition of a new course unit after click on the "Add new unit" button', async () => { it('correct addition of a new course unit after click on the "Add new unit" button', async () => {
render(<RootWrapper />); const { getByRole, getAllByTestId } = render(<RootWrapper />);
let units = null; let units = null;
const updatedCourseSectionVerticalData = cloneDeep(courseSectionVerticalMock); const updatedCourseSectionVerticalData = cloneDeep(courseSectionVerticalMock);
const updatedAncestorsChild = updatedCourseSectionVerticalData.xblock_info.ancestor_info.ancestors[0]; const updatedAncestorsChild = updatedCourseSectionVerticalData.xblock_info.ancestor_info.ancestors[0];
@@ -747,7 +784,7 @@ describe('<CourseUnit />', () => {
]); ]);
await waitFor(async () => { await waitFor(async () => {
units = screen.getAllByTestId('course-unit-btn'); units = getAllByTestId('course-unit-btn');
const courseUnits = courseSectionVerticalMock.xblock_info.ancestor_info.ancestors[0].child_info.children; const courseUnits = courseSectionVerticalMock.xblock_info.ancestor_info.ancestors[0].child_info.children;
expect(units).toHaveLength(courseUnits.length); expect(units).toHaveLength(courseUnits.length);
}); });
@@ -764,8 +801,8 @@ describe('<CourseUnit />', () => {
await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch); await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch);
const addNewUnitBtn = screen.getByRole('button', { name: courseSequenceMessages.newUnitBtnText.defaultMessage }); const addNewUnitBtn = getByRole('button', { name: courseSequenceMessages.newUnitBtnText.defaultMessage });
units = screen.getAllByTestId('course-unit-btn'); units = getAllByTestId('course-unit-btn');
const updatedCourseUnits = updatedCourseSectionVerticalData const updatedCourseUnits = updatedCourseSectionVerticalData
.xblock_info.ancestor_info.ancestors[0].child_info.children; .xblock_info.ancestor_info.ancestors[0].child_info.children;
@@ -777,7 +814,7 @@ describe('<CourseUnit />', () => {
}); });
it('the sequence unit is updated after changing the unit header', async () => { it('the sequence unit is updated after changing the unit header', async () => {
render(<RootWrapper />); const { getAllByTestId, getByTestId } = render(<RootWrapper />);
const updatedCourseSectionVerticalData = cloneDeep(courseSectionVerticalMock); const updatedCourseSectionVerticalData = cloneDeep(courseSectionVerticalMock);
const updatedAncestorsChild = updatedCourseSectionVerticalData.xblock_info.ancestor_info.ancestors[0]; const updatedAncestorsChild = updatedCourseSectionVerticalData.xblock_info.ancestor_info.ancestors[0];
set(updatedCourseSectionVerticalData, 'xblock_info.ancestor_info.ancestors[0].child_info.children', [ set(updatedCourseSectionVerticalData, 'xblock_info.ancestor_info.ancestors[0].child_info.children', [
@@ -809,7 +846,7 @@ describe('<CourseUnit />', () => {
await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch); await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch);
const unitHeaderTitle = screen.getByTestId('unit-header-title'); const unitHeaderTitle = getByTestId('unit-header-title');
const editTitleButton = within(unitHeaderTitle).getByRole('button', { name: headerTitleMessages.altButtonEdit.defaultMessage }); const editTitleButton = within(unitHeaderTitle).getByRole('button', { name: headerTitleMessages.altButtonEdit.defaultMessage });
userEvent.click(editTitleButton); userEvent.click(editTitleButton);
@@ -821,21 +858,20 @@ describe('<CourseUnit />', () => {
await userEvent.tab(); await userEvent.tab();
await waitFor(async () => { await waitFor(async () => {
const units = screen.getAllByTestId('course-unit-btn'); const units = getAllByTestId('course-unit-btn');
expect(units.some(unit => unit.title === newDisplayName)).toBe(true); expect(units.some(unit => unit.title === newDisplayName)).toBe(true);
}); });
}); });
it('handles creating Video xblock and showing editor modal using videogalleryflow', async () => { it('handles creating Video xblock and navigates to editor page', async () => {
const waffleSpy = jest.spyOn(selectors, 'getWaffleFlags').mockReturnValue({ useVideoGalleryFlow: true }); const { courseKey, locator } = courseCreateXblockMock;
axiosMock axiosMock
.onPost(postXBlockBaseApiUrl({ type: 'video', category: 'video', parentLocator: blockId })) .onPost(postXBlockBaseApiUrl({ type: 'video', category: 'video', parentLocator: blockId }))
.reply(200, courseCreateXblockMock); .reply(200, courseCreateXblockMock);
render(<RootWrapper />); const { getByText, queryByRole, getByRole } = render(<RootWrapper />);
await waitFor(() => { await waitFor(() => {
userEvent.click(screen.getByRole('button', { name: sidebarMessages.actionButtonPublishTitle.defaultMessage })); userEvent.click(getByRole('button', { name: sidebarMessages.actionButtonPublishTitle.defaultMessage }));
}); });
axiosMock axiosMock
@@ -856,102 +892,23 @@ describe('<CourseUnit />', () => {
await waitFor(() => { await waitFor(() => {
// check if the sidebar status is Published and Live // check if the sidebar status is Published and Live
expect(screen.getByText(sidebarMessages.sidebarTitlePublishedAndLive.defaultMessage)).toBeInTheDocument(); expect(getByText(sidebarMessages.sidebarTitlePublishedAndLive.defaultMessage)).toBeInTheDocument();
}); expect(getByText(
expect(screen.getByText(
sidebarMessages.publishLastPublished.defaultMessage
.replace('{publishedOn}', courseUnitIndexMock.published_on)
.replace('{publishedBy}', userName),
)).toBeInTheDocument();
expect(screen.queryByRole('button', { name: sidebarMessages.actionButtonPublishTitle.defaultMessage })).not.toBeInTheDocument();
const videoButton = screen.getByRole('button', {
name: new RegExp(`${addComponentMessages.buttonText.defaultMessage} Video`, 'i'),
hidden: true,
});
userEvent.click(videoButton);
axiosMock
.onGet(getCourseUnitApiUrl(blockId))
.reply(200, courseUnitIndexMock);
await executeThunk(editCourseUnitVisibilityAndData(blockId, PUBLISH_TYPES.makePublic, true), store.dispatch);
// after creating video xblock, the sidebar status changes to Draft (unpublished changes)
expect(screen.getByText(sidebarMessages.sidebarTitleDraftUnpublishedChanges.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.visibilityStaffAndLearnersTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.releaseStatusTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.sidebarBodyNote.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.visibilityWillBeVisibleToTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.visibilityCheckboxTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.actionButtonPublishTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.actionButtonDiscardChangesTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(courseUnitIndexMock.release_date)).toBeInTheDocument();
expect(screen.getByText(
sidebarMessages.publishInfoDraftSaved.defaultMessage
.replace('{editedOn}', courseUnitIndexMock.edited_on)
.replace('{editedBy}', courseUnitIndexMock.edited_by),
)).toBeInTheDocument();
expect(screen.getByText(
sidebarMessages.releaseInfoWithSection.defaultMessage
.replace('{sectionName}', courseUnitIndexMock.release_date_from),
)).toBeInTheDocument();
expect(screen.getByRole('heading', { name: /add video to your course/i, hidden: true })).toBeInTheDocument();
waffleSpy.mockRestore();
});
it('handles creating Video xblock and showing editor modal', async () => {
axiosMock
.onPost(postXBlockBaseApiUrl({ type: 'video', category: 'video', parentLocator: blockId }))
.reply(200, courseCreateXblockMock);
render(<RootWrapper />);
await waitFor(() => {
userEvent.click(screen.getByRole('button', { name: sidebarMessages.actionButtonPublishTitle.defaultMessage }));
});
axiosMock
.onPost(getXBlockBaseApiUrl(blockId), {
publish: PUBLISH_TYPES.makePublic,
})
.reply(200, { dummy: 'value' });
axiosMock
.onGet(getCourseUnitApiUrl(blockId))
.reply(200, {
...courseUnitIndexMock,
visibility_state: UNIT_VISIBILITY_STATES.live,
has_changes: false,
published_by: userName,
});
await executeThunk(editCourseUnitVisibilityAndData(blockId, PUBLISH_TYPES.makePublic, true), store.dispatch);
await waitFor(() => {
// check if the sidebar status is Published and Live
expect(screen.getByText(sidebarMessages.sidebarTitlePublishedAndLive.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(
sidebarMessages.publishLastPublished.defaultMessage sidebarMessages.publishLastPublished.defaultMessage
.replace('{publishedOn}', courseUnitIndexMock.published_on) .replace('{publishedOn}', courseUnitIndexMock.published_on)
.replace('{publishedBy}', userName), .replace('{publishedBy}', userName),
)).toBeInTheDocument(); )).toBeInTheDocument();
expect(screen.queryByRole('button', { name: sidebarMessages.actionButtonPublishTitle.defaultMessage })).not.toBeInTheDocument(); expect(queryByRole('button', { name: sidebarMessages.actionButtonPublishTitle.defaultMessage })).not.toBeInTheDocument();
const videoButton = screen.getByRole('button', { const videoButton = getByRole('button', {
name: new RegExp(`${addComponentMessages.buttonText.defaultMessage} Video`, 'i'), name: new RegExp(`${addComponentMessages.buttonText.defaultMessage} Video`, 'i'),
hidden: true,
}); });
userEvent.click(videoButton); userEvent.click(videoButton);
expect(mockedUsedNavigate).toHaveBeenCalled();
expect(mockedUsedNavigate).toHaveBeenCalledWith(`/course/${courseKey}/editor/video/${locator}`);
}); });
/** TODO -- fix this test.
await waitFor(() => {
expect(getByRole('textbox', { name: /paste your video id or url/i })).toBeInTheDocument();
});
*/
axiosMock axiosMock
.onGet(getCourseUnitApiUrl(blockId)) .onGet(getCourseUnitApiUrl(blockId))
.reply(200, courseUnitIndexMock); .reply(200, courseUnitIndexMock);
@@ -959,45 +916,45 @@ describe('<CourseUnit />', () => {
await executeThunk(editCourseUnitVisibilityAndData(blockId, PUBLISH_TYPES.makePublic, true), store.dispatch); await executeThunk(editCourseUnitVisibilityAndData(blockId, PUBLISH_TYPES.makePublic, true), store.dispatch);
// after creating video xblock, the sidebar status changes to Draft (unpublished changes) // after creating video xblock, the sidebar status changes to Draft (unpublished changes)
expect(screen.getByText(sidebarMessages.sidebarTitleDraftUnpublishedChanges.defaultMessage)).toBeInTheDocument(); expect(getByText(sidebarMessages.sidebarTitleDraftUnpublishedChanges.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.visibilityStaffAndLearnersTitle.defaultMessage)).toBeInTheDocument(); expect(getByText(sidebarMessages.visibilityStaffAndLearnersTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.releaseStatusTitle.defaultMessage)).toBeInTheDocument(); expect(getByText(sidebarMessages.releaseStatusTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.sidebarBodyNote.defaultMessage)).toBeInTheDocument(); expect(getByText(sidebarMessages.sidebarBodyNote.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.visibilityWillBeVisibleToTitle.defaultMessage)).toBeInTheDocument(); expect(getByText(sidebarMessages.visibilityWillBeVisibleToTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.visibilityCheckboxTitle.defaultMessage)).toBeInTheDocument(); expect(getByText(sidebarMessages.visibilityCheckboxTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.actionButtonPublishTitle.defaultMessage)).toBeInTheDocument(); expect(getByText(sidebarMessages.actionButtonPublishTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.actionButtonDiscardChangesTitle.defaultMessage)).toBeInTheDocument(); expect(getByText(sidebarMessages.actionButtonDiscardChangesTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(courseUnitIndexMock.release_date)).toBeInTheDocument(); expect(getByText(courseUnitIndexMock.release_date)).toBeInTheDocument();
expect(screen.getByText( expect(getByText(
sidebarMessages.publishInfoDraftSaved.defaultMessage sidebarMessages.publishInfoDraftSaved.defaultMessage
.replace('{editedOn}', courseUnitIndexMock.edited_on) .replace('{editedOn}', courseUnitIndexMock.edited_on)
.replace('{editedBy}', courseUnitIndexMock.edited_by), .replace('{editedBy}', courseUnitIndexMock.edited_by),
)).toBeInTheDocument(); )).toBeInTheDocument();
expect(screen.getByText( expect(getByText(
sidebarMessages.releaseInfoWithSection.defaultMessage sidebarMessages.releaseInfoWithSection.defaultMessage
.replace('{sectionName}', courseUnitIndexMock.release_date_from), .replace('{sectionName}', courseUnitIndexMock.release_date_from),
)).toBeInTheDocument(); )).toBeInTheDocument();
}); });
it('renders course unit details for a draft with unpublished changes', async () => { it('renders course unit details for a draft with unpublished changes', async () => {
render(<RootWrapper />); const { getByText } = render(<RootWrapper />);
await waitFor(() => { await waitFor(() => {
expect(screen.getByText(sidebarMessages.sidebarTitleDraftUnpublishedChanges.defaultMessage)).toBeInTheDocument(); expect(getByText(sidebarMessages.sidebarTitleDraftUnpublishedChanges.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.visibilityStaffAndLearnersTitle.defaultMessage)).toBeInTheDocument(); expect(getByText(sidebarMessages.visibilityStaffAndLearnersTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.releaseStatusTitle.defaultMessage)).toBeInTheDocument(); expect(getByText(sidebarMessages.releaseStatusTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.sidebarBodyNote.defaultMessage)).toBeInTheDocument(); expect(getByText(sidebarMessages.sidebarBodyNote.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.visibilityWillBeVisibleToTitle.defaultMessage)).toBeInTheDocument(); expect(getByText(sidebarMessages.visibilityWillBeVisibleToTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.visibilityCheckboxTitle.defaultMessage)).toBeInTheDocument(); expect(getByText(sidebarMessages.visibilityCheckboxTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.actionButtonPublishTitle.defaultMessage)).toBeInTheDocument(); expect(getByText(sidebarMessages.actionButtonPublishTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.actionButtonDiscardChangesTitle.defaultMessage)).toBeInTheDocument(); expect(getByText(sidebarMessages.actionButtonDiscardChangesTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(courseUnitIndexMock.release_date)).toBeInTheDocument(); expect(getByText(courseUnitIndexMock.release_date)).toBeInTheDocument();
expect(screen.getByText( expect(getByText(
sidebarMessages.publishInfoDraftSaved.defaultMessage sidebarMessages.publishInfoDraftSaved.defaultMessage
.replace('{editedOn}', courseUnitIndexMock.edited_on) .replace('{editedOn}', courseUnitIndexMock.edited_on)
.replace('{editedBy}', courseUnitIndexMock.edited_by), .replace('{editedBy}', courseUnitIndexMock.edited_by),
)).toBeInTheDocument(); )).toBeInTheDocument();
expect(screen.getByText( expect(getByText(
sidebarMessages.releaseInfoWithSection.defaultMessage sidebarMessages.releaseInfoWithSection.defaultMessage
.replace('{sectionName}', courseUnitIndexMock.release_date_from), .replace('{sectionName}', courseUnitIndexMock.release_date_from),
)).toBeInTheDocument(); )).toBeInTheDocument();
@@ -1005,14 +962,14 @@ describe('<CourseUnit />', () => {
}); });
it('renders course unit details in the sidebar', async () => { it('renders course unit details in the sidebar', async () => {
render(<RootWrapper />); const { getByText } = render(<RootWrapper />);
const courseUnitLocationId = extractCourseUnitId(courseUnitIndexMock.id); const courseUnitLocationId = extractCourseUnitId(courseUnitIndexMock.id);
await waitFor(() => { await waitFor(() => {
expect(screen.getByText(sidebarMessages.sidebarHeaderUnitLocationTitle.defaultMessage)).toBeInTheDocument(); expect(getByText(sidebarMessages.sidebarHeaderUnitLocationTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.unitLocationTitle.defaultMessage)).toBeInTheDocument(); expect(getByText(sidebarMessages.unitLocationTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(courseUnitLocationId)).toBeInTheDocument(); expect(getByText(courseUnitLocationId)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.unitLocationDescription.defaultMessage expect(getByText(sidebarMessages.unitLocationDescription.defaultMessage
.replace('{id}', courseUnitLocationId))).toBeInTheDocument(); .replace('{id}', courseUnitLocationId))).toBeInTheDocument();
}); });
}); });
@@ -1050,13 +1007,13 @@ describe('<CourseUnit />', () => {
}); });
it('should toggle visibility from sidebar and update course unit state accordingly', async () => { it('should toggle visibility from sidebar and update course unit state accordingly', async () => {
render(<RootWrapper />); const { getByRole, getByTestId } = render(<RootWrapper />);
let courseUnitSidebar; let courseUnitSidebar;
let draftUnpublishedChangesHeading; let draftUnpublishedChangesHeading;
let visibilityCheckbox; let visibilityCheckbox;
await waitFor(() => { await waitFor(() => {
courseUnitSidebar = screen.getByTestId('course-unit-sidebar'); courseUnitSidebar = getByTestId('course-unit-sidebar');
draftUnpublishedChangesHeading = within(courseUnitSidebar) draftUnpublishedChangesHeading = within(courseUnitSidebar)
.getByText(sidebarMessages.sidebarTitleDraftUnpublishedChanges.defaultMessage); .getByText(sidebarMessages.sidebarTitleDraftUnpublishedChanges.defaultMessage);
@@ -1093,7 +1050,7 @@ describe('<CourseUnit />', () => {
userEvent.click(visibilityCheckbox); userEvent.click(visibilityCheckbox);
const modalNotification = screen.getByRole('dialog'); const modalNotification = getByRole('dialog');
const makeVisibilityBtn = within(modalNotification).getByRole('button', { name: sidebarMessages.modalMakeVisibilityActionButtonText.defaultMessage }); const makeVisibilityBtn = within(modalNotification).getByRole('button', { name: sidebarMessages.modalMakeVisibilityActionButtonText.defaultMessage });
const cancelBtn = within(modalNotification).getByRole('button', { name: sidebarMessages.modalMakeVisibilityCancelButtonText.defaultMessage }); const cancelBtn = within(modalNotification).getByRole('button', { name: sidebarMessages.modalMakeVisibilityCancelButtonText.defaultMessage });
const headingElement = within(modalNotification).getByRole('heading', { name: sidebarMessages.modalMakeVisibilityTitle.defaultMessage, class: 'pgn__modal-title' }); const headingElement = within(modalNotification).getByRole('heading', { name: sidebarMessages.modalMakeVisibilityTitle.defaultMessage, class: 'pgn__modal-title' });
@@ -1125,12 +1082,12 @@ describe('<CourseUnit />', () => {
}); });
it('should publish course unit after click on the "Publish" button', async () => { it('should publish course unit after click on the "Publish" button', async () => {
render(<RootWrapper />); const { getByTestId } = render(<RootWrapper />);
let courseUnitSidebar; let courseUnitSidebar;
let publishBtn; let publishBtn;
await waitFor(() => { await waitFor(() => {
courseUnitSidebar = screen.getByTestId('course-unit-sidebar'); courseUnitSidebar = getByTestId('course-unit-sidebar');
publishBtn = within(courseUnitSidebar).queryByRole('button', { name: sidebarMessages.actionButtonPublishTitle.defaultMessage }); publishBtn = within(courseUnitSidebar).queryByRole('button', { name: sidebarMessages.actionButtonPublishTitle.defaultMessage });
expect(publishBtn).toBeInTheDocument(); expect(publishBtn).toBeInTheDocument();
@@ -1164,12 +1121,12 @@ describe('<CourseUnit />', () => {
}); });
it('should discard changes after click on the "Discard changes" button', async () => { it('should discard changes after click on the "Discard changes" button', async () => {
render(<RootWrapper />); const { getByTestId, getByRole } = render(<RootWrapper />);
let courseUnitSidebar; let courseUnitSidebar;
let discardChangesBtn; let discardChangesBtn;
await waitFor(() => { await waitFor(() => {
courseUnitSidebar = screen.getByTestId('course-unit-sidebar'); courseUnitSidebar = getByTestId('course-unit-sidebar');
const draftUnpublishedChangesHeading = within(courseUnitSidebar) const draftUnpublishedChangesHeading = within(courseUnitSidebar)
.getByText(sidebarMessages.sidebarTitleDraftUnpublishedChanges.defaultMessage); .getByText(sidebarMessages.sidebarTitleDraftUnpublishedChanges.defaultMessage);
@@ -1179,7 +1136,7 @@ describe('<CourseUnit />', () => {
userEvent.click(discardChangesBtn); userEvent.click(discardChangesBtn);
const modalNotification = screen.getByRole('dialog'); const modalNotification = getByRole('dialog');
expect(modalNotification).toBeInTheDocument(); expect(modalNotification).toBeInTheDocument();
expect(within(modalNotification) expect(within(modalNotification)
.getByText(sidebarMessages.modalDiscardUnitChangesDescription.defaultMessage)).toBeInTheDocument(); .getByText(sidebarMessages.modalDiscardUnitChangesDescription.defaultMessage)).toBeInTheDocument();
@@ -1216,7 +1173,7 @@ describe('<CourseUnit />', () => {
}); });
it('should toggle visibility from header configure modal and update course unit state accordingly', async () => { it('should toggle visibility from header configure modal and update course unit state accordingly', async () => {
render(<RootWrapper />); const { getByRole, getByTestId } = render(<RootWrapper />);
let courseUnitSidebar; let courseUnitSidebar;
let sidebarVisibilityCheckbox; let sidebarVisibilityCheckbox;
let modalVisibilityCheckbox; let modalVisibilityCheckbox;
@@ -1224,16 +1181,16 @@ describe('<CourseUnit />', () => {
let restrictAccessSelect; let restrictAccessSelect;
await waitFor(() => { await waitFor(() => {
courseUnitSidebar = screen.getByTestId('course-unit-sidebar'); courseUnitSidebar = getByTestId('course-unit-sidebar');
sidebarVisibilityCheckbox = within(courseUnitSidebar) sidebarVisibilityCheckbox = within(courseUnitSidebar)
.getByLabelText(sidebarMessages.visibilityCheckboxTitle.defaultMessage); .getByLabelText(sidebarMessages.visibilityCheckboxTitle.defaultMessage);
expect(sidebarVisibilityCheckbox).not.toBeChecked(); expect(sidebarVisibilityCheckbox).not.toBeChecked();
const headerConfigureBtn = screen.getByRole('button', { name: /settings/i }); const headerConfigureBtn = getByRole('button', { name: /settings/i });
expect(headerConfigureBtn).toBeInTheDocument(); expect(headerConfigureBtn).toBeInTheDocument();
userEvent.click(headerConfigureBtn); userEvent.click(headerConfigureBtn);
configureModal = screen.getByTestId('configure-modal'); configureModal = getByTestId('configure-modal');
restrictAccessSelect = within(configureModal) restrictAccessSelect = within(configureModal)
.getByRole('combobox', { name: configureModalMessages.restrictAccessTo.defaultMessage }); .getByRole('combobox', { name: configureModalMessages.restrictAccessTo.defaultMessage });
expect(within(configureModal) expect(within(configureModal)
@@ -1289,8 +1246,8 @@ describe('<CourseUnit />', () => {
...getConfig(), ...getConfig(),
ENABLE_TAGGING_TAXONOMY_PAGES: 'true', ENABLE_TAGGING_TAXONOMY_PAGES: 'true',
}); });
render(<RootWrapper />); const { getByText } = render(<RootWrapper />);
await waitFor(() => { expect(screen.getByText('Unit tags')).toBeInTheDocument(); }); await waitFor(() => { expect(getByText('Unit tags')).toBeInTheDocument(); });
}); });
it('hides the Tags sidebar when not enabled', async () => { it('hides the Tags sidebar when not enabled', async () => {
@@ -1298,13 +1255,15 @@ describe('<CourseUnit />', () => {
...getConfig(), ...getConfig(),
ENABLE_TAGGING_TAXONOMY_PAGES: 'false', ENABLE_TAGGING_TAXONOMY_PAGES: 'false',
}); });
render(<RootWrapper />); const { queryByText } = render(<RootWrapper />);
await waitFor(() => { expect(screen.queryByText('Unit tags')).not.toBeInTheDocument(); }); await waitFor(() => { expect(queryByText('Unit tags')).not.toBeInTheDocument(); });
}); });
describe('Copy paste functionality', () => { describe('Copy paste functionality', () => {
it('should copy a unit, paste it as a new unit, and update the course section vertical data', async () => { it('should copy a unit, paste it as a new unit, and update the course section vertical data', async () => {
render(<RootWrapper />); const {
getAllByTestId, getByRole,
} = render(<RootWrapper />);
axiosMock axiosMock
.onGet(getCourseUnitApiUrl(courseId)) .onGet(getCourseUnitApiUrl(courseId))
@@ -1316,8 +1275,8 @@ describe('<CourseUnit />', () => {
await executeThunk(fetchCourseUnitQuery(courseId), store.dispatch); await executeThunk(fetchCourseUnitQuery(courseId), store.dispatch);
await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch); await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch);
userEvent.click(screen.getByRole('button', { name: sidebarMessages.actionButtonCopyUnitTitle.defaultMessage })); userEvent.click(getByRole('button', { name: sidebarMessages.actionButtonCopyUnitTitle.defaultMessage }));
userEvent.click(screen.getByRole('button', { name: courseSequenceMessages.pasteAsNewUnitLink.defaultMessage })); userEvent.click(getByRole('button', { name: courseSequenceMessages.pasteAsNewUnitLink.defaultMessage }));
let units = null; let units = null;
const updatedCourseSectionVerticalData = cloneDeep(courseSectionVerticalMock); const updatedCourseSectionVerticalData = cloneDeep(courseSectionVerticalMock);
@@ -1328,7 +1287,7 @@ describe('<CourseUnit />', () => {
]); ]);
await waitFor(() => { await waitFor(() => {
units = screen.getAllByTestId('course-unit-btn'); units = getAllByTestId('course-unit-btn');
const courseUnits = courseSectionVerticalMock.xblock_info.ancestor_info.ancestors[0].child_info.children; const courseUnits = courseSectionVerticalMock.xblock_info.ancestor_info.ancestors[0].child_info.children;
expect(units).toHaveLength(courseUnits.length); expect(units).toHaveLength(courseUnits.length);
}); });
@@ -1344,7 +1303,7 @@ describe('<CourseUnit />', () => {
await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch); await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch);
units = screen.getAllByTestId('course-unit-btn'); units = getAllByTestId('course-unit-btn');
const updatedCourseUnits = updatedCourseSectionVerticalData const updatedCourseUnits = updatedCourseSectionVerticalData
.xblock_info.ancestor_info.ancestors[0].child_info.children; .xblock_info.ancestor_info.ancestors[0].child_info.children;
@@ -1355,7 +1314,7 @@ describe('<CourseUnit />', () => {
}); });
it('should increase the number of course XBlocks after copying and pasting a block', async () => { it('should increase the number of course XBlocks after copying and pasting a block', async () => {
render(<RootWrapper />); const { getByRole, getByTitle } = render(<RootWrapper />);
simulatePostMessageEvent(messageTypes.copyXBlock, { simulatePostMessageEvent(messageTypes.copyXBlock, {
id: courseVerticalChildrenMock.children[0].block_id, id: courseVerticalChildrenMock.children[0].block_id,
@@ -1374,11 +1333,11 @@ describe('<CourseUnit />', () => {
await executeThunk(fetchCourseUnitQuery(courseId), store.dispatch); await executeThunk(fetchCourseUnitQuery(courseId), store.dispatch);
await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch); await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch);
userEvent.click(screen.getByRole('button', { name: sidebarMessages.actionButtonCopyUnitTitle.defaultMessage })); userEvent.click(getByRole('button', { name: sidebarMessages.actionButtonCopyUnitTitle.defaultMessage }));
userEvent.click(screen.getByRole('button', { name: messages.pasteButtonText.defaultMessage })); userEvent.click(getByRole('button', { name: messages.pasteButtonText.defaultMessage }));
await waitFor(() => { await waitFor(() => {
const iframe = screen.getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage); const iframe = getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
expect(iframe).toHaveAttribute( expect(iframe).toHaveAttribute(
'aria-label', 'aria-label',
xblockContainerIframeMessages.xblockIframeLabel.defaultMessage xblockContainerIframeMessages.xblockIframeLabel.defaultMessage
@@ -1414,7 +1373,7 @@ describe('<CourseUnit />', () => {
await executeThunk(fetchCourseVerticalChildrenData(blockId), store.dispatch); await executeThunk(fetchCourseVerticalChildrenData(blockId), store.dispatch);
await waitFor(() => { await waitFor(() => {
const iframe = screen.getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage); const iframe = getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
expect(iframe).toHaveAttribute( expect(iframe).toHaveAttribute(
'aria-label', 'aria-label',
xblockContainerIframeMessages.xblockIframeLabel.defaultMessage xblockContainerIframeMessages.xblockIframeLabel.defaultMessage
@@ -1424,7 +1383,9 @@ describe('<CourseUnit />', () => {
}); });
it('displays a notification about new files after pasting a component', async () => { it('displays a notification about new files after pasting a component', async () => {
render(<RootWrapper />); const {
queryByTestId, getByTestId, getByRole,
} = render(<RootWrapper />);
axiosMock axiosMock
.onGet(getCourseUnitApiUrl(courseId)) .onGet(getCourseUnitApiUrl(courseId))
@@ -1436,8 +1397,8 @@ describe('<CourseUnit />', () => {
await executeThunk(fetchCourseUnitQuery(courseId), store.dispatch); await executeThunk(fetchCourseUnitQuery(courseId), store.dispatch);
await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch); await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch);
userEvent.click(screen.getByRole('button', { name: sidebarMessages.actionButtonCopyUnitTitle.defaultMessage })); userEvent.click(getByRole('button', { name: sidebarMessages.actionButtonCopyUnitTitle.defaultMessage }));
userEvent.click(screen.getByRole('button', { name: courseSequenceMessages.pasteAsNewUnitLink.defaultMessage })); userEvent.click(getByRole('button', { name: courseSequenceMessages.pasteAsNewUnitLink.defaultMessage }));
const updatedCourseSectionVerticalData = cloneDeep(courseSectionVerticalMock); const updatedCourseSectionVerticalData = cloneDeep(courseSectionVerticalMock);
const updatedAncestorsChild = updatedCourseSectionVerticalData.xblock_info.ancestor_info.ancestors[0]; const updatedAncestorsChild = updatedCourseSectionVerticalData.xblock_info.ancestor_info.ancestors[0];
@@ -1456,7 +1417,7 @@ describe('<CourseUnit />', () => {
global.localStorage.setItem('staticFileNotices', JSON.stringify(clipboardMockResponse.staticFileNotices)); global.localStorage.setItem('staticFileNotices', JSON.stringify(clipboardMockResponse.staticFileNotices));
await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch); await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch);
await executeThunk(createNewCourseXBlock(camelCaseObject(postXBlockBody), null, blockId), store.dispatch); await executeThunk(createNewCourseXBlock(camelCaseObject(postXBlockBody), null, blockId), store.dispatch);
const newFilesAlert = screen.getByTestId('has-new-files-alert'); const newFilesAlert = getByTestId('has-new-files-alert');
expect(within(newFilesAlert) expect(within(newFilesAlert)
.getByText(pasteNotificationsMessages.hasNewFilesTitle.defaultMessage)).toBeInTheDocument(); .getByText(pasteNotificationsMessages.hasNewFilesTitle.defaultMessage)).toBeInTheDocument();
@@ -1470,11 +1431,13 @@ describe('<CourseUnit />', () => {
userEvent.click(within(newFilesAlert).getByText(/Dismiss/i)); userEvent.click(within(newFilesAlert).getByText(/Dismiss/i));
expect(screen.queryByTestId('has-new-files-alert')).toBeNull(); expect(queryByTestId('has-new-files-alert')).toBeNull();
}); });
it('displays a notification about conflicting errors after pasting a component', async () => { it('displays a notification about conflicting errors after pasting a component', async () => {
render(<RootWrapper />); const {
queryByTestId, getByTestId, getByRole,
} = render(<RootWrapper />);
axiosMock axiosMock
.onGet(getCourseUnitApiUrl(courseId)) .onGet(getCourseUnitApiUrl(courseId))
@@ -1486,8 +1449,8 @@ describe('<CourseUnit />', () => {
await executeThunk(fetchCourseUnitQuery(courseId), store.dispatch); await executeThunk(fetchCourseUnitQuery(courseId), store.dispatch);
await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch); await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch);
userEvent.click(screen.getByRole('button', { name: sidebarMessages.actionButtonCopyUnitTitle.defaultMessage })); userEvent.click(getByRole('button', { name: sidebarMessages.actionButtonCopyUnitTitle.defaultMessage }));
userEvent.click(screen.getByRole('button', { name: courseSequenceMessages.pasteAsNewUnitLink.defaultMessage })); userEvent.click(getByRole('button', { name: courseSequenceMessages.pasteAsNewUnitLink.defaultMessage }));
const updatedCourseSectionVerticalData = cloneDeep(courseSectionVerticalMock); const updatedCourseSectionVerticalData = cloneDeep(courseSectionVerticalMock);
const updatedAncestorsChild = updatedCourseSectionVerticalData.xblock_info.ancestor_info.ancestors[0]; const updatedAncestorsChild = updatedCourseSectionVerticalData.xblock_info.ancestor_info.ancestors[0];
@@ -1508,7 +1471,7 @@ describe('<CourseUnit />', () => {
global.localStorage.setItem('staticFileNotices', JSON.stringify(clipboardMockResponse.staticFileNotices)); global.localStorage.setItem('staticFileNotices', JSON.stringify(clipboardMockResponse.staticFileNotices));
await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch); await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch);
await executeThunk(createNewCourseXBlock(camelCaseObject(postXBlockBody), null, blockId), store.dispatch); await executeThunk(createNewCourseXBlock(camelCaseObject(postXBlockBody), null, blockId), store.dispatch);
const conflictingErrorsAlert = screen.getByTestId('has-conflicting-errors-alert'); const conflictingErrorsAlert = getByTestId('has-conflicting-errors-alert');
expect(within(conflictingErrorsAlert) expect(within(conflictingErrorsAlert)
.getByText(pasteNotificationsMessages.hasConflictingErrorsTitle.defaultMessage)).toBeInTheDocument(); .getByText(pasteNotificationsMessages.hasConflictingErrorsTitle.defaultMessage)).toBeInTheDocument();
@@ -1522,11 +1485,13 @@ describe('<CourseUnit />', () => {
userEvent.click(within(conflictingErrorsAlert).getByText(/Dismiss/i)); userEvent.click(within(conflictingErrorsAlert).getByText(/Dismiss/i));
expect(screen.queryByTestId('has-conflicting-errors-alert')).toBeNull(); expect(queryByTestId('has-conflicting-errors-alert')).toBeNull();
}); });
it('displays a notification about error files after pasting a component', async () => { it('displays a notification about error files after pasting a component', async () => {
render(<RootWrapper />); const {
queryByTestId, getByTestId, getByRole,
} = render(<RootWrapper />);
axiosMock axiosMock
.onGet(getCourseUnitApiUrl(courseId)) .onGet(getCourseUnitApiUrl(courseId))
@@ -1538,8 +1503,8 @@ describe('<CourseUnit />', () => {
await executeThunk(fetchCourseUnitQuery(courseId), store.dispatch); await executeThunk(fetchCourseUnitQuery(courseId), store.dispatch);
await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch); await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch);
userEvent.click(screen.getByRole('button', { name: sidebarMessages.actionButtonCopyUnitTitle.defaultMessage })); userEvent.click(getByRole('button', { name: sidebarMessages.actionButtonCopyUnitTitle.defaultMessage }));
userEvent.click(screen.getByRole('button', { name: courseSequenceMessages.pasteAsNewUnitLink.defaultMessage })); userEvent.click(getByRole('button', { name: courseSequenceMessages.pasteAsNewUnitLink.defaultMessage }));
const updatedCourseSectionVerticalData = cloneDeep(courseSectionVerticalMock); const updatedCourseSectionVerticalData = cloneDeep(courseSectionVerticalMock);
const updatedAncestorsChild = updatedCourseSectionVerticalData.xblock_info.ancestor_info.ancestors[0]; const updatedAncestorsChild = updatedCourseSectionVerticalData.xblock_info.ancestor_info.ancestors[0];
@@ -1560,7 +1525,7 @@ describe('<CourseUnit />', () => {
global.localStorage.setItem('staticFileNotices', JSON.stringify(clipboardMockResponse.staticFileNotices)); global.localStorage.setItem('staticFileNotices', JSON.stringify(clipboardMockResponse.staticFileNotices));
await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch); await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch);
await executeThunk(createNewCourseXBlock(camelCaseObject(postXBlockBody), null, blockId), store.dispatch); await executeThunk(createNewCourseXBlock(camelCaseObject(postXBlockBody), null, blockId), store.dispatch);
const errorFilesAlert = screen.getByTestId('has-error-files-alert'); const errorFilesAlert = getByTestId('has-error-files-alert');
expect(within(errorFilesAlert) expect(within(errorFilesAlert)
.getByText(pasteNotificationsMessages.hasErrorsTitle.defaultMessage)).toBeInTheDocument(); .getByText(pasteNotificationsMessages.hasErrorsTitle.defaultMessage)).toBeInTheDocument();
@@ -1569,11 +1534,11 @@ describe('<CourseUnit />', () => {
userEvent.click(within(errorFilesAlert).getByText(/Dismiss/i)); userEvent.click(within(errorFilesAlert).getByText(/Dismiss/i));
expect(screen.queryByTestId('has-error-files')).toBeNull(); expect(queryByTestId('has-error-files')).toBeNull();
}); });
it('should hide the "Paste component" block if canPasteComponent is false', async () => { it('should hide the "Paste component" block if canPasteComponent is false', async () => {
render(<RootWrapper />); const { queryByText, queryByRole } = render(<RootWrapper />);
axiosMock axiosMock
.onGet(getCourseVerticalChildrenApiUrl(blockId)) .onGet(getCourseVerticalChildrenApiUrl(blockId))
@@ -1584,10 +1549,10 @@ describe('<CourseUnit />', () => {
await executeThunk(fetchCourseVerticalChildrenData(blockId), store.dispatch); await executeThunk(fetchCourseVerticalChildrenData(blockId), store.dispatch);
expect(screen.queryByRole('button', { expect(queryByRole('button', {
name: messages.pasteButtonText.defaultMessage, name: messages.pasteButtonText.defaultMessage,
})).not.toBeInTheDocument(); })).not.toBeInTheDocument();
expect(screen.queryByText( expect(queryByText(
pasteComponentMessages.pasteButtonWhatsInClipboardText.defaultMessage, pasteComponentMessages.pasteButtonWhatsInClipboardText.defaultMessage,
)).not.toBeInTheDocument(); )).not.toBeInTheDocument();
}); });
@@ -1621,7 +1586,9 @@ describe('<CourseUnit />', () => {
}); });
it('should display "Move Modal" on receive trigger message', async () => { it('should display "Move Modal" on receive trigger message', async () => {
render(<RootWrapper />); const {
getByRole,
} = render(<RootWrapper />);
await screen.findByText(unitDisplayName); await screen.findByText(unitDisplayName);
@@ -1635,12 +1602,15 @@ describe('<CourseUnit />', () => {
await screen.findByText( await screen.findByText(
moveModalMessages.moveModalTitle.defaultMessage.replace('{displayName}', requestData.title), moveModalMessages.moveModalTitle.defaultMessage.replace('{displayName}', requestData.title),
); );
expect(screen.getByRole('button', { name: moveModalMessages.moveModalSubmitButton.defaultMessage })).toBeInTheDocument(); expect(getByRole('button', { name: moveModalMessages.moveModalSubmitButton.defaultMessage })).toBeInTheDocument();
expect(screen.getByRole('button', { name: moveModalMessages.moveModalCancelButton.defaultMessage })).toBeInTheDocument(); expect(getByRole('button', { name: moveModalMessages.moveModalCancelButton.defaultMessage })).toBeInTheDocument();
}); });
it('should navigates to xBlock current unit', async () => { it('should navigates to xBlock current unit', async () => {
render(<RootWrapper />); const {
getByText,
getByRole,
} = render(<RootWrapper />);
await screen.findByText(unitDisplayName); await screen.findByText(unitDisplayName);
@@ -1656,7 +1626,7 @@ describe('<CourseUnit />', () => {
); );
const currentSection = courseOutlineInfoMock.child_info.children[1]; const currentSection = courseOutlineInfoMock.child_info.children[1];
const currentSectionItemBtn = screen.getByRole('button', { const currentSectionItemBtn = getByRole('button', {
name: `${currentSection.display_name} ${moveModalMessages.moveModalOutlineItemCurrentLocationText.defaultMessage} ${moveModalMessages.moveModalOutlineItemViewText.defaultMessage}`, name: `${currentSection.display_name} ${moveModalMessages.moveModalOutlineItemCurrentLocationText.defaultMessage} ${moveModalMessages.moveModalOutlineItemViewText.defaultMessage}`,
}); });
expect(currentSectionItemBtn).toBeInTheDocument(); expect(currentSectionItemBtn).toBeInTheDocument();
@@ -1664,7 +1634,7 @@ describe('<CourseUnit />', () => {
await waitFor(() => { await waitFor(() => {
const currentSubsection = currentSection.child_info.children[0]; const currentSubsection = currentSection.child_info.children[0];
const currentSubsectionItemBtn = screen.getByRole('button', { const currentSubsectionItemBtn = getByRole('button', {
name: `${currentSubsection.display_name} ${moveModalMessages.moveModalOutlineItemCurrentLocationText.defaultMessage} ${moveModalMessages.moveModalOutlineItemViewText.defaultMessage}`, name: `${currentSubsection.display_name} ${moveModalMessages.moveModalOutlineItemCurrentLocationText.defaultMessage} ${moveModalMessages.moveModalOutlineItemViewText.defaultMessage}`,
}); });
expect(currentSubsectionItemBtn).toBeInTheDocument(); expect(currentSubsectionItemBtn).toBeInTheDocument();
@@ -1672,7 +1642,7 @@ describe('<CourseUnit />', () => {
}); });
await waitFor(() => { await waitFor(() => {
const currentComponentLocationText = screen.getByText( const currentComponentLocationText = getByText(
moveModalMessages.moveModalOutlineItemCurrentComponentLocationText.defaultMessage, moveModalMessages.moveModalOutlineItemCurrentComponentLocationText.defaultMessage,
); );
expect(currentComponentLocationText).toBeInTheDocument(); expect(currentComponentLocationText).toBeInTheDocument();
@@ -1680,7 +1650,9 @@ describe('<CourseUnit />', () => {
}); });
it('should allow move operation and handles it successfully', async () => { it('should allow move operation and handles it successfully', async () => {
render(<RootWrapper />); const {
getByRole,
} = render(<RootWrapper />);
axiosMock axiosMock
.onPatch(postXBlockBaseApiUrl()) .onPatch(postXBlockBaseApiUrl())
@@ -1704,7 +1676,7 @@ describe('<CourseUnit />', () => {
); );
const currentSection = courseOutlineInfoMock.child_info.children[1]; const currentSection = courseOutlineInfoMock.child_info.children[1];
const currentSectionItemBtn = screen.getByRole('button', { const currentSectionItemBtn = getByRole('button', {
name: `${currentSection.display_name} ${moveModalMessages.moveModalOutlineItemCurrentLocationText.defaultMessage} ${moveModalMessages.moveModalOutlineItemViewText.defaultMessage}`, name: `${currentSection.display_name} ${moveModalMessages.moveModalOutlineItemCurrentLocationText.defaultMessage} ${moveModalMessages.moveModalOutlineItemViewText.defaultMessage}`,
}); });
expect(currentSectionItemBtn).toBeInTheDocument(); expect(currentSectionItemBtn).toBeInTheDocument();
@@ -1712,7 +1684,7 @@ describe('<CourseUnit />', () => {
const currentSubsection = currentSection.child_info.children[1]; const currentSubsection = currentSection.child_info.children[1];
await waitFor(() => { await waitFor(() => {
const currentSubsectionItemBtn = screen.getByRole('button', { const currentSubsectionItemBtn = getByRole('button', {
name: `${currentSubsection.display_name} ${moveModalMessages.moveModalOutlineItemViewText.defaultMessage}`, name: `${currentSubsection.display_name} ${moveModalMessages.moveModalOutlineItemViewText.defaultMessage}`,
}); });
expect(currentSubsectionItemBtn).toBeInTheDocument(); expect(currentSubsectionItemBtn).toBeInTheDocument();
@@ -1721,14 +1693,14 @@ describe('<CourseUnit />', () => {
await waitFor(() => { await waitFor(() => {
const currentUnit = currentSubsection.child_info.children[0]; const currentUnit = currentSubsection.child_info.children[0];
const currentUnitItemBtn = screen.getByRole('button', { const currentUnitItemBtn = getByRole('button', {
name: `${currentUnit.display_name} ${moveModalMessages.moveModalOutlineItemViewText.defaultMessage}`, name: `${currentUnit.display_name} ${moveModalMessages.moveModalOutlineItemViewText.defaultMessage}`,
}); });
expect(currentUnitItemBtn).toBeInTheDocument(); expect(currentUnitItemBtn).toBeInTheDocument();
userEvent.click(currentUnitItemBtn); userEvent.click(currentUnitItemBtn);
}); });
const moveModalBtn = screen.getByRole('button', { const moveModalBtn = getByRole('button', {
name: moveModalMessages.moveModalSubmitButton.defaultMessage, name: moveModalMessages.moveModalSubmitButton.defaultMessage,
}); });
expect(moveModalBtn).toBeInTheDocument(); expect(moveModalBtn).toBeInTheDocument();
@@ -1742,7 +1714,10 @@ describe('<CourseUnit />', () => {
}); });
it('should display "Move Confirmation" alert after moving and undo operations', async () => { it('should display "Move Confirmation" alert after moving and undo operations', async () => {
render(<RootWrapper />); const {
queryByRole,
getByText,
} = render(<RootWrapper />);
axiosMock axiosMock
.onPatch(postXBlockBaseApiUrl()) .onPatch(postXBlockBaseApiUrl())
@@ -1759,18 +1734,18 @@ describe('<CourseUnit />', () => {
simulatePostMessageEvent(messageTypes.rollbackMovedXBlock, { locator: requestData.sourceLocator }); simulatePostMessageEvent(messageTypes.rollbackMovedXBlock, { locator: requestData.sourceLocator });
const dismissButton = screen.queryByRole('button', { const dismissButton = queryByRole('button', {
name: /dismiss/i, hidden: true, name: /dismiss/i, hidden: true,
}); });
const undoButton = screen.queryByRole('button', { const undoButton = queryByRole('button', {
name: messages.undoMoveButton.defaultMessage, hidden: true, name: messages.undoMoveButton.defaultMessage, hidden: true,
}); });
const newLocationButton = screen.queryByRole('button', { const newLocationButton = queryByRole('button', {
name: messages.newLocationButton.defaultMessage, hidden: true, name: messages.newLocationButton.defaultMessage, hidden: true,
}); });
expect(screen.getByText(messages.alertMoveSuccessTitle.defaultMessage)).toBeInTheDocument(); expect(getByText(messages.alertMoveSuccessTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(`${requestData.title} has been moved`)).toBeInTheDocument(); expect(getByText(`${requestData.title} has been moved`)).toBeInTheDocument();
expect(dismissButton).toBeInTheDocument(); expect(dismissButton).toBeInTheDocument();
expect(undoButton).toBeInTheDocument(); expect(undoButton).toBeInTheDocument();
expect(newLocationButton).toBeInTheDocument(); expect(newLocationButton).toBeInTheDocument();
@@ -1778,9 +1753,9 @@ describe('<CourseUnit />', () => {
userEvent.click(undoButton); userEvent.click(undoButton);
await waitFor(() => { await waitFor(() => {
expect(screen.getByText(messages.alertMoveCancelTitle.defaultMessage)).toBeInTheDocument(); expect(getByText(messages.alertMoveCancelTitle.defaultMessage)).toBeInTheDocument();
}); });
expect(screen.getByText( expect(getByText(
messages.alertMoveCancelDescription.defaultMessage.replace('{title}', requestData.title), messages.alertMoveCancelDescription.defaultMessage.replace('{title}', requestData.title),
)).toBeInTheDocument(); )).toBeInTheDocument();
expect(dismissButton).toBeInTheDocument(); expect(dismissButton).toBeInTheDocument();
@@ -1789,7 +1764,9 @@ describe('<CourseUnit />', () => {
}); });
it('should navigate to new location by button click', async () => { it('should navigate to new location by button click', async () => {
render(<RootWrapper />); const {
queryByRole,
} = render(<RootWrapper />);
axiosMock axiosMock
.onPatch(postXBlockBaseApiUrl()) .onPatch(postXBlockBaseApiUrl())
@@ -1804,7 +1781,7 @@ describe('<CourseUnit />', () => {
callbackFn: requestData.callbackFn, callbackFn: requestData.callbackFn,
}), store.dispatch); }), store.dispatch);
const newLocationButton = screen.queryByRole('button', { const newLocationButton = queryByRole('button', {
name: messages.newLocationButton.defaultMessage, hidden: true, name: messages.newLocationButton.defaultMessage, hidden: true,
}); });
userEvent.click(newLocationButton); userEvent.click(newLocationButton);
@@ -1817,14 +1794,16 @@ describe('<CourseUnit />', () => {
describe('XBlock restrict access', () => { describe('XBlock restrict access', () => {
it('opens xblock restrict access modal successfully', async () => { it('opens xblock restrict access modal successfully', async () => {
render(<RootWrapper />); const {
getByTitle, getByTestId,
} = render(<RootWrapper />);
const modalSubtitleText = configureModalMessages.restrictAccessTo.defaultMessage; const modalSubtitleText = configureModalMessages.restrictAccessTo.defaultMessage;
const modalCancelBtnText = configureModalMessages.cancelButton.defaultMessage; const modalCancelBtnText = configureModalMessages.cancelButton.defaultMessage;
const modalSaveBtnText = configureModalMessages.saveButton.defaultMessage; const modalSaveBtnText = configureModalMessages.saveButton.defaultMessage;
await waitFor(() => { await waitFor(() => {
const iframe = screen.getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage); const iframe = getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
const usageId = courseVerticalChildrenMock.children[0].block_id; const usageId = courseVerticalChildrenMock.children[0].block_id;
expect(iframe).toBeInTheDocument(); expect(iframe).toBeInTheDocument();
@@ -1834,7 +1813,7 @@ describe('<CourseUnit />', () => {
}); });
await waitFor(() => { await waitFor(() => {
const configureModal = screen.getByTestId('configure-modal'); const configureModal = getByTestId('configure-modal');
expect(within(configureModal).getByText(modalSubtitleText)).toBeInTheDocument(); expect(within(configureModal).getByText(modalSubtitleText)).toBeInTheDocument();
expect(within(configureModal).getByRole('button', { name: modalCancelBtnText })).toBeInTheDocument(); expect(within(configureModal).getByRole('button', { name: modalCancelBtnText })).toBeInTheDocument();
@@ -1843,10 +1822,12 @@ describe('<CourseUnit />', () => {
}); });
it('closes xblock restrict access modal when cancel button is clicked', async () => { it('closes xblock restrict access modal when cancel button is clicked', async () => {
render(<RootWrapper />); const {
getByTitle, queryByTestId, getByTestId,
} = render(<RootWrapper />);
await waitFor(() => { await waitFor(() => {
const iframe = screen.getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage); const iframe = getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
expect(iframe).toBeInTheDocument(); expect(iframe).toBeInTheDocument();
simulatePostMessageEvent(messageTypes.manageXBlockAccess, { simulatePostMessageEvent(messageTypes.manageXBlockAccess, {
usageId: courseVerticalChildrenMock.children[0].block_id, usageId: courseVerticalChildrenMock.children[0].block_id,
@@ -1854,7 +1835,7 @@ describe('<CourseUnit />', () => {
}); });
await waitFor(() => { await waitFor(() => {
const configureModal = screen.getByTestId('configure-modal'); const configureModal = getByTestId('configure-modal');
expect(configureModal).toBeInTheDocument(); expect(configureModal).toBeInTheDocument();
userEvent.click(within(configureModal).getByRole('button', { userEvent.click(within(configureModal).getByRole('button', {
name: configureModalMessages.cancelButton.defaultMessage, name: configureModalMessages.cancelButton.defaultMessage,
@@ -1862,7 +1843,7 @@ describe('<CourseUnit />', () => {
expect(handleConfigureSubmitMock).not.toHaveBeenCalled(); expect(handleConfigureSubmitMock).not.toHaveBeenCalled();
}); });
expect(screen.queryByTestId('configure-modal')).not.toBeInTheDocument(); expect(queryByTestId('configure-modal')).not.toBeInTheDocument();
}); });
it('handles submit xblock restrict access data when save button is clicked', async () => { it('handles submit xblock restrict access data when save button is clicked', async () => {
@@ -1873,13 +1854,15 @@ describe('<CourseUnit />', () => {
}) })
.reply(200, { dummy: 'value' }); .reply(200, { dummy: 'value' });
render(<RootWrapper />); const {
getByTitle, getByRole, getByTestId, queryByTestId,
} = render(<RootWrapper />);
const accessGroupName1 = userPartitionInfoFormatted.selectablePartitions[0].groups[0].name; const accessGroupName1 = userPartitionInfoFormatted.selectablePartitions[0].groups[0].name;
const accessGroupName2 = userPartitionInfoFormatted.selectablePartitions[0].groups[1].name; const accessGroupName2 = userPartitionInfoFormatted.selectablePartitions[0].groups[1].name;
await waitFor(() => { await waitFor(() => {
const iframe = screen.getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage); const iframe = getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
expect(iframe).toBeInTheDocument(); expect(iframe).toBeInTheDocument();
}); });
@@ -1889,13 +1872,13 @@ describe('<CourseUnit />', () => {
}); });
}); });
const configureModal = await waitFor(() => screen.getByTestId('configure-modal')); const configureModal = await waitFor(() => getByTestId('configure-modal'));
expect(configureModal).toBeInTheDocument(); expect(configureModal).toBeInTheDocument();
expect(within(configureModal).queryByText(accessGroupName1)).not.toBeInTheDocument(); expect(within(configureModal).queryByText(accessGroupName1)).not.toBeInTheDocument();
expect(within(configureModal).queryByText(accessGroupName2)).not.toBeInTheDocument(); expect(within(configureModal).queryByText(accessGroupName2)).not.toBeInTheDocument();
const restrictAccessSelect = screen.getByRole('combobox', { const restrictAccessSelect = getByRole('combobox', {
name: configureModalMessages.restrictAccessTo.defaultMessage, name: configureModalMessages.restrictAccessTo.defaultMessage,
}); });
@@ -1925,17 +1908,17 @@ describe('<CourseUnit />', () => {
expect(axiosMock.history.post[0].url).toBe(getXBlockBaseApiUrl(id)); 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 () => { const checkLegacyEditModalOnEditMessage = async () => {
render(<RootWrapper />); const { getByTitle, getByTestId } = render(<RootWrapper />);
await waitFor(() => { await waitFor(() => {
const editButton = screen.getByTestId('header-edit-button'); const editButton = getByTestId('header-edit-button');
expect(editButton).toBeInTheDocument(); expect(editButton).toBeInTheDocument();
const xblocksIframe = screen.getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage); const xblocksIframe = getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
expect(xblocksIframe).toBeInTheDocument(); expect(xblocksIframe).toBeInTheDocument();
userEvent.click(editButton); userEvent.click(editButton);
}); });
@@ -2122,65 +2105,46 @@ describe('<CourseUnit />', () => {
}); });
it('should render split test content page correctly', async () => { it('should render split test content page correctly', async () => {
render(<RootWrapper />); const {
getByText,
getByRole,
queryByRole,
getByTestId,
queryByText,
} = render(<RootWrapper />);
const currentSectionName = courseUnitIndexMock.ancestor_info.ancestors[1].display_name; const currentSectionName = courseUnitIndexMock.ancestor_info.ancestors[1].display_name;
const currentSubSectionName = courseUnitIndexMock.ancestor_info.ancestors[1].display_name; const currentSubSectionName = courseUnitIndexMock.ancestor_info.ancestors[1].display_name;
const 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'; 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(() => { waitFor(() => {
const unitHeaderTitle = screen.getByTestId('unit-header-title'); const unitHeaderTitle = getByTestId('unit-header-title');
expect(screen.getByText(unitDisplayName)).toBeInTheDocument(); expect(getByText(unitDisplayName)).toBeInTheDocument();
expect(within(unitHeaderTitle).getByRole('button', { name: headerTitleMessages.altButtonEdit.defaultMessage })).toBeInTheDocument(); expect(within(unitHeaderTitle).getByRole('button', { name: headerTitleMessages.altButtonEdit.defaultMessage })).toBeInTheDocument();
expect(within(unitHeaderTitle).getByRole('button', { name: headerTitleMessages.altButtonSettings.defaultMessage })).toBeInTheDocument(); expect(within(unitHeaderTitle).getByRole('button', { name: headerTitleMessages.altButtonSettings.defaultMessage })).toBeInTheDocument();
expect(screen.getByRole('button', { name: currentSectionName })).toBeInTheDocument(); expect(getByRole('button', { name: currentSectionName })).toBeInTheDocument();
expect(screen.getByRole('button', { name: currentSubSectionName })).toBeInTheDocument(); expect(getByRole('button', { name: currentSubSectionName })).toBeInTheDocument();
expect(screen.queryByRole('heading', { name: addComponentMessages.title.defaultMessage })).not.toBeInTheDocument(); expect(queryByRole('heading', { name: addComponentMessages.title.defaultMessage })).not.toBeInTheDocument();
expect(screen.queryByRole('button', { name: headerNavigationsMessages.viewLiveButton.defaultMessage })).not.toBeInTheDocument(); expect(queryByRole('button', { name: headerNavigationsMessages.viewLiveButton.defaultMessage })).not.toBeInTheDocument();
expect(screen.queryByRole('button', { name: headerNavigationsMessages.previewButton.defaultMessage })).not.toBeInTheDocument(); expect(queryByRole('button', { name: headerNavigationsMessages.previewButton.defaultMessage })).not.toBeInTheDocument();
expect(screen.queryByRole('heading', { name: /unit tags/i })).not.toBeInTheDocument(); expect(queryByRole('heading', { name: /unit tags/i })).not.toBeInTheDocument();
expect(screen.queryByRole('heading', { name: /unit location/i })).not.toBeInTheDocument(); expect(queryByRole('heading', { name: /unit location/i })).not.toBeInTheDocument();
// Sidebar // Sidebar
const sidebarContent = [ const sidebarContent = [
{ query: screen.queryByRole, type: 'heading', name: sidebarMessages.sidebarSplitTestAddComponentTitle.defaultMessage }, { query: queryByRole, type: 'heading', name: sidebarMessages.sidebarSplitTestAddComponentTitle.defaultMessage },
{ query: screen.queryByText, name: sidebarMessages.sidebarSplitTestSelectComponentType.defaultMessage.replaceAll('{bold_tag}', '') }, { query: queryByText, name: sidebarMessages.sidebarSplitTestSelectComponentType.defaultMessage.replaceAll('{bold_tag}', '') },
{ query: screen.queryByText, name: sidebarMessages.sidebarSplitTestComponentAdded.defaultMessage }, { query: queryByText, name: sidebarMessages.sidebarSplitTestComponentAdded.defaultMessage },
{ query: screen.queryByRole, type: 'heading', name: sidebarMessages.sidebarSplitTestEditComponentTitle.defaultMessage }, { query: queryByRole, type: 'heading', name: sidebarMessages.sidebarSplitTestEditComponentTitle.defaultMessage },
{ { query: queryByText, name: sidebarMessages.sidebarSplitTestEditComponentInstruction.defaultMessage.replaceAll('{bold_tag}', '') },
query: screen.queryByText, { query: queryByRole, type: 'heading', name: sidebarMessages.sidebarSplitTestReorganizeComponentTitle.defaultMessage },
name: sidebarMessages.sidebarSplitTestEditComponentInstruction.defaultMessage { query: queryByText, name: sidebarMessages.sidebarSplitTestReorganizeComponentInstruction.defaultMessage },
.replaceAll('{bold_tag}', ''), { query: queryByText, name: sidebarMessages.sidebarSplitTestReorganizeGroupsInstruction.defaultMessage },
}, { query: queryByRole, type: 'heading', name: sidebarMessages.sidebarSplitTestExperimentComponentTitle.defaultMessage },
{ { query: queryByText, name: sidebarMessages.sidebarSplitTestExperimentComponentInstruction.defaultMessage },
query: screen.queryByRole, { query: queryByRole, type: 'link', name: sidebarMessages.sidebarSplitTestLearnMoreLinkLabel.defaultMessage },
type: 'heading',
name: sidebarMessages.sidebarSplitTestReorganizeComponentTitle.defaultMessage,
},
{
query: screen.queryByText,
name: sidebarMessages.sidebarSplitTestReorganizeComponentInstruction.defaultMessage,
},
{
query: screen.queryByText,
name: sidebarMessages.sidebarSplitTestReorganizeGroupsInstruction.defaultMessage,
},
{
query: screen.queryByRole,
type: 'heading',
name: sidebarMessages.sidebarSplitTestExperimentComponentTitle.defaultMessage,
},
{
query: screen.queryByText,
name: sidebarMessages.sidebarSplitTestExperimentComponentInstruction.defaultMessage,
},
{
query: screen.queryByRole,
type: 'link',
name: sidebarMessages.sidebarSplitTestLearnMoreLinkLabel.defaultMessage,
},
]; ];
sidebarContent.forEach(({ query, type, name }) => { sidebarContent.forEach(({ query, type, name }) => {
@@ -2188,7 +2152,7 @@ describe('<CourseUnit />', () => {
}); });
expect( expect(
screen.queryByRole('link', { name: sidebarMessages.sidebarSplitTestLearnMoreLinkLabel.defaultMessage }), queryByRole('link', { name: sidebarMessages.sidebarSplitTestLearnMoreLinkLabel.defaultMessage }),
).toHaveAttribute('href', helpLinkUrl); ).toHaveAttribute('href', helpLinkUrl);
}); });
}); });
@@ -2201,7 +2165,7 @@ describe('<CourseUnit />', () => {
}); });
it('renders and navigates to the new HTML XBlock editor after xblock duplicating', async () => { it('renders and navigates to the new HTML XBlock editor after xblock duplicating', async () => {
render(<RootWrapper />); const { getByTitle } = render(<RootWrapper />);
const updatedCourseVerticalChildrenMock = JSON.parse(JSON.stringify(courseVerticalChildrenMock)); const updatedCourseVerticalChildrenMock = JSON.parse(JSON.stringify(courseVerticalChildrenMock));
const targetBlockId = updatedCourseVerticalChildrenMock.children[1].block_id; const targetBlockId = updatedCourseVerticalChildrenMock.children[1].block_id;
@@ -2210,17 +2174,6 @@ describe('<CourseUnit />', () => {
? { ...child, block_type: 'html' } ? { ...child, block_type: 'html' }
: child)); : child));
axiosMock
.onGet(getCourseUnitApiUrl(blockId))
.reply(200, courseUnitIndexMock);
axiosMock
.onPost(postXBlockBaseApiUrl({
parent_locator: blockId,
duplicate_source_locator: courseVerticalChildrenMock.children[0].block_id,
}))
.replyOnce(200, { locator: '1234567890' });
axiosMock axiosMock
.onGet(getCourseVerticalChildrenApiUrl(blockId)) .onGet(getCourseVerticalChildrenApiUrl(blockId))
.reply(200, updatedCourseVerticalChildrenMock); .reply(200, updatedCourseVerticalChildrenMock);
@@ -2228,7 +2181,7 @@ describe('<CourseUnit />', () => {
await executeThunk(fetchCourseVerticalChildrenData(blockId), store.dispatch); await executeThunk(fetchCourseVerticalChildrenData(blockId), store.dispatch);
await waitFor(() => { await waitFor(() => {
const iframe = screen.getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage); const iframe = getByTitle(xblockContainerIframeMessages.xblockIframeTitle.defaultMessage);
expect(iframe).toBeInTheDocument(); expect(iframe).toBeInTheDocument();
simulatePostMessageEvent(messageTypes.currentXBlockId, { simulatePostMessageEvent(messageTypes.currentXBlockId, {
id: targetBlockId, id: targetBlockId,
@@ -2242,54 +2195,4 @@ describe('<CourseUnit />', () => {
.toHaveBeenCalledWith(`/course/${courseId}/editor/html/${targetBlockId}`, { replace: true }); .toHaveBeenCalledWith(`/course/${courseId}/editor/html/${targetBlockId}`, { replace: true });
}); });
}); });
it('renders units from libraries with some components read-only', async () => {
setConfig({
...getConfig(),
ENABLE_TAGGING_TAXONOMY_PAGES: 'true',
});
render(<RootWrapper />);
axiosMock
.onGet(getCourseUnitApiUrl(courseId))
.reply(200, {
...courseUnitIndexMock,
upstreamInfo: {
upstreamRef: 'lct:org:lib:unit:unit-1',
upstreamLink: 'some-link',
},
});
await executeThunk(fetchCourseUnitQuery(courseId), store.dispatch);
expect(screen.getByText(/this unit can only be edited from the \./i)).toBeInTheDocument();
// Disable the "Edit" button
const unitHeaderTitle = screen.getByTestId('unit-header-title');
const editButton = within(unitHeaderTitle).getByRole(
'button',
{ name: 'Edit' },
);
expect(editButton).toBeInTheDocument();
expect(editButton).toBeDisabled();
// The "Publish" button should still be enabled
const courseUnitSidebar = screen.getByTestId('course-unit-sidebar');
const publishButton = within(courseUnitSidebar).getByRole(
'button',
{ name: sidebarMessages.actionButtonPublishTitle.defaultMessage },
);
expect(publishButton).toBeInTheDocument();
expect(publishButton).toBeEnabled();
// Disable the "Manage Tags" button
const manageTagsButton = screen.getByRole(
'button',
{ name: tagsDrawerMessages.manageTagsButton.defaultMessage },
);
expect(manageTagsButton).toBeInTheDocument();
expect(manageTagsButton).toBeDisabled();
// Does not render the "Add Components" section
expect(screen.queryByText(addComponentMessages.title.defaultMessage)).not.toBeInTheDocument();
});
}); });

View File

@@ -1120,7 +1120,4 @@ module.exports = {
has_partition_group_components: false, has_partition_group_components: false,
release_date_from: 'Section "Example Week 1: Getting Started"', release_date_from: 'Section "Example Week 1: Getting Started"',
staff_lock_from: null, staff_lock_from: null,
upstreamInfo: {
upstreamLink: undefined,
},
}; };

View File

@@ -1,14 +1,13 @@
import { useCallback, useState } from 'react'; import { useCallback, useState } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import { getConfig } from '@edx/frontend-platform'; import { useNavigate } from 'react-router-dom';
import { useIntl, FormattedMessage } from '@edx/frontend-platform/i18n'; import { useIntl, FormattedMessage } from '@edx/frontend-platform/i18n';
import { import {
ActionRow, Button, StandardModal, useToggle, ActionRow, Button, StandardModal, useToggle,
} from '@openedx/paragon'; } from '@openedx/paragon';
import { getCourseSectionVertical } from '../data/selectors'; import { getCourseSectionVertical } from '../data/selectors';
import { getWaffleFlags } from '../../data/selectors';
import { COMPONENT_TYPES } from '../../generic/block-type-utils/constants'; import { COMPONENT_TYPES } from '../../generic/block-type-utils/constants';
import ComponentModalView from './add-component-modals/ComponentModalView'; import ComponentModalView from './add-component-modals/ComponentModalView';
import AddComponentButton from './add-component-btn'; import AddComponentButton from './add-component-btn';
@@ -17,8 +16,6 @@ import { ComponentPicker } from '../../library-authoring/component-picker';
import { messageTypes } from '../constants'; import { messageTypes } from '../constants';
import { useIframe } from '../../generic/hooks/context/hooks'; import { useIframe } from '../../generic/hooks/context/hooks';
import { useEventListener } from '../../generic/hooks'; import { useEventListener } from '../../generic/hooks';
import VideoSelectorPage from '../../editors/VideoSelectorPage';
import EditorPage from '../../editors/EditorPage';
const AddComponent = ({ const AddComponent = ({
parentLocator, parentLocator,
@@ -27,6 +24,7 @@ const AddComponent = ({
addComponentTemplateData, addComponentTemplateData,
handleCreateNewCourseXBlock, handleCreateNewCourseXBlock,
}) => { }) => {
const navigate = useNavigate();
const intl = useIntl(); const intl = useIntl();
const [isOpenAdvanced, openAdvanced, closeAdvanced] = useToggle(false); const [isOpenAdvanced, openAdvanced, closeAdvanced] = useToggle(false);
const [isOpenHtml, openHtml, closeHtml] = useToggle(false); const [isOpenHtml, openHtml, closeHtml] = useToggle(false);
@@ -34,17 +32,10 @@ const AddComponent = ({
const { componentTemplates = {} } = useSelector(getCourseSectionVertical); const { componentTemplates = {} } = useSelector(getCourseSectionVertical);
const blockId = addComponentTemplateData.parentLocator || parentLocator; const blockId = addComponentTemplateData.parentLocator || parentLocator;
const [isAddLibraryContentModalOpen, showAddLibraryContentModal, closeAddLibraryContentModal] = useToggle(); const [isAddLibraryContentModalOpen, showAddLibraryContentModal, closeAddLibraryContentModal] = useToggle();
const [isVideoSelectorModalOpen, showVideoSelectorModal, closeVideoSelectorModal] = useToggle();
const [isXBlockEditorModalOpen, showXBlockEditorModal, closeXBlockEditorModal] = useToggle();
const [blockType, setBlockType] = useState(null);
const [courseId, setCourseId] = useState(null);
const [newBlockId, setNewBlockId] = useState(null);
const [isSelectLibraryContentModalOpen, showSelectLibraryContentModal, closeSelectLibraryContentModal] = useToggle(); const [isSelectLibraryContentModalOpen, showSelectLibraryContentModal, closeSelectLibraryContentModal] = useToggle();
const [selectedComponents, setSelectedComponents] = useState([]); const [selectedComponents, setSelectedComponents] = useState([]);
const [usageId, setUsageId] = useState(null); const [usageId, setUsageId] = useState(null);
const { sendMessageToIframe } = useIframe(); const { sendMessageToIframe } = useIframe();
const { useVideoGalleryFlow, useReactMarkdownEditor } = useSelector(getWaffleFlags);
const receiveMessage = useCallback(({ data: { type, payload } }) => { const receiveMessage = useCallback(({ data: { type, payload } }) => {
if (type === messageTypes.showMultipleComponentPicker) { if (type === messageTypes.showMultipleComponentPicker) {
@@ -63,12 +54,6 @@ const AddComponent = ({
closeSelectLibraryContentModal(); closeSelectLibraryContentModal();
}, [selectedComponents]); }, [selectedComponents]);
const onXBlockSave = useCallback(/* istanbul ignore next */ () => {
closeXBlockEditorModal();
closeVideoSelectorModal();
sendMessageToIframe(messageTypes.refreshXBlock, null);
}, [closeXBlockEditorModal, closeVideoSelectorModal, sendMessageToIframe]);
const handleLibraryV2Selection = useCallback((selection) => { const handleLibraryV2Selection = useCallback((selection) => {
handleCreateNewCourseXBlock({ handleCreateNewCourseXBlock({
type: COMPONENT_TYPES.libraryV2, type: COMPONENT_TYPES.libraryV2,
@@ -86,27 +71,11 @@ const AddComponent = ({
handleCreateNewCourseXBlock({ type, parentLocator: blockId }); handleCreateNewCourseXBlock({ type, parentLocator: blockId });
break; break;
case COMPONENT_TYPES.problem: case COMPONENT_TYPES.problem:
handleCreateNewCourseXBlock({ type, parentLocator: blockId }, ({ courseKey, locator }) => {
setCourseId(courseKey);
setBlockType(type);
setNewBlockId(locator);
showXBlockEditorModal();
});
break;
case COMPONENT_TYPES.video: case COMPONENT_TYPES.video:
handleCreateNewCourseXBlock( handleCreateNewCourseXBlock({ type, parentLocator: blockId }, ({ courseKey, locator }) => {
{ type, parentLocator: blockId }, localStorage.setItem('createXBlockLastYPosition', window.scrollY);
/* istanbul ignore next */ ({ courseKey, locator }) => { navigate(`/course/${courseKey}/editor/${type}/${locator}`);
setCourseId(courseKey); });
setBlockType(type);
setNewBlockId(locator);
if (useVideoGalleryFlow) {
showVideoSelectorModal();
} else {
showXBlockEditorModal();
}
},
);
break; break;
// TODO: The library functional will be a bit different of current legacy (CMS) // TODO: The library functional will be a bit different of current legacy (CMS)
// behaviour and this ticket is on hold (blocked by other development team). // behaviour and this ticket is on hold (blocked by other development team).
@@ -130,11 +99,9 @@ const AddComponent = ({
type, type,
boilerplate: moduleName, boilerplate: moduleName,
parentLocator: blockId, parentLocator: blockId,
}, /* istanbul ignore next */ ({ courseKey, locator }) => { }, ({ courseKey, locator }) => {
setCourseId(courseKey); localStorage.setItem('createXBlockLastYPosition', window.scrollY);
setBlockType(type); navigate(`/course/${courseKey}/editor/html/${locator}`);
setNewBlockId(locator);
showXBlockEditorModal();
}); });
break; break;
default: default:
@@ -234,38 +201,6 @@ const AddComponent = ({
onChangeComponentSelection={setSelectedComponents} onChangeComponentSelection={setSelectedComponents}
/> />
</StandardModal> </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> </div>
); );
} }
@@ -273,6 +208,10 @@ const AddComponent = ({
return null; return null;
}; };
AddComponent.defaultProps = {
addComponentTemplateData: {},
};
AddComponent.propTypes = { AddComponent.propTypes = {
isSplitTestType: PropTypes.bool.isRequired, isSplitTestType: PropTypes.bool.isRequired,
isUnitVerticalType: PropTypes.bool.isRequired, isUnitVerticalType: PropTypes.bool.isRequired,

View File

@@ -31,11 +31,6 @@ const messages = defineMessages({
defaultMessage: 'Add selected components', defaultMessage: 'Add selected components',
description: 'Problem bank component add button text.', description: 'Problem bank component add button text.',
}, },
videoPickerModalTitle: {
id: 'course-authoring.course-unit.modal.video-title.text',
defaultMessage: 'Select video',
description: 'Video picker modal title.',
},
modalContainerTitle: { modalContainerTitle: {
id: 'course-authoring.course-unit.modal.container.title', id: 'course-authoring.course-unit.modal.container.title',
defaultMessage: 'Add {componentTitle} component', defaultMessage: 'Add {componentTitle} component',

View File

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

View File

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

View File

@@ -3,7 +3,7 @@ import { camelCaseObject, getConfig } from '@edx/frontend-platform';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import { PUBLISH_TYPES } from '../constants'; import { PUBLISH_TYPES } from '../constants';
import { isUnitReadOnly, normalizeCourseSectionVerticalData, updateXBlockBlockIdToId } from './utils'; import { normalizeCourseSectionVerticalData, updateXBlockBlockIdToId } from './utils';
const getStudioBaseUrl = () => getConfig().STUDIO_BASE_URL; const getStudioBaseUrl = () => getConfig().STUDIO_BASE_URL;
@@ -24,9 +24,7 @@ export async function getCourseUnitData(unitId) {
const { data } = await getAuthenticatedHttpClient() const { data } = await getAuthenticatedHttpClient()
.get(getCourseUnitApiUrl(unitId)); .get(getCourseUnitApiUrl(unitId));
const result = camelCaseObject(data); return camelCaseObject(data);
result.readOnly = isUnitReadOnly(result);
return result;
} }
/** /**

View File

@@ -16,7 +16,7 @@ export const getCourseVerticalChildren = (state) => state.courseUnit.courseVerti
export const getCourseOutlineInfo = (state) => state.courseUnit.courseOutlineInfo; export const getCourseOutlineInfo = (state) => state.courseUnit.courseOutlineInfo;
export const getCourseOutlineInfoLoadingStatus = (state) => state.courseUnit.courseOutlineInfoLoadingStatus; export const getCourseOutlineInfoLoadingStatus = (state) => state.courseUnit.courseOutlineInfoLoadingStatus;
export const getMovedXBlockParams = (state) => state.courseUnit.movedXBlockParams; export const getMovedXBlockParams = (state) => state.courseUnit.movedXBlockParams;
export const getLoadingStatuses = (state) => state.courseUnit.loadingStatus; const getLoadingStatuses = (state) => state.courseUnit.loadingStatus;
export const getIsLoading = createSelector( export const getIsLoading = createSelector(
[getLoadingStatuses], [getLoadingStatuses],
loadingStatus => Object.values(loadingStatus) loadingStatus => Object.values(loadingStatus)

View File

@@ -46,7 +46,6 @@ export function fetchCourseUnitQuery(courseId) {
try { try {
const courseUnit = await getCourseUnitData(courseId); const courseUnit = await getCourseUnitData(courseId);
dispatch(fetchCourseItemSuccess(courseUnit)); dispatch(fetchCourseItemSuccess(courseUnit));
dispatch(updateLoadingCourseUnitStatus({ status: RequestStatus.SUCCESSFUL })); dispatch(updateLoadingCourseUnitStatus({ status: RequestStatus.SUCCESSFUL }));
return true; return true;
@@ -261,8 +260,6 @@ export function duplicateUnitItemQuery(itemId, xblockId, callback) {
callback(courseKey, locator); callback(courseKey, locator);
const courseUnit = await getCourseUnitData(itemId); const courseUnit = await getCourseUnitData(itemId);
dispatch(fetchCourseItemSuccess(courseUnit)); dispatch(fetchCourseItemSuccess(courseUnit));
const courseVerticalChildrenData = await getCourseVerticalChildren(itemId);
dispatch(updateCourseVerticalChildren(courseVerticalChildrenData));
dispatch(hideProcessingNotification()); dispatch(hideProcessingNotification());
dispatch(updateSavingStatus({ status: RequestStatus.SUCCESSFUL })); dispatch(updateSavingStatus({ status: RequestStatus.SUCCESSFUL }));
} catch (error) { } catch (error) {

View File

@@ -84,15 +84,3 @@ export const updateXBlockBlockIdToId = (data) => {
return updatedData; 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:')
);

View File

@@ -34,8 +34,6 @@ const HeaderTitle = ({
COURSE_BLOCK_NAMES.component.id, COURSE_BLOCK_NAMES.component.id,
].includes(currentItemData.category); ].includes(currentItemData.category);
const readOnly = !!currentItemData.readOnly;
const onConfigureSubmit = (...arg) => { const onConfigureSubmit = (...arg) => {
handleConfigureSubmit(currentItemData.id, ...arg, closeConfigureModal); handleConfigureSubmit(currentItemData.id, ...arg, closeConfigureModal);
}; };
@@ -82,7 +80,6 @@ const HeaderTitle = ({
className="ml-1 flex-shrink-0" className="ml-1 flex-shrink-0"
iconAs={EditIcon} iconAs={EditIcon}
onClick={handleTitleEdit} onClick={handleTitleEdit}
disabled={readOnly}
/> />
<IconButton <IconButton
alt={intl.formatMessage(messages.altButtonSettings)} alt={intl.formatMessage(messages.altButtonSettings)}
@@ -105,8 +102,6 @@ const HeaderTitle = ({
); );
}; };
export default HeaderTitle;
HeaderTitle.propTypes = { HeaderTitle.propTypes = {
unitTitle: PropTypes.string.isRequired, unitTitle: PropTypes.string.isRequired,
isTitleEditFormOpen: PropTypes.bool.isRequired, isTitleEditFormOpen: PropTypes.bool.isRequired,
@@ -114,3 +109,5 @@ HeaderTitle.propTypes = {
handleTitleEditSubmit: PropTypes.func.isRequired, handleTitleEditSubmit: PropTypes.func.isRequired,
handleConfigureSubmit: PropTypes.func.isRequired, handleConfigureSubmit: PropTypes.func.isRequired,
}; };
export default HeaderTitle;

View File

@@ -72,27 +72,8 @@ describe('<HeaderTitle />', () => {
expect(getByRole('textbox', { name: messages.ariaLabelButtonEdit.defaultMessage })).toBeInTheDocument(); expect(getByRole('textbox', { name: messages.ariaLabelButtonEdit.defaultMessage })).toBeInTheDocument();
expect(getByRole('textbox', { name: messages.ariaLabelButtonEdit.defaultMessage })).toHaveValue(unitTitle); expect(getByRole('textbox', { name: messages.ariaLabelButtonEdit.defaultMessage })).toHaveValue(unitTitle);
expect(getByRole('button', { name: messages.altButtonEdit.defaultMessage })).toBeEnabled(); expect(getByRole('button', { name: messages.altButtonEdit.defaultMessage })).toBeInTheDocument();
expect(getByRole('button', { name: messages.altButtonSettings.defaultMessage })).toBeEnabled(); expect(getByRole('button', { name: messages.altButtonSettings.defaultMessage })).toBeInTheDocument();
});
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(getCourseUnitApiUrl(blockId))
.reply(200, {
...courseUnitIndexMock,
upstreamInfo: {
upstreamRef: 'lct:org:lib:unit:unit-1',
},
});
await executeThunk(fetchCourseUnitQuery(blockId), store.dispatch);
const { getByRole } = renderComponent();
expect(getByRole('button', { name: messages.altButtonEdit.defaultMessage })).toBeDisabled();
expect(getByRole('button', { name: messages.altButtonSettings.defaultMessage })).toBeEnabled();
}); });
it('calls toggle edit title form by clicking on Edit button', () => { it('calls toggle edit title form by clicking on Edit button', () => {

View File

@@ -35,7 +35,6 @@ import {
getSavingStatus, getSavingStatus,
getSequenceStatus, getSequenceStatus,
getStaticFileNotices, getStaticFileNotices,
getLoadingStatuses,
} from './data/selectors'; } from './data/selectors';
import { import {
changeEditTitleFormOpen, changeEditTitleFormOpen,
@@ -52,7 +51,6 @@ export const useCourseUnit = ({ courseId, blockId }) => {
const [isMoveModalOpen, openMoveModal, closeMoveModal] = useToggle(false); const [isMoveModalOpen, openMoveModal, closeMoveModal] = useToggle(false);
const courseUnit = useSelector(getCourseUnitData); const courseUnit = useSelector(getCourseUnitData);
const courseUnitLoadingStatus = useSelector(getLoadingStatuses);
const savingStatus = useSelector(getSavingStatus); const savingStatus = useSelector(getSavingStatus);
const isLoading = useSelector(getIsLoading); const isLoading = useSelector(getIsLoading);
const errorMessage = useSelector(getErrorMessage); const errorMessage = useSelector(getErrorMessage);
@@ -217,28 +215,9 @@ export const useCourseUnit = ({ courseId, blockId }) => {
} }
}, [isMoveModalOpen]); }, [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(fetchCourseSectionVerticalData(blockId, sequenceId));
dispatch(fetchCourseVerticalChildrenData(blockId, isSplitTestType));
localStorage.removeItem(event.key);
}
};
window.addEventListener('storage', handlePageRefreshUsingStorage);
return () => {
window.removeEventListener('storage', handlePageRefreshUsingStorage);
};
}, [blockId, sequenceId, isSplitTestType]);
return { return {
sequenceId, sequenceId,
courseUnit, courseUnit,
courseUnitLoadingStatus,
unitTitle, unitTitle,
unitCategory, unitCategory,
errorMessage, errorMessage,

View File

@@ -43,16 +43,6 @@ const messages = defineMessages({
defaultMessage: 'Take me to the new location', 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', 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; export default messages;

View File

@@ -12,12 +12,13 @@ import IframePreviewLibraryXBlockChanges, { LibraryChangesMessageData } from '.'
import { messageTypes } from '../constants'; import { messageTypes } from '../constants';
import { libraryBlockChangesUrl } from '../data/api'; import { libraryBlockChangesUrl } from '../data/api';
import { ToastActionData } from '../../generic/toast-context'; import { ToastActionData } from '../../generic/toast-context';
import { getLibraryBlockMetadataUrl } from '../../library-authoring/data/api';
const usageKey = 'some-id'; const usageKey = 'some-id';
const defaultEventData: LibraryChangesMessageData = { const defaultEventData: LibraryChangesMessageData = {
displayName: 'Test block', displayName: 'Test block',
downstreamBlockId: usageKey, downstreamBlockId: usageKey,
upstreamBlockId: 'lct:org:lib1:unit:1', upstreamBlockId: 'some-lib-id',
upstreamBlockVersionSynced: 1, upstreamBlockVersionSynced: 1,
isVertical: false, isVertical: false,
}; };
@@ -65,7 +66,7 @@ describe('<IframePreviewLibraryXBlockChanges />', () => {
expect(await screen.findByRole('tab', { name: 'Old version' })).toBeInTheDocument(); 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: '' }); render({ ...defaultEventData, isVertical: true, displayName: '' });
expect(await screen.findByText('Preview changes: Unit')).toBeInTheDocument(); expect(await screen.findByText('Preview changes: Unit')).toBeInTheDocument();
@@ -77,6 +78,15 @@ describe('<IframePreviewLibraryXBlockChanges />', () => {
expect(await screen.findByText('Preview changes: Component')).toBeInTheDocument(); 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 () => { it('accept changes works', async () => {
axiosMock.onPost(libraryBlockChangesUrl(usageKey)).reply(200, {}); axiosMock.onPost(libraryBlockChangesUrl(usageKey)).reply(200, {});
render(); render();
@@ -85,10 +95,7 @@ describe('<IframePreviewLibraryXBlockChanges />', () => {
const acceptBtn = await screen.findByRole('button', { name: 'Accept changes' }); const acceptBtn = await screen.findByRole('button', { name: 'Accept changes' });
userEvent.click(acceptBtn); userEvent.click(acceptBtn);
await waitFor(() => { await waitFor(() => {
expect(mockSendMessageToIframe).toHaveBeenCalledWith( expect(mockSendMessageToIframe).toHaveBeenCalledWith(messageTypes.refreshXBlock, null);
messageTypes.completeXBlockEditing,
{ locator: usageKey },
);
expect(axiosMock.history.post.length).toEqual(1); expect(axiosMock.history.post.length).toEqual(1);
expect(axiosMock.history.post[0].url).toEqual(libraryBlockChangesUrl(usageKey)); expect(axiosMock.history.post[0].url).toEqual(libraryBlockChangesUrl(usageKey));
}); });
@@ -103,6 +110,7 @@ describe('<IframePreviewLibraryXBlockChanges />', () => {
const acceptBtn = await screen.findByRole('button', { name: 'Accept changes' }); const acceptBtn = await screen.findByRole('button', { name: 'Accept changes' });
userEvent.click(acceptBtn); userEvent.click(acceptBtn);
await waitFor(() => { await waitFor(() => {
expect(mockSendMessageToIframe).not.toHaveBeenCalledWith(messageTypes.refreshXBlock, null);
expect(axiosMock.history.post.length).toEqual(1); expect(axiosMock.history.post.length).toEqual(1);
expect(axiosMock.history.post[0].url).toEqual(libraryBlockChangesUrl(usageKey)); expect(axiosMock.history.post[0].url).toEqual(libraryBlockChangesUrl(usageKey));
}); });
@@ -120,10 +128,7 @@ describe('<IframePreviewLibraryXBlockChanges />', () => {
const ignoreConfirmBtn = await screen.findByRole('button', { name: 'Ignore' }); const ignoreConfirmBtn = await screen.findByRole('button', { name: 'Ignore' });
userEvent.click(ignoreConfirmBtn); userEvent.click(ignoreConfirmBtn);
await waitFor(() => { await waitFor(() => {
expect(mockSendMessageToIframe).toHaveBeenCalledWith( expect(mockSendMessageToIframe).toHaveBeenCalledWith(messageTypes.refreshXBlock, null);
messageTypes.completeXBlockEditing,
{ locator: usageKey },
);
expect(axiosMock.history.delete.length).toEqual(1); expect(axiosMock.history.delete.length).toEqual(1);
expect(axiosMock.history.delete[0].url).toEqual(libraryBlockChangesUrl(usageKey)); expect(axiosMock.history.delete[0].url).toEqual(libraryBlockChangesUrl(usageKey));
}); });

View File

@@ -1,21 +1,20 @@
import { useCallback, useContext, useState } from 'react'; import React, { useCallback, useContext, useState } from 'react';
import { import {
ActionRow, Button, ModalDialog, useToggle, ActionRow, Button, ModalDialog, useToggle,
} from '@openedx/paragon'; } from '@openedx/paragon';
import { Warning } from '@openedx/paragon/icons';
import { useIntl, FormattedMessage } from '@edx/frontend-platform/i18n'; import { useIntl, FormattedMessage } from '@edx/frontend-platform/i18n';
import { useEventListener } from '../../generic/hooks'; import { useEventListener } from '../../generic/hooks';
import { messageTypes } from '../constants'; import { messageTypes } from '../constants';
import CompareChangesWidget from '../../library-authoring/component-comparison/CompareChangesWidget'; import CompareChangesWidget from '../../library-authoring/component-comparison/CompareChangesWidget';
import { useAcceptLibraryBlockChanges, useIgnoreLibraryBlockChanges } from '../data/apiHooks'; import { useAcceptLibraryBlockChanges, useIgnoreLibraryBlockChanges } from '../data/apiHooks';
import AlertMessage from '../../generic/alert-message';
import { useIframe } from '../../generic/hooks/context/hooks'; import { useIframe } from '../../generic/hooks/context/hooks';
import DeleteModal from '../../generic/delete-modal/DeleteModal'; import DeleteModal from '../../generic/delete-modal/DeleteModal';
import messages from './messages'; import messages from './messages';
import { ToastContext } from '../../generic/toast-context'; import { ToastContext } from '../../generic/toast-context';
import LoadingButton from '../../generic/loading-button'; import LoadingButton from '../../generic/loading-button';
import Loading from '../../generic/Loading'; import Loading from '../../generic/Loading';
import { useLibraryBlockMetadata } from '../../library-authoring/data/apiHooks';
export interface LibraryChangesMessageData { export interface LibraryChangesMessageData {
displayName: string, displayName: string,
@@ -26,10 +25,11 @@ export interface LibraryChangesMessageData {
} }
export interface PreviewLibraryXBlockChangesProps { export interface PreviewLibraryXBlockChangesProps {
blockData: LibraryChangesMessageData, blockData?: LibraryChangesMessageData,
isModalOpen: boolean, isModalOpen: boolean,
closeModal: () => void, closeModal: () => void,
postChange: (accept: boolean) => void, postChange: (accept: boolean) => void,
alertNode?: React.ReactNode,
} }
/** /**
@@ -41,16 +41,34 @@ export const PreviewLibraryXBlockChanges = ({
isModalOpen, isModalOpen,
closeModal, closeModal,
postChange, postChange,
alertNode,
}: PreviewLibraryXBlockChangesProps) => { }: PreviewLibraryXBlockChangesProps) => {
const { showToast } = useContext(ToastContext); const { showToast } = useContext(ToastContext);
const intl = useIntl(); const intl = useIntl();
// ignore changes confirmation modal toggle. // ignore changes confirmation modal toggle.
const [isConfirmModalOpen, openConfirmModal, closeConfirmModal] = useToggle(false); const [isConfirmModalOpen, openConfirmModal, closeConfirmModal] = useToggle(false);
const { data: componentMetadata } = useLibraryBlockMetadata(blockData?.upstreamBlockId);
const acceptChangesMutation = useAcceptLibraryBlockChanges(); const acceptChangesMutation = useAcceptLibraryBlockChanges();
const ignoreChangesMutation = useIgnoreLibraryBlockChanges(); 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(() => { const getBody = useCallback(() => {
if (!blockData) { if (!blockData) {
return <Loading />; return <Loading />;
@@ -60,7 +78,6 @@ export const PreviewLibraryXBlockChanges = ({
usageKey={blockData.upstreamBlockId} usageKey={blockData.upstreamBlockId}
oldVersion={blockData.upstreamBlockVersionSynced || 'published'} oldVersion={blockData.upstreamBlockVersionSynced || 'published'}
newVersion="published" newVersion="published"
isContainer={blockData.isVertical}
/> />
); );
}, [blockData]); }, [blockData]);
@@ -84,21 +101,12 @@ export const PreviewLibraryXBlockChanges = ({
} }
}, [blockData]); }, [blockData]);
const defaultTitle = intl.formatMessage(
blockData.isVertical
? messages.defaultUnitTitle
: messages.defaultComponentTitle,
);
const title = blockData.displayName
? intl.formatMessage(messages.title, { blockTitle: blockData?.displayName })
: defaultTitle;
return ( return (
<ModalDialog <ModalDialog
isOpen={isModalOpen} isOpen={isModalOpen}
onClose={closeModal} onClose={closeModal}
size="xl" size="xl"
title={title} title={getTitle()}
className="lib-preview-xblock-changes-modal" className="lib-preview-xblock-changes-modal"
hasCloseButton hasCloseButton
isFullscreenOnMobile isFullscreenOnMobile
@@ -106,16 +114,11 @@ export const PreviewLibraryXBlockChanges = ({
> >
<ModalDialog.Header> <ModalDialog.Header>
<ModalDialog.Title> <ModalDialog.Title>
{title} {getTitle()}
</ModalDialog.Title> </ModalDialog.Title>
</ModalDialog.Header> </ModalDialog.Header>
<ModalDialog.Body> <ModalDialog.Body>
<AlertMessage {alertNode}
show
variant="warning"
icon={Warning}
title={intl.formatMessage(messages.olderVersionPreviewAlert)}
/>
{getBody()} {getBody()}
</ModalDialog.Body> </ModalDialog.Body>
<ModalDialog.Footer> <ModalDialog.Footer>
@@ -176,18 +179,12 @@ const IframePreviewLibraryXBlockChanges = () => {
useEventListener('message', receiveMessage); useEventListener('message', receiveMessage);
if (!blockData) {
return null;
}
const blockPayload = { locator: blockData.downstreamBlockId };
return ( return (
<PreviewLibraryXBlockChanges <PreviewLibraryXBlockChanges
blockData={blockData} blockData={blockData}
isModalOpen={isModalOpen} isModalOpen={isModalOpen}
closeModal={closeModal} closeModal={closeModal}
postChange={() => sendMessageToIframe(messageTypes.completeXBlockEditing, blockPayload)} postChange={() => sendMessageToIframe(messageTypes.refreshXBlock, null)}
/> />
); );
}; };

View File

@@ -6,6 +6,11 @@ const messages = defineMessages({
defaultMessage: 'Preview changes: {blockTitle}', defaultMessage: 'Preview changes: {blockTitle}',
description: 'Preview changes modal title text', 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: { defaultUnitTitle: {
id: 'authoring.course-unit.preview-changes.modal-default-unit-title', id: 'authoring.course-unit.preview-changes.modal-default-unit-title',
defaultMessage: 'Preview changes: Unit', defaultMessage: 'Preview changes: Unit',
@@ -56,11 +61,6 @@ const messages = defineMessages({
defaultMessage: 'Ignore', defaultMessage: 'Ignore',
description: 'Preview changes confirmation dialog confirm button text when user clicks on ignore changes.', 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; export default messages;

View File

@@ -1,3 +1,4 @@
import PropTypes from 'prop-types';
import { useDispatch, useSelector } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import { useToggle } from '@openedx/paragon'; import { useToggle } from '@openedx/paragon';
import { InfoOutline as InfoOutlineIcon } from '@openedx/paragon/icons'; import { InfoOutline as InfoOutlineIcon } from '@openedx/paragon/icons';
@@ -11,19 +12,14 @@ import { getCourseUnitData } from '../data/selectors';
import messages from './messages'; import messages from './messages';
import ModalNotification from '../../generic/modal-notification'; import ModalNotification from '../../generic/modal-notification';
interface PublishControlsProps { const PublishControls = ({ blockId }) => {
blockId?: string,
}
const PublishControls = ({ blockId }: PublishControlsProps) => {
const unitData = useSelector(getCourseUnitData);
const { const {
title, title,
locationId, locationId,
releaseLabel, releaseLabel,
visibilityState, visibilityState,
visibleToStaffOnly, visibleToStaffOnly,
} = useCourseUnitData(unitData); } = useCourseUnitData(useSelector(getCourseUnitData));
const intl = useIntl(); const intl = useIntl();
const { sendMessageToIframe } = useIframe(); const { sendMessageToIframe } = useIframe();
@@ -94,4 +90,12 @@ const PublishControls = ({ blockId }: PublishControlsProps) => {
); );
}; };
PublishControls.propTypes = {
blockId: PropTypes.string,
};
PublishControls.defaultProps = {
blockId: null,
};
export default PublishControls; export default PublishControls;

View File

@@ -1,3 +1,4 @@
import PropTypes from 'prop-types';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import { Button } from '@openedx/paragon'; import { Button } from '@openedx/paragon';
import { useIntl } from '@edx/frontend-platform/i18n'; import { useIntl } from '@edx/frontend-platform/i18n';
@@ -7,15 +8,7 @@ import { getCanEdit, getCourseUnitData } from '../../../data/selectors';
import { useClipboard } from '../../../../generic/clipboard'; import { useClipboard } from '../../../../generic/clipboard';
import messages from '../../messages'; import messages from '../../messages';
interface ActionButtonsProps { const ActionButtons = ({ openDiscardModal, handlePublishing }) => {
openDiscardModal: () => void,
handlePublishing: () => void,
}
const ActionButtons = ({
openDiscardModal,
handlePublishing,
}: ActionButtonsProps) => {
const intl = useIntl(); const intl = useIntl();
const { const {
id, id,
@@ -29,12 +22,7 @@ const ActionButtons = ({
return ( return (
<> <>
{(!published || hasChanges) && ( {(!published || hasChanges) && (
<Button <Button size="sm" className="mt-3.5" variant="outline-primary" onClick={handlePublishing}>
size="sm"
className="mt-3.5"
variant="outline-primary"
onClick={handlePublishing}
>
{intl.formatMessage(messages.actionButtonPublishTitle)} {intl.formatMessage(messages.actionButtonPublishTitle)}
</Button> </Button>
)} )}
@@ -64,4 +52,9 @@ const ActionButtons = ({
); );
}; };
ActionButtons.propTypes = {
openDiscardModal: PropTypes.func.isRequired,
handlePublishing: PropTypes.func.isRequired,
};
export default ActionButtons; export default ActionButtons;

View File

@@ -1,3 +1,4 @@
import PropTypes from 'prop-types';
import { useDispatch, useSelector } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import { Form } from '@openedx/paragon'; import { Form } from '@openedx/paragon';
import { useIntl } from '@edx/frontend-platform/i18n'; import { useIntl } from '@edx/frontend-platform/i18n';
@@ -9,15 +10,7 @@ import { PUBLISH_TYPES } from '../../../constants';
import { getVisibilityTitle } from '../../utils'; import { getVisibilityTitle } from '../../utils';
import messages from '../../messages'; import messages from '../../messages';
interface UnitVisibilityInfoProps { const UnitVisibilityInfo = ({ openVisibleModal, visibleToStaffOnly }) => {
openVisibleModal: () => void,
visibleToStaffOnly: boolean,
}
const UnitVisibilityInfo = ({
openVisibleModal,
visibleToStaffOnly,
}: UnitVisibilityInfoProps) => {
const intl = useIntl(); const intl = useIntl();
const { blockId } = useParams(); const { blockId } = useParams();
const dispatch = useDispatch(); const dispatch = useDispatch();
@@ -66,4 +59,9 @@ const UnitVisibilityInfo = ({
); );
}; };
UnitVisibilityInfo.propTypes = {
openVisibleModal: PropTypes.func.isRequired,
visibleToStaffOnly: PropTypes.bool.isRequired,
};
export default UnitVisibilityInfo; export default UnitVisibilityInfo;

View File

@@ -1,3 +1,4 @@
import PropTypes from 'prop-types';
import { Card, Stack } from '@openedx/paragon'; import { Card, Stack } from '@openedx/paragon';
import { useIntl } from '@edx/frontend-platform/i18n'; import { useIntl } from '@edx/frontend-platform/i18n';
@@ -5,23 +6,14 @@ import messages from '../../messages';
import UnitVisibilityInfo from './UnitVisibilityInfo'; import UnitVisibilityInfo from './UnitVisibilityInfo';
import ActionButtons from './ActionButtons'; import ActionButtons from './ActionButtons';
interface SidebarFooterProps {
locationId?: string,
displayUnitLocation?: boolean,
openDiscardModal: () => void,
openVisibleModal: () => void,
handlePublishing: () => void,
visibleToStaffOnly: boolean,
}
const SidebarFooter = ({ const SidebarFooter = ({
locationId, locationId,
openVisibleModal, openVisibleModal,
handlePublishing, handlePublishing,
openDiscardModal, openDiscardModal,
visibleToStaffOnly, visibleToStaffOnly,
displayUnitLocation = false, displayUnitLocation,
}: SidebarFooterProps) => { }) => {
const intl = useIntl(); const intl = useIntl();
return ( 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; export default SidebarFooter;

View File

@@ -99,4 +99,4 @@ export const getIconVariant = (visibilityState, published, hasChanges) => {
* @param {string} id - The course unit ID. * @param {string} id - The course unit ID.
* @returns {string} The clear course unit ID extracted from the provided data. * @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];

View File

@@ -1,11 +1,11 @@
export type UseMessageHandlersTypes = { export type UseMessageHandlersTypes = {
courseId: string; courseId: string;
navigate: (path: string) => void;
dispatch: (action: any) => void; dispatch: (action: any) => void;
setIframeOffset: (height: number) => void; setIframeOffset: (height: number) => void;
handleDeleteXBlock: (usageId: string) => void; handleDeleteXBlock: (usageId: string) => void;
handleScrollToXBlock: (scrollOffset: number) => void; handleScrollToXBlock: (scrollOffset: number) => void;
handleDuplicateXBlock: (usageId: string) => void; handleDuplicateXBlock: (blockType: string, usageId: string) => void;
handleEditXBlock: (blockType: string, usageId: string) => void;
handleManageXBlockAccess: (usageId: string) => void; handleManageXBlockAccess: (usageId: string) => void;
handleShowLegacyEditXBlockModal: (id: string) => void; handleShowLegacyEditXBlockModal: (id: string) => void;
handleCloseLegacyEditorXBlockModal: () => void; handleCloseLegacyEditorXBlockModal: () => void;
@@ -14,6 +14,7 @@ export type UseMessageHandlersTypes = {
handleOpenManageTagsModal: (id: string) => void; handleOpenManageTagsModal: (id: string) => void;
handleShowProcessingNotification: (variant: string) => void; handleShowProcessingNotification: (variant: string) => void;
handleHideProcessingNotification: () => void; handleHideProcessingNotification: () => void;
handleRedirectToXBlockEditPage: (payload: { type: string, locator: string }) => void;
}; };
export type MessageHandlersTypes = Record<string, (payload: any) => void>; export type MessageHandlersTypes = Record<string, (payload: any) => void>;

View File

@@ -16,6 +16,7 @@ import { MessageHandlersTypes, UseMessageHandlersTypes } from './types';
*/ */
export const useMessageHandlers = ({ export const useMessageHandlers = ({
courseId, courseId,
navigate,
dispatch, dispatch,
setIframeOffset, setIframeOffset,
handleDeleteXBlock, handleDeleteXBlock,
@@ -29,15 +30,15 @@ export const useMessageHandlers = ({
handleOpenManageTagsModal, handleOpenManageTagsModal,
handleShowProcessingNotification, handleShowProcessingNotification,
handleHideProcessingNotification, handleHideProcessingNotification,
handleEditXBlock, handleRedirectToXBlockEditPage,
}: UseMessageHandlersTypes): MessageHandlersTypes => { }: UseMessageHandlersTypes): MessageHandlersTypes => {
const { copyToClipboard } = useClipboard(); const { copyToClipboard } = useClipboard();
return useMemo(() => ({ return useMemo(() => ({
[messageTypes.copyXBlock]: ({ usageId }) => copyToClipboard(usageId), [messageTypes.copyXBlock]: ({ usageId }) => copyToClipboard(usageId),
[messageTypes.deleteXBlock]: ({ usageId }) => handleDeleteXBlock(usageId), [messageTypes.deleteXBlock]: ({ usageId }) => handleDeleteXBlock(usageId),
[messageTypes.newXBlockEditor]: ({ blockType, usageId }) => handleEditXBlock(blockType, usageId), [messageTypes.newXBlockEditor]: ({ blockType, usageId }) => navigate(`/course/${courseId}/editor/${blockType}/${usageId}`),
[messageTypes.duplicateXBlock]: ({ usageId }) => handleDuplicateXBlock(usageId), [messageTypes.duplicateXBlock]: ({ blockType, usageId }) => handleDuplicateXBlock(blockType, usageId),
[messageTypes.manageXBlockAccess]: ({ usageId }) => handleManageXBlockAccess(usageId), [messageTypes.manageXBlockAccess]: ({ usageId }) => handleManageXBlockAccess(usageId),
[messageTypes.scrollToXBlock]: debounce(({ scrollOffset }) => handleScrollToXBlock(scrollOffset), 1000), [messageTypes.scrollToXBlock]: debounce(({ scrollOffset }) => handleScrollToXBlock(scrollOffset), 1000),
[messageTypes.toggleCourseXBlockDropdown]: ({ [messageTypes.toggleCourseXBlockDropdown]: ({
@@ -51,14 +52,9 @@ export const useMessageHandlers = ({
[messageTypes.openManageTags]: (payload) => handleOpenManageTagsModal(payload.contentId), [messageTypes.openManageTags]: (payload) => handleOpenManageTagsModal(payload.contentId),
[messageTypes.addNewComponent]: () => handleShowProcessingNotification(NOTIFICATION_MESSAGES.adding), [messageTypes.addNewComponent]: () => handleShowProcessingNotification(NOTIFICATION_MESSAGES.adding),
[messageTypes.pasteNewComponent]: () => handleShowProcessingNotification(NOTIFICATION_MESSAGES.pasting), [messageTypes.pasteNewComponent]: () => handleShowProcessingNotification(NOTIFICATION_MESSAGES.pasting),
[messageTypes.copyXBlockLegacy]: /* istanbul ignore next */ () => handleShowProcessingNotification( [messageTypes.copyXBlockLegacy]: () => handleShowProcessingNotification(NOTIFICATION_MESSAGES.copying),
NOTIFICATION_MESSAGES.copying,
),
[messageTypes.hideProcessingNotification]: handleHideProcessingNotification, [messageTypes.hideProcessingNotification]: handleHideProcessingNotification,
[messageTypes.handleRedirectToXBlockEditPage]: /* istanbul ignore next */ (payload) => handleEditXBlock( [messageTypes.handleRedirectToXBlockEditPage]: (payload) => handleRedirectToXBlockEditPage(payload),
payload.type,
payload.locator,
),
}), [ }), [
courseId, courseId,
handleDeleteXBlock, handleDeleteXBlock,

View File

@@ -1,10 +1,10 @@
import { getConfig } from '@edx/frontend-platform';
import { import {
FC, useEffect, useState, useMemo, useCallback, FC, useEffect, useState, useMemo, useCallback,
} from 'react'; } from 'react';
import { useIntl } from '@edx/frontend-platform/i18n'; import { useIntl } from '@edx/frontend-platform/i18n';
import { useToggle, Sheet, StandardModal } from '@openedx/paragon'; import { useToggle, Sheet } from '@openedx/paragon';
import { useDispatch, useSelector } from 'react-redux'; import { useDispatch } from 'react-redux';
import { useNavigate } from 'react-router-dom';
import { import {
hideProcessingNotification, hideProcessingNotification,
@@ -13,9 +13,9 @@ import {
import DeleteModal from '../../generic/delete-modal/DeleteModal'; import DeleteModal from '../../generic/delete-modal/DeleteModal';
import ConfigureModal from '../../generic/configure-modal/ConfigureModal'; import ConfigureModal from '../../generic/configure-modal/ConfigureModal';
import ModalIframe from '../../generic/modal-iframe'; import ModalIframe from '../../generic/modal-iframe';
import { getWaffleFlags } from '../../data/selectors';
import { IFRAME_FEATURE_POLICY } from '../../constants'; import { IFRAME_FEATURE_POLICY } from '../../constants';
import ContentTagsDrawer from '../../content-tags-drawer/ContentTagsDrawer'; import ContentTagsDrawer from '../../content-tags-drawer/ContentTagsDrawer';
import supportedEditors from '../../editors/supportedEditors';
import { useIframe } from '../../generic/hooks/context/hooks'; import { useIframe } from '../../generic/hooks/context/hooks';
import { import {
fetchCourseSectionVerticalData, fetchCourseSectionVerticalData,
@@ -35,29 +35,16 @@ import messages from './messages';
import { useIframeBehavior } from '../../generic/hooks/useIframeBehavior'; import { useIframeBehavior } from '../../generic/hooks/useIframeBehavior';
import { useIframeContent } from '../../generic/hooks/useIframeContent'; import { useIframeContent } from '../../generic/hooks/useIframeContent';
import { useIframeMessages } from '../../generic/hooks/useIframeMessages'; 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> = ({ const XBlockContainerIframe: FC<XBlockContainerIframeProps> = ({
courseId, courseId, blockId, unitXBlockActions, courseVerticalChildren, handleConfigureSubmit, isUnitVerticalType,
blockId,
unitXBlockActions,
courseVerticalChildren,
handleConfigureSubmit,
isUnitVerticalType,
courseUnitLoadingStatus,
}) => { }) => {
const intl = useIntl(); const intl = useIntl();
const dispatch = useDispatch(); const dispatch = useDispatch();
const navigate = useNavigate();
const [isDeleteModalOpen, openDeleteModal, closeDeleteModal] = useToggle(false); const [isDeleteModalOpen, openDeleteModal, closeDeleteModal] = useToggle(false);
const [isConfigureModalOpen, openConfigureModal, closeConfigureModal] = 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 [accessManagedXBlockData, setAccessManagedXBlockData] = useState<AccessManagedXBlockDataTypes | {}>({});
const [iframeOffset, setIframeOffset] = useState(0); const [iframeOffset, setIframeOffset] = useState(0);
const [deleteXBlockId, setDeleteXBlockId] = useState<string | null>(null); const [deleteXBlockId, setDeleteXBlockId] = useState<string | null>(null);
@@ -77,44 +64,14 @@ const XBlockContainerIframe: FC<XBlockContainerIframeProps> = ({
setIframeRef(iframeRef); setIframeRef(iframeRef);
}, [setIframeRef]); }, [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( const handleDuplicateXBlock = useCallback(
(usageId: string) => { (blockType: string, usageId: string) => {
unitXBlockActions.handleDuplicate(usageId); unitXBlockActions.handleDuplicate(usageId);
if (supportedEditors[blockType]) {
navigate(`/course/${courseId}/editor/${blockType}/${usageId}`);
}
}, },
[unitXBlockActions, courseId], [unitXBlockActions, courseId, navigate],
); );
const handleDeleteXBlock = (usageId: string) => { const handleDeleteXBlock = (usageId: string) => {
@@ -190,8 +147,13 @@ const XBlockContainerIframe: FC<XBlockContainerIframeProps> = ({
dispatch(hideProcessingNotification()); dispatch(hideProcessingNotification());
}; };
const handleRedirectToXBlockEditPage = (payload: { type: string, locator: string }) => {
navigate(`/course/${courseId}/editor/${payload.type}/${payload.locator}`);
};
const messageHandlers = useMessageHandlers({ const messageHandlers = useMessageHandlers({
courseId, courseId,
navigate,
dispatch, dispatch,
setIframeOffset, setIframeOffset,
handleDeleteXBlock, handleDeleteXBlock,
@@ -205,7 +167,7 @@ const XBlockContainerIframe: FC<XBlockContainerIframeProps> = ({
handleOpenManageTagsModal, handleOpenManageTagsModal,
handleShowProcessingNotification, handleShowProcessingNotification,
handleHideProcessingNotification, handleHideProcessingNotification,
handleEditXBlock, handleRedirectToXBlockEditPage,
}); });
useIframeMessages(messageHandlers); useIframeMessages(messageHandlers);
@@ -224,38 +186,6 @@ const XBlockContainerIframe: FC<XBlockContainerIframeProps> = ({
close={closeDeleteModal} close={closeDeleteModal}
onDeleteSubmit={onDeleteSubmit} 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 ? ( {Object.keys(accessManagedXBlockData).length ? (
<ConfigureModal <ConfigureModal
isXBlockComponent isXBlockComponent

View File

@@ -15,10 +15,6 @@ const messages = defineMessages({
id: 'course-authoring.course-unit.xblock.iframe.label', id: 'course-authoring.course-unit.xblock.iframe.label',
defaultMessage: '{xblockCount} xBlocks inside the frame', defaultMessage: '{xblockCount} xBlocks inside the frame',
}, },
videoPickerModalTitle: {
id: 'course-authoring.course-unit.xblock.video-editor.title',
defaultMessage: 'Select video',
},
}); });
export default messages; export default messages;

View File

@@ -42,11 +42,6 @@ export interface XBlockContainerIframeProps {
courseId: string; courseId: string;
blockId: string; blockId: string;
isUnitVerticalType: boolean, isUnitVerticalType: boolean,
courseUnitLoadingStatus: {
fetchUnitLoadingStatus: string;
fetchVerticalChildrenLoadingStatus: string;
fetchXBlockDataLoadingStatus: string;
};
unitXBlockActions: { unitXBlockActions: {
handleDelete: (XBlockId: string | null) => void; handleDelete: (XBlockId: string | null) => void;
handleDuplicate: (XBlockId: string | null) => void; handleDuplicate: (XBlockId: string | null) => void;

View File

@@ -1,6 +1,6 @@
import React, { useContext } from 'react'; import React, { useContext } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { useIntl } from '@edx/frontend-platform/i18n'; import { intlShape, injectIntl } from '@edx/frontend-platform/i18n';
import { import {
ActionRow, ActionRow,
IconButtonWithTooltip, IconButtonWithTooltip,
@@ -27,8 +27,9 @@ const CustomPageCard = ({
dispatch, dispatch,
deletePageStatus, deletePageStatus,
setCurrentPage, setCurrentPage,
// injected
intl,
}) => { }) => {
const intl = useIntl();
const [isDeleteConfirmationOpen, openDeleteConfirmation, closeDeleteConfirmation] = useToggle(false); const [isDeleteConfirmationOpen, openDeleteConfirmation, closeDeleteConfirmation] = useToggle(false);
const { path: customPagesPath } = useContext(CustomPagesContext); const { path: customPagesPath } = useContext(CustomPagesContext);
const navigate = useNavigate(); const navigate = useNavigate();
@@ -128,6 +129,8 @@ CustomPageCard.propTypes = {
dispatch: PropTypes.func.isRequired, dispatch: PropTypes.func.isRequired,
deletePageStatus: PropTypes.string.isRequired, deletePageStatus: PropTypes.string.isRequired,
setCurrentPage: PropTypes.func.isRequired, setCurrentPage: PropTypes.func.isRequired,
// injected
intl: intlShape.isRequired,
}; };
export default CustomPageCard; export default injectIntl(CustomPageCard);

View File

@@ -5,7 +5,7 @@ import {
} from 'react-router-dom'; } from 'react-router-dom';
import { useDispatch, useSelector } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import { AppContext, PageWrap } from '@edx/frontend-platform/react'; 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 { import {
ActionRow, ActionRow,
Breadcrumb, Breadcrumb,
@@ -45,8 +45,9 @@ import { getPagePath } from '../utils';
const CustomPages = ({ const CustomPages = ({
courseId, courseId,
// injected
intl,
}) => { }) => {
const intl = useIntl();
const navigate = useNavigate(); const navigate = useNavigate();
const dispatch = useDispatch(); const dispatch = useDispatch();
const [orderedPages, setOrderedPages] = useState([]); const [orderedPages, setOrderedPages] = useState([]);
@@ -277,6 +278,8 @@ const CustomPages = ({
CustomPages.propTypes = { CustomPages.propTypes = {
courseId: PropTypes.string.isRequired, courseId: PropTypes.string.isRequired,
// injected
intl: intlShape.isRequired,
}; };
export default CustomPages; export default injectIntl(CustomPages);

View File

@@ -26,7 +26,6 @@ const slice = createSlice({
useNewCertificatesPage: true, useNewCertificatesPage: true,
useNewTextbooksPage: true, useNewTextbooksPage: true,
useNewGroupConfigurationsPage: true, useNewGroupConfigurationsPage: true,
useVideoGalleryFlow: false,
}, },
}, },
reducers: { reducers: {

View File

@@ -7,22 +7,22 @@ import * as hooks from './hooks';
import supportedEditors from './supportedEditors'; import supportedEditors from './supportedEditors';
import type { EditorComponent } from './EditorComponent'; import type { EditorComponent } from './EditorComponent';
import { useEditorContext } from './EditorContext';
import AdvancedEditor from './AdvancedEditor'; import AdvancedEditor from './AdvancedEditor';
export interface Props extends EditorComponent { export interface Props extends EditorComponent {
blockType: string; blockType: string;
blockId: string | null; blockId: string | null;
isMarkdownEditorEnabledForCourse: boolean;
learningContextId: string | null; learningContextId: string | null;
lmsEndpointUrl: string | null; lmsEndpointUrl: string | null;
studioEndpointUrl: string | null; studioEndpointUrl: string | null;
fullScreen?: boolean; // eslint-disable-line react/no-unused-prop-types
} }
const Editor: React.FC<Props> = ({ const Editor: React.FC<Props> = ({
learningContextId, learningContextId,
blockType, blockType,
blockId, blockId,
isMarkdownEditorEnabledForCourse,
lmsEndpointUrl, lmsEndpointUrl,
studioEndpointUrl, studioEndpointUrl,
onClose = null, onClose = null,
@@ -34,12 +34,12 @@ const Editor: React.FC<Props> = ({
data: { data: {
blockId, blockId,
blockType, blockType,
isMarkdownEditorEnabledForCourse,
learningContextId, learningContextId,
lmsEndpointUrl, lmsEndpointUrl,
studioEndpointUrl, studioEndpointUrl,
}, },
}); });
const { fullScreen } = useEditorContext();
const EditorComponent = supportedEditors[blockType]; 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; export default Editor;

View File

@@ -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' }; const props = { learningContextId: 'cOuRsEId' };
describe('Editor Container', () => { describe('Editor Container', () => {

View File

@@ -5,13 +5,11 @@ import { useIntl } from '@edx/frontend-platform/i18n';
import { Button, Hyperlink } from '@openedx/paragon'; import { Button, Hyperlink } from '@openedx/paragon';
import { Warning as WarningIcon } from '@openedx/paragon/icons'; import { Warning as WarningIcon } from '@openedx/paragon/icons';
import { useSelector } from 'react-redux';
import EditorPage from './EditorPage'; import EditorPage from './EditorPage';
import AlertMessage from '../generic/alert-message'; import AlertMessage from '../generic/alert-message';
import messages from './messages'; import messages from './messages';
import { getLibraryId } from '../generic/key-utils'; import { getLibraryId } from '../generic/key-utils';
import { createCorrectInternalRoute } from '../utils'; import { createCorrectInternalRoute } from '../utils';
import { getWaffleFlags } from '../data/selectors';
interface Props { interface Props {
/** Course ID or Library ID */ /** Course ID or Library ID */
@@ -39,8 +37,6 @@ const EditorContainer: React.FC<Props> = ({
const location = useLocation(); const location = useLocation();
const [searchParams] = useSearchParams(); const [searchParams] = useSearchParams();
const upstreamLibRef = searchParams.get('upstreamLibRef'); const upstreamLibRef = searchParams.get('upstreamLibRef');
const waffleFlags = useSelector(getWaffleFlags);
const isMarkdownEditorEnabledForCourse = waffleFlags?.useReactMarkdownEditor;
if (blockType === undefined || blockId === undefined) { if (blockType === undefined || blockId === undefined) {
// istanbul ignore next - This shouldn't be possible; it's just here to satisfy the type checker. // 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} courseId={learningContextId}
blockType={blockType} blockType={blockType}
blockId={blockId} blockId={blockId}
isMarkdownEditorEnabledForCourse={isMarkdownEditorEnabledForCourse}
studioEndpointUrl={getConfig().STUDIO_BASE_URL} studioEndpointUrl={getConfig().STUDIO_BASE_URL}
lmsEndpointUrl={getConfig().LMS_BASE_URL} lmsEndpointUrl={getConfig().LMS_BASE_URL}
onClose={onClose ? () => onClose(location.state?.from) : null} onClose={onClose ? () => onClose(location.state?.from) : null}

View File

@@ -7,6 +7,14 @@ import React from 'react';
*/ */
export interface EditorContext { export interface EditorContext {
learningContextId: string; 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); const context = React.createContext<EditorContext | undefined>(undefined);
@@ -24,6 +32,7 @@ export function useEditorContext() {
export const EditorContextProvider: React.FC<{ export const EditorContextProvider: React.FC<{
children: React.ReactNode, children: React.ReactNode,
learningContextId: string; learningContextId: string;
fullScreen: boolean;
}> = ({ children, ...contextData }) => { }> = ({ children, ...contextData }) => {
const ctx: EditorContext = React.useMemo(() => ({ ...contextData }), []); const ctx: EditorContext = React.useMemo(() => ({ ...contextData }), []);
return <context.Provider value={ctx}>{children}</context.Provider>; return <context.Provider value={ctx}>{children}</context.Provider>;

View File

@@ -37,6 +37,7 @@ const defaultPropsHtml = {
lmsEndpointUrl: 'http://lms.test.none/', lmsEndpointUrl: 'http://lms.test.none/',
studioEndpointUrl: 'http://cms.test.none/', studioEndpointUrl: 'http://cms.test.none/',
onClose: jest.fn(), onClose: jest.fn(),
fullScreen: false,
}; };
const fieldsHtml = { const fieldsHtml = {
displayName: 'Introduction to Testing', displayName: 'Introduction to Testing',
@@ -65,6 +66,22 @@ describe('EditorPage', () => {
expect(modalElement.classList).not.toContain('pgn__modal-fullscreen'); 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 () => { test('it shows the Advanced Editor if there is no corresponding editor', async () => {
jest.spyOn(editorCmsApi, 'fetchBlockById').mockImplementationOnce(async () => ( // eslint-disable-next-line jest.spyOn(editorCmsApi, 'fetchBlockById').mockImplementationOnce(async () => ( // eslint-disable-next-line
{ status: 200, data: { display_name: 'Fake Un-editable Block', category: 'fake', metadata: {}, data: '' } } { status: 200, data: { display_name: 'Fake Un-editable Block', category: 'fake', metadata: {}, data: '' } }

View File

@@ -11,9 +11,9 @@ interface Props extends EditorComponent {
blockId?: string; blockId?: string;
blockType: string; blockType: string;
courseId: string; courseId: string;
isMarkdownEditorEnabledForCourse?: boolean;
lmsEndpointUrl?: string; lmsEndpointUrl?: string;
studioEndpointUrl?: string; studioEndpointUrl?: string;
fullScreen?: boolean;
children?: never; children?: never;
} }
@@ -25,11 +25,11 @@ const EditorPage: React.FC<Props> = ({
courseId, courseId,
blockType, blockType,
blockId = null, blockId = null,
isMarkdownEditorEnabledForCourse = false,
lmsEndpointUrl = null, lmsEndpointUrl = null,
studioEndpointUrl = null, studioEndpointUrl = null,
onClose = null, onClose = null,
returnFunction = null, returnFunction = null,
fullScreen = true,
}) => ( }) => (
<Provider store={store}> <Provider store={store}>
<ErrorBoundary <ErrorBoundary
@@ -38,14 +38,13 @@ const EditorPage: React.FC<Props> = ({
studioEndpointUrl, studioEndpointUrl,
}} }}
> >
<EditorContextProvider learningContextId={courseId}> <EditorContextProvider fullScreen={fullScreen} learningContextId={courseId}>
<Editor <Editor
{...{ {...{
onClose, onClose,
learningContextId: courseId, learningContextId: courseId,
blockType, blockType,
blockId, blockId,
isMarkdownEditorEnabledForCourse,
lmsEndpointUrl, lmsEndpointUrl,
studioEndpointUrl, studioEndpointUrl,
returnFunction, returnFunction,

View File

@@ -9,8 +9,6 @@ const VideoSelector = ({
learningContextId, learningContextId,
lmsEndpointUrl, lmsEndpointUrl,
studioEndpointUrl, studioEndpointUrl,
returnFunction,
onCancel,
}) => { }) => {
const dispatch = useDispatch(); const dispatch = useDispatch();
const loading = hooks.useInitializeApp({ const loading = hooks.useInitializeApp({
@@ -28,7 +26,7 @@ const VideoSelector = ({
return null; return null;
} }
return ( return (
<VideoGallery returnFunction={returnFunction} onCancel={onCancel} /> <VideoGallery />
); );
}; };
@@ -37,8 +35,6 @@ VideoSelector.propTypes = {
learningContextId: PropTypes.string.isRequired, learningContextId: PropTypes.string.isRequired,
lmsEndpointUrl: PropTypes.string.isRequired, lmsEndpointUrl: PropTypes.string.isRequired,
studioEndpointUrl: PropTypes.string.isRequired, studioEndpointUrl: PropTypes.string.isRequired,
returnFunction: PropTypes.func,
onCancel: PropTypes.func,
}; };
export default VideoSelector; export default VideoSelector;

View File

@@ -10,8 +10,6 @@ const VideoSelectorPage = ({
courseId, courseId,
lmsEndpointUrl, lmsEndpointUrl,
studioEndpointUrl, studioEndpointUrl,
returnFunction,
onCancel,
}) => ( }) => (
<Provider store={store}> <Provider store={store}>
<ErrorBoundary <ErrorBoundary
@@ -26,8 +24,6 @@ const VideoSelectorPage = ({
learningContextId: courseId, learningContextId: courseId,
lmsEndpointUrl, lmsEndpointUrl,
studioEndpointUrl, studioEndpointUrl,
returnFunction,
onCancel,
}} }}
/> />
</ErrorBoundary> </ErrorBoundary>
@@ -46,8 +42,6 @@ VideoSelectorPage.propTypes = {
courseId: PropTypes.string, courseId: PropTypes.string,
lmsEndpointUrl: PropTypes.string, lmsEndpointUrl: PropTypes.string,
studioEndpointUrl: PropTypes.string, studioEndpointUrl: PropTypes.string,
returnFunction: PropTypes.func,
onCancel: PropTypes.func,
}; };
export default VideoSelectorPage; export default VideoSelectorPage;

View File

@@ -60,7 +60,6 @@ exports[`Editor Container snapshots rendering correctly with expected Input 1`]
blockId="company-id1" blockId="company-id1"
blockType="html" blockType="html"
courseId="cOuRsEId" courseId="cOuRsEId"
isMarkdownEditorEnabledForCourse={true}
lmsEndpointUrl="http://localhost:18000" lmsEndpointUrl="http://localhost:18000"
onClose={null} onClose={null}
returnFunction={null} returnFunction={null}

View File

@@ -32,6 +32,7 @@ const defaultPropsHtml = {
lmsEndpointUrl: 'http://lms.test.none/', lmsEndpointUrl: 'http://lms.test.none/',
studioEndpointUrl: 'http://cms.test.none/', studioEndpointUrl: 'http://cms.test.none/',
onClose: jest.fn(), onClose: jest.fn(),
fullScreen: false,
}; };
const fieldsHtml = { const fieldsHtml = {
displayName: 'Introduction to Testing', displayName: 'Introduction to Testing',

View File

@@ -14,6 +14,7 @@ import { Close } from '@openedx/paragon/icons';
import { useIntl, FormattedMessage } from '@edx/frontend-platform/i18n'; import { useIntl, FormattedMessage } from '@edx/frontend-platform/i18n';
import { EditorComponent } from '../../EditorComponent'; import { EditorComponent } from '../../EditorComponent';
import { useEditorContext } from '../../EditorContext';
import TitleHeader from './components/TitleHeader'; import TitleHeader from './components/TitleHeader';
import * as hooks from './hooks'; import * as hooks from './hooks';
import messages from './messages'; import messages from './messages';
@@ -29,18 +30,37 @@ interface WrapperProps {
} }
export const EditorModalWrapper: React.FC<WrapperProps & { onClose: () => void }> = ({ children, onClose }) => { export const EditorModalWrapper: React.FC<WrapperProps & { onClose: () => void }> = ({ children, onClose }) => {
const { fullScreen } = useEditorContext();
const intl = useIntl(); 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); const title = intl.formatMessage(messages.modalTitle);
return ( return (
<ModalDialog isOpen size="xl" isOverflowVisible={false} onClose={onClose} title={title}>{children}</ModalDialog> <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 }) => {
export const FooterWrapper: React.FC<WrapperProps> = ({ children }) => <>{ 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 { interface Props extends EditorComponent {
children: React.ReactNode; children: React.ReactNode;

View File

@@ -52,8 +52,7 @@ exports[`SettingsWidget isLibrary snapshot: renders Settings widget for Advanced
<div <div
className="my-3" className="my-3"
> >
<SwitchEditorCard <SwitchToAdvancedEditorCard
editorType="advanced"
problemType="stringresponse" problemType="stringresponse"
/> />
</div> </div>
@@ -114,8 +113,7 @@ exports[`SettingsWidget isLibrary snapshot: renders Settings widget page 1`] = `
<div <div
className="my-3" className="my-3"
> >
<SwitchEditorCard <SwitchToAdvancedEditorCard
editorType="advanced"
problemType="stringresponse" problemType="stringresponse"
/> />
</div> </div>
@@ -176,8 +174,7 @@ exports[`SettingsWidget isLibrary snapshot: renders Settings widget page advance
<div <div
className="my-3" className="my-3"
> >
<SwitchEditorCard <SwitchToAdvancedEditorCard
editorType="advanced"
problemType="stringresponse" problemType="stringresponse"
/> />
</div> </div>
@@ -264,8 +261,7 @@ exports[`SettingsWidget snapshot snapshot: renders Settings widget for Advanced
<div <div
className="my-3" className="my-3"
> >
<SwitchEditorCard <SwitchToAdvancedEditorCard
editorType="advanced"
problemType="stringresponse" problemType="stringresponse"
/> />
</div> </div>
@@ -352,8 +348,7 @@ exports[`SettingsWidget snapshot snapshot: renders Settings widget page 1`] = `
<div <div
className="my-3" className="my-3"
> >
<SwitchEditorCard <SwitchToAdvancedEditorCard
editorType="advanced"
problemType="stringresponse" problemType="stringresponse"
/> />
</div> </div>
@@ -440,8 +435,7 @@ exports[`SettingsWidget snapshot snapshot: renders Settings widget page advanced
<div <div
className="my-3" className="my-3"
> >
<SwitchEditorCard <SwitchToAdvancedEditorCard
editorType="advanced"
problemType="stringresponse" problemType="stringresponse"
/> />
</div> </div>

View File

@@ -322,11 +322,11 @@ export const typeRowHooks = ({
}; };
}; };
export const handleConfirmEditorSwitch = ({ export const confirmSwitchToAdvancedEditor = ({
switchEditor, switchToAdvancedEditor,
setConfirmOpen, setConfirmOpen,
}) => { }) => {
switchEditor(); switchToAdvancedEditor();
setConfirmOpen(false); setConfirmOpen(false);
window.scrollTo({ window.scrollTo({
top: 0, top: 0,

View File

@@ -382,15 +382,15 @@ describe('Problem settings hooks', () => {
expect(typeRowProps.updateField).toHaveBeenCalledWith({ problemType: ProblemTypeKeys.TEXTINPUT }); expect(typeRowProps.updateField).toHaveBeenCalledWith({ problemType: ProblemTypeKeys.TEXTINPUT });
}); });
}); });
test('test handleConfirmEditorSwitch hook', () => { test('test confirmSwitchToAdvancedEditor hook', () => {
const switchEditor = jest.fn(); const switchToAdvancedEditor = jest.fn();
const setConfirmOpen = jest.fn(); const setConfirmOpen = jest.fn();
window.scrollTo = jest.fn(); window.scrollTo = jest.fn();
hooks.handleConfirmEditorSwitch({ hooks.confirmSwitchToAdvancedEditor({
switchEditor, switchToAdvancedEditor,
setConfirmOpen, setConfirmOpen,
}); });
expect(switchEditor).toHaveBeenCalled(); expect(switchToAdvancedEditor).toHaveBeenCalled();
expect(setConfirmOpen).toHaveBeenCalledWith(false); expect(setConfirmOpen).toHaveBeenCalledWith(false);
expect(window.scrollTo).toHaveBeenCalled(); expect(window.scrollTo).toHaveBeenCalled();
}); });

View File

@@ -14,7 +14,7 @@ import TimerCard from './settingsComponents/TimerCard';
import TypeCard from './settingsComponents/TypeCard'; import TypeCard from './settingsComponents/TypeCard';
import ToleranceCard from './settingsComponents/Tolerance'; import ToleranceCard from './settingsComponents/Tolerance';
import GroupFeedbackCard from './settingsComponents/GroupFeedback/index'; import GroupFeedbackCard from './settingsComponents/GroupFeedback/index';
import SwitchEditorCard from './settingsComponents/SwitchEditorCard'; import SwitchToAdvancedEditorCard from './settingsComponents/SwitchToAdvancedEditorCard';
import messages from './messages'; import messages from './messages';
import { showAdvancedSettingsCards } from './hooks'; import { showAdvancedSettingsCards } from './hooks';
@@ -39,9 +39,9 @@ const SettingsWidget = ({
images, images,
isLibrary, isLibrary,
learningContextId, learningContextId,
showMarkdownEditorButton,
}) => { }) => {
const { isAdvancedCardsVisible, showAdvancedCards } = showAdvancedSettingsCards(); const { isAdvancedCardsVisible, showAdvancedCards } = showAdvancedSettingsCards();
const feedbackCard = () => { const feedbackCard = () => {
if ([ProblemTypeKeys.MULTISELECT].includes(problemType)) { if ([ProblemTypeKeys.MULTISELECT].includes(problemType)) {
return ( return (
@@ -153,14 +153,8 @@ const SettingsWidget = ({
</div> </div>
)} )}
<div className="my-3"> <div className="my-3">
<SwitchEditorCard problemType={problemType} editorType="advanced" /> <SwitchToAdvancedEditorCard problemType={problemType} />
</div> </div>
{ showMarkdownEditorButton
&& (
<div className="my-3">
<SwitchEditorCard problemType={problemType} editorType="markdown" />
</div>
)}
</Collapsible.Body> </Collapsible.Body>
</Collapsible.Advanced> </Collapsible.Advanced>
</div> </div>
@@ -202,7 +196,6 @@ SettingsWidget.propTypes = {
isLibrary: PropTypes.bool.isRequired, isLibrary: PropTypes.bool.isRequired,
// eslint-disable-next-line // eslint-disable-next-line
settings: PropTypes.any.isRequired, settings: PropTypes.any.isRequired,
showMarkdownEditorButton: PropTypes.bool.isRequired,
}; };
const mapStateToProps = (state) => ({ const mapStateToProps = (state) => ({
@@ -215,8 +208,6 @@ const mapStateToProps = (state) => ({
images: selectors.app.images(state), images: selectors.app.images(state),
isLibrary: selectors.app.isLibrary(state), isLibrary: selectors.app.isLibrary(state),
learningContextId: selectors.app.learningContextId(state), learningContextId: selectors.app.learningContextId(state),
showMarkdownEditorButton: selectors.app.isMarkdownEditorEnabledForCourse(state)
&& selectors.problem.rawMarkdown(state),
}); });
export const mapDispatchToProps = { export const mapDispatchToProps = {

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