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
310 changed files with 3749 additions and 6677 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

@@ -82,7 +82,7 @@ describe('<CourseLibraries />', () => {
expect(reviewTab).toHaveAttribute('aria-selected', 'true'); expect(reviewTab).toHaveAttribute('aria-selected', 'true');
userEvent.click(allTab); userEvent.click(allTab);
const alert = (await screen.findAllByRole('alert'))[0]; const alert = await screen.findByRole('alert');
expect(await within(alert).findByText( expect(await within(alert).findByText(
'5 library components are out of sync. Review updates to accept or ignore changes', '5 library components are out of sync. Review updates to accept or ignore changes',
)).toBeInTheDocument(); )).toBeInTheDocument();
@@ -105,7 +105,7 @@ describe('<CourseLibraries />', () => {
userEvent.click(allTab); userEvent.click(allTab);
expect(allTab).toHaveAttribute('aria-selected', 'true'); expect(allTab).toHaveAttribute('aria-selected', 'true');
const alert = (await screen.findAllByRole('alert'))[0]; const alert = await screen.findByRole('alert');
expect(await within(alert).findByText( expect(await within(alert).findByText(
'5 library components are out of sync. Review updates to accept or ignore changes', '5 library components are out of sync. Review updates to accept or ignore changes',
)).toBeInTheDocument(); )).toBeInTheDocument();
@@ -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.findAllByRole('alert'))[0];
expect(await within(alert).findByText(
'5 library components are out of sync. Review updates to accept or ignore changes',
)).toBeInTheDocument();
});
it('doesnt show alert if max lastPublishedDate is less than the local storage value', async () => {
const lastPublishedDate = new Date('2025-05-01T22:20:44.989042Z');
localStorage.setItem(
`outOfSyncCountAlert-${mockGetEntityLinks.courseKey}`,
String(lastPublishedDate.getTime() + 1000),
);
await renderCourseLibrariesPage(mockGetEntityLinks.courseKey);
const allTab = await screen.findByRole('tab', { name: 'Libraries' });
const reviewTab = await screen.findByRole('tab', { name: 'Review Content Updates 5' });
// review tab should be open by default as outOfSyncCount is greater than 0
expect(reviewTab).toHaveAttribute('aria-selected', 'true');
userEvent.click(allTab);
expect(allTab).toHaveAttribute('aria-selected', 'true');
screen.logTestingPlaygroundURL();
expect(screen.queryAllByRole('alert').length).toEqual(1);
});
}); });
describe('<CourseLibraries ReviewTab />', () => { 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

@@ -17,7 +17,7 @@ import {
Tabs, Tabs,
} from '@openedx/paragon'; } from '@openedx/paragon';
import { import {
Cached, CheckCircle, Launch, Loop, Info, Cached, CheckCircle, Launch, Loop,
} from '@openedx/paragon/icons'; } from '@openedx/paragon/icons';
import sumBy from 'lodash/sumBy'; import sumBy from 'lodash/sumBy';
@@ -33,7 +33,6 @@ import { useStudioHome } from '../studio-home/hooks';
import NewsstandIcon from '../generic/NewsstandIcon'; import NewsstandIcon from '../generic/NewsstandIcon';
import ReviewTabContent from './ReviewTabContent'; import ReviewTabContent from './ReviewTabContent';
import { OutOfSyncAlert } from './OutOfSyncAlert'; import { OutOfSyncAlert } from './OutOfSyncAlert';
import AlertMessage from '../generic/alert-message';
interface Props { interface Props {
courseId: string; courseId: string;
@@ -165,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" />
@@ -200,12 +199,6 @@ export const CourseLibraries: React.FC<Props> = ({ courseId }) => {
showAlert={showReviewAlert && tabKey === CourseLibraryTabs.all} showAlert={showReviewAlert && tabKey === CourseLibraryTabs.all}
setShowAlert={setShowReviewAlert} setShowAlert={setShowReviewAlert}
/> />
{ /* TODO: Remove this alert after implement container in this page */}
<AlertMessage
title={intl.formatMessage(messages.unitsUpdatesWarning)}
icon={Info}
variant="info"
/>
<SubHeader <SubHeader
title={intl.formatMessage(messages.headingTitle)} title={intl.formatMessage(messages.headingTitle)}
subtitle={intl.formatMessage(messages.headingSubtitle)} subtitle={intl.formatMessage(messages.headingSubtitle)}

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,10 +116,10 @@ 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.',
}, },
unitsUpdatesWarning: { olderVersionPreviewAlert: {
id: 'course-authoring.course-libraries.home-tab.warning.units', id: 'course-authoring.course-libraries.reviw-tab.preview.old-version-alert',
defaultMessage: 'Currently this page only tracks component updates. To check for unit updates, go to your Course Outline.', defaultMessage: 'The old version preview is the previous library version',
description: 'Warning message shown in library sync page about units updates.', description: 'Alert message stating that older version in preview is of library block',
}, },
}); });

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

@@ -1,7 +1,6 @@
// @ts-check // @ts-check
import React, { useEffect, useRef, useState } from 'react'; import React, { useEffect, useRef, useState } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import classNames from 'classnames';
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';
import { useSearchParams } from 'react-router-dom'; import { useSearchParams } from 'react-router-dom';
@@ -11,13 +10,11 @@ import {
Hyperlink, Hyperlink,
Icon, Icon,
IconButton, IconButton,
IconButtonWithTooltip,
useToggle, useToggle,
} from '@openedx/paragon'; } from '@openedx/paragon';
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';
@@ -58,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();
@@ -135,28 +130,12 @@ const CardHeader = ({
) : ( ) : (
<> <>
{titleComponent} {titleComponent}
<IconButtonWithTooltip <IconButton
className={classNames( className="item-card-edit-icon"
'item-card-button-icon',
{
'item-card-button-icon-disabled': isDisabledEditField,
},
)}
data-testid={`${namePrefix}-edit-button`} data-testid={`${namePrefix}-edit-button`}
alt={intl.formatMessage( alt={intl.formatMessage(messages.altButtonEdit)}
isDisabledEditField ? messages.cannotEditTooltip : messages.altButtonRename,
)}
tooltipContent={(
<div>
{intl.formatMessage(
isDisabledEditField ? messages.cannotEditTooltip : messages.altButtonRename,
)}
</div>
)}
iconAs={EditIcon} iconAs={EditIcon}
onClick={onClickEdit} onClick={onClickEdit}
// @ts-ignore
disabled={isDisabledEditField}
/> />
</> </>
)} )}
@@ -168,15 +147,6 @@ const CardHeader = ({
<TagCount count={contentTagCount} onClick={openManageTagsDrawer} /> <TagCount count={contentTagCount} onClick={openManageTagsDrawer} />
)} )}
{extraActionsComponent} {extraActionsComponent}
{readyToSync && (
<IconButtonWithTooltip
data-testid={`${namePrefix}-sync-button`}
alt={intl.formatMessage(messages.readyToSyncButtonAlt)}
iconAs={SyncIcon}
tooltipContent={<div>{intl.formatMessage(messages.readyToSyncButtonAlt)}</div>}
onClick={onClickSync}
/>
)}
<Dropdown data-testid={`${namePrefix}-card-header__menu`} onClick={onClickMenuButton}> <Dropdown data-testid={`${namePrefix}-card-header__menu`} onClick={onClickMenuButton}>
<Dropdown.Toggle <Dropdown.Toggle
className="item-card-header__menu" className="item-card-header__menu"
@@ -208,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)}
@@ -216,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)}
@@ -287,8 +255,6 @@ CardHeader.defaultProps = {
parentInfo: {}, parentInfo: {},
cardId: '', cardId: '',
extraActionsComponent: null, extraActionsComponent: null,
readyToSync: false,
onClickSync: null,
}; };
CardHeader.propTypes = { CardHeader.propTypes = {
@@ -335,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,14 +23,8 @@
} }
&:hover { &:hover {
.item-card-button-icon { .item-card-edit-icon {
opacity: 1; opacity: 1;
&.item-card-button-icon-disabled {
pointer-events: all;
opacity: .5;
cursor: default;
}
} }
} }
} }

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

@@ -29,9 +29,9 @@ const messages = defineMessages({
id: 'course-authoring.course-outline.card.status-badge.draft-unpublished-changes', id: 'course-authoring.course-outline.card.status-badge.draft-unpublished-changes',
defaultMessage: 'Draft (Unpublished changes)', defaultMessage: 'Draft (Unpublished changes)',
}, },
altButtonRename: { altButtonEdit: {
id: 'course-authoring.course-outline.card.button.edit.alt', id: 'course-authoring.course-outline.card.button.edit.alt',
defaultMessage: 'Rename', defaultMessage: 'Edit',
}, },
menuPublish: { menuPublish: {
id: 'course-authoring.course-outline.card.menu.publish', id: 'course-authoring.course-outline.card.menu.publish',
@@ -77,16 +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.',
},
cannotEditTooltip: {
id: 'course-authoring.course-outline.card.button.edit.disable.tooltip',
defaultMessage: 'This object was added from a library, so it cannot be edited.',
description: 'Tooltip text of button when the object was added from a library.',
},
}); });
export default messages; 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 for unit changes at this time')).toBeInTheDocument();
// Click on accept changes
const acceptChangesButton = screen.getByText(/accept changes/i);
fireEvent.click(acceptChangesButton);
await waitFor(() => expect(mockUseAcceptLibraryBlockChanges).toHaveBeenCalled());
});
it('should decline sync unit changes from upstream', async () => {
renderComponent();
expect(await screen.findByTestId('unit-card-header')).toBeInTheDocument();
// Click on sync button
const syncButton = screen.getByRole('button', { name: /update available - click to sync/i });
fireEvent.click(syncButton);
// Should open compare preview modal
expect(screen.getByRole('heading', { name: /preview changes: unit name/i })).toBeInTheDocument();
expect(screen.getByText('Preview not available for unit changes at this time')).toBeInTheDocument();
// Click on ignore changes
const ignoreChangesButton = screen.getByRole('button', { name: /ignore changes/i });
fireEvent.click(ignoreChangesButton);
// Should open the confirmation modal
expect(screen.getByRole('heading', { name: /ignore these changes\?/i })).toBeInTheDocument();
// Click on ignore button
const ignoreButton = screen.getByRole('button', { name: /ignore/i });
fireEvent.click(ignoreButton);
await waitFor(() => expect(mockUseIgnoreLibraryBlockChanges).toHaveBeenCalled());
});
}); });

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

@@ -17,6 +17,7 @@ import { cloneDeep, set } from 'lodash';
import { import {
getCourseSectionVerticalApiUrl, getCourseSectionVerticalApiUrl,
getCourseUnitApiUrl,
getCourseVerticalChildrenApiUrl, getCourseVerticalChildrenApiUrl,
getCourseOutlineInfoUrl, getCourseOutlineInfoUrl,
getXBlockBaseApiUrl, getXBlockBaseApiUrl,
@@ -27,6 +28,7 @@ import {
deleteUnitItemQuery, deleteUnitItemQuery,
editCourseUnitVisibilityAndData, editCourseUnitVisibilityAndData,
fetchCourseSectionVerticalData, fetchCourseSectionVerticalData,
fetchCourseUnitQuery,
fetchCourseVerticalChildrenData, fetchCourseVerticalChildrenData,
getCourseOutlineInfoQuery, getCourseOutlineInfoQuery,
patchUnitItemQuery, patchUnitItemQuery,
@@ -35,12 +37,13 @@ import initializeStore from '../store';
import { import {
courseCreateXblockMock, courseCreateXblockMock,
courseSectionVerticalMock, courseSectionVerticalMock,
courseUnitIndexMock,
courseUnitMock, courseUnitMock,
courseVerticalChildrenMock, courseVerticalChildrenMock,
clipboardMockResponse, clipboardMockResponse,
courseOutlineInfoMock, courseOutlineInfoMock,
} from './__mocks__'; } from './__mocks__';
import { clipboardUnit } from '../__mocks__'; import { clipboardUnit, clipboardXBlock } from '../__mocks__';
import { executeThunk } from '../utils'; import { executeThunk } from '../utils';
import { IFRAME_FEATURE_POLICY } from '../constants'; import { IFRAME_FEATURE_POLICY } from '../constants';
import pasteComponentMessages from '../generic/clipboard/paste-component/messages'; import pasteComponentMessages from '../generic/clipboard/paste-component/messages';
@@ -62,15 +65,13 @@ 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;
let queryClient; let queryClient;
const courseId = '123'; const courseId = '123';
const blockId = '567890'; const blockId = '567890';
const sequenceId = 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@19a30717eff543078a5d94ae9d6c18a5'; const unitDisplayName = courseUnitIndexMock.metadata.display_name;
const unitDisplayName = courseSectionVerticalMock.xblock_info.display_name;
const mockedUsedNavigate = jest.fn(); const mockedUsedNavigate = jest.fn();
const userName = 'openedx'; const userName = 'openedx';
const handleConfigureSubmitMock = jest.fn(); const handleConfigureSubmitMock = jest.fn();
@@ -88,7 +89,7 @@ const postXBlockBody = {
jest.mock('react-router-dom', () => ({ jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'), ...jest.requireActual('react-router-dom'),
useParams: () => ({ blockId, sequenceId }), useParams: () => ({ blockId }),
useNavigate: () => mockedUsedNavigate, useNavigate: () => mockedUsedNavigate,
})); }));
@@ -144,10 +145,14 @@ describe('<CourseUnit />', () => {
axiosMock axiosMock
.onGet(getClipboardUrl()) .onGet(getClipboardUrl())
.reply(200, clipboardUnit); .reply(200, clipboardUnit);
axiosMock
.onGet(getCourseUnitApiUrl(courseId))
.reply(200, courseUnitIndexMock);
await executeThunk(fetchCourseUnitQuery(courseId), store.dispatch);
axiosMock axiosMock
.onGet(getCourseSectionVerticalApiUrl(blockId)) .onGet(getCourseSectionVerticalApiUrl(blockId))
.reply(200, courseSectionVerticalMock); .reply(200, courseSectionVerticalMock);
await executeThunk(fetchCourseSectionVerticalData(blockId, courseId), store.dispatch); await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch);
axiosMock axiosMock
.onGet(getCourseVerticalChildrenApiUrl(blockId)) .onGet(getCourseVerticalChildrenApiUrl(blockId))
.reply(200, courseVerticalChildrenMock); .reply(200, courseVerticalChildrenMock);
@@ -161,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 = courseSectionVerticalMock.xblock_info.ancestor_info.ancestors[1].display_name; const currentSectionName = courseUnitIndexMock.ancestor_info.ancestors[1].display_name;
const currentSubSectionName = courseSectionVerticalMock.xblock_info.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;');
@@ -205,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();
@@ -243,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();
@@ -258,32 +263,29 @@ 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();
}); });
axiosMock axiosMock
.onGet(getCourseSectionVerticalApiUrl(blockId)) .onGet(getCourseUnitApiUrl(blockId))
.reply(200, { .reply(200, {
...courseSectionVerticalMock, ...courseUnitIndexMock,
xblock_info: { has_changes: true,
...courseSectionVerticalMock.xblock_info, published_by: userName,
has_changes: true,
published_by: userName,
},
}); });
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();
@@ -302,27 +304,24 @@ 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);
}); });
axiosMock axiosMock
.onGet(getCourseSectionVerticalApiUrl(blockId)) .onGet(getCourseUnitApiUrl(blockId))
.reply(200, { .reply(200, {
...courseSectionVerticalMock, ...courseUnitIndexMock,
xblock_info: { has_changes: true,
...courseSectionVerticalMock.xblock_info, published_by: userName,
has_changes: true,
published_by: userName,
},
}); });
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);
}); });
@@ -381,36 +382,30 @@ describe('<CourseUnit />', () => {
}) })
.reply(200, { dummy: 'value' }); .reply(200, { dummy: 'value' });
axiosMock axiosMock
.onGet(getCourseSectionVerticalApiUrl(blockId)) .onGet(getCourseUnitApiUrl(blockId))
.reply(200, { .reply(200, {
...courseSectionVerticalMock, ...courseUnitIndexMock,
xblock_info: { visibility_state: UNIT_VISIBILITY_STATES.live,
...courseSectionVerticalMock.xblock_info, has_changes: false,
visibility_state: UNIT_VISIBILITY_STATES.live, published_by: userName,
has_changes: false,
published_by: userName,
},
}); });
await executeThunk(editCourseUnitVisibilityAndData(blockId, PUBLISH_TYPES.makePublic, true), store.dispatch); await executeThunk(editCourseUnitVisibilityAndData(blockId, PUBLISH_TYPES.makePublic, true), store.dispatch);
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}', courseSectionVerticalMock.xblock_info.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
.onDelete(getXBlockBaseApiUrl(courseVerticalChildrenMock.children[0].block_id)) .onDelete(getXBlockBaseApiUrl(courseVerticalChildrenMock.children[0].block_id))
.reply(200, { dummy: 'value' }); .reply(200, { dummy: 'value' });
axiosMock
.onGet(getCourseSectionVerticalApiUrl(courseId))
.reply(200, courseSectionVerticalMock);
await executeThunk(deleteUnitItemQuery( await executeThunk(deleteUnitItemQuery(
courseId, courseId,
courseVerticalChildrenMock.children[0].block_id, courseVerticalChildrenMock.children[0].block_id,
@@ -431,41 +426,43 @@ describe('<CourseUnit />', () => {
await executeThunk(fetchCourseVerticalChildrenData(blockId), store.dispatch); await executeThunk(fetchCourseVerticalChildrenData(blockId), store.dispatch);
axiosMock axiosMock
.onGet(getCourseSectionVerticalApiUrl(blockId)) .onGet(getCourseUnitApiUrl(blockId))
.reply(200, courseSectionVerticalMock); .reply(200, courseUnitIndexMock);
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(courseSectionVerticalMock.xblock_info.release_date)).toBeInTheDocument(); expect(getByText(courseUnitIndexMock.release_date)).toBeInTheDocument();
expect(screen.getByText( expect(getByText(
sidebarMessages.publishInfoDraftSaved.defaultMessage sidebarMessages.publishInfoDraftSaved.defaultMessage
.replace('{editedOn}', courseSectionVerticalMock.xblock_info.edited_on) .replace('{editedOn}', courseUnitIndexMock.edited_on)
.replace('{editedBy}', courseSectionVerticalMock.xblock_info.edited_by), .replace('{editedBy}', courseUnitIndexMock.edited_by),
)).toBeInTheDocument(); )).toBeInTheDocument();
expect(screen.getByText( expect(getByText(
sidebarMessages.releaseInfoWithSection.defaultMessage sidebarMessages.releaseInfoWithSection.defaultMessage
.replace('{sectionName}', courseSectionVerticalMock.xblock_info.release_date_from), .replace('{sectionName}', courseUnitIndexMock.release_date_from),
)).toBeInTheDocument(); )).toBeInTheDocument();
}); });
}); });
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,
@@ -481,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: '',
},
}, },
]; ];
@@ -500,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
@@ -520,37 +511,34 @@ describe('<CourseUnit />', () => {
}) })
.reply(200, { dummy: 'value' }); .reply(200, { dummy: 'value' });
axiosMock axiosMock
.onGet(getCourseSectionVerticalApiUrl(blockId)) .onGet(getCourseUnitApiUrl(blockId))
.reply(200, { .reply(200, {
...courseSectionVerticalMock, ...courseUnitIndexMock,
xblock_info: { visibility_state: UNIT_VISIBILITY_STATES.live,
...courseSectionVerticalMock.xblock_info, has_changes: false,
visibility_state: UNIT_VISIBILITY_STATES.live, published_by: userName,
has_changes: false,
published_by: userName,
},
}); });
await executeThunk(editCourseUnitVisibilityAndData(blockId, PUBLISH_TYPES.makePublic, true), store.dispatch); await executeThunk(editCourseUnitVisibilityAndData(blockId, PUBLISH_TYPES.makePublic, true), store.dispatch);
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}', courseSectionVerticalMock.xblock_info.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
.onGet(getCourseSectionVerticalApiUrl(blockId)) .onGet(getCourseUnitApiUrl(blockId))
.reply(200, courseSectionVerticalMock); .reply(200, courseUnitIndexMock);
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
@@ -558,23 +546,23 @@ 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(courseSectionVerticalMock.xblock_info.release_date)).toBeInTheDocument(); expect(getByText(courseUnitIndexMock.release_date)).toBeInTheDocument();
expect(screen.getByText( expect(getByText(
sidebarMessages.publishInfoDraftSaved.defaultMessage sidebarMessages.publishInfoDraftSaved.defaultMessage
.replace('{editedOn}', courseSectionVerticalMock.xblock_info.edited_on) .replace('{editedOn}', courseUnitIndexMock.edited_on)
.replace('{editedBy}', courseSectionVerticalMock.xblock_info.edited_by), .replace('{editedBy}', courseUnitIndexMock.edited_by),
)).toBeInTheDocument(); )).toBeInTheDocument();
expect(screen.getByText( expect(getByText(
sidebarMessages.releaseInfoWithSection.defaultMessage sidebarMessages.releaseInfoWithSection.defaultMessage
.replace('{sectionName}', courseSectionVerticalMock.xblock_info.release_date_from), .replace('{sectionName}', courseUnitIndexMock.release_date_from),
)).toBeInTheDocument(); )).toBeInTheDocument();
}); });
}); });
@@ -582,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');
@@ -604,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`;
@@ -617,15 +610,12 @@ describe('<CourseUnit />', () => {
})) }))
.reply(200, { dummy: 'value' }); .reply(200, { dummy: 'value' });
axiosMock axiosMock
.onGet(getCourseSectionVerticalApiUrl(blockId)) .onGet(getCourseUnitApiUrl(blockId))
.reply(200, { .reply(200, {
...courseSectionVerticalMock, ...courseUnitIndexMock,
xblock_info: { metadata: {
...courseSectionVerticalMock.xblock_info, ...courseUnitIndexMock.metadata,
metadata: { display_name: newDisplayName,
...courseSectionVerticalMock.xblock_info.metadata,
display_name: newDisplayName,
},
}, },
}); });
axiosMock axiosMock
@@ -643,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)
@@ -651,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);
@@ -659,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 () => {
@@ -682,14 +671,15 @@ describe('<CourseUnit />', () => {
}); });
}); });
it('handle creating Problem xblock and showing editor modal', async () => { it('handle creating Problem xblock and navigate to editor page', async () => {
const { courseKey, locator } = courseCreateXblockMock;
axiosMock 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
@@ -698,57 +688,93 @@ describe('<CourseUnit />', () => {
}) })
.reply(200, { dummy: 'value' }); .reply(200, { dummy: 'value' });
axiosMock axiosMock
.onGet(getCourseSectionVerticalApiUrl(blockId)) .onGet(getCourseUnitApiUrl(blockId))
.reply(200, { .reply(200, {
...courseSectionVerticalMock, ...courseUnitIndexMock,
xblock_info: { visibility_state: UNIT_VISIBILITY_STATES.live,
...courseSectionVerticalMock.xblock_info, has_changes: false,
visibility_state: UNIT_VISIBILITY_STATES.live, published_by: userName,
has_changes: false,
published_by: userName,
},
}); });
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
.onGet(getCourseSectionVerticalApiUrl(blockId)) .onGet(getCourseUnitApiUrl(blockId))
.reply(200, courseSectionVerticalMock); .reply(200, courseUnitIndexMock);
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(courseSectionVerticalMock.xblock_info.release_date)).toBeInTheDocument(); expect(getByText(courseUnitIndexMock.release_date)).toBeInTheDocument();
expect(screen.getByText( expect(getByText(
sidebarMessages.publishInfoDraftSaved.defaultMessage sidebarMessages.publishInfoDraftSaved.defaultMessage
.replace('{editedOn}', courseSectionVerticalMock.xblock_info.edited_on) .replace('{editedOn}', courseUnitIndexMock.edited_on)
.replace('{editedBy}', courseSectionVerticalMock.xblock_info.edited_by), .replace('{editedBy}', courseUnitIndexMock.edited_by),
)).toBeInTheDocument(); )).toBeInTheDocument();
expect(screen.getByText( expect(getByText(
sidebarMessages.releaseInfoWithSection.defaultMessage sidebarMessages.releaseInfoWithSection.defaultMessage
.replace('{sectionName}', courseSectionVerticalMock.xblock_info.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];
@@ -758,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);
}); });
@@ -775,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;
@@ -788,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', [
@@ -805,15 +831,12 @@ describe('<CourseUnit />', () => {
}, },
})) }))
.reply(200, { dummy: 'value' }) .reply(200, { dummy: 'value' })
.onGet(getCourseSectionVerticalApiUrl(blockId)) .onGet(getCourseUnitApiUrl(blockId))
.reply(200, { .reply(200, {
...courseSectionVerticalMock, ...courseUnitIndexMock,
xblock_info: { metadata: {
...courseSectionVerticalMock.xblock_info, ...courseUnitIndexMock.metadata,
metadata: { display_name: newDisplayName,
...courseSectionVerticalMock.xblock_info.metadata,
display_name: newDisplayName,
},
}, },
}) })
.onGet(getCourseSectionVerticalApiUrl(blockId)) .onGet(getCourseSectionVerticalApiUrl(blockId))
@@ -823,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);
@@ -835,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
@@ -858,181 +880,96 @@ describe('<CourseUnit />', () => {
}) })
.reply(200, { dummy: 'value' }); .reply(200, { dummy: 'value' });
axiosMock axiosMock
.onGet(getCourseSectionVerticalApiUrl(blockId)) .onGet(getCourseUnitApiUrl(blockId))
.reply(200, { .reply(200, {
...courseSectionVerticalMock, ...courseUnitIndexMock,
xblock_info: { visibility_state: UNIT_VISIBILITY_STATES.live,
...courseSectionVerticalMock.xblock_info, has_changes: false,
visibility_state: UNIT_VISIBILITY_STATES.live, published_by: userName,
has_changes: false,
published_by: userName,
},
}); });
await executeThunk(editCourseUnitVisibilityAndData(blockId, PUBLISH_TYPES.makePublic, true), store.dispatch); await executeThunk(editCourseUnitVisibilityAndData(blockId, PUBLISH_TYPES.makePublic, true), store.dispatch);
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}', courseSectionVerticalMock.xblock_info.published_on)
.replace('{publishedBy}', userName),
)).toBeInTheDocument();
expect(screen.queryByRole('button', { name: sidebarMessages.actionButtonPublishTitle.defaultMessage })).not.toBeInTheDocument();
const videoButton = screen.getByRole('button', {
name: new RegExp(`${addComponentMessages.buttonText.defaultMessage} Video`, 'i'),
hidden: true,
});
userEvent.click(videoButton);
axiosMock
.onGet(getCourseSectionVerticalApiUrl(blockId))
.reply(200, courseSectionVerticalMock);
await executeThunk(editCourseUnitVisibilityAndData(blockId, PUBLISH_TYPES.makePublic, true), store.dispatch);
// after creating video xblock, the sidebar status changes to Draft (unpublished changes)
expect(screen.getByText(sidebarMessages.sidebarTitleDraftUnpublishedChanges.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.visibilityStaffAndLearnersTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.releaseStatusTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.sidebarBodyNote.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.visibilityWillBeVisibleToTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.visibilityCheckboxTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.actionButtonPublishTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(sidebarMessages.actionButtonDiscardChangesTitle.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(courseSectionVerticalMock.xblock_info.release_date)).toBeInTheDocument();
expect(screen.getByText(
sidebarMessages.publishInfoDraftSaved.defaultMessage
.replace('{editedOn}', courseSectionVerticalMock.xblock_info.edited_on)
.replace('{editedBy}', courseSectionVerticalMock.xblock_info.edited_by),
)).toBeInTheDocument();
expect(screen.getByText(
sidebarMessages.releaseInfoWithSection.defaultMessage
.replace('{sectionName}', courseSectionVerticalMock.xblock_info.release_date_from),
)).toBeInTheDocument();
expect(screen.getByRole('heading', { name: /add video to your course/i, hidden: true })).toBeInTheDocument();
waffleSpy.mockRestore();
});
it('handles creating Video xblock and showing editor modal', async () => {
axiosMock
.onPost(postXBlockBaseApiUrl({ type: 'video', category: 'video', parentLocator: blockId }))
.reply(200, courseCreateXblockMock);
render(<RootWrapper />);
await waitFor(() => {
userEvent.click(screen.getByRole('button', { name: sidebarMessages.actionButtonPublishTitle.defaultMessage }));
});
axiosMock
.onPost(getXBlockBaseApiUrl(blockId), {
publish: PUBLISH_TYPES.makePublic,
})
.reply(200, { dummy: 'value' });
axiosMock
.onGet(getCourseSectionVerticalApiUrl(blockId))
.reply(200, {
...courseSectionVerticalMock,
xblock_info: {
...courseSectionVerticalMock.xblock_info,
visibility_state: UNIT_VISIBILITY_STATES.live,
has_changes: false,
published_by: userName,
},
});
await executeThunk(editCourseUnitVisibilityAndData(blockId, PUBLISH_TYPES.makePublic, true), store.dispatch);
await waitFor(() => {
// check if the sidebar status is Published and Live
expect(screen.getByText(sidebarMessages.sidebarTitlePublishedAndLive.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(
sidebarMessages.publishLastPublished.defaultMessage sidebarMessages.publishLastPublished.defaultMessage
.replace('{publishedOn}', courseSectionVerticalMock.xblock_info.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(getCourseSectionVerticalApiUrl(blockId)) .onGet(getCourseUnitApiUrl(blockId))
.reply(200, courseSectionVerticalMock); .reply(200, courseUnitIndexMock);
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(courseSectionVerticalMock.xblock_info.release_date)).toBeInTheDocument(); expect(getByText(courseUnitIndexMock.release_date)).toBeInTheDocument();
expect(screen.getByText( expect(getByText(
sidebarMessages.publishInfoDraftSaved.defaultMessage sidebarMessages.publishInfoDraftSaved.defaultMessage
.replace('{editedOn}', courseSectionVerticalMock.xblock_info.edited_on) .replace('{editedOn}', courseUnitIndexMock.edited_on)
.replace('{editedBy}', courseSectionVerticalMock.xblock_info.edited_by), .replace('{editedBy}', courseUnitIndexMock.edited_by),
)).toBeInTheDocument(); )).toBeInTheDocument();
expect(screen.getByText( expect(getByText(
sidebarMessages.releaseInfoWithSection.defaultMessage sidebarMessages.releaseInfoWithSection.defaultMessage
.replace('{sectionName}', courseSectionVerticalMock.xblock_info.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(courseSectionVerticalMock.xblock_info.release_date)).toBeInTheDocument(); expect(getByText(courseUnitIndexMock.release_date)).toBeInTheDocument();
expect(screen.getByText( expect(getByText(
sidebarMessages.publishInfoDraftSaved.defaultMessage sidebarMessages.publishInfoDraftSaved.defaultMessage
.replace('{editedOn}', courseSectionVerticalMock.xblock_info.edited_on) .replace('{editedOn}', courseUnitIndexMock.edited_on)
.replace('{editedBy}', courseSectionVerticalMock.xblock_info.edited_by), .replace('{editedBy}', courseUnitIndexMock.edited_by),
)).toBeInTheDocument(); )).toBeInTheDocument();
expect(screen.getByText( expect(getByText(
sidebarMessages.releaseInfoWithSection.defaultMessage sidebarMessages.releaseInfoWithSection.defaultMessage
.replace('{sectionName}', courseSectionVerticalMock.xblock_info.release_date_from), .replace('{sectionName}', courseUnitIndexMock.release_date_from),
)).toBeInTheDocument(); )).toBeInTheDocument();
}); });
}); });
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(courseSectionVerticalMock.xblock_info.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();
}); });
}); });
@@ -1053,16 +990,13 @@ describe('<CourseUnit />', () => {
render(<RootWrapper />); render(<RootWrapper />);
axiosMock axiosMock
.onGet(getCourseSectionVerticalApiUrl(courseId)) .onGet(getCourseUnitApiUrl(courseId))
.reply(200, { .reply(200, {
...courseSectionVerticalMock, ...courseUnitIndexMock,
xblock_info: { currently_visible_to_students: false,
...courseSectionVerticalMock.xblock_info,
currently_visible_to_students: false,
},
}); });
await executeThunk(fetchCourseSectionVerticalData(courseId), store.dispatch); await executeThunk(fetchCourseUnitQuery(courseId), store.dispatch);
await waitFor(() => { await waitFor(() => {
const alert = screen.queryAllByRole('alert').find( const alert = screen.queryAllByRole('alert').find(
@@ -1073,13 +1007,13 @@ describe('<CourseUnit />', () => {
}); });
it('should toggle visibility from sidebar and update course unit state accordingly', async () => { 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);
@@ -1099,14 +1033,11 @@ describe('<CourseUnit />', () => {
}) })
.reply(200, { dummy: 'value' }); .reply(200, { dummy: 'value' });
axiosMock axiosMock
.onGet(getCourseSectionVerticalApiUrl(blockId)) .onGet(getCourseUnitApiUrl(blockId))
.reply(200, { .reply(200, {
...courseSectionVerticalMock, ...courseUnitIndexMock,
xblock_info: { visibility_state: UNIT_VISIBILITY_STATES.staffOnly,
...courseSectionVerticalMock.xblock_info, has_explicit_staff_lock: true,
visibility_state: UNIT_VISIBILITY_STATES.staffOnly,
has_explicit_staff_lock: true,
},
}); });
await executeThunk(editCourseUnitVisibilityAndData(blockId, PUBLISH_TYPES.republish, true), store.dispatch); await executeThunk(editCourseUnitVisibilityAndData(blockId, PUBLISH_TYPES.republish, true), store.dispatch);
@@ -1119,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' });
@@ -1139,8 +1070,8 @@ describe('<CourseUnit />', () => {
}) })
.reply(200, { dummy: 'value' }); .reply(200, { dummy: 'value' });
axiosMock axiosMock
.onGet(getCourseSectionVerticalApiUrl(blockId)) .onGet(getCourseUnitApiUrl(blockId))
.reply(200, courseSectionVerticalMock); .reply(200, courseUnitIndexMock);
await executeThunk(editCourseUnitVisibilityAndData(blockId, PUBLISH_TYPES.republish, null), store.dispatch); await executeThunk(editCourseUnitVisibilityAndData(blockId, PUBLISH_TYPES.republish, null), store.dispatch);
@@ -1151,12 +1082,12 @@ describe('<CourseUnit />', () => {
}); });
it('should publish course unit after click on the "Publish" button', async () => { 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();
@@ -1169,15 +1100,12 @@ describe('<CourseUnit />', () => {
}) })
.reply(200, { dummy: 'value' }); .reply(200, { dummy: 'value' });
axiosMock axiosMock
.onGet(getCourseSectionVerticalApiUrl(blockId)) .onGet(getCourseUnitApiUrl(blockId))
.reply(200, { .reply(200, {
...courseSectionVerticalMock, ...courseUnitIndexMock,
xblock_info: { visibility_state: UNIT_VISIBILITY_STATES.live,
...courseSectionVerticalMock.xblock_info, has_changes: false,
visibility_state: UNIT_VISIBILITY_STATES.live, published_by: userName,
has_changes: false,
published_by: userName,
},
}); });
await executeThunk(editCourseUnitVisibilityAndData(blockId, PUBLISH_TYPES.makePublic, true), store.dispatch); await executeThunk(editCourseUnitVisibilityAndData(blockId, PUBLISH_TYPES.makePublic, true), store.dispatch);
@@ -1186,19 +1114,19 @@ describe('<CourseUnit />', () => {
.getByText(sidebarMessages.sidebarTitlePublishedAndLive.defaultMessage)).toBeInTheDocument(); .getByText(sidebarMessages.sidebarTitlePublishedAndLive.defaultMessage)).toBeInTheDocument();
expect(within(courseUnitSidebar).getByText( expect(within(courseUnitSidebar).getByText(
sidebarMessages.publishLastPublished.defaultMessage sidebarMessages.publishLastPublished.defaultMessage
.replace('{publishedOn}', courseSectionVerticalMock.xblock_info.published_on) .replace('{publishedOn}', courseUnitIndexMock.published_on)
.replace('{publishedBy}', userName), .replace('{publishedBy}', userName),
)).toBeInTheDocument(); )).toBeInTheDocument();
expect(publishBtn).not.toBeInTheDocument(); expect(publishBtn).not.toBeInTheDocument();
}); });
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);
@@ -1208,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();
@@ -1228,14 +1156,9 @@ describe('<CourseUnit />', () => {
}) })
.reply(200, { dummy: 'value' }); .reply(200, { dummy: 'value' });
axiosMock axiosMock
.onGet(getCourseSectionVerticalApiUrl(blockId)) .onGet(getCourseUnitApiUrl(blockId))
.reply(200, { .reply(200, {
...courseSectionVerticalMock, ...courseUnitIndexMock, published: true, has_changes: false,
xblock_info: {
...courseSectionVerticalMock.xblock_info,
published: true,
has_changes: false,
},
}); });
await executeThunk(editCourseUnitVisibilityAndData( await executeThunk(editCourseUnitVisibilityAndData(
@@ -1250,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;
@@ -1258,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)
@@ -1292,20 +1215,17 @@ describe('<CourseUnit />', () => {
}); });
axiosMock axiosMock
.onPost(getXBlockBaseApiUrl(courseSectionVerticalMock.xblock_info.id), { .onPost(getXBlockBaseApiUrl(courseUnitIndexMock.id), {
publish: null, publish: null,
metadata: { visible_to_staff_only: true, group_access: { 50: [2] }, discussion_enabled: true }, metadata: { visible_to_staff_only: true, group_access: { 50: [2] }, discussion_enabled: true },
}) })
.reply(200, { dummy: 'value' }); .reply(200, { dummy: 'value' });
axiosMock axiosMock
.onGet(getCourseSectionVerticalApiUrl(blockId)) .onGet(getCourseUnitApiUrl(blockId))
.reply(200, { .replyOnce(200, {
...courseSectionVerticalMock, ...courseUnitIndexMock,
xblock_info: { visibility_state: UNIT_VISIBILITY_STATES.staffOnly,
...courseSectionVerticalMock.xblock_info, has_explicit_staff_lock: true,
visibility_state: UNIT_VISIBILITY_STATES.staffOnly,
has_explicit_staff_lock: true,
},
}); });
const modalSaveBtn = within(configureModal) const modalSaveBtn = within(configureModal)
@@ -1326,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 () => {
@@ -1335,28 +1255,28 @@ 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(getCourseSectionVerticalApiUrl(blockId)) .onGet(getCourseUnitApiUrl(courseId))
.reply(200, { .reply(200, {
...courseSectionVerticalMock, ...courseUnitIndexMock,
xblock_info: { enable_copy_paste_units: true,
...courseSectionVerticalMock.xblock_info,
enable_copy_paste_units: true,
},
}); });
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);
@@ -1367,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);
}); });
@@ -1383,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;
@@ -1394,27 +1314,30 @@ describe('<CourseUnit />', () => {
}); });
it('should increase the number of course XBlocks after copying and pasting a block', async () => { it('should increase the number of course XBlocks after copying and pasting a block', async () => {
render(<RootWrapper />); const { getByRole, getByTitle } = render(<RootWrapper />);
simulatePostMessageEvent(messageTypes.copyXBlock, { simulatePostMessageEvent(messageTypes.copyXBlock, {
id: courseVerticalChildrenMock.children[0].block_id, id: courseVerticalChildrenMock.children[0].block_id,
}); });
axiosMock axiosMock
.onGet(getCourseSectionVerticalApiUrl(blockId)) .onGet(getClipboardUrl())
.reply(200, clipboardXBlock);
axiosMock
.onGet(getCourseUnitApiUrl(courseId))
.reply(200, { .reply(200, {
...courseSectionVerticalMock, ...courseUnitIndexMock,
xblock_info: { enable_copy_paste_units: true,
...courseSectionVerticalMock.xblock_info,
enable_copy_paste_units: true,
},
}); });
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(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
@@ -1450,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
@@ -1460,22 +1383,22 @@ 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(getCourseSectionVerticalApiUrl(blockId)) .onGet(getCourseUnitApiUrl(courseId))
.reply(200, { .reply(200, {
...courseSectionVerticalMock, ...courseUnitIndexMock,
xblock_info: { enable_copy_paste_units: true,
...courseSectionVerticalMock.xblock_info,
enable_copy_paste_units: true,
},
}); });
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];
@@ -1494,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();
@@ -1508,26 +1431,26 @@ 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(getCourseSectionVerticalApiUrl(blockId)) .onGet(getCourseUnitApiUrl(courseId))
.reply(200, { .reply(200, {
...courseSectionVerticalMock, ...courseUnitIndexMock,
xblock_info: { enable_copy_paste_units: true,
...courseSectionVerticalMock.xblock_info,
enable_copy_paste_units: true,
},
}); });
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];
@@ -1548,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();
@@ -1562,26 +1485,26 @@ 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(getCourseSectionVerticalApiUrl(blockId)) .onGet(getCourseUnitApiUrl(courseId))
.reply(200, { .reply(200, {
...courseSectionVerticalMock, ...courseUnitIndexMock,
xblock_info: { enable_copy_paste_units: true,
...courseSectionVerticalMock.xblock_info,
enable_copy_paste_units: true,
},
}); });
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];
@@ -1602,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();
@@ -1611,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))
@@ -1626,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();
}); });
@@ -1663,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);
@@ -1677,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);
@@ -1698,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();
@@ -1706,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();
@@ -1714,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();
@@ -1722,15 +1650,17 @@ 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())
.reply(200, {}); .reply(200, {});
axiosMock axiosMock
.onGet(getCourseSectionVerticalApiUrl(blockId)) .onGet(getCourseUnitApiUrl(blockId))
.reply(200, courseSectionVerticalMock); .reply(200, courseUnitIndexMock);
await screen.findByText(unitDisplayName); await screen.findByText(unitDisplayName);
@@ -1746,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();
@@ -1754,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();
@@ -1763,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();
@@ -1784,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())
@@ -1801,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();
@@ -1820,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();
@@ -1831,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())
@@ -1846,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);
@@ -1859,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();
@@ -1876,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();
@@ -1885,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,
@@ -1896,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,
@@ -1904,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 () => {
@@ -1915,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();
}); });
@@ -1931,13 +1872,13 @@ describe('<CourseUnit />', () => {
}); });
}); });
const configureModal = await waitFor(() => screen.getByTestId('configure-modal')); const configureModal = await waitFor(() => getByTestId('configure-modal'));
expect(configureModal).toBeInTheDocument(); expect(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,
}); });
@@ -1967,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);
}); });
@@ -2012,6 +1953,7 @@ describe('<CourseUnit />', () => {
describe('Library Content page', () => { describe('Library Content page', () => {
const newUnitId = '12345'; const newUnitId = '12345';
const sequenceId = courseSectionVerticalMock.subsection_location;
beforeEach(async () => { beforeEach(async () => {
axiosMock axiosMock
@@ -2028,6 +1970,20 @@ describe('<CourseUnit />', () => {
}, },
}); });
await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch); await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch);
axiosMock
.onGet(getCourseUnitApiUrl(courseId))
.reply(200, {
...courseUnitIndexMock,
category: 'library_content',
ancestor_info: {
...courseUnitIndexMock.ancestor_info,
child_info: {
...courseUnitIndexMock.ancestor_info.child_info,
category: 'library_content',
},
},
});
await executeThunk(fetchCourseUnitQuery(courseId), store.dispatch);
}); });
it('navigates to library content page on receive window event', async () => { it('navigates to library content page on receive window event', async () => {
@@ -2047,8 +2003,8 @@ describe('<CourseUnit />', () => {
findByTestId, findByTestId,
} = render(<RootWrapper />); } = render(<RootWrapper />);
const currentSectionName = courseSectionVerticalMock.xblock_info.ancestor_info.ancestors[1].display_name; const currentSectionName = courseUnitIndexMock.ancestor_info.ancestors[1].display_name;
const currentSubSectionName = courseSectionVerticalMock.xblock_info.ancestor_info.ancestors[1].display_name; const currentSubSectionName = courseUnitIndexMock.ancestor_info.ancestors[1].display_name;
const unitHeaderTitle = await findByTestId('unit-header-title'); const unitHeaderTitle = await findByTestId('unit-header-title');
await findByText(unitDisplayName); await findByText(unitDisplayName);
@@ -2076,6 +2032,7 @@ describe('<CourseUnit />', () => {
describe('Split Test Content page', () => { describe('Split Test Content page', () => {
const newUnitId = '12345'; const newUnitId = '12345';
const sequenceId = courseSectionVerticalMock.subsection_location;
beforeEach(async () => { beforeEach(async () => {
axiosMock axiosMock
@@ -2092,6 +2049,20 @@ describe('<CourseUnit />', () => {
}, },
}); });
await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch); await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch);
axiosMock
.onGet(getCourseUnitApiUrl(courseId))
.reply(200, {
...courseUnitIndexMock,
category: 'split_test',
ancestor_info: {
...courseUnitIndexMock.ancestor_info,
child_info: {
...courseUnitIndexMock.ancestor_info.child_info,
category: 'split_test',
},
},
});
await executeThunk(fetchCourseUnitQuery(courseId), store.dispatch);
}); });
it('navigates to split test content page on receive window event', async () => { it('navigates to split test content page on receive window event', async () => {
@@ -2134,65 +2105,46 @@ describe('<CourseUnit />', () => {
}); });
it('should render split test content page correctly', async () => { it('should render split test content page correctly', async () => {
render(<RootWrapper />); const {
getByText,
getByRole,
queryByRole,
getByTestId,
queryByText,
} = render(<RootWrapper />);
const currentSectionName = courseSectionVerticalMock.xblock_info.ancestor_info.ancestors[1].display_name; const currentSectionName = courseUnitIndexMock.ancestor_info.ancestors[1].display_name;
const currentSubSectionName = courseSectionVerticalMock.xblock_info.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 }) => {
@@ -2200,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);
}); });
}); });
@@ -2213,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;
@@ -2222,13 +2174,6 @@ describe('<CourseUnit />', () => {
? { ...child, block_type: 'html' } ? { ...child, block_type: 'html' }
: child)); : child));
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);
@@ -2236,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,
@@ -2250,58 +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(getCourseSectionVerticalApiUrl(courseId))
.reply(200, {
...courseSectionVerticalMock,
xblock_info: {
...courseSectionVerticalMock.xblock_info,
upstreamInfo: {
...courseSectionVerticalMock.xblock_info,
upstreamRef: 'lct:org:lib:unit:unit-1',
upstreamLink: 'some-link',
},
},
});
await executeThunk(fetchCourseSectionVerticalData(courseId), store.dispatch);
expect(screen.getByText(/this unit can only be edited from the \./i)).toBeInTheDocument();
// Disable the "Edit" button
const unitHeaderTitle = screen.getByTestId('unit-header-title');
const editButton = within(unitHeaderTitle).getByRole(
'button',
{ name: 'Edit' },
);
expect(editButton).toBeInTheDocument();
expect(editButton).toBeDisabled();
// The "Publish" button should still be enabled
const courseUnitSidebar = screen.getByTestId('course-unit-sidebar');
const publishButton = within(courseUnitSidebar).getByRole(
'button',
{ name: sidebarMessages.actionButtonPublishTitle.defaultMessage },
);
expect(publishButton).toBeInTheDocument();
expect(publishButton).toBeEnabled();
// Disable the "Manage Tags" button
const manageTagsButton = screen.getByRole(
'button',
{ name: tagsDrawerMessages.manageTagsButton.defaultMessage },
);
expect(manageTagsButton).toBeInTheDocument();
expect(manageTagsButton).toBeDisabled();
// Does not render the "Add Components" section
expect(screen.queryByText(addComponentMessages.title.defaultMessage)).not.toBeInTheDocument();
});
}); });

View File

@@ -0,0 +1,1123 @@
module.exports = {
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@867dddb6f55d410caaa9c1eb9c6743ec',
display_name: 'Getting Started',
category: 'vertical',
has_children: true,
edited_on: 'Jan 03, 2024 at 12:06 UTC',
published: true,
published_on: 'Dec 28, 2023 at 10:00 UTC',
studio_url: '/container/block-v1:edX+DemoX+Demo_Course+type@vertical+block@867dddb6f55d410caaa9c1eb9c6743ec',
released_to_students: true,
release_date: 'Feb 05, 2013 at 05:00 UTC',
visibility_state: 'needs_attention',
has_explicit_staff_lock: false,
start: '2013-02-05T05:00:00Z',
graded: false,
due_date: '',
due: null,
relative_weeks_due: null,
format: null,
course_graders: [
'Homework',
'Exam',
],
has_changes: true,
actions: {
deletable: true,
draggable: true,
childAddable: true,
duplicable: true,
},
explanatory_message: null,
group_access: {},
user_partitions: [
{
id: 50,
name: 'Enrollment Track Groups',
scheme: 'enrollment_track',
groups: [
{
id: 2,
name: 'Verified Certificate',
selected: false,
deleted: false,
},
{
id: 1,
name: 'Audit',
selected: false,
deleted: false,
},
],
},
],
show_correctness: 'always',
discussion_enabled: true,
data: '',
metadata: {
display_name: 'Getting Started',
xml_attributes: {
filename: [
'vertical/867dddb6f55d410caaa9c1eb9c6743ec.xml',
'vertical/867dddb6f55d410caaa9c1eb9c6743ec.xml',
],
},
},
ancestor_info: {
ancestors: [
{
id: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@19a30717eff543078a5d94ae9d6c18a5',
display_name: 'Lesson 1 - Getting Started',
category: 'sequential',
has_children: true,
edited_on: 'Jan 03, 2024 at 12:06 UTC',
published: true,
published_on: 'Dec 28, 2023 at 10:00 UTC',
studio_url: '/course/course-v1:edX+DemoX+Demo_Course?show=block-v1%3AedX%2BDemoX%2BDemo_Course%2Btype%40sequential%2Bblock%4019a30717eff543078a5d94ae9d6c18a5',
released_to_students: true,
release_date: 'Feb 05, 2013 at 05:00 UTC',
visibility_state: 'needs_attention',
has_explicit_staff_lock: false,
start: '2013-02-05T05:00:00Z',
graded: false,
due_date: '',
due: null,
relative_weeks_due: null,
format: null,
course_graders: [
'Homework',
'Exam',
],
has_changes: null,
actions: {
deletable: true,
draggable: true,
childAddable: true,
duplicable: true,
},
explanatory_message: null,
group_access: {},
user_partitions: [
{
id: 50,
name: 'Enrollment Track Groups',
scheme: 'enrollment_track',
groups: [
{
id: 2,
name: 'Verified Certificate',
selected: false,
deleted: false,
},
{
id: 1,
name: 'Audit',
selected: false,
deleted: false,
},
],
},
],
show_correctness: 'always',
hide_after_due: false,
is_proctored_exam: false,
was_exam_ever_linked_with_external: false,
online_proctoring_rules: '',
is_practice_exam: false,
is_onboarding_exam: false,
is_time_limited: false,
exam_review_rules: '',
default_time_limit_minutes: null,
proctoring_exam_configuration_link: null,
supports_onboarding: false,
show_review_rules: true,
child_info: {
category: 'vertical',
display_name: 'Unit',
children: [
{
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@867dddb6f55d410caaa9c1eb9c6743ec',
display_name: 'Getting Started',
category: 'vertical',
has_children: true,
edited_on: 'Jan 03, 2024 at 12:06 UTC',
published: true,
published_on: 'Dec 28, 2023 at 10:00 UTC',
studio_url: '/container/block-v1:edX+DemoX+Demo_Course+type@vertical+block@867dddb6f55d410caaa9c1eb9c6743ec',
released_to_students: true,
release_date: 'Feb 05, 2013 at 05:00 UTC',
visibility_state: 'needs_attention',
has_explicit_staff_lock: false,
start: '2013-02-05T05:00:00Z',
graded: false,
due_date: '',
due: null,
relative_weeks_due: null,
format: null,
course_graders: [
'Homework',
'Exam',
],
has_changes: true,
actions: {
deletable: true,
draggable: true,
childAddable: true,
duplicable: true,
},
explanatory_message: null,
group_access: {},
user_partitions: [
{
id: 50,
name: 'Enrollment Track Groups',
scheme: 'enrollment_track',
groups: [
{
id: 2,
name: 'Verified Certificate',
selected: false,
deleted: false,
},
{
id: 1,
name: 'Audit',
selected: false,
deleted: false,
},
],
},
],
show_correctness: 'always',
discussion_enabled: true,
ancestor_has_staff_lock: false,
user_partition_info: {
selectable_partitions: [
{
id: 50,
name: 'Enrollment Track Groups',
scheme: 'enrollment_track',
groups: [
{
id: 2,
name: 'Verified Certificate',
selected: false,
deleted: false,
},
{
id: 1,
name: 'Audit',
selected: false,
deleted: false,
},
],
},
],
selected_partition_index: -1,
selected_groups_label: '',
},
enable_copy_paste_units: false,
},
{
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@4f6c1b4e316a419ab5b6bf30e6c708e9',
display_name: 'Working with Videos',
category: 'vertical',
has_children: true,
edited_on: 'Dec 28, 2023 at 10:00 UTC',
published: true,
published_on: 'Dec 28, 2023 at 10:00 UTC',
studio_url: '/container/block-v1:edX+DemoX+Demo_Course+type@vertical+block@4f6c1b4e316a419ab5b6bf30e6c708e9',
released_to_students: true,
release_date: 'Feb 05, 2013 at 05:00 UTC',
visibility_state: 'live',
has_explicit_staff_lock: false,
start: '2013-02-05T05:00:00Z',
graded: false,
due_date: '',
due: null,
relative_weeks_due: null,
format: null,
course_graders: [
'Homework',
'Exam',
],
has_changes: false,
actions: {
deletable: true,
draggable: true,
childAddable: true,
duplicable: true,
},
explanatory_message: null,
group_access: {},
user_partitions: [
{
id: 50,
name: 'Enrollment Track Groups',
scheme: 'enrollment_track',
groups: [
{
id: 2,
name: 'Verified Certificate',
selected: false,
deleted: false,
},
{
id: 1,
name: 'Audit',
selected: false,
deleted: false,
},
],
},
],
show_correctness: 'always',
discussion_enabled: true,
ancestor_has_staff_lock: false,
user_partition_info: {
selectable_partitions: [
{
id: 50,
name: 'Enrollment Track Groups',
scheme: 'enrollment_track',
groups: [
{
id: 2,
name: 'Verified Certificate',
selected: false,
deleted: false,
},
{
id: 1,
name: 'Audit',
selected: false,
deleted: false,
},
],
},
],
selected_partition_index: -1,
selected_groups_label: '',
},
enable_copy_paste_units: false,
},
{
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@3dc16db8d14842e38324e95d4030b8a0',
display_name: 'Videos on edX',
category: 'vertical',
has_children: true,
edited_on: 'Dec 28, 2023 at 10:00 UTC',
published: true,
published_on: 'Dec 28, 2023 at 10:00 UTC',
studio_url: '/container/block-v1:edX+DemoX+Demo_Course+type@vertical+block@3dc16db8d14842e38324e95d4030b8a0',
released_to_students: true,
release_date: 'Feb 05, 2013 at 05:00 UTC',
visibility_state: 'live',
has_explicit_staff_lock: false,
start: '2013-02-05T05:00:00Z',
graded: false,
due_date: '',
due: null,
relative_weeks_due: null,
format: null,
course_graders: [
'Homework',
'Exam',
],
has_changes: false,
actions: {
deletable: true,
draggable: true,
childAddable: true,
duplicable: true,
},
explanatory_message: null,
group_access: {},
user_partitions: [
{
id: 50,
name: 'Enrollment Track Groups',
scheme: 'enrollment_track',
groups: [
{
id: 2,
name: 'Verified Certificate',
selected: false,
deleted: false,
},
{
id: 1,
name: 'Audit',
selected: false,
deleted: false,
},
],
},
],
show_correctness: 'always',
discussion_enabled: true,
ancestor_has_staff_lock: false,
user_partition_info: {
selectable_partitions: [
{
id: 50,
name: 'Enrollment Track Groups',
scheme: 'enrollment_track',
groups: [
{
id: 2,
name: 'Verified Certificate',
selected: false,
deleted: false,
},
{
id: 1,
name: 'Audit',
selected: false,
deleted: false,
},
],
},
],
selected_partition_index: -1,
selected_groups_label: '',
},
enable_copy_paste_units: false,
},
{
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@4a1bba2a403f40bca5ec245e945b0d76',
display_name: 'Video Demonstrations',
category: 'vertical',
has_children: true,
edited_on: 'Dec 28, 2023 at 10:00 UTC',
published: true,
published_on: 'Dec 28, 2023 at 10:00 UTC',
studio_url: '/container/block-v1:edX+DemoX+Demo_Course+type@vertical+block@4a1bba2a403f40bca5ec245e945b0d76',
released_to_students: true,
release_date: 'Feb 05, 2013 at 05:00 UTC',
visibility_state: 'live',
has_explicit_staff_lock: false,
start: '2013-02-05T05:00:00Z',
graded: false,
due_date: '',
due: null,
relative_weeks_due: null,
format: null,
course_graders: [
'Homework',
'Exam',
],
has_changes: false,
actions: {
deletable: true,
draggable: true,
childAddable: true,
duplicable: true,
},
explanatory_message: null,
group_access: {},
user_partitions: [
{
id: 50,
name: 'Enrollment Track Groups',
scheme: 'enrollment_track',
groups: [
{
id: 2,
name: 'Verified Certificate',
selected: false,
deleted: false,
},
{
id: 1,
name: 'Audit',
selected: false,
deleted: false,
},
],
},
],
show_correctness: 'always',
discussion_enabled: true,
ancestor_has_staff_lock: false,
user_partition_info: {
selectable_partitions: [
{
id: 50,
name: 'Enrollment Track Groups',
scheme: 'enrollment_track',
groups: [
{
id: 2,
name: 'Verified Certificate',
selected: false,
deleted: false,
},
{
id: 1,
name: 'Audit',
selected: false,
deleted: false,
},
],
},
],
selected_partition_index: -1,
selected_groups_label: '',
},
enable_copy_paste_units: false,
},
{
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@256f17a44983429fb1a60802203ee4e0',
display_name: 'Video Presentation Styles',
category: 'vertical',
has_children: true,
edited_on: 'Dec 28, 2023 at 10:00 UTC',
published: true,
published_on: 'Dec 28, 2023 at 10:00 UTC',
studio_url: '/container/block-v1:edX+DemoX+Demo_Course+type@vertical+block@256f17a44983429fb1a60802203ee4e0',
released_to_students: true,
release_date: 'Feb 05, 2013 at 05:00 UTC',
visibility_state: 'live',
has_explicit_staff_lock: false,
start: '2013-02-05T05:00:00Z',
graded: false,
due_date: '',
due: null,
relative_weeks_due: null,
format: null,
course_graders: [
'Homework',
'Exam',
],
has_changes: false,
actions: {
deletable: true,
draggable: true,
childAddable: true,
duplicable: true,
},
explanatory_message: null,
group_access: {},
user_partitions: [
{
id: 50,
name: 'Enrollment Track Groups',
scheme: 'enrollment_track',
groups: [
{
id: 2,
name: 'Verified Certificate',
selected: false,
deleted: false,
},
{
id: 1,
name: 'Audit',
selected: false,
deleted: false,
},
],
},
],
show_correctness: 'always',
discussion_enabled: true,
ancestor_has_staff_lock: false,
user_partition_info: {
selectable_partitions: [
{
id: 50,
name: 'Enrollment Track Groups',
scheme: 'enrollment_track',
groups: [
{
id: 2,
name: 'Verified Certificate',
selected: false,
deleted: false,
},
{
id: 1,
name: 'Audit',
selected: false,
deleted: false,
},
],
},
],
selected_partition_index: -1,
selected_groups_label: '',
},
enable_copy_paste_units: false,
},
{
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@e3601c0abee6427d8c17e6d6f8fdddd1',
display_name: 'Interactive Questions',
category: 'vertical',
has_children: true,
edited_on: 'Dec 28, 2023 at 10:00 UTC',
published: true,
published_on: 'Dec 28, 2023 at 10:00 UTC',
studio_url: '/container/block-v1:edX+DemoX+Demo_Course+type@vertical+block@e3601c0abee6427d8c17e6d6f8fdddd1',
released_to_students: true,
release_date: 'Feb 05, 2013 at 05:00 UTC',
visibility_state: 'live',
has_explicit_staff_lock: false,
start: '2013-02-05T05:00:00Z',
graded: false,
due_date: '',
due: null,
relative_weeks_due: null,
format: null,
course_graders: [
'Homework',
'Exam',
],
has_changes: false,
actions: {
deletable: true,
draggable: true,
childAddable: true,
duplicable: true,
},
explanatory_message: null,
group_access: {},
user_partitions: [
{
id: 50,
name: 'Enrollment Track Groups',
scheme: 'enrollment_track',
groups: [
{
id: 2,
name: 'Verified Certificate',
selected: false,
deleted: false,
},
{
id: 1,
name: 'Audit',
selected: false,
deleted: false,
},
],
},
],
show_correctness: 'always',
discussion_enabled: true,
ancestor_has_staff_lock: false,
user_partition_info: {
selectable_partitions: [
{
id: 50,
name: 'Enrollment Track Groups',
scheme: 'enrollment_track',
groups: [
{
id: 2,
name: 'Verified Certificate',
selected: false,
deleted: false,
},
{
id: 1,
name: 'Audit',
selected: false,
deleted: false,
},
],
},
],
selected_partition_index: -1,
selected_groups_label: '',
},
enable_copy_paste_units: false,
},
{
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@a79d59cd72034188a71d388f4954a606',
display_name: 'Exciting Labs and Tools',
category: 'vertical',
has_children: true,
edited_on: 'Dec 28, 2023 at 10:00 UTC',
published: true,
published_on: 'Dec 28, 2023 at 10:00 UTC',
studio_url: '/container/block-v1:edX+DemoX+Demo_Course+type@vertical+block@a79d59cd72034188a71d388f4954a606',
released_to_students: true,
release_date: 'Feb 05, 2013 at 05:00 UTC',
visibility_state: 'live',
has_explicit_staff_lock: false,
start: '2013-02-05T05:00:00Z',
graded: false,
due_date: '',
due: null,
relative_weeks_due: null,
format: null,
course_graders: [
'Homework',
'Exam',
],
has_changes: false,
actions: {
deletable: true,
draggable: true,
childAddable: true,
duplicable: true,
},
explanatory_message: null,
group_access: {},
user_partitions: [
{
id: 50,
name: 'Enrollment Track Groups',
scheme: 'enrollment_track',
groups: [
{
id: 2,
name: 'Verified Certificate',
selected: false,
deleted: false,
},
{
id: 1,
name: 'Audit',
selected: false,
deleted: false,
},
],
},
],
show_correctness: 'always',
discussion_enabled: true,
ancestor_has_staff_lock: false,
user_partition_info: {
selectable_partitions: [
{
id: 50,
name: 'Enrollment Track Groups',
scheme: 'enrollment_track',
groups: [
{
id: 2,
name: 'Verified Certificate',
selected: false,
deleted: false,
},
{
id: 1,
name: 'Audit',
selected: false,
deleted: false,
},
],
},
],
selected_partition_index: -1,
selected_groups_label: '',
},
enable_copy_paste_units: false,
},
{
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@134df56c516a4a0dbb24dd5facef746e',
display_name: 'Reading Assignments',
category: 'vertical',
has_children: true,
edited_on: 'Dec 28, 2023 at 10:00 UTC',
published: true,
published_on: 'Dec 28, 2023 at 10:00 UTC',
studio_url: '/container/block-v1:edX+DemoX+Demo_Course+type@vertical+block@134df56c516a4a0dbb24dd5facef746e',
released_to_students: true,
release_date: 'Feb 05, 2013 at 05:00 UTC',
visibility_state: 'live',
has_explicit_staff_lock: false,
start: '2013-02-05T05:00:00Z',
graded: false,
due_date: '',
due: null,
relative_weeks_due: null,
format: null,
course_graders: [
'Homework',
'Exam',
],
has_changes: false,
actions: {
deletable: true,
draggable: true,
childAddable: true,
duplicable: true,
},
explanatory_message: null,
group_access: {},
user_partitions: [
{
id: 50,
name: 'Enrollment Track Groups',
scheme: 'enrollment_track',
groups: [
{
id: 2,
name: 'Verified Certificate',
selected: false,
deleted: false,
},
{
id: 1,
name: 'Audit',
selected: false,
deleted: false,
},
],
},
],
show_correctness: 'always',
discussion_enabled: true,
ancestor_has_staff_lock: false,
user_partition_info: {
selectable_partitions: [
{
id: 50,
name: 'Enrollment Track Groups',
scheme: 'enrollment_track',
groups: [
{
id: 2,
name: 'Verified Certificate',
selected: false,
deleted: false,
},
{
id: 1,
name: 'Audit',
selected: false,
deleted: false,
},
],
},
],
selected_partition_index: -1,
selected_groups_label: '',
},
enable_copy_paste_units: false,
},
{
id: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@d91b9e5d8bc64d57a1332d06bf2f2193',
display_name: 'When Are Your Exams? ',
category: 'vertical',
has_children: true,
edited_on: 'Dec 28, 2023 at 10:00 UTC',
published: true,
published_on: 'Dec 28, 2023 at 10:00 UTC',
studio_url: '/container/block-v1:edX+DemoX+Demo_Course+type@vertical+block@d91b9e5d8bc64d57a1332d06bf2f2193',
released_to_students: true,
release_date: 'Feb 05, 2013 at 05:00 UTC',
visibility_state: 'live',
has_explicit_staff_lock: false,
start: '2013-02-05T05:00:00Z',
graded: false,
due_date: '',
due: null,
relative_weeks_due: null,
format: null,
course_graders: [
'Homework',
'Exam',
],
has_changes: false,
actions: {
deletable: true,
draggable: true,
childAddable: true,
duplicable: true,
},
explanatory_message: null,
group_access: {},
user_partitions: [
{
id: 50,
name: 'Enrollment Track Groups',
scheme: 'enrollment_track',
groups: [
{
id: 2,
name: 'Verified Certificate',
selected: false,
deleted: false,
},
{
id: 1,
name: 'Audit',
selected: false,
deleted: false,
},
],
},
],
show_correctness: 'always',
discussion_enabled: true,
ancestor_has_staff_lock: false,
user_partition_info: {
selectable_partitions: [
{
id: 50,
name: 'Enrollment Track Groups',
scheme: 'enrollment_track',
groups: [
{
id: 2,
name: 'Verified Certificate',
selected: false,
deleted: false,
},
{
id: 1,
name: 'Audit',
selected: false,
deleted: false,
},
],
},
],
selected_partition_index: -1,
selected_groups_label: '',
},
enable_copy_paste_units: false,
},
],
},
ancestor_has_staff_lock: false,
user_partition_info: {
selectable_partitions: [
{
id: 50,
name: 'Enrollment Track Groups',
scheme: 'enrollment_track',
groups: [
{
id: 2,
name: 'Verified Certificate',
selected: false,
deleted: false,
},
{
id: 1,
name: 'Audit',
selected: false,
deleted: false,
},
],
},
],
selected_partition_index: -1,
selected_groups_label: '',
},
},
{
id: 'block-v1:edX+DemoX+Demo_Course+type@chapter+block@graded_interactions',
display_name: 'Example Week 1: Getting Started',
category: 'chapter',
has_children: true,
edited_on: 'Jan 03, 2024 at 12:06 UTC',
published: true,
published_on: 'Dec 28, 2023 at 10:00 UTC',
studio_url: '/course/course-v1:edX+DemoX+Demo_Course?show=block-v1%3AedX%2BDemoX%2BDemo_Course%2Btype%40chapter%2Bblock%40interactive_demonstrations',
released_to_students: true,
release_date: 'Feb 05, 2013 at 05:00 UTC',
visibility_state: 'live',
has_explicit_staff_lock: false,
start: '2013-02-05T05:00:00Z',
graded: false,
due_date: '',
due: null,
relative_weeks_due: null,
format: null,
course_graders: [
'Homework',
'Exam',
],
has_changes: null,
actions: {
deletable: true,
draggable: true,
childAddable: true,
duplicable: true,
},
explanatory_message: null,
group_access: {},
user_partitions: [
{
id: 50,
name: 'Enrollment Track Groups',
scheme: 'enrollment_track',
groups: [
{
id: 2,
name: 'Verified Certificate',
selected: false,
deleted: false,
},
{
id: 1,
name: 'Audit',
selected: false,
deleted: false,
},
],
},
],
show_correctness: 'always',
highlights: [],
highlights_enabled: true,
highlights_preview_only: false,
highlights_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/manage_course_highlight_emails.html',
ancestor_has_staff_lock: false,
user_partition_info: {
selectable_partitions: [
{
id: 50,
name: 'Enrollment Track Groups',
scheme: 'enrollment_track',
groups: [
{
id: 2,
name: 'Verified Certificate',
selected: false,
deleted: false,
},
{
id: 1,
name: 'Audit',
selected: false,
deleted: false,
},
],
},
],
selected_partition_index: -1,
selected_groups_label: '',
},
},
{
id: 'block-v1:edX+DemoX+Demo_Course+type@course+block@course',
display_name: 'Demonstration Course',
category: 'course',
has_children: true,
unit_level_discussions: false,
edited_on: 'Jan 03, 2024 at 12:06 UTC',
published: true,
published_on: 'Jan 03, 2024 at 08:57 UTC',
studio_url: '/course/course-v1:edX+DemoX+Demo_Course',
released_to_students: true,
release_date: 'Feb 05, 2013 at 05:00 UTC',
visibility_state: null,
has_explicit_staff_lock: false,
start: '2013-02-05T05:00:00Z',
graded: false,
due_date: '',
due: null,
relative_weeks_due: null,
format: null,
course_graders: [
'Homework',
'Exam',
],
has_changes: null,
actions: {
deletable: true,
draggable: true,
childAddable: true,
duplicable: true,
},
explanatory_message: null,
group_access: {},
user_partitions: [
{
id: 50,
name: 'Enrollment Track Groups',
scheme: 'enrollment_track',
groups: [
{
id: 2,
name: 'Verified Certificate',
selected: false,
deleted: false,
},
{
id: 1,
name: 'Audit',
selected: false,
deleted: false,
},
],
},
],
show_correctness: 'always',
highlights_enabled_for_messaging: false,
highlights_enabled: true,
highlights_preview_only: false,
highlights_doc_url: 'https://docs.openedx.org/en/latest/educators/how-tos/course_development/manage_course_highlight_emails.html',
enable_proctored_exams: false,
create_zendesk_tickets: true,
enable_timed_exams: true,
ancestor_has_staff_lock: false,
user_partition_info: {
selectable_partitions: [
{
id: 50,
name: 'Enrollment Track Groups',
scheme: 'enrollment_track',
groups: [
{
id: 2,
name: 'Verified Certificate',
selected: false,
deleted: false,
},
{
id: 1,
name: 'Audit',
selected: false,
deleted: false,
},
],
},
],
selected_partition_index: -1,
selected_groups_label: '',
},
},
],
},
ancestor_has_staff_lock: false,
user_partition_info: {
selectable_partitions: [
{
id: 50,
name: 'Enrollment Track Groups',
scheme: 'enrollment_track',
groups: [
{
id: 2,
name: 'Verified Certificate',
selected: false,
deleted: false,
},
{
id: 1,
name: 'Audit',
selected: false,
deleted: false,
},
],
},
],
selected_partition_index: -1,
selected_groups_label: '',
},
enable_copy_paste_units: false,
edited_by: 'edx',
published_by: null,
currently_visible_to_students: true,
has_partition_group_components: false,
release_date_from: 'Section "Example Week 1: Getting Started"',
staff_lock_from: null,
};

View File

@@ -1,3 +1,4 @@
export { default as courseUnitIndexMock } from './courseUnitIndex';
export { default as courseSectionVerticalMock } from './courseSectionVertical'; export { default as courseSectionVerticalMock } from './courseSectionVertical';
export { default as courseUnitMock } from './courseUnit'; export { default as courseUnitMock } from './courseUnit';
export { default as courseCreateXblockMock } from './courseCreateXblock'; export { default as courseCreateXblockMock } from './courseCreateXblock';

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

@@ -5,11 +5,11 @@ import {
} from '../../testUtils'; } from '../../testUtils';
import { executeThunk } from '../../utils'; import { executeThunk } from '../../utils';
import { getCourseSectionVerticalApiUrl } from '../data/api'; import { getCourseSectionVerticalApiUrl, getCourseUnitApiUrl } from '../data/api';
import { getApiWaffleFlagsUrl } from '../../data/api'; import { getApiWaffleFlagsUrl } from '../../data/api';
import { fetchWaffleFlags } from '../../data/thunks'; import { fetchWaffleFlags } from '../../data/thunks';
import { fetchCourseSectionVerticalData } from '../data/thunk'; import { fetchCourseSectionVerticalData, fetchCourseUnitQuery } from '../data/thunk';
import { courseSectionVerticalMock } from '../__mocks__'; import { courseSectionVerticalMock, courseUnitIndexMock } from '../__mocks__';
import Breadcrumbs from './Breadcrumbs'; import Breadcrumbs from './Breadcrumbs';
let axiosMock; let axiosMock;
@@ -43,9 +43,9 @@ describe('<Breadcrumbs />', () => {
reduxStore = mocks.reduxStore; reduxStore = mocks.reduxStore;
axiosMock axiosMock
.onGet(getCourseSectionVerticalApiUrl(courseId)) .onGet(getCourseUnitApiUrl(courseId))
.reply(200, courseSectionVerticalMock); .reply(200, courseUnitIndexMock);
await executeThunk(fetchCourseSectionVerticalData(courseId), reduxStore.dispatch); await executeThunk(fetchCourseUnitQuery(courseId), reduxStore.dispatch);
axiosMock axiosMock
.onGet(getCourseSectionVerticalApiUrl(courseId)) .onGet(getCourseSectionVerticalApiUrl(courseId))
.reply(200, courseSectionVerticalMock); .reply(200, courseSectionVerticalMock);

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,10 +3,11 @@ 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;
export const getCourseUnitApiUrl = (itemId) => `${getStudioBaseUrl()}/xblock/container/${itemId}`;
export const getXBlockBaseApiUrl = (itemId) => `${getStudioBaseUrl()}/xblock/${itemId}`; export const getXBlockBaseApiUrl = (itemId) => `${getStudioBaseUrl()}/xblock/${itemId}`;
export const getCourseSectionVerticalApiUrl = (itemId) => `${getStudioBaseUrl()}/api/contentstore/v1/container_handler/${itemId}`; export const getCourseSectionVerticalApiUrl = (itemId) => `${getStudioBaseUrl()}/api/contentstore/v1/container_handler/${itemId}`;
export const getCourseVerticalChildrenApiUrl = (itemId) => `${getStudioBaseUrl()}/api/contentstore/v1/container/vertical/${itemId}/children`; export const getCourseVerticalChildrenApiUrl = (itemId) => `${getStudioBaseUrl()}/api/contentstore/v1/container/vertical/${itemId}/children`;
@@ -14,6 +15,18 @@ export const getCourseOutlineInfoUrl = (courseId) => `${getStudioBaseUrl()}/cour
export const postXBlockBaseApiUrl = () => `${getStudioBaseUrl()}/xblock/`; export const postXBlockBaseApiUrl = () => `${getStudioBaseUrl()}/xblock/`;
export const libraryBlockChangesUrl = (blockId) => `${getStudioBaseUrl()}/api/contentstore/v2/downstreams/${blockId}/sync`; export const libraryBlockChangesUrl = (blockId) => `${getStudioBaseUrl()}/api/contentstore/v2/downstreams/${blockId}/sync`;
/**
* Get course unit.
* @param {string} unitId
* @returns {Promise<Object>}
*/
export async function getCourseUnitData(unitId) {
const { data } = await getAuthenticatedHttpClient()
.get(getCourseUnitApiUrl(unitId));
return camelCaseObject(data);
}
/** /**
* Edit course unit display name. * Edit course unit display name.
* @param {string} unitId * @param {string} unitId
@@ -32,18 +45,15 @@ export async function editUnitDisplayName(unitId, displayName) {
} }
/** /**
* Fetch vertical block data from the container_handler endpoint. * Get an object containing course section vertical data.
* @param {string} unitId * @param {string} unitId
* @returns {Promise<Object>} * @returns {Promise<Object>}
*/ */
export async function getVerticalData(unitId) { export async function getCourseSectionVerticalData(unitId) {
const { data } = await getAuthenticatedHttpClient() const { data } = await getAuthenticatedHttpClient()
.get(getCourseSectionVerticalApiUrl(unitId)); .get(getCourseSectionVerticalApiUrl(unitId));
const courseSectionVerticalData = normalizeCourseSectionVerticalData(data); return normalizeCourseSectionVerticalData(data);
courseSectionVerticalData.xblockInfo.readOnly = isUnitReadOnly(courseSectionVerticalData.xblockInfo);
return courseSectionVerticalData;
} }
/** /**

View File

@@ -1,7 +1,7 @@
import { createSelector } from '@reduxjs/toolkit'; import { createSelector } from '@reduxjs/toolkit';
import { RequestStatus } from 'CourseAuthoring/data/constants'; import { RequestStatus } from 'CourseAuthoring/data/constants';
export const getCourseUnitData = (state) => state.courseUnit.courseSectionVertical.xblockInfo ?? {}; export const getCourseUnitData = (state) => state.courseUnit.unit;
export const getCanEdit = (state) => state.courseUnit.canEdit; export const getCanEdit = (state) => state.courseUnit.canEdit;
export const getStaticFileNotices = (state) => state.courseUnit.staticFileNotices; export const getStaticFileNotices = (state) => state.courseUnit.staticFileNotices;
export const getCourseUnit = (state) => state.courseUnit; export const getCourseUnit = (state) => state.courseUnit;
@@ -16,7 +16,7 @@ export const getCourseVerticalChildren = (state) => state.courseUnit.courseVerti
export const getCourseOutlineInfo = (state) => state.courseUnit.courseOutlineInfo; export const 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

@@ -12,9 +12,11 @@ const slice = createSlice({
isTitleEditFormOpen: false, isTitleEditFormOpen: false,
canEdit: true, canEdit: true,
loadingStatus: { loadingStatus: {
fetchUnitLoadingStatus: RequestStatus.IN_PROGRESS,
courseSectionVerticalLoadingStatus: RequestStatus.IN_PROGRESS, courseSectionVerticalLoadingStatus: RequestStatus.IN_PROGRESS,
courseVerticalChildrenLoadingStatus: RequestStatus.IN_PROGRESS, courseVerticalChildrenLoadingStatus: RequestStatus.IN_PROGRESS,
}, },
unit: {},
courseSectionVertical: {}, courseSectionVertical: {},
courseVerticalChildren: { children: [], isPublished: true }, courseVerticalChildren: { children: [], isPublished: true },
staticFileNotices: {}, staticFileNotices: {},
@@ -29,6 +31,15 @@ const slice = createSlice({
}, },
}, },
reducers: { reducers: {
fetchCourseItemSuccess: (state, { payload }) => {
state.unit = payload;
},
updateLoadingCourseUnitStatus: (state, { payload }) => {
state.loadingStatus = {
...state.loadingStatus,
fetchUnitLoadingStatus: payload.status,
};
},
updateQueryPendingStatus: (state, { payload }) => { updateQueryPendingStatus: (state, { payload }) => {
state.isQueryPending = payload; state.isQueryPending = payload;
}, },
@@ -70,6 +81,12 @@ const slice = createSlice({
createUnitXblockLoadingStatus: payload.status, createUnitXblockLoadingStatus: payload.status,
}; };
}, },
addNewUnitStatus: (state, { payload }) => {
state.loadingStatus = {
...state.loadingStatus,
fetchUnitLoadingStatus: payload.status,
};
},
updateCourseVerticalChildren: (state, { payload }) => { updateCourseVerticalChildren: (state, { payload }) => {
state.courseVerticalChildren = payload; state.courseVerticalChildren = payload;
}, },
@@ -92,6 +109,8 @@ const slice = createSlice({
}); });
export const { export const {
fetchCourseItemSuccess,
updateLoadingCourseUnitStatus,
updateSavingStatus, updateSavingStatus,
updateModel, updateModel,
fetchSequenceRequest, fetchSequenceRequest,

View File

@@ -10,8 +10,9 @@ import { NOTIFICATION_MESSAGES } from '../../constants';
import { updateModel, updateModels } from '../../generic/model-store'; import { updateModel, updateModels } from '../../generic/model-store';
import { messageTypes } from '../constants'; import { messageTypes } from '../constants';
import { import {
getCourseUnitData,
editUnitDisplayName, editUnitDisplayName,
getVerticalData, getCourseSectionVerticalData,
createCourseXblock, createCourseXblock,
getCourseVerticalChildren, getCourseVerticalChildren,
handleCourseUnitVisibilityAndData, handleCourseUnitVisibilityAndData,
@@ -21,6 +22,8 @@ import {
patchUnitItem, patchUnitItem,
} from './api'; } from './api';
import { import {
updateLoadingCourseUnitStatus,
fetchCourseItemSuccess,
updateSavingStatus, updateSavingStatus,
fetchSequenceRequest, fetchSequenceRequest,
fetchSequenceFailure, fetchSequenceFailure,
@@ -37,13 +40,29 @@ import {
} from './slice'; } from './slice';
import { getNotificationMessage } from './utils'; import { getNotificationMessage } from './utils';
export function fetchCourseUnitQuery(courseId) {
return async (dispatch) => {
dispatch(updateLoadingCourseUnitStatus({ status: RequestStatus.IN_PROGRESS }));
try {
const courseUnit = await getCourseUnitData(courseId);
dispatch(fetchCourseItemSuccess(courseUnit));
dispatch(updateLoadingCourseUnitStatus({ status: RequestStatus.SUCCESSFUL }));
return true;
} catch (error) {
dispatch(updateLoadingCourseUnitStatus({ status: RequestStatus.FAILED }));
return false;
}
};
}
export function fetchCourseSectionVerticalData(courseId, sequenceId) { export function fetchCourseSectionVerticalData(courseId, sequenceId) {
return async (dispatch) => { return async (dispatch) => {
dispatch(updateLoadingCourseSectionVerticalDataStatus({ status: RequestStatus.IN_PROGRESS })); dispatch(updateLoadingCourseSectionVerticalDataStatus({ status: RequestStatus.IN_PROGRESS }));
dispatch(fetchSequenceRequest({ sequenceId })); dispatch(fetchSequenceRequest({ sequenceId }));
try { try {
const courseSectionVerticalData = await getVerticalData(courseId); const courseSectionVerticalData = await getCourseSectionVerticalData(courseId);
dispatch(fetchCourseSectionVerticalDataSuccess(courseSectionVerticalData)); dispatch(fetchCourseSectionVerticalDataSuccess(courseSectionVerticalData));
dispatch(updateLoadingCourseSectionVerticalDataStatus({ status: RequestStatus.SUCCESSFUL })); dispatch(updateLoadingCourseSectionVerticalDataStatus({ status: RequestStatus.SUCCESSFUL }));
dispatch(updateModel({ dispatch(updateModel({
@@ -74,7 +93,8 @@ export function editCourseItemQuery(itemId, displayName, sequenceId) {
try { try {
await editUnitDisplayName(itemId, displayName).then(async (result) => { await editUnitDisplayName(itemId, displayName).then(async (result) => {
if (result) { if (result) {
const courseSectionVerticalData = await getVerticalData(itemId); const courseUnit = await getCourseUnitData(itemId);
const courseSectionVerticalData = await getCourseSectionVerticalData(itemId);
dispatch(fetchCourseSectionVerticalDataSuccess(courseSectionVerticalData)); dispatch(fetchCourseSectionVerticalDataSuccess(courseSectionVerticalData));
dispatch(updateLoadingCourseSectionVerticalDataStatus({ status: RequestStatus.SUCCESSFUL })); dispatch(updateLoadingCourseSectionVerticalDataStatus({ status: RequestStatus.SUCCESSFUL }));
dispatch(updateModel({ dispatch(updateModel({
@@ -86,6 +106,7 @@ export function editCourseItemQuery(itemId, displayName, sequenceId) {
models: courseSectionVerticalData.units || [], models: courseSectionVerticalData.units || [],
})); }));
dispatch(fetchSequenceSuccess({ sequenceId })); dispatch(fetchSequenceSuccess({ sequenceId }));
dispatch(fetchCourseItemSuccess(courseUnit));
dispatch(hideProcessingNotification()); dispatch(hideProcessingNotification());
dispatch(updateSavingStatus({ status: RequestStatus.SUCCESSFUL })); dispatch(updateSavingStatus({ status: RequestStatus.SUCCESSFUL }));
} }
@@ -124,8 +145,8 @@ export function editCourseUnitVisibilityAndData(
if (callback) { if (callback) {
callback(); callback();
} }
const courseSectionVerticalData = await getVerticalData(blockId); const courseUnit = await getCourseUnitData(blockId);
dispatch(fetchCourseSectionVerticalDataSuccess(courseSectionVerticalData)); dispatch(fetchCourseItemSuccess(courseUnit));
const courseVerticalChildrenData = await getCourseVerticalChildren(blockId); const courseVerticalChildrenData = await getCourseVerticalChildren(blockId);
dispatch(updateCourseVerticalChildren(courseVerticalChildrenData)); dispatch(updateCourseVerticalChildren(courseVerticalChildrenData));
dispatch(hideProcessingNotification()); dispatch(hideProcessingNotification());
@@ -152,7 +173,7 @@ export function createNewCourseXBlock(body, callback, blockId, sendMessageToIfra
if (result) { if (result) {
const formattedResult = camelCaseObject(result); const formattedResult = camelCaseObject(result);
if (body.category === 'vertical') { if (body.category === 'vertical') {
const courseSectionVerticalData = await getVerticalData(formattedResult.locator); const courseSectionVerticalData = await getCourseSectionVerticalData(formattedResult.locator);
dispatch(fetchCourseSectionVerticalDataSuccess(courseSectionVerticalData)); dispatch(fetchCourseSectionVerticalDataSuccess(courseSectionVerticalData));
} }
if (body.stagedContent) { if (body.stagedContent) {
@@ -172,8 +193,8 @@ export function createNewCourseXBlock(body, callback, blockId, sendMessageToIfra
sendMessageToIframe(messageTypes.addXBlock, { data: result }); sendMessageToIframe(messageTypes.addXBlock, { data: result });
} }
const currentBlockId = body.category === 'vertical' ? formattedResult.locator : blockId; const currentBlockId = body.category === 'vertical' ? formattedResult.locator : blockId;
const courseSectionVerticalData = await getVerticalData(currentBlockId); const courseUnit = await getCourseUnitData(currentBlockId);
dispatch(fetchCourseSectionVerticalDataSuccess(courseSectionVerticalData)); dispatch(fetchCourseItemSuccess(courseUnit));
} }
}); });
} catch (error) { } catch (error) {
@@ -218,8 +239,8 @@ export function deleteUnitItemQuery(itemId, xblockId, sendMessageToIframe) {
try { try {
await deleteUnitItem(xblockId); await deleteUnitItem(xblockId);
sendMessageToIframe(messageTypes.completeXBlockDeleting, { locator: xblockId }); sendMessageToIframe(messageTypes.completeXBlockDeleting, { locator: xblockId });
const courseSectionVerticalData = await getVerticalData(itemId); const courseUnit = await getCourseUnitData(itemId);
dispatch(fetchCourseSectionVerticalDataSuccess(courseSectionVerticalData)); dispatch(fetchCourseItemSuccess(courseUnit));
dispatch(hideProcessingNotification()); dispatch(hideProcessingNotification());
dispatch(updateSavingStatus({ status: RequestStatus.SUCCESSFUL })); dispatch(updateSavingStatus({ status: RequestStatus.SUCCESSFUL }));
} catch (error) { } catch (error) {
@@ -237,10 +258,8 @@ export function duplicateUnitItemQuery(itemId, xblockId, callback) {
try { try {
const { courseKey, locator } = await duplicateUnitItem(itemId, xblockId); const { courseKey, locator } = await duplicateUnitItem(itemId, xblockId);
callback(courseKey, locator); callback(courseKey, locator);
const courseSectionVerticalData = await getVerticalData(itemId); const courseUnit = await getCourseUnitData(itemId);
dispatch(fetchCourseSectionVerticalDataSuccess(courseSectionVerticalData)); 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) {
@@ -294,8 +313,8 @@ export function patchUnitItemQuery({
dispatch(updateCourseOutlineInfoLoadingStatus({ status: RequestStatus.IN_PROGRESS })); dispatch(updateCourseOutlineInfoLoadingStatus({ status: RequestStatus.IN_PROGRESS }));
callbackFn(sourceLocator); callbackFn(sourceLocator);
try { try {
const courseSectionVerticalData = await getVerticalData(currentParentLocator); const courseUnit = await getCourseUnitData(currentParentLocator);
dispatch(fetchCourseSectionVerticalDataSuccess(courseSectionVerticalData)); dispatch(fetchCourseItemSuccess(courseUnit));
} catch (error) { } catch (error) {
handleResponseErrors(error, dispatch, updateSavingStatus); handleResponseErrors(error, dispatch, updateSavingStatus);
} }
@@ -313,8 +332,8 @@ export function updateCourseUnitSidebar(itemId) {
dispatch(showProcessingNotification(NOTIFICATION_MESSAGES.saving)); dispatch(showProcessingNotification(NOTIFICATION_MESSAGES.saving));
try { try {
const courseSectionVerticalData = await getVerticalData(itemId); const courseUnit = await getCourseUnitData(itemId);
dispatch(fetchCourseSectionVerticalDataSuccess(courseSectionVerticalData)); dispatch(fetchCourseItemSuccess(courseUnit));
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

@@ -8,9 +8,9 @@ import { initializeMockApp } from '@edx/frontend-platform';
import initializeStore from '../../store'; import initializeStore from '../../store';
import { executeThunk } from '../../utils'; import { executeThunk } from '../../utils';
import { getCourseSectionVerticalApiUrl } from '../data/api'; import { getCourseUnitApiUrl } from '../data/api';
import { fetchCourseSectionVerticalData } from '../data/thunk'; import { fetchCourseUnitQuery } from '../data/thunk';
import { courseSectionVerticalMock } from '../__mocks__'; import { courseUnitIndexMock } from '../__mocks__';
import HeaderTitle from './HeaderTitle'; import HeaderTitle from './HeaderTitle';
import messages from './messages'; import messages from './messages';
@@ -52,9 +52,9 @@ describe('<HeaderTitle />', () => {
store = initializeStore(); store = initializeStore();
axiosMock = new MockAdapter(getAuthenticatedHttpClient()); axiosMock = new MockAdapter(getAuthenticatedHttpClient());
axiosMock axiosMock
.onGet(getCourseSectionVerticalApiUrl(blockId)) .onGet(getCourseUnitApiUrl(blockId))
.reply(200, courseSectionVerticalMock); .reply(200, courseUnitIndexMock);
await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch); await executeThunk(fetchCourseUnitQuery(blockId), store.dispatch);
}); });
it('render HeaderTitle component correctly', () => { it('render HeaderTitle component correctly', () => {
@@ -72,31 +72,8 @@ describe('<HeaderTitle />', () => {
expect(getByRole('textbox', { name: messages.ariaLabelButtonEdit.defaultMessage })).toBeInTheDocument(); expect(getByRole('textbox', { name: messages.ariaLabelButtonEdit.defaultMessage })).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(getCourseSectionVerticalApiUrl(blockId))
.reply(200, {
...courseSectionVerticalMock,
xblock_info: {
...courseSectionVerticalMock.xblock_info,
upstreamInfo: {
...courseSectionVerticalMock.xblock_info.upstreamInfo,
upstreamRef: 'lct:org:lib:unit:unit-1',
},
},
});
await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch);
const { getByRole } = renderComponent();
expect(getByRole('button', { name: messages.altButtonEdit.defaultMessage })).toBeDisabled();
expect(getByRole('button', { name: messages.altButtonSettings.defaultMessage })).toBeEnabled();
}); });
it('calls toggle edit title form by clicking on Edit button', () => { it('calls toggle edit title form by clicking on Edit button', () => {
@@ -126,19 +103,16 @@ describe('<HeaderTitle />', () => {
it('displays a visibility message with the selected groups for the unit', async () => { it('displays a visibility message with the selected groups for the unit', async () => {
axiosMock axiosMock
.onGet(getCourseSectionVerticalApiUrl(blockId)) .onGet(getCourseUnitApiUrl(blockId))
.reply(200, { .reply(200, {
...courseSectionVerticalMock, ...courseUnitIndexMock,
xblock_info: { user_partition_info: {
...courseSectionVerticalMock.xblock_info, ...courseUnitIndexMock.user_partition_info,
user_partition_info: { selected_partition_index: 1,
...courseSectionVerticalMock.xblock_info.user_partition_info, selected_groups_label: 'Visibility group 1',
selected_partition_index: 1,
selected_groups_label: 'Visibility group 1',
},
}, },
}); });
await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch); await executeThunk(fetchCourseUnitQuery(blockId), store.dispatch);
const { getByText } = renderComponent(); const { getByText } = renderComponent();
const visibilityMessage = messages.definedVisibilityMessage.defaultMessage const visibilityMessage = messages.definedVisibilityMessage.defaultMessage
.replace('{selectedGroupsLabel}', 'Visibility group 1'); .replace('{selectedGroupsLabel}', 'Visibility group 1');
@@ -150,15 +124,12 @@ describe('<HeaderTitle />', () => {
it('displays a visibility message with the selected groups for some of xblock', async () => { it('displays a visibility message with the selected groups for some of xblock', async () => {
axiosMock axiosMock
.onGet(getCourseSectionVerticalApiUrl(blockId)) .onGet(getCourseUnitApiUrl(blockId))
.reply(200, { .reply(200, {
...courseSectionVerticalMock, ...courseUnitIndexMock,
xblock_info: { has_partition_group_components: true,
...courseSectionVerticalMock.xblock_info,
has_partition_group_components: true,
},
}); });
await executeThunk(fetchCourseSectionVerticalData(blockId), store.dispatch); await executeThunk(fetchCourseUnitQuery(blockId), store.dispatch);
const { getByText } = renderComponent(); const { getByText } = renderComponent();
await waitFor(() => { await waitFor(() => {

View File

@@ -18,10 +18,10 @@ import {
editCourseItemQuery, editCourseItemQuery,
editCourseUnitVisibilityAndData, editCourseUnitVisibilityAndData,
fetchCourseSectionVerticalData, fetchCourseSectionVerticalData,
fetchCourseUnitQuery,
fetchCourseVerticalChildrenData, fetchCourseVerticalChildrenData,
getCourseOutlineInfoQuery, getCourseOutlineInfoQuery,
patchUnitItemQuery, patchUnitItemQuery,
updateCourseUnitSidebar,
} from './data/thunk'; } from './data/thunk';
import { import {
getCanEdit, getCanEdit,
@@ -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);
@@ -198,6 +196,7 @@ export const useCourseUnit = ({ courseId, blockId }) => {
}, [savingStatus]); }, [savingStatus]);
useEffect(() => { useEffect(() => {
dispatch(fetchCourseUnitQuery(blockId));
dispatch(fetchCourseSectionVerticalData(blockId, sequenceId)); dispatch(fetchCourseSectionVerticalData(blockId, sequenceId));
dispatch(fetchCourseVerticalChildrenData(blockId, isSplitTestType)); dispatch(fetchCourseVerticalChildrenData(blockId, isSplitTestType));
handleNavigate(sequenceId); handleNavigate(sequenceId);
@@ -216,27 +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(updateCourseUnitSidebar(blockId));
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

@@ -10,10 +10,10 @@ import userEvent from '@testing-library/user-event';
import initializeStore from '../../../../store'; import initializeStore from '../../../../store';
import { executeThunk } from '../../../../utils'; import { executeThunk } from '../../../../utils';
import { clipboardUnit } from '../../../../__mocks__'; import { clipboardUnit } from '../../../../__mocks__';
import { getCourseSectionVerticalApiUrl } from '../../../data/api'; import { getCourseUnitApiUrl } from '../../../data/api';
import { getClipboardUrl } from '../../../../generic/data/api'; import { getClipboardUrl } from '../../../../generic/data/api';
import { fetchCourseSectionVerticalData } from '../../../data/thunk'; import { fetchCourseUnitQuery } from '../../../data/thunk';
import { courseSectionVerticalMock } from '../../../__mocks__'; import { courseUnitIndexMock } from '../../../__mocks__';
import messages from '../../messages'; import messages from '../../messages';
import ActionButtons from './ActionButtons'; import ActionButtons from './ActionButtons';
@@ -46,14 +46,8 @@ describe('<ActionButtons />', () => {
store = initializeStore(); store = initializeStore();
axiosMock = new MockAdapter(getAuthenticatedHttpClient()); axiosMock = new MockAdapter(getAuthenticatedHttpClient());
axiosMock axiosMock
.onGet(getCourseSectionVerticalApiUrl(courseId)) .onGet(getCourseUnitApiUrl(courseId))
.reply(200, { .reply(200, { ...courseUnitIndexMock, enable_copy_paste_units: true });
...courseSectionVerticalMock,
xblock_info: {
...courseSectionVerticalMock.xblock_info,
enable_copy_paste_units: true,
},
});
axiosMock axiosMock
.onPost(getClipboardUrl()) .onPost(getClipboardUrl())
.reply(200, clipboardUnit); .reply(200, clipboardUnit);
@@ -63,7 +57,7 @@ describe('<ActionButtons />', () => {
queryClient = new QueryClient(); queryClient = new QueryClient();
await executeThunk(fetchCourseSectionVerticalData(courseId), store.dispatch); await executeThunk(fetchCourseUnitQuery(courseId), store.dispatch);
}); });
it('render ActionButtons component with Copy to clipboard', () => { it('render ActionButtons component with Copy to clipboard', () => {
@@ -80,9 +74,7 @@ describe('<ActionButtons />', () => {
userEvent.click(copyXBlockBtn); userEvent.click(copyXBlockBtn);
expect(axiosMock.history.post.length).toBe(1); expect(axiosMock.history.post.length).toBe(1);
expect(axiosMock.history.post[0].data).toBe( expect(axiosMock.history.post[0].data).toBe(JSON.stringify({ usage_key: courseUnitIndexMock.id }));
JSON.stringify({ usage_key: courseSectionVerticalMock.xblock_info.id }),
);
jest.resetAllMocks(); jest.resetAllMocks();
}); });
}); });

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;

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