Files
frontend-app-authoring/src/grading-settings/grading-scale/GradingScale.jsx
Kshitij Sobti 4d4adce715 feat: Use configured DEFAULT_GRADE_DESIGNATIONS (#1227)
Support was recently added to edx-platform to add customisised default grade
designations, this change adds support for them to this MFE as well to bring it
to partiy with the edx-platform UI

It also refactors the grading-settings page to use React Query and updates the
logic used when partitioning grades by default to make it work better when there
are more than 5 partitions.

Co-authored-by: Farhaan Bukhsh <farhaan@opencraft.com>
2024-10-25 15:34:04 +05:30

269 lines
8.7 KiB
JavaScript

import { useIntl } from '@edx/frontend-platform/i18n';
import { Icon, IconButtonWithTooltip } from '@openedx/paragon';
import { Add as IconAdd } from '@openedx/paragon/icons';
import PropTypes from 'prop-types';
import React, { useEffect, useState } from 'react';
import { GradingScaleHandle, GradingScaleSegment, GradingScaleTicks } from './components';
import messages from './messages';
import { useRanger } from './react-ranger';
import { convertGradeData, MAXIMUM_SCALE_LENGTH } from './utils';
const DEFAULT_GRADE_LETTERS = ['A', 'B', 'C', 'D'];
const getDefaultPassText = intl => intl.formatMessage(messages.defaultPassText);
const GradingScale = ({
showSavePrompt,
gradeCutoffs,
setShowSuccessAlert,
setGradingData,
resetDataRef,
gradeLetters,
sortedGrades,
setOverrideInternetConnectionAlert,
setEligibleGrade,
defaultGradeDesignations,
}) => {
const intl = useIntl();
const [gradingSegments, setGradingSegments] = useState(sortedGrades);
const [letters, setLetters] = useState(gradeLetters);
const [convertedResult, setConvertedResult] = useState({});
const gradingSegmentsValues = Object.values(gradingSegments);
const eligibleValue = gradingSegmentsValues[gradingSegmentsValues.length - 1];
useEffect(() => {
if (resetDataRef.current) {
setGradingSegments(sortedGrades);
setLetters(gradeLetters);
// eslint-disable-next-line no-param-reassign
resetDataRef.current = false;
}
}, [gradeCutoffs]);
useEffect(() => {
setGradingSegments(sortedGrades);
setLetters(gradeLetters);
}, [sortedGrades.length]);
useEffect(() => {
setGradingData(prevData => ({ ...prevData, gradeCutoffs: convertedResult }));
setEligibleGrade(eligibleValue?.current);
}, [JSON.stringify(convertedResult)]);
useEffect(() => {
convertGradeData(letters, gradingSegments, setConvertedResult);
}, [gradingSegments, letters]);
const addNewGradingSegment = () => {
setGradingSegments((prevSegments) => {
let updatedGradingSegment = [];
if (prevSegments.length >= 5) {
const segSize = MAXIMUM_SCALE_LENGTH / (prevSegments.length + 1);
updatedGradingSegment = Array.from({
length: prevSegments.length + 1,
}).map((_, i) => ({
current: 100 - i * segSize,
previous: 100 - (i + 1) * segSize,
}));
} else {
const firstSegment = prevSegments[prevSegments.length - 1];
const secondSegment = prevSegments[prevSegments.length - 2];
const newCurrentValue = Math.ceil(
(secondSegment.current - secondSegment.previous) / 2,
);
const newSegment = {
current: firstSegment.current + newCurrentValue,
previous: firstSegment.current,
};
const updatedSecondSegment = {
...secondSegment,
previous: firstSegment.current + newCurrentValue,
};
updatedGradingSegment = [
...prevSegments.slice(0, prevSegments.length - 2),
updatedSecondSegment,
newSegment,
firstSegment,
];
}
showSavePrompt(true);
setShowSuccessAlert(false);
setOverrideInternetConnectionAlert(false);
return updatedGradingSegment;
});
const nextIndex = (letters.length % defaultGradeDesignations.length);
if (gradingSegments.length === 2) {
setLetters([defaultGradeDesignations[0], defaultGradeDesignations[nextIndex]]);
} else {
setLetters(prevLetters => [...prevLetters, defaultGradeDesignations[nextIndex]]);
}
};
const updateGradingSegments = (newGradingSegmentData, activeHandleIndex) => {
const gapToSegment = 1;
const sortedSegments = newGradingSegmentData.sort((currentValue, previousValue) => currentValue - previousValue);
const newSegmentValue = sortedSegments[sortedSegments.length - 1 - activeHandleIndex];
const prevSegmentBoundary = (gradingSegments[activeHandleIndex + 1]
&& gradingSegments[activeHandleIndex + 1].current) || 0;
const nextSegmentBoundary = gradingSegments[activeHandleIndex - 1].current;
showSavePrompt(true);
setGradingSegments(gradingSegments.map((gradingSegment, idx) => {
const upperBoundaryValue = (newSegmentValue < nextSegmentBoundary - gapToSegment)
? newSegmentValue : (nextSegmentBoundary - gapToSegment);
const lowerBoundaryValue = (upperBoundaryValue > prevSegmentBoundary + gapToSegment)
? upperBoundaryValue : (prevSegmentBoundary + gapToSegment);
if (idx === activeHandleIndex - 1) {
return {
previous: lowerBoundaryValue,
current: gradingSegment.current,
};
}
if (idx === activeHandleIndex) {
return {
current: lowerBoundaryValue,
previous: gradingSegment.previous,
};
}
return gradingSegment;
}));
};
const removeGradingSegment = (gradingSegmentIndex) => {
setGradingSegments(prevSegments => {
const updatedSegments = [...prevSegments];
const removedSegment = updatedSegments.splice(gradingSegmentIndex - 1, 1)[0];
const previousSegment = updatedSegments[gradingSegmentIndex - 2];
if (previousSegment) {
previousSegment.previous = removedSegment.previous;
}
return updatedSegments;
});
showSavePrompt(true);
setShowSuccessAlert(false);
setOverrideInternetConnectionAlert(false);
setLetters(prevLetters => {
const updatedLetters = [...prevLetters];
updatedLetters.splice(updatedLetters.length - 1, 1);
return updatedLetters.length === 1 ? [getDefaultPassText(intl)] : updatedLetters;
});
};
const handleLetterChange = (e, idx) => {
const { value } = e.target;
showSavePrompt(true);
setShowSuccessAlert(false);
setOverrideInternetConnectionAlert(false);
setLetters(prevLetters => {
const updatedLetters = [...prevLetters];
const emptyString = '\u200B';
updatedLetters[idx - 1] = value || emptyString;
return updatedLetters;
});
};
const handleSegmentChange = () => {
setShowSuccessAlert(false);
setOverrideInternetConnectionAlert(false);
setGradingData(prevData => ({ ...prevData, gradeCutoffs: convertedResult }));
};
const {
getTrackProps,
ticks,
segments,
handles,
activeHandleIndex,
} = useRanger({
min: 0,
max: MAXIMUM_SCALE_LENGTH,
stepSize: 1,
values: gradingSegments?.map(segment => segment.current),
onDrag: (segmentDataArray) => updateGradingSegments(segmentDataArray, activeHandleIndex),
onChange: handleSegmentChange,
});
return (
<div className="grading-scale">
<IconButtonWithTooltip
tooltipPlacement="top"
tooltipContent={intl.formatMessage(messages.addNewSegmentButtonAltText)}
disabled={gradingSegments.length >= (defaultGradeDesignations.length + 1)}
data-testid="grading-scale-btn-add-segment"
className="mr-3"
src={IconAdd}
iconAs={Icon}
alt={intl.formatMessage(messages.addNewSegmentButtonAltText)}
onClick={addNewGradingSegment}
/>
<div className="grading-scale-segments-and-ticks" {...getTrackProps()}>
{ticks.map(({ value, getTickProps }) => (
<GradingScaleTicks key={value} value={value} getTickProps={getTickProps} />
))}
{segments.reverse().map(({ value, getSegmentProps }, idx = 1) => (
<GradingScaleSegment
key={idx}
getSegmentProps={getSegmentProps}
removeGradingSegment={removeGradingSegment}
gradingSegments={gradingSegments}
value={value}
idx={idx}
handleLetterChange={handleLetterChange}
letters={letters}
/>
))}
{handles.map(({ value, getHandleProps }, idx) => (
<GradingScaleHandle
key={value}
getHandleProps={getHandleProps}
gradingSegments={gradingSegments}
value={value}
idx={idx}
/>
))}
</div>
</div>
);
};
GradingScale.propTypes = {
showSavePrompt: PropTypes.func.isRequired,
gradeCutoffs: PropTypes.objectOf(PropTypes.number).isRequired,
gradeLetters: PropTypes.arrayOf(PropTypes.string).isRequired,
setShowSuccessAlert: PropTypes.func.isRequired,
setGradingData: PropTypes.func.isRequired,
setOverrideInternetConnectionAlert: PropTypes.func.isRequired,
resetDataRef: PropTypes.objectOf(PropTypes.bool).isRequired,
sortedGrades: PropTypes.arrayOf(
PropTypes.shape({
current: PropTypes.number.isRequired,
previous: PropTypes.number.isRequired,
}),
).isRequired,
setEligibleGrade: PropTypes.func.isRequired,
defaultGradeDesignations: PropTypes.arrayOf(PropTypes.string),
};
GradingScale.defaultProps = {
defaultGradeDesignations: DEFAULT_GRADE_LETTERS,
};
export default GradingScale;