@instructure/quiz-grading
Version:
The Quiz React SDK by Instructure Inc.
295 lines (264 loc) • 8.06 kB
JavaScript
/** @jsx jsx */
import {Component} from 'react'
import PropTypes from 'prop-types'
import {Alert} from '@instructure/ui-alerts'
import {Button, CloseButton} from '@instructure/ui-buttons'
import {Text} from '@instructure/ui-text'
import {Heading} from '@instructure/ui-heading'
import {RadioInput, RadioInputGroup} from '@instructure/ui-radio-input'
import {jsx, InstUISettingsProvider} from '@instructure/emotion'
import {
Modal,
ModalHeader,
ModalBody,
ModalFooter,
} from '@instructure/quiz-common/components/Modal/index'
import {XSMALL_SIDE_MARGIN} from '@instructure/quiz-common/constants'
import {withStyleOverrides} from '@instructure/quiz-common/util/withStyleOverrides'
import generateStyle from './styles'
import generateComponentTheme from './theme'
import t from '@instructure/quiz-i18n/format-message'
class BaseRegradingModal extends Component {
static displayName = 'RegradingModal'
static componentId = `Quizzes${this.displayName}`
static propTypes = {
addSuccess: PropTypes.func.isRequired,
closeModal: PropTypes.func.isRequired,
createQuizEntryRegrade: PropTypes.func.isRequired,
modalOpen: PropTypes.bool.isRequired,
quizId: PropTypes.string.isRequired,
regradingPayload: PropTypes.shape({
quizEntryId: PropTypes.string.isRequired,
scoringAlgorithm: PropTypes.string,
scoringData: PropTypes.object,
}).isRequired,
scoreReconciliation: PropTypes.string.isRequired,
setScoreReconciliation: PropTypes.func.isRequired,
toggleRegradingEditMode: PropTypes.func.isRequired,
handleValidationErrorsFromApi: PropTypes.func,
styles: PropTypes.object,
}
state = {
hasGenericErrorFromApi: false,
}
static defaultProps = {
handleValidationErrorsFromApi: Function.prototype,
}
// Getters
get optionInputs() {
return {
new_score: {
hint: t('some scores may be reduced, some may be raised'),
text: t('award points for correct answer only'),
},
old_score: {
hint: t('no scores will be reduced'),
text: t('award points for both current and previous answers'),
},
any_response: {
hint: t('everyone who answers gets full credit'),
text: t('award points for any submitted answer'),
},
thrown_out: {
hint: t('full credit awarded to all who have submitted the quiz'),
text: t('give everyone full credit'),
},
}
}
get scoreReconciliation() {
return this.props.scoreReconciliation
}
get selectHint() {
const hint = this.optionInputs[this.scoreReconciliation].hint
return [
{
text: t('Result: {hint}', {hint}),
type: 'hint',
},
]
}
// Handlers
clearGenericErrorFromApi = () => {
this.setState({hasGenericErrorFromApi: false})
}
cancelAction = () => {
this.clearGenericErrorFromApi()
this.props.closeModal()
}
onSpeedgraderOptionChange = (event, newValue) => {
this.props.setScoreReconciliation(newValue)
}
onSubmit = () => {
const {createQuizEntryRegrade, quizId, regradingPayload, scoreReconciliation} = this.props
this.clearGenericErrorFromApi()
createQuizEntryRegrade(quizId, {...regradingPayload, scoreReconciliation})
.then(() => {
this.props.toggleRegradingEditMode('')
this.props.addSuccess(
t('Regrade scheduled successfully; check back later to see it applied.'),
)
})
.catch(e => {
if (e.validationErrorsFromApi) {
// Close the modal so the user can view the input validation errors and correct them
this.cancelAction()
this.props.handleValidationErrorsFromApi(e.validationErrorsFromApi)
} else {
// Some other type of error occurred
this.setState({hasGenericErrorFromApi: true})
}
throw e
})
}
// Rendering
cancelButton() {
return (
<Button
margin={XSMALL_SIDE_MARGIN}
onClick={this.cancelAction}
ref={this.setRef('cancelImportButton')}
>
{t('Cancel')}
</Button>
)
}
regradeButton() {
return (
<Button
margin={XSMALL_SIDE_MARGIN}
onClick={this.onSubmit}
ref={this.setRef('confirmButton')}
color="primary"
data-automation="sdk-regrade-button"
>
{t('Regrade')}
</Button>
)
}
setRef(refName) {
return node => {
this[refName] = node
}
}
renderOptionInputs() {
return Object.entries(this.optionInputs).map(optionInput => {
return (
<option key={optionInput[0]} value={optionInput[0]}>
{optionInput[1].text}
</option>
)
})
}
renderRadioInputs() {
return Object.entries(this.optionInputs).map(optionInput => {
return (
<RadioInput
label={optionInput[1].text}
value={optionInput[0]}
key={optionInput[0]}
data-automation={`sdk-regrade-option-${optionInput[0]}`}
/>
)
})
}
renderModalBody() {
return (
<div>
<Alert margin="small 0" transition="none" variant="info">
<div css={this.props.styles.alert}>
<span>
{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>
),
],
)}
</span>
</div>
</Alert>
<div css={this.props.styles.radioInputGroup} role="form">
<RadioInputGroup
description={t('Select how you would like to apply this regrade.')}
name="scoring_reconciliation"
onChange={this.onSpeedgraderOptionChange}
messages={this.selectHint}
value={this.scoreReconciliation}
>
{this.renderRadioInputs()}
</RadioInputGroup>
</div>
</div>
)
}
renderErrorFromApi() {
if (this.state.hasGenericErrorFromApi) {
return (
<div css={this.props.styles.errorAlert}>
<InstUISettingsProvider
theme={{
componentOverrides: {
Alert: {
contentPadding: '0.4rem 1.5rem',
},
},
}}
>
<Alert margin="0 0" transition="fade" variant="error">
<Text size="small">
{t('Regrade did not run due to server error. Please try again later!')}
</Text>
</Alert>
</InstUISettingsProvider>
</div>
)
}
}
render() {
return (
<Modal
open={this.props.modalOpen}
size="medium"
onDismiss={this.cancelAction}
label={t('Question Regrade Options')}
ref={this.setRef('modal')}
data-automation="sdk-regrade-modal"
shouldCloseOnDocumentClick={false}
>
<ModalHeader>
<Heading level="reset" as="h2">
{t('Question Regrade Options')}
</Heading>
<CloseButton
onClick={this.cancelAction}
placement="end"
offset="medium"
screenReaderLabel={t('Close')}
/>
</ModalHeader>
<ModalBody>{this.renderModalBody()}</ModalBody>
<ModalFooter>
{this.renderErrorFromApi()}
{this.cancelButton()}
{this.regradeButton()}
</ModalFooter>
</Modal>
)
}
}
export const RegradingModal = withStyleOverrides(
generateStyle,
generateComponentTheme,
)(BaseRegradingModal)
export default RegradingModal