diff --git a/package-lock.json b/package-lock.json index cdc083bf..81aa7fac 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,8 +10,8 @@ "license": "AGPL-3.0", "dependencies": { "@edx/brand": "npm:@edx/brand-edx.org@2.1.2", - "@edx/frontend-component-footer": "12.4.0", - "@edx/frontend-component-header": "4.7.1", + "@edx/frontend-component-footer": "12.5.0", + "@edx/frontend-component-header": "4.8.0", "@edx/frontend-platform": "5.6.1", "@edx/paragon": "^21.5.3", "@edx/react-unit-test-utils": "1.7.0", @@ -37,13 +37,14 @@ "react-dom": "^17.0.2", "react-pdf": "^7.4.0", "react-redux": "7.2.9", - "react-router": "6.16.0", - "react-router-dom": "6.16.0", + "react-router": "6.17.0", + "react-router-dom": "6.17.0", "redux": "4.2.1", "redux-devtools-extension": "^2.13.9", "redux-logger": "^3.0.6", "regenerator-runtime": "0.14.0", - "tinymce": "5.10.7" + "tinymce": "5.10.8", + "uuid": "^9.0.1" }, "devDependencies": { "@edx/browserslist-config": "^1.1.1", @@ -3121,9 +3122,9 @@ } }, "node_modules/@edx/frontend-component-footer": { - "version": "12.4.0", - "resolved": "https://registry.npmjs.org/@edx/frontend-component-footer/-/frontend-component-footer-12.4.0.tgz", - "integrity": "sha512-DmAC8eTB4ARYlzBewzxWB3Afn9v8j9nSudqDd6LiXHUYDZ8Z7QPrMfmrmfDHllflVd22NvljmP2hp+WdtjkgMA==", + "version": "12.5.0", + "resolved": "https://registry.npmjs.org/@edx/frontend-component-footer/-/frontend-component-footer-12.5.0.tgz", + "integrity": "sha512-kICrEglLQyMtM3Rp0Hw2ewMsqRtBojNrNgbNbQuQTjBFAWu3N137+WBb1Hljko59kJ97iiY8cIIEit9CnM3YnA==", "dependencies": { "@edx/paragon": "^21.3.1", "@fortawesome/fontawesome-svg-core": "6.4.2", @@ -3134,7 +3135,7 @@ "lodash": "^4.17.21" }, "peerDependencies": { - "@edx/frontend-platform": "^4.0.0 || ^5.0.0", + "@edx/frontend-platform": "^4.0.0 || ^5.0.0 || ^6.0.0", "prop-types": "^15.5.10", "react": "^16.9.0 || ^17.0.0", "react-dom": "^16.9.0 || ^17.0.0" @@ -3198,11 +3199,11 @@ } }, "node_modules/@edx/frontend-component-header": { - "version": "4.7.1", - "resolved": "https://registry.npmjs.org/@edx/frontend-component-header/-/frontend-component-header-4.7.1.tgz", - "integrity": "sha512-Fs4KXXgLBLjI6fH+AxpCyS2ItoGAtbWQ6J1GqK9MeoUsEFHBzL4//fBTbU2HwCJSpn7W8p63CYtk2EOy3eY1Og==", + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/@edx/frontend-component-header/-/frontend-component-header-4.8.0.tgz", + "integrity": "sha512-1uzT4LbDgiAYw9+WhbX6r3NGcA5DgscW7SzxXiqSqBM2ND9ym411eczVrxFzc7XGr0eZkQTrsgNDX7xagunYsg==", "dependencies": { - "@edx/paragon": "21.3.1", + "@edx/paragon": "21.5.3", "@fortawesome/fontawesome-svg-core": "6.4.2", "@fortawesome/free-brands-svg-icons": "6.4.2", "@fortawesome/free-regular-svg-icons": "6.4.2", @@ -3214,67 +3215,12 @@ "react-transition-group": "4.4.5" }, "peerDependencies": { - "@edx/frontend-platform": "^4.0.0 || ^5.0.0", + "@edx/frontend-platform": "^4.0.0 || ^5.0.0 || ^6.0.0", "prop-types": "^15.5.10", "react": "^16.9.0 || ^17.0.0", "react-dom": "^16.9.0 || ^17.0.0" } }, - "node_modules/@edx/frontend-component-header/node_modules/@edx/paragon": { - "version": "21.3.1", - "resolved": "https://registry.npmjs.org/@edx/paragon/-/paragon-21.3.1.tgz", - "integrity": "sha512-bXTUaOEmT8XLnDQzYS8QLMvWK5K2BN4jHlx25lO8N0XWRQeDiQTdbx8OrEbv8QOPTlrv0an5MZc+qjlleJFObg==", - "dependencies": { - "@fortawesome/fontawesome-svg-core": "^6.1.1", - "@fortawesome/react-fontawesome": "^0.1.18", - "@popperjs/core": "^2.11.4", - "bootstrap": "^4.6.2", - "chalk": "^4.1.2", - "child_process": "^1.0.2", - "classnames": "^2.3.1", - "email-prop-type": "^3.0.0", - "file-selector": "^0.6.0", - "font-awesome": "^4.7.0", - "glob": "^8.0.3", - "inquirer": "^8.2.5", - "lodash.uniqby": "^4.7.0", - "mailto-link": "^2.0.0", - "prop-types": "^15.8.1", - "react-bootstrap": "^1.6.5", - "react-colorful": "^5.6.1", - "react-dropzone": "^14.2.1", - "react-focus-on": "^3.5.4", - "react-loading-skeleton": "^3.1.0", - "react-popper": "^2.2.5", - "react-proptype-conditional-require": "^1.0.4", - "react-responsive": "^8.2.0", - "react-table": "^7.7.0", - "react-transition-group": "^4.4.2", - "tabbable": "^5.3.3", - "uncontrollable": "^7.2.1", - "uuid": "^9.0.0" - }, - "bin": { - "paragon": "bin/paragon-scripts.js" - }, - "peerDependencies": { - "react": "^16.8.6 || ^17.0.0", - "react-dom": "^16.8.6 || ^17.0.0", - "react-intl": "^5.25.1 || ^6.4.0" - } - }, - "node_modules/@edx/frontend-component-header/node_modules/@edx/paragon/node_modules/@fortawesome/react-fontawesome": { - "version": "0.1.19", - "resolved": "https://registry.npmjs.org/@fortawesome/react-fontawesome/-/react-fontawesome-0.1.19.tgz", - "integrity": "sha512-Hyb+lB8T18cvLNX0S3llz7PcSOAJMLwiVKBuuzwM/nI5uoBw+gQjnf9il0fR1C3DKOI5Kc79pkJ4/xB0Uw9aFQ==", - "dependencies": { - "prop-types": "^15.8.1" - }, - "peerDependencies": { - "@fortawesome/fontawesome-svg-core": "~1 || ~6", - "react": ">=16.x" - } - }, "node_modules/@edx/frontend-component-header/node_modules/@fortawesome/fontawesome-common-types": { "version": "6.4.2", "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.4.2.tgz", @@ -3332,43 +3278,6 @@ "node": ">=6" } }, - "node_modules/@edx/frontend-component-header/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/@edx/frontend-component-header/node_modules/glob": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", - "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^5.0.1", - "once": "^1.3.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@edx/frontend-component-header/node_modules/minimatch": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", - "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/@edx/frontend-platform": { "version": "5.6.1", "resolved": "https://registry.npmjs.org/@edx/frontend-platform/-/frontend-platform-5.6.1.tgz", @@ -3719,6 +3628,12 @@ "url": "https://opencollective.com/core-js" } }, + "node_modules/@edx/react-unit-test-utils/node_modules/isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==", + "peer": true + }, "node_modules/@edx/react-unit-test-utils/node_modules/minimatch": { "version": "5.1.6", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", @@ -3730,6 +3645,59 @@ "node": ">=10" } }, + "node_modules/@edx/react-unit-test-utils/node_modules/path-to-regexp": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz", + "integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==", + "peer": true, + "dependencies": { + "isarray": "0.0.1" + } + }, + "node_modules/@edx/react-unit-test-utils/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "peer": true + }, + "node_modules/@edx/react-unit-test-utils/node_modules/react-router": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-5.3.4.tgz", + "integrity": "sha512-Ys9K+ppnJah3QuaRiLxk+jDWOR1MekYQrlytiXxC1RyfbdsZkS5pvKAzCCr031xHixZwpnsYNT5xysdFHQaYsA==", + "peer": true, + "dependencies": { + "@babel/runtime": "^7.12.13", + "history": "^4.9.0", + "hoist-non-react-statics": "^3.1.0", + "loose-envify": "^1.3.1", + "path-to-regexp": "^1.7.0", + "prop-types": "^15.6.2", + "react-is": "^16.6.0", + "tiny-invariant": "^1.0.2", + "tiny-warning": "^1.0.0" + }, + "peerDependencies": { + "react": ">=15" + } + }, + "node_modules/@edx/react-unit-test-utils/node_modules/react-router-dom": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-5.3.4.tgz", + "integrity": "sha512-m4EqFMHv/Ih4kpcBCONHbkT68KoAeHN4p3lAGoNryfHi0dMy0kCzEZakiKRsvg5wHZ/JLrLW8o8KomWiz/qbYQ==", + "peer": true, + "dependencies": { + "@babel/runtime": "^7.12.13", + "history": "^4.9.0", + "loose-envify": "^1.3.1", + "prop-types": "^15.6.2", + "react-router": "5.3.4", + "tiny-invariant": "^1.0.2", + "tiny-warning": "^1.0.0" + }, + "peerDependencies": { + "react": ">=15" + } + }, "node_modules/@edx/reactifex": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/@edx/reactifex/-/reactifex-2.2.0.tgz", @@ -5377,9 +5345,9 @@ } }, "node_modules/@remix-run/router": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.9.0.tgz", - "integrity": "sha512-bV63itrKBC0zdT27qYm6SDZHlkXwFL1xMBuhkn+X7l0+IIhNaH5wuuvZKp6eKhCD4KFhujhfhCT1YxXW6esUIA==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.10.0.tgz", + "integrity": "sha512-Lm+fYpMfZoEucJ7cMxgt4dYt8jLfbpwRCzAjm9UgSLOkmlqo9gupxt6YX3DY0Fk155NT9l17d/ydi+964uS9Lw==", "engines": { "node": ">=14.0.0" } @@ -19028,11 +18996,11 @@ } }, "node_modules/react-router": { - "version": "6.16.0", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.16.0.tgz", - "integrity": "sha512-VT4Mmc4jj5YyjpOi5jOf0I+TYzGpvzERy4ckNSvSh2RArv8LLoCxlsZ2D+tc7zgjxcY34oTz2hZaeX5RVprKqA==", + "version": "6.17.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.17.0.tgz", + "integrity": "sha512-YJR3OTJzi3zhqeJYADHANCGPUu9J+6fT5GLv82UWRGSxu6oJYCKVmxUcaBQuGm9udpWmPsvpme/CdHumqgsoaA==", "dependencies": { - "@remix-run/router": "1.9.0" + "@remix-run/router": "1.10.0" }, "engines": { "node": ">=14.0.0" @@ -19042,12 +19010,12 @@ } }, "node_modules/react-router-dom": { - "version": "6.16.0", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.16.0.tgz", - "integrity": "sha512-aTfBLv3mk/gaKLxgRDUPbPw+s4Y/O+ma3rEN1u8EgEpLpPe6gNjIsWt9rxushMHHMb7mSwxRGdGlGdvmFsyPIg==", + "version": "6.17.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.17.0.tgz", + "integrity": "sha512-qWHkkbXQX+6li0COUUPKAUkxjNNqPJuiBd27dVwQGDNsuFBdMbrS6UZ0CLYc4CsbdLYTckn4oB4tGDuPZpPhaQ==", "dependencies": { - "@remix-run/router": "1.9.0", - "react-router": "6.16.0" + "@remix-run/router": "1.10.0", + "react-router": "6.17.0" }, "engines": { "node": ">=14.0.0" @@ -21691,9 +21659,9 @@ "integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==" }, "node_modules/tinymce": { - "version": "5.10.7", - "resolved": "https://registry.npmjs.org/tinymce/-/tinymce-5.10.7.tgz", - "integrity": "sha512-9UUjaO0R7FxcFo0oxnd1lMs7H+D0Eh+dDVo5hKbVe1a+VB0nit97vOqlinj+YwgoBDt6/DSCUoWqAYlLI8BLYA==" + "version": "5.10.8", + "resolved": "https://registry.npmjs.org/tinymce/-/tinymce-5.10.8.tgz", + "integrity": "sha512-iyoo3VGMAJhLMDdblAefKvYgBRk9kQi58GTwAmoieqsyggGsKZWlQl/YY6nTILFHUCA1FhYu0HdmM5YYjs17UQ==" }, "node_modules/tmp": { "version": "0.0.33", diff --git a/package.json b/package.json index 2b67931f..8b93eda9 100644 --- a/package.json +++ b/package.json @@ -68,7 +68,8 @@ "redux-devtools-extension": "^2.13.9", "redux-logger": "^3.0.6", "regenerator-runtime": "0.14.0", - "tinymce": "5.10.8" + "tinymce": "5.10.8", + "uuid": "^9.0.1" }, "devDependencies": { "@edx/browserslist-config": "^1.1.1", diff --git a/src/components/CollapsibleFeedback/AssessmentCriterion.jsx b/src/components/Assessment/ReadonlyAssessment/AssessmentCriterion.jsx similarity index 100% rename from src/components/CollapsibleFeedback/AssessmentCriterion.jsx rename to src/components/Assessment/ReadonlyAssessment/AssessmentCriterion.jsx diff --git a/src/components/CollapsibleFeedback/CollapsibleFeedback.jsx b/src/components/Assessment/ReadonlyAssessment/CollapsibleAssessment.jsx similarity index 65% rename from src/components/CollapsibleFeedback/CollapsibleFeedback.jsx rename to src/components/Assessment/ReadonlyAssessment/CollapsibleAssessment.jsx index cd48b2ff..e9386113 100644 --- a/src/components/CollapsibleFeedback/CollapsibleFeedback.jsx +++ b/src/components/Assessment/ReadonlyAssessment/CollapsibleAssessment.jsx @@ -5,20 +5,27 @@ import { Collapsible } from '@edx/paragon'; import { useIntl } from '@edx/frontend-platform/i18n'; import messages from './messages'; -const CollapsibleFeedback = ({ children, stepScore, stepLabel, defaultOpen }) => { +const CollapsibleAssessment = ({ + children, + stepScore, + stepLabel, + defaultOpen, +}) => { const { formatMessage } = useIntl(); const [open, setOpen] = React.useState(defaultOpen); - const toggle = () => setOpen(!open); return ( - {formatMessage(messages.grade, { stepLabel })} + {formatMessage( + stepScore ? messages.grade : messages.unweightedGrade, + { stepLabel }, + )} {stepScore && formatMessage(messages.gradePoints, stepScore)} - } + )} open={open} onToggle={toggle} > @@ -26,15 +33,17 @@ const CollapsibleFeedback = ({ children, stepScore, stepLabel, defaultOpen }) => ); }; -CollapsibleFeedback.defaultProps = {}; -CollapsibleFeedback.propTypes = { +CollapsibleAssessment.defaultProps = { + defaultOpen: false, +}; +CollapsibleAssessment.propTypes = { stepLabel: PropTypes.string.isRequired, stepScore: PropTypes.shape({ earned: PropTypes.number, possible: PropTypes.number, - }), + }).isRequired, children: PropTypes.node.isRequired, defaultOpen: PropTypes.bool, }; -export default CollapsibleFeedback; +export default CollapsibleAssessment; diff --git a/src/components/CollapsibleFeedback/Feedback.jsx b/src/components/Assessment/ReadonlyAssessment/Feedback.jsx similarity index 71% rename from src/components/CollapsibleFeedback/Feedback.jsx rename to src/components/Assessment/ReadonlyAssessment/Feedback.jsx index 5ac7137c..fd465ef8 100644 --- a/src/components/CollapsibleFeedback/Feedback.jsx +++ b/src/components/Assessment/ReadonlyAssessment/Feedback.jsx @@ -23,9 +23,9 @@ const Feedback = ({ return ( <> -
-
-
{criterionName}
+
+
+
{criterionName}
{criterionDescription && ( {}}>

{criterionDescription}

@@ -33,28 +33,26 @@ const Feedback = ({ )}
{selectedOption && ( -

- {selectedOption} -- {selectedPoints} points -

+

{selectedOption} -- {selectedPoints} points

)}
-
+
- -
{commentHeader} Comment
+ +
{commentHeader} Comment
{isExpanded ? ( -
+
{formatMessage(messages.readLess)}
) : ( -
+
{formatMessage(messages.readMore)}
)} - +

{commentBody}

@@ -68,9 +66,9 @@ Feedback.defaultProps = { Feedback.propTypes = { defaultOpen: PropTypes.bool, criterionName: PropTypes.string.isRequired, - criterionDescription: PropTypes.string, - selectedOption: PropTypes.string, - selectedPoints: PropTypes.number, + criterionDescription: PropTypes.string.isRequired, + selectedOption: PropTypes.string.isRequired, + selectedPoints: PropTypes.number.isRequired, commentHeader: PropTypes.string.isRequired, commentBody: PropTypes.string.isRequired, }; diff --git a/src/components/Assessment/ReadonlyAssessment/hooks.js b/src/components/Assessment/ReadonlyAssessment/hooks.js deleted file mode 100644 index e28ac224..00000000 --- a/src/components/Assessment/ReadonlyAssessment/hooks.js +++ /dev/null @@ -1,22 +0,0 @@ -import { useRubricConfig } from 'data/services/lms/hooks/selectors'; - -export const useReadonlyAssessmentData = ({ assessment }) => { - const { criteria, feedbackConfig } = useRubricConfig(); - - const criterionData = (criterion) => ({ - ...criterion, - optionsValue: assessment.optionsSelected[criterion.name], - optionsIsInvalid: false, - feedbackValue: assessment.criterionFeedback[criterion.name], - feedbackIsInvalid: false, - }); - - return { - criteria: criteria.map(criterionData), - // overall feedback - overallFeedbackPrompt: feedbackConfig.defaultText, - overallFeedback: assessment.overallFeedback, - }; -}; - -export default useReadonlyAssessmentData; diff --git a/src/components/Assessment/ReadonlyAssessment/index.jsx b/src/components/Assessment/ReadonlyAssessment/index.jsx index 888c0635..6b3a827a 100644 --- a/src/components/Assessment/ReadonlyAssessment/index.jsx +++ b/src/components/Assessment/ReadonlyAssessment/index.jsx @@ -1,57 +1,55 @@ import React from 'react'; import PropTypes from 'prop-types'; +import { v4 as uuid } from 'uuid'; -import { Card } from '@edx/paragon'; -import { useIntl } from '@edx/frontend-platform/i18n'; +import AssessmentCriterion from './AssessmentCriterion'; +import CollapsibleAssessment from './CollapsibleAssessment'; -import CriterionContainer from 'components/CriterionContainer'; -import GradedCriterion from 'components/CriterionContainer/GradedCriterion'; -import { useReadonlyAssessmentData } from './hooks'; -import messages from '../messages'; - -/** - * - */ -const ReadonlyAssessment = ({ assessment }) => { +const ReadOnlyAssessment = (stepData) => { const { - criteria, - overallFeedbackDisabled, - } = useReadonlyAssessmentData({ assessment }); - - const { formatMessage } = useIntl(); - + stepLabel, + step, + stepScore, + defaultOpen, + } = stepData; + const collapsibleProps = { stepLabel, stepScore, defaultOpen }; + if (stepData.assessments) { + return ( +
+ + {stepData.assessments.map((assessment, index) => ( + +

{stepLabel} {index + 1}:

+ +
+
+ ))} +
+
+ ); + } return ( - - -

{formatMessage(messages.rubric)}

-
- {criteria.map((criterion) => ( - - )} - /> - ))} - {!overallFeedbackDisabled && ( -

{assessment.overallFeedback}

- )} -
-
-
+ + + ); }; - -ReadonlyAssessment.propTypes = { - assessment: PropTypes.shape({ - optionsSelected: PropTypes.objectOf(PropTypes.string).isRequired, - criterionFeedback: PropTypes.objectOf(PropTypes.string).isRequired, - overallFeedback: PropTypes.string, - }).isRequired, +ReadOnlyAssessment.defaultProps = { + defaultOpen: false, + assessment: null, + assessments: null, + stepScore: null, +}; +ReadOnlyAssessment.propTypes = { + stepLabel: PropTypes.string.isRequired, + step: PropTypes.string.isRequired, + stepScore: PropTypes.shape({ + earned: PropTypes.number, + total: PropTypes.number, + }), + defaultOpen: PropTypes.bool, + assessment: PropTypes.shape({}), + assessments: PropTypes.arrayOf(PropTypes.shape({})), }; -export default ReadonlyAssessment; +export default ReadOnlyAssessment; diff --git a/src/components/CollapsibleFeedback/messages.js b/src/components/Assessment/ReadonlyAssessment/messages.js similarity index 80% rename from src/components/CollapsibleFeedback/messages.js rename to src/components/Assessment/ReadonlyAssessment/messages.js index b6dadfc3..5a9a01bf 100644 --- a/src/components/CollapsibleFeedback/messages.js +++ b/src/components/Assessment/ReadonlyAssessment/messages.js @@ -11,10 +11,15 @@ const messages = defineMessages({ defaultMessage: 'Read less', description: 'Read less button text', }, + unweightedGrade: { + id: 'ora-collapsible-comment.grade', + defaultMessage: '{stepLabel} Grade', + description: 'Unweighted grade group text', + }, grade: { id: 'ora-collapsible-comment.grade', - defaultMessage: '{stepLabel} Grade:', - description: 'Grade button text', + defaultMessage: '{stepLabel} Grade: ', + description: 'Grade group text', }, gradePoints: { id: 'ora-collapsible-comment.gradePoints', diff --git a/src/components/CollapsibleFeedback/MultipleAssessmentStep.jsx b/src/components/CollapsibleFeedback/MultipleAssessmentStep.jsx deleted file mode 100644 index 9f5c7d04..00000000 --- a/src/components/CollapsibleFeedback/MultipleAssessmentStep.jsx +++ /dev/null @@ -1,48 +0,0 @@ -import React, { Fragment } from 'react'; -import PropTypes from 'prop-types'; - -import CollapsibleFeedback from './CollapsibleFeedback'; -import AssessmentCriterion from './AssessmentCriterion'; - -const MultipleAssessmentStep = ({ - stepLabel, - step, - stepScore, - assessments, - defaultOpen, -}) => ( -
- - {assessments?.map((assessment, index) => ( - -

- {stepLabel} {index + 1}: -

- -
-
- ))} -
-
-); - -MultipleAssessmentStep.defaultProps = { - defaultOpen: false, -}; -MultipleAssessmentStep.propTypes = { - stepLabel: PropTypes.string.isRequired, - stepScore: PropTypes.shape({ - earned: PropTypes.number, - possible: PropTypes.number, - }), - assessments: PropTypes.arrayOf( - PropTypes.shape({ - selectedOption: PropTypes.number, - // selectedPoints: PropTypes.number, - feedback: PropTypes.string, - }) - ), - defaultOpen: PropTypes.bool, -}; - -export default MultipleAssessmentStep; diff --git a/src/components/CollapsibleFeedback/SingleAssessmentStep.jsx b/src/components/CollapsibleFeedback/SingleAssessmentStep.jsx deleted file mode 100644 index ffb830ce..00000000 --- a/src/components/CollapsibleFeedback/SingleAssessmentStep.jsx +++ /dev/null @@ -1,40 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; - -import CollapsibleFeedback from './CollapsibleFeedback'; -import AssessmentCriterion from './AssessmentCriterion'; - -const SingleAssessmentStep = ({ - stepLabel, - step, - stepScore, - assessment, - defaultOpen, -}) => ( - - - -); - -SingleAssessmentStep.defaultProps = { - defaultOpen: false, -}; -SingleAssessmentStep.propTypes = { - stepLabel: PropTypes.string.isRequired, - stepScore: PropTypes.shape({ - earned: PropTypes.number, - possible: PropTypes.number, - }), - assessment: PropTypes.shape({ - selectedOption: PropTypes.number, - // selectedPoints: PropTypes.number, - feedback: PropTypes.string, - }), - defaultOpen: PropTypes.bool, -}; - -export default SingleAssessmentStep; diff --git a/src/components/CollapsibleFeedback/index.jsx b/src/components/CollapsibleFeedback/index.jsx deleted file mode 100644 index d84328df..00000000 --- a/src/components/CollapsibleFeedback/index.jsx +++ /dev/null @@ -1,5 +0,0 @@ -export { default as CollapsibleFeedback } from './CollapsibleFeedback'; -export { default as MultipleAssessmentStep } from './MultipleAssessmentStep'; -export { default as SingleAssessmentStep } from './SingleAssessmentStep'; -export { default as AssessmentCriterion } from './AssessmentCriterion'; -export { default as Feedback } from './Feedback'; \ No newline at end of file diff --git a/src/components/ModalActions/actionConfigs.js b/src/components/ModalActions/actionConfigs.js new file mode 100644 index 00000000..3ca3fc82 --- /dev/null +++ b/src/components/ModalActions/actionConfigs.js @@ -0,0 +1,20 @@ + +const actions = { + submitResponse: { + onClick: submitResponseHandler, + state: submitResponseMutation.status, + messages: submitActionMessages, + }, + saveAndFinishLater: { + onClick: saveResponse, + state: saveResponseStatus, + messages: saveActionMessages, + }, + finishLater: { + message: saveActionMessages[MutationStatus.idle], + }, + startTraining: { + onClick: saveResponse, + message: messages.startTraining, + }, +}; diff --git a/src/components/ModalActions/index.jsx b/src/components/ModalActions/index.jsx index edb348e6..4a4ea3f3 100644 --- a/src/components/ModalActions/index.jsx +++ b/src/components/ModalActions/index.jsx @@ -4,42 +4,37 @@ import PropTypes from 'prop-types'; import { Button, StatefulButton } from '@edx/paragon'; import { MutationStatus } from 'data/services/lms/constants'; -import { useCloseModal } from 'hooks'; +import useModalActionConfig from './useModalActionConfig'; -const ModalActions = (props) => { - // const { secondary } = props; - const closeModal = useCloseModal(); - const { primary } = props; - const secondary = props.secondary && { - ...props.secondary, - onClick: closeModal, - }; - const className = 'w-100'; - const disabledStates = [MutationStatus.loading]; - console.log(props); - const genButton = (variant, btnProps) => (btnProps.state +const className = 'w-100'; +const disabledStates = [MutationStatus.loading]; + +const ModalActions = ({ step, options }) => { + const actions = useModalActionConfig({ step, options }); + console.log({ actions }); + const { primary, secondary } = actions || {}; + + const actionButton = (variant, btnProps) => (btnProps.state ? : , - , - ]} - submitAssessment={() => {}} - > - - - -); +export const PeerAssessmentView = () => { + const prompts = usePrompts(); + const response = useResponseData(); + if (!useIsORAConfigLoaded()) { + return null; + } + return ( + {}}> + +
+ {React.Children.toArray( + prompts.map((prompt, index) => ( +
+ + +
+ )), + )} + +
+ +
+ ); +}; export default PeerAssessmentView; diff --git a/src/views/SelfAssessmentView/Content.jsx b/src/views/SelfAssessmentView/Content.jsx deleted file mode 100644 index 73fe80a1..00000000 --- a/src/views/SelfAssessmentView/Content.jsx +++ /dev/null @@ -1,30 +0,0 @@ -import React from 'react'; - -import { - usePrompts, - useResponseData, -} from 'data/services/lms/hooks/selectors'; - -import Prompt from 'components/Prompt'; -import TextResponse from 'components/TextResponse'; -import FileUpload from 'components/FileUpload'; - -const AssessmentContent = () => { - const prompts = usePrompts(); - const response = useResponseData(); - return ( -
- {React.Children.toArray( - prompts.map((prompt, index) => ( -
- - -
- )), - )} - -
- ); -}; - -export default AssessmentContent; diff --git a/src/views/SelfAssessmentView/index.jsx b/src/views/SelfAssessmentView/index.jsx index ab668e94..421765a2 100644 --- a/src/views/SelfAssessmentView/index.jsx +++ b/src/views/SelfAssessmentView/index.jsx @@ -1,26 +1,42 @@ import React from 'react'; import { Button } from '@edx/paragon'; -import { useIsORAConfigLoaded } from 'data/services/lms/hooks/selectors'; +import { + useIsORAConfigLoaded, + usePrompts, + useResponseData, +} from 'data/services/lms/hooks/selectors'; +import { stepNames } from 'data/services/lms/constants'; + +import FileUpload from 'components/FileUpload'; +import ModalActions from 'components/ModalActions'; +import Prompt from 'components/Prompt'; +import TextResponse from 'components/TextResponse'; import StatusAlert from 'components/StatusAlert'; import BaseAssessmentView from 'components/BaseAssessmentView'; -import AssessmentContent from './Content'; export const SelfAssessmentView = () => { + const prompts = usePrompts(); + const response = useResponseData(); if (!useIsORAConfigLoaded()) { return null; } return ( - Cancel, - , - ]} - submitAssessment={() => {}} - > + {}}> - +
+ {React.Children.toArray( + prompts.map((prompt, index) => ( +
+ + +
+ )), + )} + + +
); }; diff --git a/src/views/StudentTrainingView/Content.jsx b/src/views/StudentTrainingView/Content.jsx deleted file mode 100644 index 73fe80a1..00000000 --- a/src/views/StudentTrainingView/Content.jsx +++ /dev/null @@ -1,30 +0,0 @@ -import React from 'react'; - -import { - usePrompts, - useResponseData, -} from 'data/services/lms/hooks/selectors'; - -import Prompt from 'components/Prompt'; -import TextResponse from 'components/TextResponse'; -import FileUpload from 'components/FileUpload'; - -const AssessmentContent = () => { - const prompts = usePrompts(); - const response = useResponseData(); - return ( -
- {React.Children.toArray( - prompts.map((prompt, index) => ( -
- - -
- )), - )} - -
- ); -}; - -export default AssessmentContent; diff --git a/src/views/StudentTrainingView/index.jsx b/src/views/StudentTrainingView/index.jsx index 6c04b487..fdf66bae 100644 --- a/src/views/StudentTrainingView/index.jsx +++ b/src/views/StudentTrainingView/index.jsx @@ -1,25 +1,41 @@ import React from 'react'; -import { Button } from '@edx/paragon'; - -import { useIsORAConfigLoaded } from 'data/services/lms/hooks/selectors'; +import { + useIsORAConfigLoaded, + usePrompts, + useResponseData, +} from 'data/services/lms/hooks/selectors'; +import { stepNames } from 'data/services/lms/constants'; +import Prompt from 'components/Prompt'; +import TextResponse from 'components/TextResponse'; +import FileUpload from 'components/FileUpload'; +import ModalActions from 'components/ModalActions'; import BaseAssessmentView from 'components/BaseAssessmentView'; import StatusAlert from 'components/StatusAlert'; -import AssessmentContent from './Content'; - -export const StudentTrainingView = () => useIsORAConfigLoaded() && ( - Cancel, - , - ]} - submitAssessment={() => {}} - > - - - -); - +export const StudentTrainingView = () => { + const prompts = usePrompts(); + const response = useResponseData(); + if (!useIsORAConfigLoaded()) { + return null; + } + return ( + {}}> + +
+ {React.Children.toArray( + prompts.map((prompt, index) => ( +
+ + +
+ )), + )} + + +
+
+ ); +}; export default StudentTrainingView; diff --git a/src/views/SubmissionView/Content.jsx b/src/views/SubmissionView/Content.jsx deleted file mode 100644 index ef8573ed..00000000 --- a/src/views/SubmissionView/Content.jsx +++ /dev/null @@ -1,94 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; - -import { Icon } from '@edx/paragon'; -import { CheckCircle } from '@edx/paragon/icons'; -import { useIntl } from '@edx/frontend-platform/i18n'; - -import { - usePrompts, - useSubmissionConfig, - useStepState, -} from 'data/services/lms/hooks/selectors'; -import { stepNames, stepStates } from 'data/services/lms/constants'; - -import FileUpload from 'components/FileUpload'; -import Instructions from 'components/Instructions'; -import Prompt from 'components/Prompt'; -import TextResponse from 'components/TextResponse'; -import TextResponseEditor from 'components/TextResponseEditor'; -import StatusAlert from 'components/StatusAlert'; - -import messages from './messages'; - -const SubmissionContent = ({ - textResponses, - uploadedFiles, -}) => { - const submissionConfig = useSubmissionConfig(); - - const stepState = useStepState({ step: stepNames.submission }); - const isReadOnly = stepState === stepStates.completed; - const prompts = usePrompts(); - const { formatMessage } = useIntl(); - const createTextResponse = (index) => (isReadOnly - ? - : ( - - )); - - return ( -
-
-

{formatMessage(messages.yourResponse)}

- {(!isReadOnly && textResponses.draftSaved) && ( -

- - {formatMessage(messages.draftSaved)} -

- )} -
- - - - - {prompts.map((prompt, index) => ( - // eslint-disable-next-line react/no-array-index-key -
- - {createTextResponse(index)} -
- ))} - - -
- ); -}; - -SubmissionContent.propTypes = { - textResponses: PropTypes.shape({ - value: PropTypes.arrayOf(PropTypes.string).isRequired, - onChange: PropTypes.func.isRequired, - draftSaved: PropTypes.bool.isRequired, - }).isRequired, - uploadedFiles: PropTypes.shape({ - value: PropTypes.arrayOf(PropTypes.shape({ - fileDescription: PropTypes.string, - fileName: PropTypes.string, - fileSize: PropTypes.number, - })), - onDeletedFile: PropTypes.func.isRequired, - onFileUploaded: PropTypes.func.isRequired, - }).isRequired, -}; - -export default SubmissionContent; diff --git a/src/views/SubmissionView/SubmissionPrompts.jsx b/src/views/SubmissionView/SubmissionPrompts.jsx new file mode 100644 index 00000000..5bac6c32 --- /dev/null +++ b/src/views/SubmissionView/SubmissionPrompts.jsx @@ -0,0 +1,50 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +import { + usePrompts, + useSubmissionConfig, +} from 'data/services/lms/hooks/selectors'; + +import Prompt from 'components/Prompt'; +import TextResponse from 'components/TextResponse'; +import TextResponseEditor from './TextResponseEditor'; + +const SubmissionPrompts = ({ + textResponses, + onUpdateTextResponse, + isReadOnly, +}) => { + const submissionConfig = useSubmissionConfig(); + + const response = (index) => { + if (!submissionConfig.textResponseConfig.enabled) { + return null; + } + return isReadOnly + ? + : ( + + ); + }; + + return usePrompts().map((prompt, index) => ( + // eslint-disable-next-line react/no-array-index-key +
+ + {response(index)} +
+ )); +}; +SubmissionPrompts.defaultProps = { + textResponses: null, +}; +SubmissionPrompts.propTypes = { + textResponses: PropTypes.arrayOf(PropTypes.string), + onUpdateTextResponse: PropTypes.func.isRequired, +}; + +export default SubmissionPrompts; diff --git a/src/components/TextResponseEditor/RichTextEditor.jsx b/src/views/SubmissionView/TextResponseEditor/RichTextEditor.jsx similarity index 100% rename from src/components/TextResponseEditor/RichTextEditor.jsx rename to src/views/SubmissionView/TextResponseEditor/RichTextEditor.jsx diff --git a/src/components/TextResponseEditor/RichTextEditor.test.jsx b/src/views/SubmissionView/TextResponseEditor/RichTextEditor.test.jsx similarity index 100% rename from src/components/TextResponseEditor/RichTextEditor.test.jsx rename to src/views/SubmissionView/TextResponseEditor/RichTextEditor.test.jsx diff --git a/src/components/TextResponseEditor/TextEditor.jsx b/src/views/SubmissionView/TextResponseEditor/TextEditor.jsx similarity index 100% rename from src/components/TextResponseEditor/TextEditor.jsx rename to src/views/SubmissionView/TextResponseEditor/TextEditor.jsx diff --git a/src/components/TextResponseEditor/TextEditor.test.jsx b/src/views/SubmissionView/TextResponseEditor/TextEditor.test.jsx similarity index 100% rename from src/components/TextResponseEditor/TextEditor.test.jsx rename to src/views/SubmissionView/TextResponseEditor/TextEditor.test.jsx diff --git a/src/components/TextResponseEditor/__snapshots__/RichTextEditor.test.jsx.snap b/src/views/SubmissionView/TextResponseEditor/__snapshots__/RichTextEditor.test.jsx.snap similarity index 100% rename from src/components/TextResponseEditor/__snapshots__/RichTextEditor.test.jsx.snap rename to src/views/SubmissionView/TextResponseEditor/__snapshots__/RichTextEditor.test.jsx.snap diff --git a/src/components/TextResponseEditor/__snapshots__/TextEditor.test.jsx.snap b/src/views/SubmissionView/TextResponseEditor/__snapshots__/TextEditor.test.jsx.snap similarity index 100% rename from src/components/TextResponseEditor/__snapshots__/TextEditor.test.jsx.snap rename to src/views/SubmissionView/TextResponseEditor/__snapshots__/TextEditor.test.jsx.snap diff --git a/src/components/TextResponseEditor/__snapshots__/index.test.jsx.snap b/src/views/SubmissionView/TextResponseEditor/__snapshots__/index.test.jsx.snap similarity index 100% rename from src/components/TextResponseEditor/__snapshots__/index.test.jsx.snap rename to src/views/SubmissionView/TextResponseEditor/__snapshots__/index.test.jsx.snap diff --git a/src/views/SubmissionView/TextResponseEditor/index.jsx b/src/views/SubmissionView/TextResponseEditor/index.jsx new file mode 100644 index 00000000..55bcbd87 --- /dev/null +++ b/src/views/SubmissionView/TextResponseEditor/index.jsx @@ -0,0 +1,37 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +import { useSubmissionConfig } from 'data/services/lms/hooks/selectors'; + +import TextEditor from './TextEditor'; +import RichTextEditor from './RichTextEditor'; + +import './index.scss'; + +const TextResponseEditor = ({ value, onChange }) => { + const { textResponseConfig } = useSubmissionConfig(); + const { + optional, + enabled, + editorType, + } = textResponseConfig || {}; + + if (!enabled) { + return null; + } + + const EditorComponent = editorType === 'text' ? TextEditor : RichTextEditor; + + return ( +
+ +
+ ); +}; + +TextResponseEditor.propTypes = { + value: PropTypes.string.isRequired, + onChange: PropTypes.func.isRequired, +}; + +export default TextResponseEditor; diff --git a/src/components/TextResponseEditor/index.scss b/src/views/SubmissionView/TextResponseEditor/index.scss similarity index 100% rename from src/components/TextResponseEditor/index.scss rename to src/views/SubmissionView/TextResponseEditor/index.scss diff --git a/src/components/TextResponseEditor/index.test.jsx b/src/views/SubmissionView/TextResponseEditor/index.test.jsx similarity index 100% rename from src/components/TextResponseEditor/index.test.jsx rename to src/views/SubmissionView/TextResponseEditor/index.test.jsx diff --git a/src/components/TextResponseEditor/messages.js b/src/views/SubmissionView/TextResponseEditor/messages.js similarity index 100% rename from src/components/TextResponseEditor/messages.js rename to src/views/SubmissionView/TextResponseEditor/messages.js diff --git a/src/views/SubmissionView/hooks.js b/src/views/SubmissionView/hooks.js deleted file mode 100644 index 6da3ee39..00000000 --- a/src/views/SubmissionView/hooks.js +++ /dev/null @@ -1,131 +0,0 @@ -import { useCallback, useEffect } from 'react'; - -import { StrictDict, useKeyedState } from '@edx/react-unit-test-utils'; -import { useIntl } from '@edx/frontend-platform/i18n'; -import { - usePageData, - usePrompts, - useRubricConfig, - useResponseData, -} from 'data/services/lms/hooks/selectors'; - -import { - useSubmitResponse, useSaveResponse, useUploadFiles, useDeleteFile, -} from 'data/services/lms/hooks/actions'; -import { MutationStatus } from 'data/services/lms/constants'; -import messages from './messages'; - -export const stateKeys = StrictDict({ - textResponses: 'textResponses', - uploadedFiles: 'uploadedFiles', - isDirty: 'isDirty', -}); - -export const useTextResponses = () => { - const response = useResponseData(); - const prompts = usePrompts(); - - const [isDirty, setIsDirty] = useKeyedState(stateKeys.isDirty, false); - const [value, setValue] = useKeyedState( - stateKeys.textResponses, - response ? response.textResponses : prompts.map(() => ''), - ); - - const saveResponseMutation = useSaveResponse(); - - const saveResponseHandler = useCallback(() => { - setIsDirty(false); - saveResponseMutation.mutate({ textResponses: value }); - }, [setIsDirty, saveResponseMutation, value]); - - const onChange = useCallback((index) => (textResponse) => { - setValue(oldResponses => { - const out = [...oldResponses]; - out[index] = textResponse; - return out; - }); - setIsDirty(true); - }, [setValue, setIsDirty]); - - return { - formProps: { - value, - onChange, - draftSaved: saveResponseMutation.status === MutationStatus.success && !isDirty, - }, - saveResponse: { - handler: saveResponseHandler, - status: saveResponseMutation.status, - }, - }; -}; - -export const useUploadedFiles = () => { - const deleteFileMutation = useDeleteFile(); - const uploadFilesMutation = useUploadFiles(); - - const response = useResponseData(); - - const [value, setValue] = useKeyedState( - stateKeys.uploadedFiles, - response ? response.uploadedFiles : [], - ); - - const onFileUploaded = useCallback(async (data) => { - const { fileData, queryClient } = data; - const uploadResponse = await uploadFilesMutation.mutateAsync(data); - if (uploadResponse) { - setValue((oldFiles) => [...oldFiles, uploadResponse.uploadedFiles[0]]); - } - }, [uploadFilesMutation, setValue]); - - const onDeletedFile = deleteFileMutation.mutateAsync; - - return { value, onFileUploaded, onDeletedFile }; -}; - -const useSubmissionViewData = () => { - const submitResponseMutation = useSubmitResponse(); - const textResponses = useTextResponses(); - const uploadedFiles = useUploadedFiles(); - const { showDuringResponse } = useRubricConfig(); - const { formatMessage } = useIntl(); - - const submitResponseHandler = useCallback(() => { - submitResponseMutation.mutate({ - textResponses: textResponses.formProps.value, - uploadedFiles: uploadedFiles.value, - }); - }, [submitResponseMutation, textResponses, uploadedFiles]); - - return { - actionsProps: { - primary: { - onClick: submitResponseHandler, - state: submitResponseMutation.status, - disabledStates: [MutationStatus.loading], - labels: { - [MutationStatus.idle]: formatMessage(messages.submissionActionSubmit), - [MutationStatus.loading]: formatMessage(messages.submissionActionSubmitting), - [MutationStatus.success]: formatMessage(messages.submissionActionSubmitted), - }, - }, - secondary: { - onClick: textResponses.saveResponse.handler, - state: textResponses.saveResponse.status, - disabledStates: [MutationStatus.loading], - labels: { - [MutationStatus.idle]: formatMessage(messages.saveActionSave), - [MutationStatus.loading]: formatMessage(messages.saveActionSaving), - }, - }, - }, - formProps: { - textResponses: textResponses.formProps, - uploadedFiles, - }, - showRubric: showDuringResponse, - }; -}; - -export default useSubmissionViewData; diff --git a/src/views/SubmissionView/index.jsx b/src/views/SubmissionView/index.jsx index 95cdd123..e61f9e28 100644 --- a/src/views/SubmissionView/index.jsx +++ b/src/views/SubmissionView/index.jsx @@ -1,24 +1,70 @@ import React from 'react'; -import { Col, Row } from '@edx/paragon'; +import { Col, Icon, Row } from '@edx/paragon'; +import { CheckCircle } from '@edx/paragon/icons'; +import { useIntl } from '@edx/frontend-platform/i18n'; + +import { useStepState } from 'data/services/lms/hooks/selectors'; +import { stepNames, stepStates } from 'data/services/lms/constants'; import Rubric from 'components/Rubric'; -import Actions from 'components/ModalActions'; +import ModalActions from 'components/ModalActions'; +import FileUpload from 'components/FileUpload'; +import Instructions from 'components/Instructions'; +import StatusAlert from 'components/StatusAlert'; -import Content from './Content'; -import useSubmissionViewData from './hooks'; +import SubmissionPrompts from './SubmissionPrompts'; +import useSubmissionViewData from './useSubmissionViewData'; import './index.scss'; +import messages from './messages'; + export const SubmissionView = () => { - const { actionsProps, formProps, showRubric } = useSubmissionViewData(); + const { + actionOptions, + showRubric, + textResponses, + onUpdateTextResponse, + isDraftSaved, + uploadedFiles, + onDeletedFile, + onFileUploaded, + } = useSubmissionViewData(); + + const stepState = useStepState({ step: stepNames.submission }); + const isReadOnly = stepState === stepStates.completed; + const { formatMessage } = useIntl(); + + const draftIndicator = (!isReadOnly && isDraftSaved) && ( +

+ + {formatMessage(messages.draftSaved)} +

+ ); + return (
- - +
+
+

{formatMessage(messages.yourResponse)}

+ {draftIndicator} +
+ + + + + +
+ {showRubric && }
diff --git a/src/views/SubmissionView/messages.js b/src/views/SubmissionView/messages.js index 23762b09..b28d4cef 100644 --- a/src/views/SubmissionView/messages.js +++ b/src/views/SubmissionView/messages.js @@ -1,4 +1,5 @@ import { defineMessages } from '@edx/frontend-platform/i18n'; +import { MutationStatus } from 'data/services/lms/constants'; const messages = defineMessages({ yourResponse: { @@ -24,31 +25,47 @@ const messages = defineMessages({ it.`, description: 'Description for the instructions textarea', }, - submissionActionSubmit: { + startTraining: { + id: 'ora-grading.SubmissionView.startTraining', + defaultMessage: 'Start practice grading', + description: 'Action button text for start action to Student Training step', + }, +}); + +export const submitActionMessages = defineMessages({ + [MutationStatus.idle]: { id: 'ora-grading.SubmissionAction.submit', defaultMessage: 'Submit response', description: 'Submit button text', }, - submissionActionSubmitting: { + [MutationStatus.loading]: { id: 'ora-grading.SubmissionAction.submitting', defaultMessage: 'Submitting response', description: 'Submit button text while submitting', }, - submissionActionSubmitted: { + [MutationStatus.success]: { id: 'ora-grading.SubmissionAction.submitted', defaultMessage: 'Response submitted', description: 'Submit button text after successful submission', }, - saveActionSave: { + +}); + +export const saveActionMessages = defineMessages({ + [MutationStatus.idle]: { id: 'ora-grading.SaveAction.save', defaultMessage: 'Finish later', description: 'Save for later button text', }, - saveActionSaving: { + [MutationStatus.loading]: { id: 'ora-grading.SaveAction.saving', defaultMessage: 'Saving response', description: 'Save for later button text while saving', }, }); -export default messages; +export default { + ...messages, + ...submitActionMessages, + ...saveActionMessages, +}; diff --git a/src/views/SubmissionView/useSubmissionViewData.js b/src/views/SubmissionView/useSubmissionViewData.js new file mode 100644 index 00000000..cfcaf4e0 --- /dev/null +++ b/src/views/SubmissionView/useSubmissionViewData.js @@ -0,0 +1,48 @@ +import { useCallback } from 'react'; + +import { useStepState, useRubricConfig } from 'data/services/lms/hooks/selectors'; +import { useSubmitResponse } from 'data/services/lms/hooks/actions'; +import { stepNames } from 'data/services/lms/constants'; + +import useTextResponsesData from './useTextResponsesData'; +import useUploadedFilesData from './useUploadedFilesData'; + +const useSubmissionViewData = () => { + const submitResponseMutation = useSubmitResponse(); + const rubricConfig = useRubricConfig(); + + const { + textResponses, + onUpdateTextResponse, + isDraftSaved, + saveResponse, + saveResponseStatus, + } = useTextResponsesData(); + const { + uploadedFiles, + onFileUploaded, + onDeletedFile, + } = useUploadedFilesData(); + + const submitResponseHandler = useCallback(() => { + submitResponseMutation.mutate({ textResponses, uploadedFiles }); + }, [submitResponseMutation, textResponses, uploadedFiles]); + + return { + actionOptions: { + saveResponse, + saveResponseStatus, + submit: submitResponseHandler, + submitStatus: submitResponseMutation.status, + }, + textResponses, + onUpdateTextResponse, + isDraftSaved, + uploadedFiles, + onDeletedFile, + onFileUploaded, + showRubric: rubricConfig.showDuringResponse, + }; +}; + +export default useSubmissionViewData; diff --git a/src/views/SubmissionView/useTextResponsesData.js b/src/views/SubmissionView/useTextResponsesData.js new file mode 100644 index 00000000..ba5b3d36 --- /dev/null +++ b/src/views/SubmissionView/useTextResponsesData.js @@ -0,0 +1,45 @@ +import { useCallback } from 'react'; + +import { StrictDict, useKeyedState } from '@edx/react-unit-test-utils'; +import { useTextResponses } from 'data/services/lms/hooks/selectors'; + +import { useSaveResponse } from 'data/services/lms/hooks/actions'; +import { MutationStatus } from 'data/services/lms/constants'; + +export const stateKeys = StrictDict({ + textResponses: 'textResponses', + isDirty: 'isDirty', +}); + +const useTextResponsesData = () => { + const textResponses = useTextResponses(); + + const [isDirty, setIsDirty] = useKeyedState(stateKeys.isDirty, false); + const [value, setValue] = useKeyedState(stateKeys.textResponses, textResponses); + + const saveResponseMutation = useSaveResponse(); + + const saveResponse = useCallback(() => { + setIsDirty(false); + return saveResponseMutation.mutateAsync({ textResponses: value }); + }, [setIsDirty, saveResponseMutation, value]); + + const onChange = useCallback((index) => (textResponse) => { + setValue(oldResponses => { + const out = [...oldResponses]; + out[index] = textResponse; + return out; + }); + setIsDirty(true); + }, [setValue, setIsDirty]); + + return { + textResponses: value, + onUpdateTextResponse: onChange, + isDraftSaved: saveResponseMutation.status === MutationStatus.success && !isDirty, + saveResponse, + saveResponseStatus: saveResponseMutation.status, + }; +}; + +export default useTextResponsesData; diff --git a/src/views/SubmissionView/useUploadedFilesData.js b/src/views/SubmissionView/useUploadedFilesData.js new file mode 100644 index 00000000..4aaf4484 --- /dev/null +++ b/src/views/SubmissionView/useUploadedFilesData.js @@ -0,0 +1,40 @@ +import { useCallback } from 'react'; + +import { StrictDict, useKeyedState } from '@edx/react-unit-test-utils'; +import { useResponseData } from 'data/services/lms/hooks/selectors'; + +import { + useUploadFiles, useDeleteFile, +} from 'data/services/lms/hooks/actions'; + +export const stateKeys = StrictDict({ uploadedFiles: 'uploadedFiles' }); + +const useUploadedFilesData = () => { + const deleteFileMutation = useDeleteFile(); + const uploadFilesMutation = useUploadFiles(); + + const response = useResponseData(); + + const [value, setValue] = useKeyedState( + stateKeys.uploadedFiles, + response ? response.uploadedFiles : [], + ); + + const onFileUploaded = useCallback(async (data) => { + // const { fileData, queryClient } = data; + const uploadResponse = await uploadFilesMutation.mutateAsync(data); + if (uploadResponse) { + setValue((oldFiles) => [...oldFiles, uploadResponse.uploadedFiles[0]]); + } + }, [uploadFilesMutation, setValue]); + + const onDeletedFile = deleteFileMutation.mutateAsync; + + return { + uploadedFiles: value, + onFileUploaded, + onDeletedFile, + }; +}; + +export default useUploadedFilesData;