@instructure/quiz-grading
Version:
The Quiz React SDK by Instructure Inc.
335 lines (304 loc) • 11 kB
JavaScript
/** @jsx jsx */
import {Component} from 'react'
import PropTypes from 'prop-types'
import ImmutablePropTypes from 'react-immutable-proptypes'
import {List} from 'immutable'
import {Alert} from '@instructure/ui-alerts'
import {Button} from '@instructure/ui-buttons'
import {ScreenReaderContent} from '@instructure/ui-a11y-content'
import {Text} from '@instructure/ui-text'
import {jsx} from '@instructure/emotion'
import {CustomPropTypes} from '@instructure/quiz-core/common/util/CustomPropTypes'
import {Spinner} from '@instructure/quiz-core/common/components/shared/spinner/Spinner'
import {componentForItem} from '@instructure/quiz-core/common/util/componentForItem'
import {propsForInteractionEdit} from '@instructure/quiz-core/common/util/interactionTypePropsHelper'
import {UndecoratedQuizEntryEdit} from '@instructure/quiz-core/building/components/resources/quizEntry/QuizEntryEdit/presenter'
import {COMPONENT_EDIT, PARENT_TYPE_QUIZ, REGRADE_MODAL} from '@instructure/quiz-common/constants'
import {withStyleOverrides} from '@instructure/quiz-common/util/withStyleOverrides'
import RegradingModal from '../../../RegradingModal'
import generateStyle from './styles'
import generateComponentTheme from './theme'
import t from '@instructure/quiz-i18n/format-message'
const noop = () => {}
class BaseRegradeOverride extends Component {
static displayName = 'RegradeOverride'
static componentId = `Quizzes${this.displayName}`
static propTypes = {
appContainer: CustomPropTypes.selectors.isRequired,
changeQuizEntryPoints: PropTypes.func.isRequired,
entry: ImmutablePropTypes.record.isRequired,
hideError: PropTypes.func.isRequired,
guid: PropTypes.string.isRequired,
workingEntry: ImmutablePropTypes.record.isRequired,
clearModifications: PropTypes.func.isRequired,
getItem: PropTypes.func.isRequired,
getQuizEntry: PropTypes.func.isRequired,
itemId: PropTypes.string.isRequired,
onCancel: PropTypes.func.isRequired,
openModal: PropTypes.func.isRequired,
quizEntry: ImmutablePropTypes.record.isRequired,
workingQuizEntry: ImmutablePropTypes.record.isRequired,
quizId: PropTypes.string.isRequired,
sessionItem: ImmutablePropTypes.record.isRequired,
setRegradingPayload: PropTypes.func.isRequired,
showError: PropTypes.func.isRequired,
toggleRegradingEditMode: PropTypes.func.isRequired,
handleValidationErrorsFromApi: PropTypes.func.isRequired,
validationErrorsFromApi: PropTypes.objectOf(
PropTypes.arrayOf(
PropTypes.shape({
type: PropTypes.string,
text: PropTypes.string,
}),
),
),
// required by propsForInteractionEdit
addError: PropTypes.func.isRequired,
changeItemState: PropTypes.func.isRequired,
displayPosition: PropTypes.number.isRequired,
errorsAreShowing: PropTypes.bool,
interactionFileUpload: PropTypes.func.isRequired,
oneQuestionAtATime: PropTypes.bool.isRequired,
partialDeepScoringEnabled: PropTypes.bool,
partialScoringEnabled: PropTypes.bool,
parentId: PropTypes.string.isRequired,
parentType: PropTypes.string.isRequired,
screenreaderNotification: PropTypes.func.isRequired,
setOneQuestionAtATime: PropTypes.func.isRequired,
styles: PropTypes.object,
}
static defaultProps = {
errorsAreShowing: false,
partialScoringEnabled: false,
partialDeepScoringEnabled: false,
validationErrorsFromApi: {},
}
quizEntryProps() {
return {
...this.props,
alternativeRightFooter: this.renderActionFooter,
changeItemTitle: noop,
clearModifications: noop,
clearTemporaryItem: noop,
cloneQuizEntry: noop,
connectDragPreview: noop,
connectDragSource: noop,
createItem: noop,
createQuizEntry: noop,
disabledForRegrade: true,
draggingAboveHalf: false,
draggingOver: false,
haveValidationErrorsFromApi: Boolean(
Object.values(this.props.validationErrorsFromApi).length,
),
hideError: noop,
isOccluded: false,
itemFeedbackOn: false,
name: '',
openModal: noop,
openRegradeModal: noop,
quizEntries: List(),
removeQuizEntry: noop,
screenreaderNotification: noop,
setUi: noop,
shouldClone: false,
showError: noop,
switchOffEditing: noop,
updateItem: noop,
updateQuizEntry: noop,
withConfirm: noop,
}
}
itemEditProps() {
return {
...this.props,
handleQuizEntryPointsChange: this.handleQuizEntryPointsChange,
openImportModal: () => {},
parentId: this.props.quizId,
parentType: PARENT_TYPE_QUIZ,
title: this.props.entry.title,
workingItem: this.props.workingEntry,
}
}
handleQuizEntryPointsChange = points => {
const {workingQuizEntry, workingEntry} = this.props
const qerIndexToUpdate = workingQuizEntry.quizEntryRegrades.findIndex(
qer => qer.get('itemId') == workingEntry.id,
)
const newQuizEntryRegrades =
qerIndexToUpdate > -1
? workingQuizEntry.quizEntryRegrades.setIn([qerIndexToUpdate, 'pointsPossible'], null)
: workingQuizEntry.quizEntryRegrades
this.props.changeQuizEntryPoints(workingQuizEntry.id, points, newQuizEntryRegrades.toJS())
}
navigateToTop = () => {
setTimeout(() => {
this.props.screenreaderNotification(
t('Focus moved to top of question. Navigate down to continue.'),
)
if (this.alertNode) {
this.alertNode.focus()
}
})
}
componentDidMount() {
if (this.props.quizEntry.id === '0') {
this.props.getQuizEntry(this.props.quizId, this.props.sessionItem.quizEntryId)
}
if (!this.props.entry.isLoaded()) {
this.props.getItem(this.props.itemId)
} else {
this.navigateToTop()
}
}
componentDidUpdate(prevProps) {
if (!prevProps.entry.isLoaded() && this.props.entry.isLoaded()) {
this.navigateToTop()
}
}
turnOffRegradingEditMode = () => {
this.props.clearModifications('items', this.props.itemId)
this.props.clearModifications('quizEntries', this.props.sessionItem.quizEntryId)
this.props.toggleRegradingEditMode('')
this.props.onCancel(this.props.sessionItem.id)
}
triggerRegradingModal = e => {
const {displayPosition, workingQuizEntry, itemId} = this.props
e && e.preventDefault()
if (this.isValid()) {
this.props.hideError(this.props.guid)
const {scoringAlgorithm, scoringData} = this.props.workingEntry
const pointsPossible =
workingQuizEntry.regradeAdjustedPointsPossible(itemId) || workingQuizEntry.pointsPossible
this.props.setRegradingPayload({
itemId: this.props.itemId,
quizEntryId: this.props.quizEntry.id,
pointsPossible,
scoringAlgorithm,
scoringData,
})
this.props.openModal(REGRADE_MODAL)
} else {
const msg = t(
'Question {displayPosition, number} cannot be saved due to invalid input data.',
{displayPosition},
)
this.props.screenreaderNotification(msg)
this.props.showError(this.props.guid)
}
}
isValid() {
return this.props.workingEntry.isValid() && this.props.workingQuizEntry.isValid()
}
setAlertRef = node => {
this.alertNode = node
}
renderActionFooter = () => {
return (
<div>
<Button margin="small" onClick={this.turnOffRegradingEditMode}>
{t('Cancel')}
</Button>
<Button
margin="small"
color="primary"
onClick={this.triggerRegradingModal}
data-automation="sdk-trigger-regrade-button"
>
{t.rich('Regrade <0>question {position, number}</0>', {
0: ({children}) => <ScreenReaderContent key="0">{children}</ScreenReaderContent>,
position: this.props.quizEntry.position,
})}
</Button>
</div>
)
}
renderRegradingAlert() {
return (
<div>
<Alert margin="small 0" transition="none" variant="info">
<div css={this.props.styles.alert}>
{t.rich(
'<0>Regrading only applies to completed submissions.</0><1>If all students are affected please wait for all submissions before Regrading.</1>',
[
({children}) => (
<Text as="div" weight="bold" key="0">
{children}
</Text>
),
({children}) => (
<Text as="span" key="1">
{children}
</Text>
),
],
)}
</div>
</Alert>
<Alert margin="small 0" transition="none" variant="info">
<div css={this.props.styles.alert}>
{t.rich(
`<0>You are regrading this question.</0>
<1>Regrade options for this question type:</1>
<2>
<3>change the correct answer</3>
<4>change points value for the whole question</4>
<5>change points values on individual answers (if applicable)</5>
</2>`,
[
({children}) => (
<Text as="div" weight="bold" key="0">
{children}
</Text>
),
({children}) => (
<Text as="span" key="1">
{children}
</Text>
),
({children}) => <ul key="2">{children}</ul>,
({children}) => <li key="3">{children}</li>,
({children}) => <li key="4">{children}</li>,
({children}) => <li key="5">{children}</li>,
],
)}
</div>
</Alert>
</div>
)
}
renderEditableView() {
const propsForInteraction = propsForInteractionEdit(this.itemEditProps())
const interactionType = this.props.entry.getInteractionType().slug
const Interaction = componentForItem(interactionType, COMPONENT_EDIT)
return (
<UndecoratedQuizEntryEdit {...this.quizEntryProps()}>
{this.renderRegradingAlert()}
<div css={this.props.styles.container}>
<Interaction
{...propsForInteraction}
itemId={this.props.sessionItem.id}
overrideEditableForRegrading
/>
</div>
</UndecoratedQuizEntryEdit>
)
}
render() {
return (
<div tabIndex="-1" ref={this.setAlertRef}>
<RegradingModal
fromSpeedgrader
quizId={this.props.quizId}
handleValidationErrorsFromApi={this.props.handleValidationErrorsFromApi}
/>
{this.props.quizEntry.id !== '0' ? this.renderEditableView() : <Spinner />}
</div>
)
}
}
export const RegradeOverride = withStyleOverrides(
generateStyle,
generateComponentTheme,
)(BaseRegradeOverride)
export default RegradeOverride