diff --git a/src/App.jsx b/src/App.jsx
index 6a3cc588..d135fb0e 100644
--- a/src/App.jsx
+++ b/src/App.jsx
@@ -10,6 +10,7 @@ import SelfAssessmentView from 'views/SelfAssessmentView';
import StudentTrainingView from 'views/StudentTrainingView';
import SubmissionView from 'views/SubmissionView';
import XBlockView from 'views/XBlockView';
+import GradeView from 'views/GradeView';
import AppContainer from 'components/AppContainer';
import ModalContainer from 'components/ModalContainer';
@@ -49,6 +50,7 @@ const RouterRoot = () => {
modalRoute(routes.selfAssessmentEmbed, SelfAssessmentView, 'ORA Self Assessment'),
modalRoute(routes.studentTrainingEmbed, StudentTrainingView, 'ORA Student Training'),
modalRoute(routes.submissionEmbed, SubmissionView, 'ORA Submission'),
+ modalRoute(routes.gradedEmbed, GradeView, 'My Grade'),
} />,
];
const baseRoutes = [
@@ -57,6 +59,7 @@ const RouterRoot = () => {
modalRoute(routes.selfAssessment, SelfAssessmentView, 'Assess yourself'),
modalRoute(routes.studentTraining, StudentTrainingView, 'Practice grading'),
modalRoute(routes.submission, SubmissionView, 'Your response'),
+ modalRoute(routes.graded, GradeView, 'My Grade'),
} />,
];
diff --git a/src/components/CollapsibleFeedback/Feedback.jsx b/src/components/CollapsibleFeedback/Feedback.jsx
new file mode 100644
index 00000000..7eddbe52
--- /dev/null
+++ b/src/components/CollapsibleFeedback/Feedback.jsx
@@ -0,0 +1,53 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+import { Collapsible, Icon } from '@edx/paragon';
+import { ExpandMore, ExpandLess } from '@edx/paragon/icons';
+import { useIntl } from '@edx/frontend-platform/i18n';
+import messages from './messages';
+
+const Feedback = ({ criterion, selectedOption, selectedPoints, commentHeader, commentBody, defaultOpen }) => {
+ const [isExpanded, setIsExpanded] = React.useState(defaultOpen);
+ const { formatMessage } = useIntl();
+
+ return (
+ <>
+
+
{criterion.name}
+ {selectedOption &&
{selectedOption} -- {selectedPoints} points
}
+
+
+
setIsExpanded(!isExpanded)}
+ >
+
+ {commentHeader} Comment
+ {isExpanded ? (
+
+ {formatMessage(messages.readLess)}
+
+
+ ) : (
+
+ {formatMessage(messages.readMore)}
+
+
+ )}
+
+
+ {commentBody}
+
+
+
+ >
+ );
+};
+Feedback.defaultProps = {
+ defaultOpen: false,
+};
+Feedback.propTypes = {
+ defaultOpen: PropTypes.bool,
+};
+
+export default Feedback;
diff --git a/src/components/CollapsibleFeedback/index.jsx b/src/components/CollapsibleFeedback/index.jsx
new file mode 100644
index 00000000..33521816
--- /dev/null
+++ b/src/components/CollapsibleFeedback/index.jsx
@@ -0,0 +1,52 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+import { Collapsible } from '@edx/paragon';
+import { useIntl } from '@edx/frontend-platform/i18n';
+import Feedback from './Feedback';
+import messages from './messages';
+
+const CollapsibleFeedback = ({ assessment, stepScore, stepName }) => {
+ const assessmentCriterions = assessment.assessmentCriterions;
+ const { formatMessage } = useIntl();
+
+ return (
+
+ {formatMessage(messages.grade, {
+ stepName,
+ })}
+ {stepScore && formatMessage(messages.gradePoints, stepScore)}
+
+ }
+ >
+ {assessmentCriterions.map((criterion) => {
+ return (
+
+ );
+ })}
+
+
+ );
+};
+CollapsibleFeedback.defaultProps = {};
+CollapsibleFeedback.propTypes = {
+ // assessment: PropTypes.shape({
+ // overallFeedback: PropTypes.string,
+ // }),
+ stepName: PropTypes.string.isRequired,
+};
+
+export default CollapsibleFeedback;
diff --git a/src/components/CollapsibleFeedback/messages.js b/src/components/CollapsibleFeedback/messages.js
new file mode 100644
index 00000000..ed38e668
--- /dev/null
+++ b/src/components/CollapsibleFeedback/messages.js
@@ -0,0 +1,31 @@
+import { defineMessages } from '@edx/frontend-platform/i18n';
+
+const messages = defineMessages({
+ readMore: {
+ id: 'ora-collapsible-comment.readMore',
+ defaultMessage: 'Read more',
+ description: 'Read more button text',
+ },
+ readLess: {
+ id: 'ora-collapsible-comment.readLess',
+ defaultMessage: 'Read less',
+ description: 'Read less button text',
+ },
+ grade: {
+ id: 'ora-collapsible-comment.grade',
+ defaultMessage: '{stepName} Grade:',
+ description: 'Grade button text',
+ },
+ gradePoints: {
+ id: 'ora-collapsible-comment.gradePoints',
+ defaultMessage: '{earned} / {possible}',
+ description: 'Grade points button text',
+ },
+ notWeightedGradeLabel: {
+ id: 'ora-collapsible-comment.notWeightedGradeLabel',
+ defaultMessage: '(Not weighted toward final grade))',
+ description: 'Not weighted grade label',
+ },
+});
+
+export default messages;
diff --git a/src/components/FilePreview/components/FileRenderer/FileCard/index.jsx b/src/components/FilePreview/components/FileRenderer/FileCard/index.jsx
index 9b622883..d84be372 100644
--- a/src/components/FilePreview/components/FileRenderer/FileCard/index.jsx
+++ b/src/components/FilePreview/components/FileRenderer/FileCard/index.jsx
@@ -8,11 +8,11 @@ import './FileCard.scss';
/**
*
*/
-const FileCard = ({ file, children }) => (
+const FileCard = ({ file, children, defaultOpen }) => (
{file.fileName}}
>
@@ -24,6 +24,7 @@ const FileCard = ({ file, children }) => (
FileCard.propTypes = {
file: PropTypes.shape({ fileName: PropTypes.string.isRequired }).isRequired,
children: PropTypes.node.isRequired,
+ defaultOpen: PropTypes.bool.isRequired,
};
export default FileCard;
diff --git a/src/components/FilePreview/components/FileRenderer/index.jsx b/src/components/FilePreview/components/FileRenderer/index.jsx
index 679e664e..5120ea82 100644
--- a/src/components/FilePreview/components/FileRenderer/index.jsx
+++ b/src/components/FilePreview/components/FileRenderer/index.jsx
@@ -10,7 +10,7 @@ import { useRenderData } from './hooks';
/**
*
*/
-export const FileRenderer = ({ file }) => {
+export const FileRenderer = ({ file, defaultOpen }) => {
const { formatMessage } = useIntl();
const {
Renderer,
@@ -21,7 +21,7 @@ export const FileRenderer = ({ file }) => {
} = useRenderData({ file, formatMessage });
return (
-
+
{isLoading && }
{errorStatus ? (
@@ -32,12 +32,15 @@ export const FileRenderer = ({ file }) => {
);
};
-FileRenderer.defaultProps = {};
+FileRenderer.defaultProps = {
+ defaultOpen: true,
+};
FileRenderer.propTypes = {
file: PropTypes.shape({
fileName: PropTypes.string,
fileUrl: PropTypes.string,
}).isRequired,
+ defaultOpen: PropTypes.bool,
// injected
// intl: intlShape.isRequired,
};
diff --git a/src/components/FilePreview/index.jsx b/src/components/FilePreview/index.jsx
index 38a32520..599d8641 100644
--- a/src/components/FilePreview/index.jsx
+++ b/src/components/FilePreview/index.jsx
@@ -3,12 +3,12 @@ import React from 'react';
import { useResponseData } from 'data/services/lms/hooks/selectors';
import { FileRenderer, isSupported } from './components';
-const FilePreview = () => {
+const FilePreview = ({ defaultCollapsePreview }) => {
const { uploadedFiles } = useResponseData();
return (
{uploadedFiles.filter(isSupported).map((file) => (
-
+
))}
);
diff --git a/src/components/FileUpload/index.jsx b/src/components/FileUpload/index.jsx
index 8aa3b12a..b3d49d16 100644
--- a/src/components/FileUpload/index.jsx
+++ b/src/components/FileUpload/index.jsx
@@ -24,6 +24,7 @@ const FileUpload = ({
uploadedFiles,
onFileUploaded,
onDeletedFile,
+ defaultCollapsePreview,
}) => {
const { formatMessage } = useIntl();
@@ -39,7 +40,7 @@ const FileUpload = ({
return (
File Upload
- {isReadOnly &&
}
+ {isReadOnly &&
}
{uploadedFiles.length > 0 && (
<>
Uploaded Files
@@ -91,6 +92,7 @@ FileUpload.defaultProps = {
uploadedFiles: [],
onFileUploaded: nullMethod,
onDeletedFile: nullMethod,
+ defaultCollapsePreview: false,
};
FileUpload.propTypes = {
isReadOnly: PropTypes.bool,
@@ -103,6 +105,7 @@ FileUpload.propTypes = {
),
onFileUploaded: PropTypes.func,
onDeletedFile: PropTypes.func,
+ defaultCollapsePreview: PropTypes.bool,
};
export default FileUpload;
diff --git a/src/components/Prompt/hooks.js b/src/components/Prompt/hooks.js
index 2cc77124..a688665f 100644
--- a/src/components/Prompt/hooks.js
+++ b/src/components/Prompt/hooks.js
@@ -1,7 +1,7 @@
import { useState } from 'react';
-const usePromptHooks = () => {
- const [open, setOpen] = useState(true);
+const usePromptHooks = ({ defaultOpen }) => {
+ const [open, setOpen] = useState(defaultOpen);
const toggleOpen = () => setOpen(!open);
diff --git a/src/components/Prompt/index.jsx b/src/components/Prompt/index.jsx
index 4e37a4ba..4931cba4 100644
--- a/src/components/Prompt/index.jsx
+++ b/src/components/Prompt/index.jsx
@@ -5,8 +5,8 @@ import { Collapsible } from '@edx/paragon';
import usePromptHooks from './hooks';
-const Prompt = ({ prompt }) => {
- const { open, toggleOpen } = usePromptHooks();
+const Prompt = ({ prompt, defaultOpen }) => {
+ const { open, toggleOpen } = usePromptHooks({ defaultOpen });
return (
@@ -14,7 +14,12 @@ const Prompt = ({ prompt }) => {
);
};
+Prompt.defaultProps = {
+ defaultOpen: true,
+};
+
Prompt.propTypes = {
+ defaultOpen: PropTypes.bool,
prompt: PropTypes.string.isRequired,
};
diff --git a/src/data/services/lms/constants.js b/src/data/services/lms/constants.js
index 50585d91..da3fb68c 100644
--- a/src/data/services/lms/constants.js
+++ b/src/data/services/lms/constants.js
@@ -50,7 +50,7 @@ export const routeSteps = StrictDict({
peer_assessment: stepNames.peer,
self_assessment: stepNames.self,
student_training: stepNames.studentTraining,
- my_grades: stepNames.done,
+ graded: stepNames.done,
});
export const stepRoutes = StrictDict(Object.keys(routeSteps).reduce(
diff --git a/src/data/services/lms/fakeData/constants.js b/src/data/services/lms/fakeData/constants.js
index 00dacbcb..d95b5852 100644
--- a/src/data/services/lms/fakeData/constants.js
+++ b/src/data/services/lms/fakeData/constants.js
@@ -7,7 +7,7 @@ export const viewKeys = StrictDict({
studentTraining: 'student_training',
self: 'self_assessment',
peer: 'peer_assessment',
- done: 'my_grades',
+ done: 'graded',
});
export const progressKeys = StrictDict({
diff --git a/src/data/services/lms/fakeData/pageData/assessments.js b/src/data/services/lms/fakeData/pageData/assessments.js
index abef9a96..fa255753 100644
--- a/src/data/services/lms/fakeData/pageData/assessments.js
+++ b/src/data/services/lms/fakeData/pageData/assessments.js
@@ -2,36 +2,20 @@ import { stepNames } from 'data/services/lms/constants';
import { progressKeys } from '../constants';
export const createAssessmentState = ({
- options_selected = [],
- criterion_feedback,
+ assessment_criterions = [],
overall_feedback = '',
}) => ({
- options_selected,
- criterion_feedback,
+ assessment_criterions,
overall_feedback,
});
-export const emptySelections = {
- 'Criterion 1 name': null,
- 'Criterion 2 name': null,
- 'Criterion 3 name': null,
- 'Criterion 4 name': null,
-};
-export const filledSelections = {
- 'Criterion 1 name': 'Option 4 name',
- 'Criterion 2 name': 'Option 3 name',
- 'Criterion 3 name': 'Option 2 name',
- 'Criterion 4 name': 'Option 1 name',
-};
-
const gradedState = createAssessmentState({
- options_selected: filledSelections,
- criterion_feedback: {
- 'Criterion 1 name': 'feedback 1',
- 'Criterion 2 name': 'feedback 2',
- 'Criterion 3 name': 'feedback 3',
- 'Criterion 4 name': 'feedback 4',
- },
+ assessment_criterions: new Array(4).fill(0).map((_, i) => ({
+ name: `Criterion ${i + 1} name`,
+ selectedOption: `Option ${i + 1} name`,
+ selectedPoints: i,
+ feedback: `feedback ${i + 1}`,
+ })),
overall_feedback: 'nice job',
});
@@ -66,6 +50,12 @@ export const getAssessmentState = ({ progressKey, stepConfig }) => {
],
};
}
+ if (stepConfig.includes(stepNames.self)) {
+ out.self = {
+ stepScore: { earned: 10, possible: 10 },
+ assessment: gradedState,
+ };
+ }
return out;
};
diff --git a/src/data/services/lms/hooks/data.ts b/src/data/services/lms/hooks/data.ts
index 01d04d19..90fb4cbc 100644
--- a/src/data/services/lms/hooks/data.ts
+++ b/src/data/services/lms/hooks/data.ts
@@ -1,9 +1,6 @@
import { useQuery, useMutation } from '@tanstack/react-query';
-import {
- useParams,
- useLocation,
-} from 'react-router-dom';
+import { useParams, useLocation } from 'react-router-dom';
import { camelCaseObject } from '@edx/frontend-platform';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
@@ -25,17 +22,19 @@ export const useORAConfig = (): types.QueryData => {
({ data }) => camelCaseObject(data)
);
*/
- return Promise.resolve(camelCaseObject(fakeData.oraConfig.assessmentTinyMCE));
+ return Promise.resolve(
+ camelCaseObject(fakeData.oraConfig.assessmentTinyMCE)
+ );
},
});
-}
+};
export const usePageData = (): types.QueryData => {
const location = useLocation();
const { progressKey } = useParams();
const view = location.pathname.split('/')[1];
const pageDataUrl = usePageDataUrl(view);
-
+
return useQuery({
queryKey: [queryKeys.pageData],
queryFn: () => {
@@ -44,14 +43,20 @@ export const usePageData = (): types.QueryData => {
({ data }) => camelCaseObject(data)
);
*/
- return Promise.resolve(camelCaseObject(loadState({ view, progressKey })));
+ // const data = camelCaseObject(loadState({ view, progressKey }))
+ const data = loadState({ view, progressKey });
+ const result = {
+ ...camelCaseObject(data),
+ };
+ return Promise.resolve(result);
},
});
};
-export const useSubmitResponse = () => useMutation({
- mutationFn: (response) => {
- console.log({ submit: response });
- return Promise.resolve();
- },
-});
+export const useSubmitResponse = () =>
+ useMutation({
+ mutationFn: (response) => {
+ console.log({ submit: response });
+ return Promise.resolve();
+ },
+ });
diff --git a/src/data/services/lms/types/pageData.ts b/src/data/services/lms/types/pageData.ts
index fdc8d362..4401d7b0 100644
--- a/src/data/services/lms/types/pageData.ts
+++ b/src/data/services/lms/types/pageData.ts
@@ -78,6 +78,12 @@ export interface ResponseData {
export interface AssessmentData {
optionsSelected: { [key: string]: string | null },
criterionFeedback: { [key: string]: string },
+ assessmentCriterions: {
+ name: string,
+ selectedOption: string | null,
+ selectedPoints: number | null,
+ feedback: string,
+ }[],
overallFeedback: string | null,
}
@@ -88,10 +94,10 @@ export interface AssessmentsData {
stepScore: { earned: number, possible: number },
assessment: AssessmentData,
},
- peer?: {
+ peers?: {
stepScore: { earned: number, possible: number },
assessments: AssessmentData[],
- },
+ }[],
peerUnweighted?: {
stepScore: null,
assessmenst: AssessmentData[],
diff --git a/src/routes.ts b/src/routes.ts
index 0101274d..a5e09b2d 100644
--- a/src/routes.ts
+++ b/src/routes.ts
@@ -4,11 +4,13 @@ export default {
selfAssessmentEmbed: 'self_assessment/embedded/:courseId/:xblockId/:progressKey?',
studentTrainingEmbed: 'student_training/embedded/:courseId/:xblockId/:progressKey?',
submissionEmbed: 'submission/embedded/:courseId/:xblockId/:progressKey?',
+ gradedEmbed: 'graded/embedded/:courseId/:xblockId/:progressKey?',
rootEmbed: 'embedded/*',
xblock: 'xblock/:courseId/:xblockId/:progressKey?',
peerAssessment: 'peer_assessment/:courseId/:xblockId/:progressKey?',
selfAssessment: 'self_assessment/:courseId/:xblockId/:progressKey?',
studentTraining: 'student_training/:courseId/:xblockId/:progressKey?',
submission: 'submission/:courseId/:xblockId/:progressKey?',
+ graded: 'graded/:courseId/:xblockId/:progressKey?',
root: '/*',
};
diff --git a/src/views/GradeView/Content.jsx b/src/views/GradeView/Content.jsx
new file mode 100644
index 00000000..c7851ebb
--- /dev/null
+++ b/src/views/GradeView/Content.jsx
@@ -0,0 +1,46 @@
+import React from 'react';
+
+import { useIntl } from '@edx/frontend-platform/i18n';
+
+import {
+ usePrompts,
+ useResponseData,
+} from 'data/services/lms/hooks/selectors';
+
+import FileUpload from 'components/FileUpload';
+import Prompt from 'components/Prompt';
+import TextResponse from 'components/TextResponse';
+import messages from './messages';
+
+const Content = () => {
+ const prompts = usePrompts();
+ const response = useResponseData();
+ const { formatMessage } = useIntl();
+ return (
+
+
{formatMessage(messages.aboutYourGrade)}
+
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod
+ tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim
+ veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea
+ commodo consequat.
+
+
+ {
+ prompts.map((prompt, index) => (
+
+ ))
+ }
+
+
+
+ );
+};
+
+Content.defaultProps = {};
+Content.propTypes = {};
+
+export default Content;
diff --git a/src/views/GradeView/FinalGrade.jsx b/src/views/GradeView/FinalGrade.jsx
new file mode 100644
index 00000000..77458dc0
--- /dev/null
+++ b/src/views/GradeView/FinalGrade.jsx
@@ -0,0 +1,77 @@
+import React from 'react';
+
+import { useIntl } from '@edx/frontend-platform/i18n';
+
+import CollapsibleFeedback from 'components/CollapsibleFeedback';
+import { useAssessmentsData } from 'data/services/lms/hooks/selectors';
+import messages from './messages';
+
+const FinalGrade = () => {
+ const { formatMessage } = useIntl();
+ const assessments = useAssessmentsData();
+
+ const result = [];
+ let finalStepScore = null;
+ if (assessments.staff) {
+ finalStepScore = assessments.staff.stepScore;
+ result.push(
+
+ );
+ }
+ if (assessments.peer) {
+ finalStepScore = finalStepScore || assessments.peer.stepScore;
+ result.push(
+
+ {assessments.peer.assessment?.map((peer, index) => (
+
+ ))}
+
+ );
+ }
+ if (assessments.peerUnweighted) {
+ result.push(
+
+ {assessments.peerUnweighted.assessment?.map((peer, index) => (
+
+ ))}
+
+ );
+ }
+ if (assessments.self) {
+ finalStepScore = finalStepScore || assessments.self.stepScore;
+ result.push(
+
+ );
+ }
+
+ const [finalGrade, ...rest] = result;
+
+ return (
+
+
{formatMessage(messages.yourFinalGrade, finalStepScore)}
+ {finalGrade}
+
+
{formatMessage(messages.unweightedGrades)}
+ {rest}
+
+ );
+};
+
+FinalGrade.defaultProps = {};
+FinalGrade.propTypes = {};
+
+export default FinalGrade;
diff --git a/src/views/GradeView/index.jsx b/src/views/GradeView/index.jsx
new file mode 100644
index 00000000..a5516bfd
--- /dev/null
+++ b/src/views/GradeView/index.jsx
@@ -0,0 +1,30 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+import { ActionRow, Col, Row } from '@edx/paragon';
+
+import { AssessmentContextProvider } from 'components/AssessmentContext';
+
+import FinalGrade from './FinalGrade';
+import Content from './Content';
+
+import './index.scss';
+
+const GradeView = ({}) => (
+
+
+
+);
+GradeView.defaultProps = {};
+GradeView.propTypes = {};
+
+export default GradeView;
diff --git a/src/views/GradeView/index.scss b/src/views/GradeView/index.scss
new file mode 100644
index 00000000..22fb8803
--- /dev/null
+++ b/src/views/GradeView/index.scss
@@ -0,0 +1,14 @@
+@import "@edx/paragon/scss/core/core";
+
+.grade-view-body {
+ max-width: $max-width-xl;
+ flex-wrap: nowrap !important;
+ gap: 1em;
+}
+
+@include media-breakpoint-down(md) {
+ .grade-view-body {
+ flex-wrap: wrap !important;
+ gap: 0;
+ }
+}
diff --git a/src/views/GradeView/messages.js b/src/views/GradeView/messages.js
new file mode 100644
index 00000000..7db1be84
--- /dev/null
+++ b/src/views/GradeView/messages.js
@@ -0,0 +1,41 @@
+import { defineMessages } from '@edx/frontend-platform/i18n';
+
+const messages = defineMessages({
+ aboutYourGrade: {
+ id: 'ora-grade-view.aboutYourGrade',
+ defaultMessage: 'About your grade: ',
+ description: 'About your grade',
+ },
+ yourFinalGrade: {
+ id: 'ora-grade-view.yourFinalGrade',
+ defaultMessage: 'Your final grade: {earned}/{possible}',
+ description: 'Your final grade',
+ },
+ unweightedGrades: {
+ id: 'ora-grade-view.unweightedGrades',
+ defaultMessage: 'Unweighted Grades',
+ description: 'Unweighted grades',
+ },
+ selfStep: {
+ id: 'ora-grade-view.selfStep',
+ defaultMessage: 'Self',
+ description: 'Self step',
+ },
+ peerStep: {
+ id: 'ora-grade-view.peerStep',
+ defaultMessage: 'Peer',
+ description: 'Peer step',
+ },
+ staffStep: {
+ id: 'ora-grade-view.staffStep',
+ defaultMessage: 'Staff',
+ description: 'Staff step',
+ },
+ unweightedPeerStep: {
+ id: 'ora-grade-view.unweightedPeerStep',
+ defaultMessage: 'Unweighted Peer',
+ description: 'Unweighted peer step',
+ },
+});
+
+export default messages;