diff --git a/src/components/Assessment/ReadonlyAssessment/AssessmentCriterion.jsx b/src/components/Assessment/ReadonlyAssessment/AssessmentCriterion.jsx new file mode 100644 index 00000000..bdd19cc5 --- /dev/null +++ b/src/components/Assessment/ReadonlyAssessment/AssessmentCriterion.jsx @@ -0,0 +1,52 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +import { useIntl } from '@edx/frontend-platform/i18n'; +import Feedback from './Feedback'; +import messages from './messages'; +import { useORAConfigData } from 'data/services/lms/hooks/selectors'; + +const AssessmentCriterion = ({ + criteria, + overallFeedback, + stepLabel, +}) => { + const { formatMessage } = useIntl(); + const { rubricConfig } = useORAConfigData(); + return ( + <> + {rubricConfig.criteria.map((criterion, i) => { + const assessmentCriterion = criteria[i]; + const option = criterion.options[assessmentCriterion.selectedOption]; + return ( + + ); + })} + + + ); +}; +AssessmentCriterion.defaultProps = {}; +AssessmentCriterion.propTypes = { + criteria: PropTypes.arrayOf(PropTypes.shape({ + selectedOption: PropTypes.number, + // selectedPoints: PropTypes.number, + feedback: PropTypes.string, + })), + overallFeedback: PropTypes.string, + stepLabel: PropTypes.string.isRequired, +}; + +export default AssessmentCriterion; diff --git a/src/components/Assessment/ReadonlyAssessment/Feedback.jsx b/src/components/Assessment/ReadonlyAssessment/Feedback.jsx index a624a2c5..14ed9cbe 100644 --- a/src/components/Assessment/ReadonlyAssessment/Feedback.jsx +++ b/src/components/Assessment/ReadonlyAssessment/Feedback.jsx @@ -33,7 +33,7 @@ const Feedback = ({
{criterionName}
{criterionDescription && ( - {}}> +

{criterionDescription}

)} diff --git a/src/components/FileUpload/index.jsx b/src/components/FileUpload/index.jsx index a531876d..7c02e241 100644 --- a/src/components/FileUpload/index.jsx +++ b/src/components/FileUpload/index.jsx @@ -6,6 +6,7 @@ import { DataTable, Dropzone } from '@edx/paragon'; import { useIntl } from '@edx/frontend-platform/i18n'; import { nullMethod } from 'hooks'; +import { useFileUploadEnabled } from 'data/services/lms/hooks/selectors'; import UploadConfirmModal from './UploadConfirmModal'; import ActionCell from './ActionCell'; @@ -28,6 +29,9 @@ const FileUpload = ({ defaultCollapsePreview, }) => { const { formatMessage } = useIntl(); + if ( !useFileUploadEnabled() ) { + return null; + } const { confirmUpload, diff --git a/src/components/ProgressBar/hooks.js b/src/components/ProgressBar/hooks.js index 649af80e..9c26ed62 100644 --- a/src/components/ProgressBar/hooks.js +++ b/src/components/ProgressBar/hooks.js @@ -20,14 +20,14 @@ export const useProgressStepData = ({ step, canRevisit = false }) => { const isEnabled = ( isActive || (stepState === stepStates.inProgress) - || (canRevisit && stepState === stepStates.completed) + || (canRevisit && stepState === stepStates.done) ); const myGrade = useEffectiveGrade()?.stepScore; return { href, isEnabled, isActive, - isComplete: stepState === stepStates.completed, + isComplete: stepState === stepStates.done, inProgress: stepState === stepStates.inProgress, isPastDue: stepState === stepStates.closed, myGrade, diff --git a/src/components/ProgressBar/index.jsx b/src/components/ProgressBar/index.jsx index cc0abb7a..7607d397 100644 --- a/src/components/ProgressBar/index.jsx +++ b/src/components/ProgressBar/index.jsx @@ -7,7 +7,9 @@ import { Navbar } from '@edx/paragon'; import { useAssessmentStepOrder, + useHasReceivedFinalGrade, useIsPageDataLoaded, + useStepInfo, } from 'data/services/lms/hooks/selectors'; import { stepNames } from 'data/services/lms/constants'; @@ -34,33 +36,35 @@ export const stepCanRevisit = { export const ProgressBar = ({ className }) => { const isLoaded = useIsPageDataLoaded(); + const stepInfo = useStepInfo(); + const hasReceivedFinalGrade = useHasReceivedFinalGrade(); - const stepOrder = useAssessmentStepOrder(); + const stepOrders = [ + stepNames.submission, + ...useAssessmentStepOrder(), + stepNames.done, + ]; const { formatMessage } = useIntl(); if (!isLoaded) { return null; } - const stepEl = (step) => ( - stepLabels[step] - ? ( - - ) : null - ); + const stepEl = (curStep) => + stepLabels[curStep] ? ( + + ) : null; return ( - -
- {stepEl(stepNames.submission)} - {stepOrder.map(stepEl)} - {stepEl(stepNames.done)} + +
+ {stepOrders.map(stepEl)}
); diff --git a/src/components/StatusAlert/index.jsx b/src/components/StatusAlert/index.jsx index 3bba6e2f..d0c052ac 100644 --- a/src/components/StatusAlert/index.jsx +++ b/src/components/StatusAlert/index.jsx @@ -11,6 +11,9 @@ const StatusAlert = ({ step, showTrainingError, }) => { + if ( step === null ) { + return null; + } const { variant, icon, diff --git a/src/components/StatusAlert/useStatusAlert.js b/src/components/StatusAlert/useStatusAlert.js new file mode 100644 index 00000000..d3134f9d --- /dev/null +++ b/src/components/StatusAlert/useStatusAlert.js @@ -0,0 +1,92 @@ +import { useIntl } from '@edx/frontend-platform/i18n'; +import { CheckCircle, Info, WarningFilled } from '@edx/paragon/icons'; +import { StrictDict } from '@edx/react-unit-test-utils'; + +import { + stepNames, + stepStates, +} from 'data/services/lms/constants'; +import { useGlobalState } from 'data/services/lms/hooks/selectors'; +import messages from './messages'; +import alertMessages from './alertMessages'; +import headingMessages from './alertHeadingMessages'; + +export const alertMap = { + [stepStates.done]: { + variant: 'success', + icon: CheckCircle, + }, + [stepStates.closed]: { + variant: 'danger', + icon: Info, + }, + [stepStates.teamAlreadySubmitted]: { + variant: 'warning', + icon: WarningFilled, + }, + [stepStates.cancelled]: { + variant: 'warning', + icon: WarningFilled, + }, + [stepStates.inProgress]: { + variant: 'dark', + icon: null, + }, +}; + +const useStatusAlertMessages = (step = null) => { + const { formatMessage } = useIntl(); + const { + activeStepName, + stepState, + cancellationInfo, + } = useGlobalState({ step }); + const stepName = step || activeStepName; + const isRevisit = stepName !== activeStepName; + if (cancellationInfo.hasCancelled) { + const { cancelledBy, cancelledAt } = cancellationInfo; + if (cancelledBy) { + return { + message: formatMessage( + alertMessages.submission.cancelledBy, + { cancelledBy, cancelledAt }, + ), + heading: formatMessage(headingMessages.submission.cancelledBy), + }; + } + return { + message: formatMessage(alertMessages.submission.cancelledAt, { cancelledAt }), + heading: formatMessage(headingMessages.submission.cancelledAt), + }; + } + if (stepName === stepNames.submission && isRevisit) { + return { + message: formatMessage(alertMessages.submission.finished), + heading: formatMessage(headingMessages.submission.finished), + }; + } + if (stepName === stepNames.peer && isRevisit && stepState !== stepStates.waiting) { + return { + message: formatMessage(alertMessages.peer.finished), + heading: formatMessage(headingMessages.peer.finished), + }; + } + return { + message: formatMessage(alertMessages[stepName][stepState]), + heading: formatMessage(headingMessages[stepName][stepState]), + }; +}; + +const useStatusAlert = (step = null) => { + const { stepState } = useGlobalState({ step }); + const { variant, icon } = alertMap[stepState]; + const { message, heading } = useStatusAlertMessages(step); + return { + variant, + icon, + message, + heading, + }; +}; + +export default useStatusAlert; diff --git a/src/components/StatusAlert/useStatusAlertData.jsx b/src/components/StatusAlert/useStatusAlertData.jsx index e6021a40..745eb8aa 100644 --- a/src/components/StatusAlert/useStatusAlertData.jsx +++ b/src/components/StatusAlert/useStatusAlertData.jsx @@ -15,7 +15,7 @@ import alertMessages from './alertMessages'; import headingMessages from './alertHeadingMessages'; export const alertMap = { - [stepStates.completed]: { + [stepStates.done]: { variant: 'success', icon: CheckCircle, }, diff --git a/src/data/services/lms/constants.js b/src/data/services/lms/constants.js index da3fb68c..5a02d624 100644 --- a/src/data/services/lms/constants.js +++ b/src/data/services/lms/constants.js @@ -20,7 +20,7 @@ export const MutationStatus = StrictDict({ export const stepStates = StrictDict({ inProgress: 'inProgress', - completed: 'completed', + done: 'done', cancelled: 'cancelled', closed: 'closed', notAvailable: 'notAvailable', diff --git a/src/data/services/lms/fakeData/dataStates.js b/src/data/services/lms/fakeData/dataStates.js index d467c19a..2aa5867c 100644 --- a/src/data/services/lms/fakeData/dataStates.js +++ b/src/data/services/lms/fakeData/dataStates.js @@ -31,7 +31,7 @@ export const loadState = (opts) => { const state = { progress: pageData.getProgressState({ progressKey, stepConfig, viewStep }), response: pageData.getResponseState({ progressKey, isTeam }), - assessments: pageData.getAssessmentState({ progressKey, stepConfig }), + assessment: pageData.getAssessmentState({ progressKey, stepConfig }), }; console.log({ opts, progressKey, state, isTeam, diff --git a/src/data/services/lms/hooks/data.ts b/src/data/services/lms/hooks/data.ts index 8a2cb484..ac5efde9 100644 --- a/src/data/services/lms/hooks/data.ts +++ b/src/data/services/lms/hooks/data.ts @@ -17,15 +17,9 @@ export const useORAConfig = (): types.QueryData => { return useQuery({ queryKey: [queryKeys.oraConfig], queryFn: () => { - /* return getAuthenticatedHttpClient().post(oraConfigUrl, {}).then( ({ data }) => camelCaseObject(data) ); - */ - console.log({ oraConfig: camelCaseObject(fakeData.oraConfig.assessmentTinyMCE) }); - return Promise.resolve( - camelCaseObject(fakeData.oraConfig.assessmentTinyMCE) - ); }, }); }; @@ -39,12 +33,9 @@ export const usePageData = (): types.QueryData => { return useQuery({ queryKey: [queryKeys.pageData], queryFn: () => { - /* return getAuthenticatedHttpClient().post(pageDataUrl, {}).then( ({ data }) => camelCaseObject(data) ); - */ - return Promise.resolve(camelCaseObject(loadState({ view, progressKey }))); }, }); }; diff --git a/src/data/services/lms/hooks/selectors/index.ts b/src/data/services/lms/hooks/selectors/index.ts index bb9b87e9..b8cca2f3 100644 --- a/src/data/services/lms/hooks/selectors/index.ts +++ b/src/data/services/lms/hooks/selectors/index.ts @@ -30,7 +30,7 @@ export const useStepState = ({ step = null } = {}) => { const stepIndex = selectors.useStepIndex({ step: stepName }); const subState = selectors.useSubmissionState(); if (hasReceivedFinalGrade) { - return stepStates.completed; + return stepStates.done; } if (step === stepNames.submission) { @@ -41,14 +41,14 @@ export const useStepState = ({ step = null } = {}) => { if (hasCancelled) { return stepStates.cancelled; } if (step === stepNames.done) { - return hasReceivedFinalGrade ? stepStates.completed : stepStates.notAvailable; + return hasReceivedFinalGrade ? stepStates.done : stepStates.notAvailable; } if (step === stepNames.peer && stepInfo?.peer?.isWaitingForSubmissions) { return stepStates.waiting; } // For Assessment steps - if (stepIndex < activeStepIndex) { return stepStates.completed; } + if (stepIndex < activeStepIndex) { return stepStates.done; } if (stepIndex > activeStepIndex) { return stepStates.notAvailable; } // only check for closed or not-available on active step diff --git a/src/data/services/lms/hooks/selectors/oraConfig.ts b/src/data/services/lms/hooks/selectors/oraConfig.ts index e9fc5607..eb81e80b 100644 --- a/src/data/services/lms/hooks/selectors/oraConfig.ts +++ b/src/data/services/lms/hooks/selectors/oraConfig.ts @@ -36,6 +36,8 @@ export const useAssessmentStepConfig = (): types.AssessmentStepConfig => ( useORAConfigData().assessmentSteps ); +export const useFileUploadEnabled = (): boolean => useSubmissionConfig().fileResponseConfig.enabled; + export const useAssessmentStepOrder = (): string[] => useAssessmentStepConfig()?.order; export const useStepIndex = ({ step }): number => useAssessmentStepOrder().indexOf(step); diff --git a/src/data/services/lms/hooks/selectors/pageData.ts b/src/data/services/lms/hooks/selectors/pageData.ts index 572a2cd1..b4c71a52 100644 --- a/src/data/services/lms/hooks/selectors/pageData.ts +++ b/src/data/services/lms/hooks/selectors/pageData.ts @@ -54,7 +54,7 @@ export const useSubmissionState = () => { } if (subStatus.hasSubmitted) { - return stepStates.completed; + return stepStates.done; } if (subStatus.isClosed) { if (subStatus.closedReason === closedReasons.pastDue) { @@ -68,13 +68,10 @@ export const useSubmissionState = () => { return stepStates.inProgress; }; -// Assessments -export const useAssessmentsData = (): types.AssessmentsData => { - console.log({ pageData: usePageData() }); - return usePageData().assessments; -}; -export const useHasReceivedFinalGrade = (): boolean => useAssessmentsData() !== null; +// Assessment +export const useAssessmentData = (): types.AssessmentsData => usePageData().assessment; +export const useHasReceivedFinalGrade = (): boolean => useAssessmentData() !== null; export const useEffectiveGrade = () => { - const assessments = useAssessmentsData(); - return assessments ? assessments[assessments.effectiveAssessmentType] : null; + const assessment = useAssessmentData(); + return assessment ? assessment[assessment.effectiveAssessmentType] : null; }; diff --git a/src/data/services/lms/types/pageData.ts b/src/data/services/lms/types/pageData.ts index 14d2b6f3..d81a82d1 100644 --- a/src/data/services/lms/types/pageData.ts +++ b/src/data/services/lms/types/pageData.ts @@ -76,7 +76,7 @@ export interface ResponseData { // Assessments Data export interface AssessmentData { - assessmentCriterions: { + criteria: { selectedOption: number | null, feedback: string, }[], @@ -106,5 +106,5 @@ export interface AssessmentsData { export interface PageData { progress: ProgressData, response: ResponseData, - assessments: AssessmentsData + assessment: AssessmentsData } diff --git a/src/data/services/lms/urls.js b/src/data/services/lms/urls.js index c5fe9839..5b74c4ac 100644 --- a/src/data/services/lms/urls.js +++ b/src/data/services/lms/urls.js @@ -22,10 +22,10 @@ export const useViewUrl = () => { export const usePageDataUrl = (step) => { const baseUrl = useBaseUrl(); - if ([stepNames.submission, stepNames.xblock].includes(step)) { - return `${baseUrl}/get_block_learner_submission_data`; + if ( [stepNames.submission, stepNames.peer].includes(step) ) { + return `${baseUrl}/get_learner_data/${step}`; } - return `${baseUrl}/get_block_learner_assessment_data/${step}`; + return `${baseUrl}/get_learner_data`; }; export default StrictDict({ diff --git a/src/index.jsx b/src/index.jsx index 899fba76..cefb6bbc 100644 --- a/src/index.jsx +++ b/src/index.jsx @@ -27,7 +27,7 @@ subscribe(APP_READY, () => { // This is a hack to prevent the Paragon Modal overlay stop query devtools from clickable rootEl.removeAttribute('data-focus-on-hidden'); rootEl.removeAttribute('aria-hidden'); - }, 1000); + }, 3000); } ReactDOM.render( diff --git a/src/views/GradeView/FinalGrade.jsx b/src/views/GradeView/FinalGrade.jsx index 780c5a8c..e7a3afc8 100644 --- a/src/views/GradeView/FinalGrade.jsx +++ b/src/views/GradeView/FinalGrade.jsx @@ -2,14 +2,18 @@ import React from 'react'; import { useIntl } from '@edx/frontend-platform/i18n'; -import { useAssessmentsData } from 'data/services/lms/hooks/selectors'; +import { + useAssessmentData, + useStepInfo, +} from 'data/services/lms/hooks/selectors'; import InfoPopover from 'components/InfoPopover'; import ReadOnlyAssessment from 'components/Assessment/ReadonlyAssessment'; import messages, { labelMessages } from './messages'; const FinalGrade = () => { const { formatMessage } = useIntl(); - const { effectiveAssessmentType, ...assessments } = useAssessmentsData(); + const { effectiveAssessmentType, ...assessments } = useAssessmentData(); + const stepInfo = useStepInfo(); const loadStepData = (step) => ({ ...assessments[step], @@ -22,7 +26,7 @@ const FinalGrade = () => { const finalStepScore = effectiveAssessment?.stepScore; const extraGrades = Object.keys(assessments) - .filter(type => type !== effectiveAssessmentType) + .filter((type) => !!stepInfo[type] && type !== effectiveAssessmentType) .map(loadStepData); const renderAssessment = (stepData, defaultOpen = false) => ( @@ -33,23 +37,27 @@ const FinalGrade = () => {

{formatMessage(messages.yourFinalGrade, finalStepScore)} - {}}> +

{effectiveAssessmentType === 'peer' ? formatMessage(messages.peerAsFinalGradeInfo) - : formatMessage(messages.finalGradeInfo, { step: effectiveAssessmentType })} + : formatMessage(messages.finalGradeInfo, { + step: effectiveAssessmentType, + })}

{renderAssessment(effectiveAssessment, true)} -
-

- {formatMessage(messages.unweightedGrades)} - {}}> -

{formatMessage(messages.unweightedGradesInfo)}

-
-

- {extraGrades.map(assessment => renderAssessment(assessment, false))} +
+ {extraGrades.length > 0 && ( +

+ {formatMessage(messages.unweightedGrades)} + +

{formatMessage(messages.unweightedGradesInfo)}

+
+

+ )} + {extraGrades.map((assessment) => renderAssessment(assessment, false))}
); }; diff --git a/src/views/SubmissionView/index.jsx b/src/views/SubmissionView/index.jsx index e61f9e28..60b0741e 100644 --- a/src/views/SubmissionView/index.jsx +++ b/src/views/SubmissionView/index.jsx @@ -33,7 +33,7 @@ export const SubmissionView = () => { } = useSubmissionViewData(); const stepState = useStepState({ step: stepNames.submission }); - const isReadOnly = stepState === stepStates.completed; + const isReadOnly = stepState === stepStates.done; const { formatMessage } = useIntl(); const draftIndicator = (!isReadOnly && isDraftSaved) && (