Skip to content

Commit e022e66

Browse files
authored
chore: prevent step from clickable when the date for the step is invalid (#179)
* chore: prevent step from clickable when the date for the step is invalid * chore: update linting * chore: add error boundary for error step
1 parent 2d814c0 commit e022e66

File tree

8 files changed

+144
-45
lines changed

8 files changed

+144
-45
lines changed

src/components/AppContainer.jsx

Lines changed: 0 additions & 35 deletions
This file was deleted.

src/components/AppContainer/index.jsx

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import React from 'react';
2+
import PropTypes from 'prop-types';
3+
4+
import { useIntl } from '@edx/frontend-platform/i18n';
5+
import { ErrorPage } from '@edx/frontend-platform/react';
6+
import { Spinner } from '@edx/paragon';
7+
8+
import {
9+
useIsPageDataLoaded,
10+
useIsORAConfigLoaded,
11+
usePageDataError,
12+
useORAConfigDataError,
13+
} from 'hooks/app';
14+
15+
import messages from './messages';
16+
17+
/* The purpose of this component is to wrap views with a header/footer for situations
18+
* where we need to run non-embedded
19+
*/
20+
21+
const AppContainer = ({ children }) => {
22+
const { formatMessage } = useIntl();
23+
const isConfigLoaded = useIsORAConfigLoaded();
24+
const isPageDataLoaded = useIsPageDataLoaded();
25+
const pageDataError = usePageDataError();
26+
const oraConfigDataError = useORAConfigDataError();
27+
const isLoaded = isConfigLoaded && isPageDataLoaded;
28+
29+
if (pageDataError || oraConfigDataError) {
30+
const { response } = pageDataError || oraConfigDataError;
31+
const errorMessage = messages[response?.data?.error?.errorCode] || messages.unknownError;
32+
33+
return <ErrorPage message={formatMessage(errorMessage)} />;
34+
}
35+
return (
36+
<div className="w-100 h-100">
37+
{isLoaded ? (
38+
children
39+
) : (
40+
<Spinner
41+
animation="border"
42+
screenReaderText="loading"
43+
className="app-loading"
44+
/>
45+
)}
46+
</div>
47+
);
48+
};
49+
AppContainer.propTypes = {
50+
children: PropTypes.node.isRequired,
51+
};
52+
53+
export default AppContainer;
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { defineMessages } from '@edx/frontend-platform/i18n';
2+
3+
const messages = defineMessages({
4+
ERR_INVALID_STATE_FOR_ASSESSMENT: {
5+
defaultMessage: 'This step is not available. Unable to retrieve the assessment.',
6+
description: 'Error message for invalid state for assessment',
7+
id: 'frontend-app-ora.StatusAlerts.ERR_INVALID_STATE_FOR_ASSESSMENT',
8+
},
9+
unknownError: {
10+
defaultMessage: 'An unknown error occurred. Please try again.',
11+
description: 'Error message for unknown error',
12+
id: 'frontend-app-ora.StatusAlerts.unknownError',
13+
},
14+
errorHeader: {
15+
defaultMessage: 'Something went wrong.',
16+
description: 'Error message header',
17+
id: 'frontend-app-ora.StatusAlerts.errorHeader',
18+
},
19+
});
20+
21+
export default messages;

src/components/ProgressBar/hooks.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ export const useProgressStepData = ({ step, canRevisit = false }) => {
1313
effectiveGrade,
1414
stepState,
1515
activeStepName,
16+
stepIsUnavailable,
1617
} = useGlobalState({ step });
1718
const stepInfo = useStepInfo();
1819
const openModal = useOpenModal();
@@ -23,9 +24,10 @@ export const useProgressStepData = ({ step, canRevisit = false }) => {
2324
const isActive = isXblock
2425
? activeStepName === step
2526
: viewStep === step;
26-
let isEnabled = isActive
27+
let isEnabled = !stepIsUnavailable
28+
&& (isActive
2729
|| stepState === stepStates.inProgress
28-
|| (canRevisit && stepState === stepStates.done);
30+
|| (canRevisit && stepState === stepStates.done));
2931

3032
if (step === stepNames.peer) {
3133
const isPeerComplete = stepInfo.peer?.numberOfReceivedAssessments > 0;

src/components/StatusAlert/messages.js

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,11 @@ const studentTrainingHeadings = defineMessages({
9797
});
9898

9999
const selfAlerts = defineMessages({
100+
[stepStates.notAvailable]: {
101+
id: 'frontend-app-ora.StatusAlert.self.notAvailable',
102+
defaultMessage: 'Self assessment is not available yet. Check back to complete the assignment once this section has opened',
103+
description: 'Self assessment not available status alert',
104+
},
100105
[stepStates.closed]: {
101106
id: 'frontend-app-ora.StatusAlert.self.closed',
102107
defaultMessage: 'The due date for this step has passed. This step is now closed. You can no longer complete a self assessment or continue with this assignment, and you will receive a grade of incomplete',
@@ -109,6 +114,11 @@ const selfAlerts = defineMessages({
109114
},
110115
});
111116
const selfHeadings = defineMessages({
117+
[stepStates.notAvailable]: {
118+
id: 'frontend-app-ora.StatusAlert.Heading.self.notAvailable',
119+
defaultMessage: 'Self assessment not available',
120+
description: 'Self assessment not available status alert heading',
121+
},
112122
[stepStates.submitted]: {
113123
id: 'frontend-app-ora.StatusAlert.Heading.self.submitted',
114124
defaultMessage: 'Self assessment: Completed',
@@ -221,6 +231,16 @@ const messages = defineMessages({
221231
defaultMessage: 'Exit',
222232
description: 'Status alert exit button text',
223233
},
234+
stepNotStarted: {
235+
id: 'frontend-app-ora.StatusAlert.step.notStarted',
236+
defaultMessage: '{stepName} step has not started yet. Check back on {startDatetime} to begin.',
237+
description: 'Step not started status alert',
238+
},
239+
stepNotStartedHeading: {
240+
id: 'frontend-app-ora.StatusAlert.Heading.step.notStarted',
241+
defaultMessage: '{stepName} not started',
242+
description: 'Step not started status alert heading',
243+
},
224244
});
225245

226246
export default {

src/data/services/lms/hooks/selectors/index.ts

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { StrictDict } from '@edx/react-unit-test-utils';
2+
import moment from 'moment';
23

34
import {
45
stepNames,
@@ -29,6 +30,7 @@ export const useStepState = ({ step = null } = {}) => {
2930
const subState = selectors.useSubmissionState();
3031
const trainingStepIsCompleted = selectors.useTrainingStepIsCompleted();
3132
const stepConfig = selectors.useAssessmentStepConfig()?.settings || {};
33+
const now = moment();
3234

3335
if (!stepInfo || !activeStepName || stepIndex === undefined || activeStepIndex === undefined) {
3436
return '';
@@ -53,20 +55,37 @@ export const useStepState = ({ step = null } = {}) => {
5355
return stepStates.done;
5456
}
5557

56-
if (activeStepName === stepNames.peer && stepInfo?.peer) {
57-
const config = stepConfig[stepNames.peer];
58+
if (stepName === stepNames.peer && stepInfo?.peer) {
59+
const config = stepConfig[stepName];
5860
const { numberOfAssessmentsCompleted, numberOfReceivedAssessments } = stepInfo.peer;
59-
const { minNumberToGrade, minNumberToBeGradedBy } = config;
61+
const { minNumberToGrade, minNumberToBeGradedBy, startDatetime, endDatetime } = config;
6062
const gradingDone = minNumberToGrade <= numberOfAssessmentsCompleted;
6163
const receivingDone = minNumberToBeGradedBy <= numberOfReceivedAssessments;
62-
if (gradingDone && !receivingDone) {
64+
if (now.isBefore(startDatetime)) {
65+
return stepStates.notAvailable;
66+
}
67+
else if (now.isAfter(endDatetime)) {
68+
return stepStates.closed;
69+
}
70+
else if (gradingDone && !receivingDone) {
6371
return stepStates.waitingForPeerGrades;
6472
}
65-
if (stepInfo.peer.isWaitingForSubmissions) {
73+
else if (stepInfo.peer.isWaitingForSubmissions) {
6674
return stepStates.waiting;
6775
}
6876
}
6977

78+
if (stepName === stepNames.self && stepConfig[stepName]) {
79+
const config = stepConfig[stepName];
80+
const { startDatetime, endDatetime } = config;
81+
if (now.isBefore(startDatetime)) {
82+
return stepStates.notAvailable;
83+
}
84+
else if (now.isAfter(endDatetime)) {
85+
return stepStates.closed;
86+
}
87+
}
88+
7089
// For Assessment steps
7190
if (stepIndex < activeStepIndex) { return stepStates.done; }
7291
if (stepIndex > activeStepIndex) { return stepStates.notAvailable; }
@@ -113,6 +132,8 @@ export const useGlobalState = ({ step = null } = {}) => {
113132
const effectiveGrade = selectors.useEffectiveGrade();
114133
const cancellationInfo = selectors.useCancellationInfo();
115134
const hasReceivedFinalGrade = selectors.useHasReceivedFinalGrade();
135+
const stepIsUnavailable = [stepStates.notAvailable, stepStates.closed].includes(stepState);
136+
116137
return {
117138
activeStepName,
118139
activeStepState,
@@ -121,6 +142,7 @@ export const useGlobalState = ({ step = null } = {}) => {
121142
hasReceivedFinalGrade,
122143
lastStep,
123144
stepState,
145+
stepIsUnavailable,
124146
};
125147
};
126148

src/data/services/lms/hooks/selectors/oraConfig.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import React from 'react';
2+
import moment from 'moment';
23
import * as data from 'data/services/lms/hooks/data';
34
import * as types from 'data/services/lms/types';
45
import { stepNames } from 'constants/index';

src/views/XBlockView/index.jsx

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
useORAConfigData,
55
usePrompts,
66
useRubricConfig,
7+
useGlobalState,
78
} from 'hooks/app';
89

910
import ProgressBar from 'components/ProgressBar';
@@ -23,10 +24,18 @@ export const XBlockView = () => {
2324
const prompts = usePrompts();
2425
const rubricConfig = useRubricConfig();
2526

27+
const { stepIsUnavailable } = useGlobalState();
28+
2629
useEffect(() => {
2730
if (window.parent.length > 0) {
2831
new ResizeObserver(() => {
29-
window.parent.postMessage({ type: 'plugin.resize', payload: { height: document.body.scrollHeight } }, document.referrer);
32+
window.parent.postMessage(
33+
{
34+
type: 'plugin.resize',
35+
payload: { height: document.body.scrollHeight },
36+
},
37+
document.referrer,
38+
);
3039
}).observe(document.body);
3140
}
3241
}, []);
@@ -40,8 +49,14 @@ export const XBlockView = () => {
4049
<HotjarSurvey />
4150
<Instructions />
4251
<Actions />
43-
{prompts.map(prompt => <Prompt key={prompt} prompt={prompt} />)}
44-
{rubricConfig.showDuringResponse && <Rubric isCollapsible />}
52+
{!stepIsUnavailable && (
53+
<>
54+
{prompts.map((prompt) => (
55+
<Prompt key={prompt} prompt={prompt} />
56+
))}
57+
{rubricConfig.showDuringResponse && <Rubric isCollapsible />}
58+
</>
59+
)}
4560
</div>
4661
);
4762
};

0 commit comments

Comments
 (0)