A2 edit date-times
fixes: ADMIN-2488 - Enables editing of override date-times - Because the dates required cross-field validation, changes were made to how validation failure messages are handled. TESTING NOTES: - The due/available/until date editor has issues - is completely broken in firefox - cannot close with ESC in firefox or Edge - VO and NVDA get stuck on the edit button after the editor opens see ADMIN-2568 - only test the date editing. anything below that are just placeholders. test plan: - edit an assignment (you no longer need #edit in the URL) - edit due, available, and until dates > expect validation to require available < due < until dates > expect the error message to appear in the popup when editing and to be shown below the 3 dates when no dates are being edited (note: you may get a slightly different message) - try to navigate away from the page > if you have changed anything but not saved, expect a confirmation popup - click Cancel > expect the date values (and anything else changed) to revert to their original values and the footer goes away - edit stuff - click Save > expect the Everyone Else dates to be saved, but not override. (that's another ticket) also - if the assignment is assigned to everyone in the class via sections and/or individual students, there will be no "Everyone Else" - if the assignment is simply assigned to everyone, there will be a single "Everyone" override - if the assignment is assigned to some students via sections or individually, there will be an "Everyone Else" override Change-Id: Ib22c747aef7b3541501e0a8fecd6e519147a62b7 Reviewed-on: https://gerrit.instructure.com/184622 Tested-by: Jenkins Reviewed-by: Carl Kibler <ckibler@instructure.com> QA-Review: Daniel Sasaki <dsasaki@instructure.com> Product-Review: Ed Schiebel <eschiebel@instructure.com>
This commit is contained in:
parent
bedff1a221
commit
069b1ecaf5
|
@ -0,0 +1,176 @@
|
|||
/*
|
||||
* Copyright (C) 2019 - present Instructure, Inc.
|
||||
*
|
||||
* This file is part of Canvas.
|
||||
*
|
||||
* Canvas is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||
* A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import I18n from 'i18n!assignments_2'
|
||||
|
||||
export default class AssignmentFieldValidator {
|
||||
messages = {}
|
||||
|
||||
// beause isNan is not the same as Number.isNaN
|
||||
/* eslint-disable no-restricted-globals */
|
||||
isPointsValid = value => {
|
||||
const strValue = `${value}`
|
||||
if (!strValue) {
|
||||
this.messages.pointsPossible = I18n.t('Points is required')
|
||||
return false // it's required
|
||||
}
|
||||
if (isNaN(strValue)) {
|
||||
this.messages.pointsPossible = I18n.t('Points must be a number >= 0')
|
||||
return false // must be a number
|
||||
}
|
||||
if (parseFloat(strValue) < 0) {
|
||||
// must be non-negative
|
||||
this.messages.pointsPossible = I18n.t('Points must >= 0')
|
||||
return false
|
||||
}
|
||||
delete this.messages.pointsPossible
|
||||
return true
|
||||
}
|
||||
|
||||
/* eslint-enable no-restricted-globals */
|
||||
isNameValid = value => {
|
||||
if (!value || value.trim().length === 0) {
|
||||
this.messages.name = I18n.t('Assignment name is required')
|
||||
return false
|
||||
}
|
||||
delete this.messages.name
|
||||
return true
|
||||
}
|
||||
|
||||
// the raw date-time value is invalid
|
||||
// set an appropriate error message
|
||||
getInvalidDateTimeMessage = ({rawDateValue, rawTimeValue}) => {
|
||||
let msg
|
||||
if (rawDateValue) {
|
||||
msg = I18n.t('The date is not valid.', {value: rawDateValue})
|
||||
} else if (rawTimeValue) {
|
||||
msg = I18n.t('You must provide a date with a time.')
|
||||
} else {
|
||||
msg = I18n.t('Invalid date or time')
|
||||
}
|
||||
return msg
|
||||
}
|
||||
|
||||
// A note on the date cross-field validators.
|
||||
// Though we want error messages specific to the field being edited,
|
||||
// we only want one at a time to be logged.
|
||||
// So if date A fails in comparison to B, we set A's error message
|
||||
// and clear B's
|
||||
isDueAtValid = (value, path, context) => {
|
||||
let isValid = true
|
||||
if (value && typeof value === 'object') {
|
||||
this.messages[path] = this.getInvalidDateTimeMessage(value)
|
||||
isValid = false
|
||||
} else {
|
||||
if (value && context.unlockAt) {
|
||||
if (value < context.unlockAt) {
|
||||
this.messages[path] = I18n.t('Due date must be after the Available date')
|
||||
isValid = false
|
||||
}
|
||||
}
|
||||
if (value && context.lockAt) {
|
||||
if (value > context.lockAt) {
|
||||
this.messages[path] = I18n.t('Due date must be before the Until date')
|
||||
isValid = false
|
||||
}
|
||||
}
|
||||
}
|
||||
if (isValid) {
|
||||
delete this.messages[path]
|
||||
} else {
|
||||
delete this.messages[path.replace('dueAt', 'unlockAt')]
|
||||
delete this.messages[path.replace('dueAt', 'lockAt')]
|
||||
}
|
||||
return isValid
|
||||
}
|
||||
|
||||
isUnlockAtValid = (value, path, context) => {
|
||||
let isValid = true
|
||||
if (value && typeof value === 'object') {
|
||||
this.messages[path] = this.getInvalidDateTimeMessage(value)
|
||||
isValid = false
|
||||
} else {
|
||||
if (value && context.dueAt) {
|
||||
if (value > context.dueAt) {
|
||||
this.messages[path] = I18n.t('Available date must be before the Due date')
|
||||
isValid = false
|
||||
}
|
||||
}
|
||||
if (value && context.lockAt) {
|
||||
if (value > context.lockAt) {
|
||||
this.messages[path] = I18n.t('Available date must be before the Until date')
|
||||
isValid = false
|
||||
}
|
||||
}
|
||||
}
|
||||
if (isValid) {
|
||||
delete this.messages[path]
|
||||
} else {
|
||||
delete this.messages[path.replace('unlockAt', 'dueAt')]
|
||||
delete this.messages[path.replace('unlockAt', 'lockAt')]
|
||||
}
|
||||
return isValid
|
||||
}
|
||||
|
||||
isLockAtValid = (value, path, context) => {
|
||||
let isValid = true
|
||||
if (value && typeof value === 'object') {
|
||||
this.messages[path] = this.getInvalidDateTimeMessage(value)
|
||||
isValid = false
|
||||
} else {
|
||||
if (value && context.dueAt) {
|
||||
if (value < context.dueAt) {
|
||||
this.messages[path] = I18n.t('Until date must be after the Due date')
|
||||
isValid = false
|
||||
}
|
||||
}
|
||||
if (value && context.unlockAt) {
|
||||
if (value < context.unlockAt) {
|
||||
this.messages[path] = I18n.t('Until date must be after the Available date')
|
||||
isValid = false
|
||||
}
|
||||
}
|
||||
}
|
||||
if (isValid) {
|
||||
delete this.messages[path]
|
||||
} else {
|
||||
delete this.messages[path.replace('lockAt', 'dueAt')]
|
||||
delete this.messages[path.replace('lockAt', 'unlockAt')]
|
||||
}
|
||||
return isValid
|
||||
}
|
||||
|
||||
validators = {
|
||||
pointsPossible: this.isPointsValid,
|
||||
name: this.isNameValid,
|
||||
dueAt: this.isDueAtValid,
|
||||
unlockAt: this.isUnlockAtValid,
|
||||
lockAt: this.isLockAtValid
|
||||
}
|
||||
|
||||
invalidFields = () => this.messages
|
||||
|
||||
validate = (path, value, context) => {
|
||||
const validationPath = path.replace(/.*\./, '')
|
||||
return this.validators[validationPath]
|
||||
? this.validators[validationPath](value, path, context)
|
||||
: true
|
||||
}
|
||||
|
||||
errorMessage = path => this.messages[path]
|
||||
}
|
|
@ -1,38 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2019 - present Instructure, Inc.
|
||||
*
|
||||
* This file is part of Canvas.
|
||||
*
|
||||
* Canvas is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||
* A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
// beause isNan is not the same as Number.isNaN
|
||||
/* eslint-disable no-restricted-globals */
|
||||
export const isPointsValid = value => {
|
||||
const strValue = `${value}`
|
||||
if (!strValue) return false // it's required
|
||||
if (isNaN(strValue)) return false // must be a number
|
||||
return parseFloat(strValue) >= 0 // must be non-negative
|
||||
}
|
||||
/* eslint-enable no-restricted-globals */
|
||||
|
||||
export const isNameValid = value => !!value && value.trim().length > 0
|
||||
|
||||
const validators = {
|
||||
pointsPossible: isPointsValid,
|
||||
name: isNameValid
|
||||
}
|
||||
|
||||
export const validate = (path, value) => (validators[path] ? validators[path](value) : true)
|
||||
|
||||
export default validators
|
|
@ -0,0 +1,160 @@
|
|||
/*
|
||||
* Copyright (C) 2019 - present Instructure, Inc.
|
||||
*
|
||||
* This file is part of Canvas.
|
||||
*
|
||||
* Canvas is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||
* A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import AssignmentFieldValidator from '../AssignentFieldValidator'
|
||||
|
||||
describe('Assignment field validators', () => {
|
||||
const afv = new AssignmentFieldValidator()
|
||||
|
||||
it('validates points', () => {
|
||||
expect(afv.isPointsValid('')).toBeFalsy()
|
||||
expect(afv.errorMessage('pointsPossible')).toBeDefined()
|
||||
|
||||
expect(afv.isPointsValid(-1)).toBeFalsy()
|
||||
expect(afv.errorMessage('pointsPossible')).toBeDefined()
|
||||
|
||||
expect(afv.isPointsValid('jibberish')).toBeFalsy()
|
||||
expect(afv.errorMessage('pointsPossible')).toBeDefined()
|
||||
|
||||
expect(afv.isPointsValid(17)).toBeTruthy()
|
||||
expect(afv.errorMessage('pointsPossible')).not.toBeDefined()
|
||||
|
||||
expect(afv.isPointsValid('17')).toBeTruthy()
|
||||
expect(afv.errorMessage('pointsPossible')).not.toBeDefined()
|
||||
})
|
||||
|
||||
it('validates the name', () => {
|
||||
expect(afv.isNameValid('')).toBeFalsy()
|
||||
expect(afv.errorMessage('name')).toBeDefined()
|
||||
|
||||
expect(afv.isNameValid()).toBeFalsy()
|
||||
expect(afv.errorMessage('name')).toBeDefined()
|
||||
|
||||
expect(afv.isNameValid(' ')).toBeFalsy()
|
||||
expect(afv.errorMessage('name')).toBeDefined()
|
||||
|
||||
expect(afv.isNameValid('hello')).toBeTruthy()
|
||||
expect(afv.errorMessage('name')).not.toBeDefined()
|
||||
})
|
||||
|
||||
it('gets the proper invalid date-time message', () => {
|
||||
expect(afv.getInvalidDateTimeMessage({rawDateValue: 'jibberish', rawTimeValue: ''})).toBe(
|
||||
'The date is not valid.'
|
||||
)
|
||||
|
||||
expect(afv.getInvalidDateTimeMessage({rawDateValue: '', rawTimeValue: 'jibberish'})).toBe(
|
||||
'You must provide a date with a time.'
|
||||
)
|
||||
|
||||
expect(afv.getInvalidDateTimeMessage({rawDateValue: '', rawTimeValue: ''})).toBe(
|
||||
'Invalid date or time'
|
||||
)
|
||||
})
|
||||
|
||||
const A = '2018-01-02T00:00Z'
|
||||
const B = '2018-01-04T00:00Z'
|
||||
const C = '2018-01-06T00:00Z'
|
||||
|
||||
it('validates dueAt', () => {
|
||||
// the invalid date-time callback from DateTimeInputis routed correctly
|
||||
expect(afv.isDueAtValid({rawDateValue: '', rawTimeValue: ''}, 'dueAt')).toBeFalsy()
|
||||
expect(afv.errorMessage('dueAt')).toBeDefined()
|
||||
expect(afv.errorMessage('unlockAt')).not.toBeDefined()
|
||||
expect(afv.errorMessage('lockAt')).not.toBeDefined()
|
||||
|
||||
// due before unlock
|
||||
expect(afv.isDueAtValid(A, 'dueAt', {unlockAt: B, lockAt: C})).toBeFalsy()
|
||||
expect(afv.errorMessage('dueAt')).toBeDefined()
|
||||
expect(afv.errorMessage('unlockAt')).not.toBeDefined()
|
||||
expect(afv.errorMessage('lockAt')).not.toBeDefined()
|
||||
|
||||
// due after lock
|
||||
expect(afv.isDueAtValid(C, 'dueAt', {unlockAt: A, lockAt: B})).toBeFalsy()
|
||||
expect(afv.errorMessage('dueAt')).toBeDefined()
|
||||
expect(afv.errorMessage('unlockAt')).not.toBeDefined()
|
||||
expect(afv.errorMessage('lockAt')).not.toBeDefined()
|
||||
|
||||
expect(afv.isDueAtValid(B, 'dueAt', {unlockAt: A, lockAt: C})).toBeTruthy()
|
||||
expect(afv.errorMessage('dueAt')).not.toBeDefined()
|
||||
expect(afv.errorMessage('unlockAt')).not.toBeDefined()
|
||||
expect(afv.errorMessage('lockAt')).not.toBeDefined()
|
||||
})
|
||||
|
||||
it('validates unlockAt', () => {
|
||||
// the invalid date-time callback from DateTimeInputis routed correctly
|
||||
expect(afv.isUnlockAtValid({rawDateValue: '', rawTimeValue: ''}, 'unlockAt')).toBeFalsy()
|
||||
expect(afv.errorMessage('dueAt')).not.toBeDefined()
|
||||
expect(afv.errorMessage('unlockAt')).toBeDefined()
|
||||
expect(afv.errorMessage('lockAt')).not.toBeDefined()
|
||||
|
||||
// unlock before due
|
||||
expect(afv.isUnlockAtValid(B, 'unlockAt', {dueAt: A, lockAt: C})).toBeFalsy()
|
||||
expect(afv.errorMessage('dueAt')).not.toBeDefined()
|
||||
expect(afv.errorMessage('unlockAt')).toBeDefined()
|
||||
expect(afv.errorMessage('lockAt')).not.toBeDefined()
|
||||
|
||||
// unlock after lock
|
||||
expect(afv.isUnlockAtValid(B, 'unlockAt', {dueAt: C, lockAt: A})).toBeFalsy()
|
||||
expect(afv.errorMessage('dueAt')).not.toBeDefined()
|
||||
expect(afv.errorMessage('unlockAt')).toBeDefined()
|
||||
expect(afv.errorMessage('lockAt')).not.toBeDefined()
|
||||
|
||||
expect(afv.isUnlockAtValid(A, 'unlockAt', {dueAt: B, lockAt: C})).toBeTruthy()
|
||||
expect(afv.errorMessage('dueAt')).not.toBeDefined()
|
||||
expect(afv.errorMessage('unlockAt')).not.toBeDefined()
|
||||
expect(afv.errorMessage('lockAt')).not.toBeDefined()
|
||||
})
|
||||
|
||||
it('validates lockAt', () => {
|
||||
// the invalid date-time callback from DateTimeInputis routed correctly
|
||||
expect(afv.isLockAtValid({rawDateValue: '', rawTimeValue: ''}, 'lockAt')).toBeFalsy()
|
||||
expect(afv.errorMessage('dueAt')).not.toBeDefined()
|
||||
expect(afv.errorMessage('unlockAt')).not.toBeDefined()
|
||||
expect(afv.errorMessage('lockAt')).toBeDefined()
|
||||
|
||||
// lock before due
|
||||
expect(afv.isLockAtValid(B, 'lockAt', {dueAt: C, unlockAt: A})).toBeFalsy()
|
||||
expect(afv.errorMessage('dueAt')).not.toBeDefined()
|
||||
expect(afv.errorMessage('unlockAt')).not.toBeDefined()
|
||||
expect(afv.errorMessage('lockAt')).toBeDefined()
|
||||
|
||||
// lock before unlock
|
||||
expect(afv.isLockAtValid(B, 'lockAt', {dueAt: A, unlockAt: C})).toBeFalsy()
|
||||
expect(afv.errorMessage('dueAt')).not.toBeDefined()
|
||||
expect(afv.errorMessage('unlockAt')).not.toBeDefined()
|
||||
expect(afv.errorMessage('lockAt')).toBeDefined()
|
||||
|
||||
expect(afv.isLockAtValid(C, 'lockAt', {dueAt: B, unlockAt: A})).toBeTruthy()
|
||||
expect(afv.errorMessage('dueAt')).not.toBeDefined()
|
||||
expect(afv.errorMessage('unlockAt')).not.toBeDefined()
|
||||
expect(afv.errorMessage('lockAt')).not.toBeDefined()
|
||||
})
|
||||
|
||||
it('returns the invalid messages collection', () => {
|
||||
expect(afv.isNameValid('')).toBeFalsy()
|
||||
expect(Object.keys(afv.invalidFields())).toContain('name')
|
||||
})
|
||||
|
||||
it('routes to the correct validator', () => {
|
||||
expect(afv.validate('foo.bar.name', '', {})).toBeFalsy()
|
||||
expect(afv.errorMessage('name')).toBeDefined()
|
||||
})
|
||||
|
||||
it('returns true for no validator', () => {
|
||||
expect(afv.validate('foo')).toBeTruthy()
|
||||
})
|
||||
})
|
|
@ -65,6 +65,7 @@ export const TEACHER_QUERY = gql`
|
|||
pointsPossible
|
||||
state
|
||||
needsGradingCount
|
||||
onlyVisibleToOverrides
|
||||
lockInfo {
|
||||
isLocked
|
||||
}
|
||||
|
@ -288,6 +289,8 @@ export const SAVE_ASSIGNMENT = gql`
|
|||
$name: String
|
||||
$description: String
|
||||
$dueAt: DateTime
|
||||
$unlockAt: DateTime
|
||||
$lockAt: DateTime
|
||||
$pointsPossible: Float
|
||||
$state: AssignmentState
|
||||
) {
|
||||
|
@ -297,6 +300,8 @@ export const SAVE_ASSIGNMENT = gql`
|
|||
name: $name
|
||||
description: $description
|
||||
dueAt: $dueAt
|
||||
unlockAt: $unlockAt
|
||||
lockAt: $lockAt
|
||||
pointsPossible: $pointsPossible
|
||||
state: $state
|
||||
}
|
||||
|
@ -307,6 +312,8 @@ export const SAVE_ASSIGNMENT = gql`
|
|||
lid: _id
|
||||
gid: id
|
||||
dueAt
|
||||
unlockAt
|
||||
lockAt
|
||||
name
|
||||
description
|
||||
pointsPossible
|
||||
|
|
|
@ -28,6 +28,7 @@ ContentTabs.propTypes = {
|
|||
assignment: TeacherAssignmentShape.isRequired,
|
||||
onChangeAssignment: func.isRequired,
|
||||
onValidate: func.isRequired,
|
||||
invalidMessage: func.isRequired,
|
||||
readOnly: bool
|
||||
}
|
||||
|
||||
|
@ -44,6 +45,7 @@ export default function ContentTabs(props) {
|
|||
assignment={assignment}
|
||||
onChangeAssignment={props.onChangeAssignment}
|
||||
onValidate={props.onValidate}
|
||||
invalidMessage={props.invalidMessage}
|
||||
readOnly={props.readOnly}
|
||||
/>
|
||||
</TabPanel>
|
||||
|
|
|
@ -31,6 +31,7 @@ Details.propTypes = {
|
|||
assignment: TeacherAssignmentShape.isRequired,
|
||||
onChangeAssignment: func.isRequired,
|
||||
onValidate: func.isRequired,
|
||||
invalidMessage: func.isRequired,
|
||||
readOnly: bool
|
||||
}
|
||||
Details.defaultProps = {
|
||||
|
@ -44,13 +45,13 @@ export default function Details(props) {
|
|||
<AssignmentDescription
|
||||
text={props.assignment.description}
|
||||
onChange={handleDescriptionChange}
|
||||
onValidate={props.onValidate}
|
||||
readOnly={props.readOnly}
|
||||
/>
|
||||
<Overrides
|
||||
assignment={props.assignment}
|
||||
onChangeAssignment={props.onChangeAssignment}
|
||||
onValidate={props.onValidate}
|
||||
invalidMessage={props.invalidMessage}
|
||||
readOnly={props.readOnly}
|
||||
/>
|
||||
{props.readOnly ? null : (
|
||||
|
|
|
@ -0,0 +1,122 @@
|
|||
/*
|
||||
* Copyright (C) 2018 - present Instructure, Inc.
|
||||
*
|
||||
* This file is part of Canvas.
|
||||
*
|
||||
* Canvas is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||
* A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import React from 'react'
|
||||
import {bool, func, oneOf, string} from 'prop-types'
|
||||
import I18n from 'i18n!assignments_2'
|
||||
|
||||
import {showFlashAlert} from 'jsx/shared/FlashAlert'
|
||||
|
||||
import FormField from '@instructure/ui-form-field/lib/components/FormField'
|
||||
import uid from '@instructure/uid'
|
||||
|
||||
import TeacherViewContext from '../TeacherViewContext'
|
||||
import EditableDateTime from './EditableDateTime'
|
||||
|
||||
const fallbackErrorMessage = I18n.t('Invalid date-time')
|
||||
|
||||
export default class AssignmentDate extends React.Component {
|
||||
static contextType = TeacherViewContext
|
||||
|
||||
static propTypes = {
|
||||
mode: oneOf(['view', 'edit']).isRequired,
|
||||
label: string.isRequired,
|
||||
onChange: func.isRequired,
|
||||
onChangeMode: func.isRequired,
|
||||
onValidate: func.isRequired,
|
||||
invalidMessage: func.isRequired,
|
||||
value: string,
|
||||
readOnly: bool
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
readOnly: false
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
isValid: props.onValidate(props.value)
|
||||
}
|
||||
this.id = uid() // FormField reqires an id
|
||||
}
|
||||
|
||||
static getDerivedStateFromProps(props, state) {
|
||||
const isValid = props.onValidate(props.value)
|
||||
return isValid !== state.isValid ? {isValid} : null
|
||||
}
|
||||
|
||||
handleDateChange = value => {
|
||||
const isValid = this.props.onValidate(value)
|
||||
this.setState({isValid}, () => {
|
||||
if (!isValid) {
|
||||
showFlashAlert({
|
||||
message: this.props.invalidMessage() || fallbackErrorMessage,
|
||||
type: 'error',
|
||||
srOnly: true
|
||||
})
|
||||
}
|
||||
this.props.onChange(value)
|
||||
})
|
||||
}
|
||||
|
||||
invalidDateTimeMessage = (rawDateValue, rawTimeValue) => {
|
||||
this.props.onValidate({rawDateValue, rawTimeValue})
|
||||
const message = this.props.invalidMessage() || fallbackErrorMessage
|
||||
this.setState({isValid: false})
|
||||
showFlashAlert({
|
||||
message,
|
||||
type: 'error',
|
||||
srOnly: true
|
||||
})
|
||||
return message
|
||||
}
|
||||
|
||||
getMessages = () =>
|
||||
this.state.isValid
|
||||
? null
|
||||
: [{type: 'error', text: this.props.invalidMessage() || fallbackErrorMessage}]
|
||||
|
||||
render() {
|
||||
const lbl = I18n.t('%{label}:', {label: this.props.label})
|
||||
const placeholder = I18n.t('No %{label} Date', {label: this.props.label})
|
||||
const messages = this.getMessages()
|
||||
// can remove the outer DIV once instui is updated
|
||||
// to forward data-* attrs from FormField into the dom
|
||||
return (
|
||||
<div data-testid="AssignmentDate">
|
||||
<FormField id={this.id} label={lbl} layout="stacked">
|
||||
<EditableDateTime
|
||||
mode={this.props.mode}
|
||||
onChange={this.handleDateChange}
|
||||
onChangeMode={this.props.onChangeMode}
|
||||
invalidMessage={this.invalidDateTimeMessage}
|
||||
messages={messages}
|
||||
value={this.props.value || undefined}
|
||||
label={this.props.label}
|
||||
locale={this.context.locale}
|
||||
timeZone={this.context.timeZone}
|
||||
readOnly={this.props.readOnly}
|
||||
placeholder={placeholder}
|
||||
/>
|
||||
</FormField>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
|
@ -27,7 +27,6 @@ import {Text} from '@instructure/ui-elements'
|
|||
import EditableHeading from './EditableHeading'
|
||||
|
||||
const nameLabel = I18n.t('Edit assignment name')
|
||||
const invalidMessage = I18n.t('Assignment name is required')
|
||||
const namePlaceholder = I18n.t('Assignment name')
|
||||
|
||||
export default class AssignmentName extends React.Component {
|
||||
|
@ -37,6 +36,7 @@ export default class AssignmentName extends React.Component {
|
|||
onChange: func.isRequired,
|
||||
onChangeMode: func.isRequired,
|
||||
onValidate: func.isRequired,
|
||||
invalidMessage: func.isRequired,
|
||||
readOnly: bool
|
||||
}
|
||||
|
||||
|
@ -62,7 +62,7 @@ export default class AssignmentName extends React.Component {
|
|||
this.setState({isValid}, () => {
|
||||
if (!isValid) {
|
||||
showFlashAlert({
|
||||
message: invalidMessage,
|
||||
message: this.props.invalidMessage('name') || I18n.t('Error'),
|
||||
type: 'error',
|
||||
srOnly: true
|
||||
})
|
||||
|
@ -74,7 +74,7 @@ export default class AssignmentName extends React.Component {
|
|||
render() {
|
||||
const msg = this.state.isValid ? null : (
|
||||
<div>
|
||||
<Text color="error">{invalidMessage}</Text>
|
||||
<Text color="error">{this.props.invalidMessage('name')}</Text>
|
||||
</div>
|
||||
)
|
||||
return (
|
||||
|
|
|
@ -27,7 +27,6 @@ import {Text} from '@instructure/ui-elements'
|
|||
|
||||
import EditableNumber from './EditableNumber'
|
||||
|
||||
const invalidMessage = I18n.t('Points must be a number >= 0')
|
||||
const editLabel = I18n.t('Edit Points')
|
||||
const label = I18n.t('Points')
|
||||
|
||||
|
@ -38,6 +37,7 @@ export default class AssignmentPoints extends React.Component {
|
|||
onChange: func.isRequired,
|
||||
onChangeMode: func.isRequired,
|
||||
onValidate: func.isRequired,
|
||||
invalidMessage: func.isRequired,
|
||||
readOnly: bool
|
||||
}
|
||||
|
||||
|
@ -79,7 +79,7 @@ export default class AssignmentPoints extends React.Component {
|
|||
this.setState({isValid}, () => {
|
||||
if (!isValid) {
|
||||
showFlashAlert({
|
||||
message: invalidMessage,
|
||||
message: this.props.invalidMessage('pointsPossible') || I18n.t('Error'),
|
||||
type: 'error',
|
||||
srOnly: true
|
||||
})
|
||||
|
@ -93,7 +93,7 @@ export default class AssignmentPoints extends React.Component {
|
|||
const msg = this.state.isValid ? null : (
|
||||
<View as="div" textAlign="end" margin="xx-small 0 0 0">
|
||||
<span style={{whiteSpace: 'nowrap'}}>
|
||||
<Text color="error">{invalidMessage}</Text>
|
||||
<Text color="error">{this.props.invalidMessage('pointsPossible')}</Text>
|
||||
</span>
|
||||
</View>
|
||||
)
|
||||
|
|
|
@ -0,0 +1,272 @@
|
|||
/*
|
||||
* Copyright (C) 2019 - present Instructure, Inc.
|
||||
*
|
||||
* This file is part of Canvas.
|
||||
*
|
||||
* Canvas is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||
* A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import React from 'react'
|
||||
import {arrayOf, bool, func, shape, string} from 'prop-types'
|
||||
import I18n from 'i18n!assignments_2'
|
||||
|
||||
import {toLocaleString} from '@instructure/ui-i18n/lib/DateTime'
|
||||
import Button from '@instructure/ui-buttons/lib/components/Button'
|
||||
import DateTimeInput from '@instructure/ui-forms/lib/components/DateTimeInput'
|
||||
import IconCalendarMonth from '@instructure/ui-icons/lib/Line/IconCalendarMonth'
|
||||
import {Editable} from '@instructure/ui-editable'
|
||||
import {Flex, FlexItem, View} from '@instructure/ui-layout'
|
||||
import {FocusableView} from '@instructure/ui-focusable'
|
||||
import ScreenReaderContent from '@instructure/ui-a11y/lib/components/ScreenReaderContent'
|
||||
import Text from '@instructure/ui-elements/lib/components/Text'
|
||||
|
||||
export default class EditableDateTime extends React.Component {
|
||||
static propTypes = {
|
||||
label: string.isRequired,
|
||||
locale: string.isRequired,
|
||||
timeZone: string.isRequired,
|
||||
displayFormat: string,
|
||||
value: string, // iso8601 datetime
|
||||
mode: string.isRequired,
|
||||
onChange: func.isRequired,
|
||||
onChangeMode: func.isRequired,
|
||||
invalidMessage: func.isRequired,
|
||||
messages: arrayOf(shape({type: string.isRequired, text: string.isRequired})),
|
||||
readOnly: bool,
|
||||
required: bool,
|
||||
placeholder: string
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
displayFormat: 'lll',
|
||||
readOnly: false,
|
||||
required: false
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
initialValue: props.value,
|
||||
isValid: true
|
||||
}
|
||||
this._timers = [] // track the in-flight setTimeout timers
|
||||
this._elementRef = null
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this._timers.forEach(t => window.clearTimeout(t))
|
||||
}
|
||||
|
||||
// if a new value comes in while we're in view mode,
|
||||
// reset our initial value
|
||||
static getDerivedStateFromProps(props, _state) {
|
||||
if (props.mode === 'view') {
|
||||
return {
|
||||
initialValue: props.value
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
elementRef = el => {
|
||||
this._elementRef = el
|
||||
}
|
||||
|
||||
isValid() {
|
||||
return this.state.isValid
|
||||
}
|
||||
|
||||
// onChange handler from DateTimeInput
|
||||
handleDateTimeChange = (_event, newValue) => {
|
||||
this.setState({isValid: true}, () => {
|
||||
this.props.onChange(newValue)
|
||||
})
|
||||
}
|
||||
|
||||
// onChange handler from Editable
|
||||
handleChange = newValue => {
|
||||
this.setState({isValid: true}, () => {
|
||||
this.props.onChange(newValue)
|
||||
})
|
||||
}
|
||||
|
||||
handleChangeMode = mode => {
|
||||
if (!this.props.readOnly) {
|
||||
if (mode === 'view') {
|
||||
if (!this.isValid()) {
|
||||
// can't leave edit mode with a bad value
|
||||
return
|
||||
}
|
||||
}
|
||||
this.props.onChangeMode(mode)
|
||||
}
|
||||
}
|
||||
|
||||
// Because DateTimeInput has an asynchronous onBlur handler, we need to delay our call to
|
||||
// this EditableDateTime's onFocus handler or focus gets yanked from this to the edit button of
|
||||
// the one we just left if the user clicks from one to the next
|
||||
delayedHandler = handler => {
|
||||
return event => {
|
||||
event.persist()
|
||||
const t = window.setTimeout(() => {
|
||||
this._timers.splice(this._timers.findIndex(tid => tid === t), 1)
|
||||
handler(event)
|
||||
}, 100)
|
||||
this._timers.push(t)
|
||||
}
|
||||
}
|
||||
|
||||
// similar issue when clicking on the view
|
||||
viewClickHandler(editableClickHandler) {
|
||||
if (editableClickHandler) {
|
||||
return this.delayedHandler(editableClickHandler)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
handleKey = event => {
|
||||
if (event.key === 'Enter' && event.type === 'keydown') {
|
||||
if (!this.props.readOnly) {
|
||||
// let EditableDateTime handle the value change,
|
||||
// then flip me to view mode
|
||||
const t = window.setTimeout(() => {
|
||||
this._timers.splice(this._timers.findIndex(tid => tid === t), 1)
|
||||
this.props.onChangeMode('view')
|
||||
}, 100)
|
||||
this._timers.push(t)
|
||||
}
|
||||
} else if (event.key === 'Escape' && event.type === 'keyup') {
|
||||
// Editable's keypup handler is what flips us to view mode
|
||||
// so we'll reset to initial value on that event, not keydown
|
||||
this.props.onChange(this.state.initialValue)
|
||||
}
|
||||
}
|
||||
|
||||
renderViewer = ({readOnly, mode}) => {
|
||||
if (readOnly || mode === 'view') {
|
||||
if (this.props.value) {
|
||||
const dt = this.props.value
|
||||
? toLocaleString(
|
||||
this.props.value,
|
||||
this.props.locale,
|
||||
this.props.timeZone,
|
||||
this.props.displayFormat
|
||||
)
|
||||
: ''
|
||||
return <Text>{dt}</Text>
|
||||
}
|
||||
return <Text color="secondary">{this.props.placeholder}</Text>
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/* eslint-disable jsx-a11y/no-static-element-interactions */
|
||||
renderEditor = ({mode, readOnly, onBlur, editorRef}) => {
|
||||
if (!readOnly && mode === 'edit') {
|
||||
return (
|
||||
<div
|
||||
onKeyDown={this.handleKey}
|
||||
onKeyUp={this.handleKey}
|
||||
data-testid="EditableDateTime-editor"
|
||||
>
|
||||
<FocusableView display="block" width="100%" focused>
|
||||
<View display="inline-block" padding="x-small">
|
||||
<DateTimeInput
|
||||
layout="stacked"
|
||||
description={<ScreenReaderContent>{this.props.label}</ScreenReaderContent>}
|
||||
dateLabel={I18n.t('Date')}
|
||||
datePreviousLabel={I18n.t('previous')}
|
||||
dateNextLabel={I18n.t('next')}
|
||||
timeLabel={I18n.t('Time')}
|
||||
invalidDateTimeMessage={this.props.invalidMessage}
|
||||
messages={this.props.messages}
|
||||
value={this.props.value}
|
||||
onChange={this.handleDateTimeChange}
|
||||
onBlur={onBlur}
|
||||
dateInputRef={editorRef}
|
||||
required={this.props.required}
|
||||
/>
|
||||
</View>
|
||||
</FocusableView>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return null
|
||||
}
|
||||
/* eslint-enable jsx-a11y/no-static-element-interactions */
|
||||
|
||||
// Renders the edit button.
|
||||
// Returns a custom edit button with the calendar icon and is always visible in view mode
|
||||
renderEditButton = ({onClick, onFocus, onBlur, buttonRef}) => {
|
||||
if (!this.props.readOnly && this.props.mode === 'view') {
|
||||
return (
|
||||
<Button
|
||||
size="small"
|
||||
variant="icon"
|
||||
margin="0 0 0 x-small"
|
||||
icon={IconCalendarMonth}
|
||||
onClick={onClick}
|
||||
onFocus={this.delayedHandler(onFocus)}
|
||||
onBlur={onBlur}
|
||||
buttonRef={buttonRef}
|
||||
readOnly={this.props.readOnly}
|
||||
>
|
||||
<ScreenReaderContent>
|
||||
{I18n.t('Edit %{when}', {when: this.props.label})}
|
||||
</ScreenReaderContent>
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
renderAll = ({getContainerProps, getViewerProps, getEditorProps, getEditButtonProps}) => {
|
||||
const borderWidth = this.props.mode === 'view' ? 'small' : 'none'
|
||||
const padding = this.props.mode === 'view' ? 'x-small' : '0'
|
||||
const containerProps = {...getContainerProps()}
|
||||
containerProps.onMouseDown = this.viewClickHandler(containerProps.onMouseDown)
|
||||
return (
|
||||
<View
|
||||
data-testid="EditableDateTime"
|
||||
as="div"
|
||||
padding={padding}
|
||||
borderWidth={borderWidth}
|
||||
borderRadius="medium"
|
||||
elementRef={this.elementRef}
|
||||
{...containerProps}
|
||||
>
|
||||
<Flex inline direction="row" justifyItems="space-between" width="100%">
|
||||
<FlexItem grow shrink>
|
||||
{this.renderEditor(getEditorProps())}
|
||||
{this.renderViewer(getViewerProps())}
|
||||
</FlexItem>
|
||||
<FlexItem margin="0 0 0 xx-small">{this.renderEditButton(getEditButtonProps())}</FlexItem>
|
||||
</Flex>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Editable
|
||||
mode={this.props.mode}
|
||||
onChangeMode={this.handleChangeMode}
|
||||
render={this.renderAll}
|
||||
value={this.props.value}
|
||||
onChange={this.handleChange}
|
||||
readOnly={this.props.readOnly}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
|
@ -60,11 +60,11 @@ export default class EditableHeading extends React.Component {
|
|||
this._hiddenTextRef = null
|
||||
}
|
||||
|
||||
getSnapshotBeforeUpdate(prevProps, _prevState) {
|
||||
if (prevProps.mode === 'view' && this._headingRef) {
|
||||
const fontSize = this.getFontSize(this._headingRef)
|
||||
// we'll set the width of the <input> to the width of the text + 1 char
|
||||
return {width: this._headingRef.clientWidth + fontSize}
|
||||
static getDerivedStateFromProps(props, _state) {
|
||||
if (props.mode === 'view') {
|
||||
return {
|
||||
initialValue: props.value
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
@ -175,6 +175,7 @@ export default class EditableHeading extends React.Component {
|
|||
value={this.props.value}
|
||||
onChange={this.handleInputChange}
|
||||
onKeyDown={this.handleKey}
|
||||
onKeyUp={this.handleKey}
|
||||
aria-label={this.props.label}
|
||||
onBlur={onBlur}
|
||||
elementRef={createChainedFunction(this.getInputRef, editorRef)}
|
||||
|
@ -194,13 +195,13 @@ export default class EditableHeading extends React.Component {
|
|||
// don't have to check what mode is, because
|
||||
// this is the editor's key handler
|
||||
handleKey = event => {
|
||||
if (event.key === 'Enter') {
|
||||
if (event.key === 'Enter' && event.type === 'keydown') {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
if (!this.props.readOnly) {
|
||||
this.props.onChangeMode('view')
|
||||
}
|
||||
} else if (event.key === 'Escape') {
|
||||
} else if (event.key === 'Escape' && event.type === 'keyup') {
|
||||
// reset to initial value
|
||||
this.props.onChange(this.state.initialValue)
|
||||
}
|
||||
|
@ -210,17 +211,8 @@ export default class EditableHeading extends React.Component {
|
|||
this.props.onChange(event.target.value)
|
||||
}
|
||||
|
||||
// InPlaceEdit.onChange is fired when changing from edit to view
|
||||
// mode. Reset the initialValue now.
|
||||
handleChange = newValue => {
|
||||
this.setState(
|
||||
{
|
||||
initialValue: newValue
|
||||
},
|
||||
() => {
|
||||
this.props.onChange(newValue)
|
||||
}
|
||||
)
|
||||
this.props.onChange(newValue)
|
||||
}
|
||||
|
||||
handleModeChange = mode => {
|
||||
|
|
|
@ -66,6 +66,17 @@ export default class EditableNumber extends React.Component {
|
|||
this._hiddenTextRef = null
|
||||
}
|
||||
|
||||
// if a new value comes in while we're in view mode,
|
||||
// reset our initial value
|
||||
static getDerivedStateFromProps(props, _state) {
|
||||
if (props.mode === 'view') {
|
||||
return {
|
||||
initialValue: props.value
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps, _prevState, _snapshot) {
|
||||
if (this._inputRef) {
|
||||
this._inputRef.style.width = this.getWidth()
|
||||
|
@ -212,14 +223,7 @@ export default class EditableNumber extends React.Component {
|
|||
// InPlaceEdit.onChange is fired when changing from edit to view
|
||||
// mode. Reset the initialValue now.
|
||||
handleChange = newValue => {
|
||||
this.setState(
|
||||
{
|
||||
initialValue: newValue
|
||||
},
|
||||
() => {
|
||||
this.props.onChange(newValue)
|
||||
}
|
||||
)
|
||||
this.props.onChange(newValue)
|
||||
}
|
||||
|
||||
handleModeChange = mode => {
|
||||
|
|
|
@ -0,0 +1,124 @@
|
|||
/*
|
||||
* Copyright (C) 2019 - present Instructure, Inc.
|
||||
*
|
||||
* This file is part of Canvas.
|
||||
*
|
||||
* Canvas is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||
* A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import React from 'react'
|
||||
import {render, fireEvent} from 'react-testing-library'
|
||||
import {toLocaleString, browserTimeZone} from '@instructure/ui-i18n/lib/DateTime'
|
||||
import AssignmentDate from '../AssignmentDate'
|
||||
|
||||
const locale = 'en'
|
||||
const timeZone = browserTimeZone()
|
||||
|
||||
function renderAssignmentDate(props) {
|
||||
return render(
|
||||
<AssignmentDate
|
||||
mode="view"
|
||||
label="Due"
|
||||
onChange={() => {}}
|
||||
onChangeMode={() => {}}
|
||||
onValidate={() => true}
|
||||
invalidMessage={() => 'oh no!'}
|
||||
value="2108-03-13T15:15:00-07:00"
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
describe('AssignmentDate', () => {
|
||||
it('renders in view mode', () => {
|
||||
const {getByTestId} = renderAssignmentDate()
|
||||
|
||||
expect(getByTestId('AssignmentDate')).toBeInTheDocument()
|
||||
expect(getByTestId('EditableDateTime')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('renders in edit mode', () => {
|
||||
const {getByTestId} = renderAssignmentDate({mode: 'edit'})
|
||||
|
||||
expect(getByTestId('AssignmentDate')).toBeInTheDocument()
|
||||
expect(getByTestId('EditableDateTime')).toBeInTheDocument()
|
||||
expect(getByTestId('EditableDateTime-editor')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('shows error message with invalid value when in edit mode', () => {
|
||||
// because the error message is rendered by the instui DateTimeInput
|
||||
const {getByText} = renderAssignmentDate({mode: 'edit', onValidate: () => false})
|
||||
|
||||
expect(getByText('oh no!')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('does not show error message in view mode', () => {
|
||||
// because the error message is hoisted to the parent OverrideDates
|
||||
const {queryByText} = renderAssignmentDate({mode: 'view', onValidate: () => false})
|
||||
|
||||
expect(queryByText('oh no!')).toBeNull()
|
||||
})
|
||||
|
||||
it('shows the placeholder when the value is empty', () => {
|
||||
const {getByText} = renderAssignmentDate({value: null})
|
||||
|
||||
expect(getByText('No Due Date')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('handles jibberish date input', () => {
|
||||
const value = '2108-03-13T15:15:00-07:00'
|
||||
const invalidMessage = jest.fn()
|
||||
const {getByDisplayValue} = renderAssignmentDate({
|
||||
mode: 'edit',
|
||||
onValidate: () => true,
|
||||
invalidMessage,
|
||||
value
|
||||
})
|
||||
|
||||
const dateDisplay = toLocaleString(value, locale, timeZone, 'LL')
|
||||
const dinput = getByDisplayValue(dateDisplay)
|
||||
dinput.focus()
|
||||
fireEvent.change(dinput, {target: {value: 'x'}})
|
||||
const timeDisplay = toLocaleString(value, locale, timeZone, 'LT')
|
||||
const tinput = getByDisplayValue(timeDisplay)
|
||||
tinput.focus()
|
||||
|
||||
expect(invalidMessage).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('handles input', () => {
|
||||
function validator(value) {
|
||||
const d = new Date(value)
|
||||
const reference = new Date('2108-04-13T15:15:00-07:00')
|
||||
return d.valueOf() < reference.valueOf()
|
||||
}
|
||||
const value = '2108-03-13T15:15:00-07:00'
|
||||
const invalidMessage = jest.fn()
|
||||
const {getByDisplayValue} = renderAssignmentDate({
|
||||
mode: 'edit',
|
||||
onValidate: validator,
|
||||
invalidMessage,
|
||||
value
|
||||
})
|
||||
|
||||
const dateDisplay = toLocaleString(value, locale, timeZone, 'LL')
|
||||
const dinput = getByDisplayValue(dateDisplay)
|
||||
dinput.focus()
|
||||
fireEvent.change(dinput, {target: {value: '2108-05-13T15:15:00-07:00'}})
|
||||
const timeDisplay = toLocaleString(value, locale, timeZone, 'LT')
|
||||
const tinput = getByDisplayValue(timeDisplay)
|
||||
tinput.focus()
|
||||
|
||||
expect(invalidMessage).toHaveBeenCalled()
|
||||
})
|
||||
})
|
|
@ -19,7 +19,15 @@
|
|||
import React from 'react'
|
||||
import {render, fireEvent} from 'react-testing-library'
|
||||
import AssignmentName from '../AssignmentName'
|
||||
import {validate} from '../../../Validators'
|
||||
import AssignmentFieldValidator from '../../../AssignentFieldValidator'
|
||||
|
||||
const afv = new AssignmentFieldValidator()
|
||||
function validate() {
|
||||
return afv.validate(...arguments)
|
||||
}
|
||||
function errorMessage() {
|
||||
return afv.errorMessage(...arguments)
|
||||
}
|
||||
|
||||
function renderAssignmentName(props) {
|
||||
return render(
|
||||
|
@ -28,6 +36,7 @@ function renderAssignmentName(props) {
|
|||
onChange={() => {}}
|
||||
onChangeMode={() => {}}
|
||||
onValidate={validate}
|
||||
invalidMessage={errorMessage}
|
||||
name="the name"
|
||||
{...props}
|
||||
/>
|
||||
|
@ -86,6 +95,7 @@ describe('AssignmentName', () => {
|
|||
onChange={onChange}
|
||||
onChangeMode={onChangeMode}
|
||||
onValidate={() => true}
|
||||
invalidMessage={() => undefined}
|
||||
name="the name"
|
||||
/>
|
||||
<span id="focus-me" tabIndex="-1">
|
||||
|
@ -101,15 +111,4 @@ describe('AssignmentName', () => {
|
|||
expect(onChangeMode).toHaveBeenCalledWith('view')
|
||||
expect(onChange).toHaveBeenCalledWith('new name')
|
||||
})
|
||||
|
||||
it('reverts to the old value on Escape', () => {
|
||||
const onChange = jest.fn()
|
||||
const {getByDisplayValue} = renderAssignmentName({mode: 'edit', onChange})
|
||||
|
||||
const input = getByDisplayValue('the name')
|
||||
fireEvent.input(input, {target: {value: 'x'}})
|
||||
fireEvent.keyDown(input, {key: 'Escape', code: 27})
|
||||
expect(onChange).toHaveBeenCalledWith('x')
|
||||
expect(onChange).toHaveBeenCalledWith('the name')
|
||||
})
|
||||
})
|
||||
|
|
|
@ -19,7 +19,15 @@
|
|||
import React from 'react'
|
||||
import {render, fireEvent} from 'react-testing-library'
|
||||
import AssignmentPoints from '../AssignmentPoints'
|
||||
import {validate} from '../../../Validators'
|
||||
import AssignmentFieldValidator from '../../../AssignentFieldValidator'
|
||||
|
||||
const afv = new AssignmentFieldValidator()
|
||||
function validate() {
|
||||
return afv.validate(...arguments)
|
||||
}
|
||||
function errorMessage() {
|
||||
return afv.errorMessage(...arguments)
|
||||
}
|
||||
|
||||
function renderAssignmentPoints(props) {
|
||||
return render(
|
||||
|
@ -28,6 +36,7 @@ function renderAssignmentPoints(props) {
|
|||
onChange={() => {}}
|
||||
onChangeMode={() => {}}
|
||||
onValidate={validate}
|
||||
invalidMessage={errorMessage}
|
||||
pointsPossible={1432}
|
||||
{...props}
|
||||
/>
|
||||
|
@ -73,6 +82,7 @@ describe('AssignmentPoints', () => {
|
|||
onChange={onChange}
|
||||
onChangeMode={onChangeMode}
|
||||
onValidate={() => true}
|
||||
invalidMessage={() => undefined}
|
||||
pointsPossible={12}
|
||||
/>
|
||||
<span id="focus-me" tabIndex="-1">
|
||||
|
@ -108,6 +118,7 @@ describe('AssignmentPoints', () => {
|
|||
onChange={onChange}
|
||||
onChangeMode={() => {}}
|
||||
onValidate={validate}
|
||||
invalidMessage={errorMessage}
|
||||
pointsPossible="1.247"
|
||||
/>
|
||||
<input data-testid="focusme" />
|
||||
|
|
|
@ -0,0 +1,169 @@
|
|||
/*
|
||||
* Copyright (C) 2019 - present Instructure, Inc.
|
||||
*
|
||||
* This file is part of Canvas.
|
||||
*
|
||||
* Canvas is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||
* A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import React from 'react'
|
||||
import {render, fireEvent} from 'react-testing-library'
|
||||
import {toLocaleString, browserTimeZone} from '@instructure/ui-i18n/lib/DateTime'
|
||||
import moment from 'moment'
|
||||
|
||||
import EditableDateTime from '../EditableDateTime'
|
||||
|
||||
const locale = 'en'
|
||||
const timeZone = browserTimeZone()
|
||||
|
||||
function renderEditableDateTime(props = {}) {
|
||||
return render(
|
||||
<EditableDateTime
|
||||
mode="view"
|
||||
onChange={() => {}}
|
||||
onChangeMode={() => {}}
|
||||
value="2019-04-11T13:00:00-05:00"
|
||||
label="Due"
|
||||
locale={locale}
|
||||
timeZone={timeZone}
|
||||
readOnly={false}
|
||||
placeholder="No due date"
|
||||
displayFormat="lll"
|
||||
invalidMessage={() => undefined}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
describe('EditableDateTime', () => {
|
||||
it('renders in view mode', () => {
|
||||
const value = '2019-04-11T13:00:00-05:00'
|
||||
const {getByText} = renderEditableDateTime({value})
|
||||
|
||||
const dtstring = toLocaleString(value, locale, timeZone, 'lll')
|
||||
expect(getByText('Edit Due')).toBeInTheDocument()
|
||||
expect(getByText(dtstring)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('renders in edit mode', () => {
|
||||
const value = '2019-04-11T13:00:00-05:00'
|
||||
const {getByText, getByLabelText} = renderEditableDateTime({mode: 'edit', value})
|
||||
|
||||
const dtstring = toLocaleString(value, locale, timeZone, 'LLL')
|
||||
const datestr = toLocaleString(value, locale, timeZone, 'LL')
|
||||
const timestr = toLocaleString(value, locale, timeZone, 'LT')
|
||||
expect(getByLabelText('Date').value).toBe(datestr)
|
||||
expect(getByLabelText('Time').value).toBe(timestr)
|
||||
expect(getByText(dtstring)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('exits edit mode and reverts to previous value on Escape', () => {
|
||||
const value = '2019-04-11T13:00:00-05:00'
|
||||
const onChangeMode = jest.fn()
|
||||
const onChange = jest.fn()
|
||||
const {getByDisplayValue} = renderEditableDateTime({
|
||||
mode: 'edit',
|
||||
onChangeMode,
|
||||
onChange,
|
||||
value
|
||||
})
|
||||
|
||||
const datestr = toLocaleString(value, locale, timeZone, 'LL')
|
||||
const timestr = toLocaleString(value, locale, timeZone, 'LT')
|
||||
const dinput = getByDisplayValue(datestr)
|
||||
const tinput = getByDisplayValue(timestr)
|
||||
// enter a new date
|
||||
fireEvent.change(dinput, {target: {value: 'April 4, 2018'}})
|
||||
tinput.focus()
|
||||
const newDateIsoStr = moment('2018-04-04T13:00:00-05:00').toISOString(true)
|
||||
expect(onChange).toHaveBeenLastCalledWith(newDateIsoStr)
|
||||
|
||||
dinput.focus()
|
||||
fireEvent.keyUp(dinput, {key: 'Escape', code: 27})
|
||||
expect(onChange).toHaveBeenLastCalledWith(value)
|
||||
expect(onChangeMode).toHaveBeenLastCalledWith('view')
|
||||
})
|
||||
|
||||
// I've spent a day trying to get the folloning specs to work, but I have failed
|
||||
// to find the combination of simulated events that get the right event
|
||||
// handlers called. Events are getting lost among
|
||||
// input -> TextInput -> DateInput -> DateTimeInput -> Editable -> EditableDateTime
|
||||
// It works in the UI, but I've failed to simulate the user's input
|
||||
|
||||
// it('saves new value on Enter', async () => {
|
||||
// const value = '2018-04-11T13:00:00-05:00'
|
||||
// const onChange = jest.fn()
|
||||
// const onChangeMode = jest.fn()
|
||||
// const {container} = renderEditableDateTime({
|
||||
// mode: 'edit',
|
||||
// onChange,
|
||||
// onChangeMode,
|
||||
// value
|
||||
// })
|
||||
|
||||
// const input = container.querySelector('[data-testid="EditableDateTime-editor"] input')
|
||||
// const newdt = toLocaleString('2019-04-11T13:00:00-05:00', locale, timeZone, 'LL')
|
||||
// fireEvent.change(input, {target: {value: newdt}})
|
||||
|
||||
// fireEvent.keyDown(input, {key: 'Enter', code: 13, target: {value: input.value}})
|
||||
// await wait(() => {
|
||||
// expect(onChangeMode).toHaveBeenCalledWith('view')
|
||||
// })
|
||||
|
||||
// await wait(() => {
|
||||
// expect(onChange).toHaveBeenCalled()
|
||||
// expect(onChange).toHaveBeenCalledWith('2019-04-11T13:00:00-05:00')
|
||||
// })
|
||||
// })
|
||||
|
||||
// it('saves the new value on blur', () => {
|
||||
// const value = '2019-04-11T13:00:00-05:00'
|
||||
// const onChange = jest.fn()
|
||||
// const onChangeMode = jest.fn()
|
||||
// const {container, getByDisplayValue} = render(
|
||||
// <div>
|
||||
// <EditableDateTime
|
||||
// mode="edit"
|
||||
// onChange={onChange}
|
||||
// onChangeMode={onChangeMode}
|
||||
// value={value}
|
||||
// label="Due"
|
||||
// locale={locale}
|
||||
// timeZone={timeZone}
|
||||
// readOnly={false}
|
||||
// placeholder="No due date"
|
||||
// displayFormat="lll"
|
||||
// invalidMessage={() => undefined}
|
||||
// />
|
||||
// <span id="focus-me" tabIndex="-1">
|
||||
// just here to get focus
|
||||
// </span>
|
||||
// </div>
|
||||
// )
|
||||
|
||||
// let displayValue = toLocaleString(value, locale, timeZone, 'LL')
|
||||
// const input = getByDisplayValue(displayValue)
|
||||
// input.focus()
|
||||
// const newValue = '2019-04-12T13:00:00-05:00'
|
||||
// displayValue = toLocaleString(newValue, locale, timeZone, 'LL')
|
||||
// fireEvent.change(input, {target: {value: displayValue}})
|
||||
// container.querySelector('#focus-me').focus()
|
||||
|
||||
// expect(onChangeMode).toHaveBeenCalledWith('view')
|
||||
// expect(onChange).toHaveBeenCalledWith(newValue)
|
||||
// })
|
||||
|
||||
// it('reverts to the old value on Escape', () => {
|
||||
// // if I can't get change to work, I can't revert...
|
||||
// })
|
||||
})
|
|
@ -20,65 +20,90 @@ import React from 'react'
|
|||
import {render, fireEvent} from 'react-testing-library'
|
||||
import EditableHeading from '../EditableHeading'
|
||||
|
||||
it('renders the value in view mode', () => {
|
||||
const {getByText} = render(
|
||||
<EditableHeading
|
||||
mode="view"
|
||||
onChange={() => {}}
|
||||
onChangeMode={() => {}}
|
||||
label="Book title"
|
||||
value="Another Roadside Attraction"
|
||||
level="h3"
|
||||
/>
|
||||
)
|
||||
describe('EditableHeading', () => {
|
||||
it('renders the value in view mode', () => {
|
||||
const {getByText} = render(
|
||||
<EditableHeading
|
||||
mode="view"
|
||||
onChange={() => {}}
|
||||
onChangeMode={() => {}}
|
||||
label="Book title"
|
||||
value="Another Roadside Attraction"
|
||||
level="h3"
|
||||
/>
|
||||
)
|
||||
|
||||
expect(getByText('Another Roadside Attraction')).toBeInTheDocument()
|
||||
expect(document.querySelector('h3')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('renders the value in edit mode', () => {
|
||||
const {getByDisplayValue} = render(
|
||||
<EditableHeading
|
||||
mode="edit"
|
||||
onChange={() => {}}
|
||||
onChangeMode={() => {}}
|
||||
label="Book title"
|
||||
value="Still Life with Woodpecker"
|
||||
level="h3"
|
||||
/>
|
||||
)
|
||||
|
||||
expect(getByDisplayValue('Still Life with Woodpecker')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('does not render edit button when readOnly', () => {
|
||||
const {queryByText} = render(
|
||||
<EditableHeading
|
||||
mode="view"
|
||||
onChange={() => {}}
|
||||
onChangeMode={() => {}}
|
||||
label="Edit title"
|
||||
value="Still Life with Woodpecker"
|
||||
level="h3"
|
||||
readOnly
|
||||
/>
|
||||
)
|
||||
expect(queryByText('Edit title')).toBeNull()
|
||||
})
|
||||
|
||||
it('exits edit mode on <Enter>', () => {
|
||||
const onChangeMode = jest.fn()
|
||||
const {getByDisplayValue} = render(
|
||||
<EditableHeading
|
||||
mode="edit"
|
||||
onChange={() => {}}
|
||||
onChangeMode={onChangeMode}
|
||||
label="Book title"
|
||||
value="Jitterbug Perfume"
|
||||
level="h3"
|
||||
/>
|
||||
)
|
||||
const input = getByDisplayValue('Jitterbug Perfume')
|
||||
fireEvent.keyDown(input, {key: 'Enter', code: 13})
|
||||
expect(onChangeMode).toHaveBeenCalledWith('view')
|
||||
expect(getByText('Another Roadside Attraction')).toBeInTheDocument()
|
||||
expect(document.querySelector('h3')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('renders the value in edit mode', () => {
|
||||
const {getByDisplayValue} = render(
|
||||
<EditableHeading
|
||||
mode="edit"
|
||||
onChange={() => {}}
|
||||
onChangeMode={() => {}}
|
||||
label="Book title"
|
||||
value="Still Life with Woodpecker"
|
||||
level="h3"
|
||||
/>
|
||||
)
|
||||
|
||||
expect(getByDisplayValue('Still Life with Woodpecker')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('does not render edit button when readOnly', () => {
|
||||
const {queryByText} = render(
|
||||
<EditableHeading
|
||||
mode="view"
|
||||
onChange={() => {}}
|
||||
onChangeMode={() => {}}
|
||||
label="Edit title"
|
||||
value="Even Cowgirls Get the Blues"
|
||||
level="h3"
|
||||
readOnly
|
||||
/>
|
||||
)
|
||||
expect(queryByText('Edit title')).toBeNull()
|
||||
})
|
||||
|
||||
it('exits edit mode on <Enter>', () => {
|
||||
const onChangeMode = jest.fn()
|
||||
const {getByDisplayValue} = render(
|
||||
<EditableHeading
|
||||
mode="edit"
|
||||
onChange={() => {}}
|
||||
onChangeMode={onChangeMode}
|
||||
label="Book title"
|
||||
value="Jitterbug Perfume"
|
||||
level="h3"
|
||||
/>
|
||||
)
|
||||
const input = getByDisplayValue('Jitterbug Perfume')
|
||||
fireEvent.keyDown(input, {key: 'Enter', code: 13})
|
||||
expect(onChangeMode).toHaveBeenCalledWith('view')
|
||||
})
|
||||
|
||||
it('reverts to the old value and exits edit mode on Escape', () => {
|
||||
const onChange = jest.fn()
|
||||
const onChangeMode = jest.fn()
|
||||
const {getByDisplayValue} = render(
|
||||
<EditableHeading
|
||||
mode="edit"
|
||||
onChange={onChange}
|
||||
onChangeMode={onChangeMode}
|
||||
label="Book title"
|
||||
value="Half Asleep in Frog Pajamas"
|
||||
level="h3"
|
||||
/>
|
||||
)
|
||||
|
||||
const input = getByDisplayValue('Half Asleep in Frog Pajamas')
|
||||
fireEvent.change(input, {target: {value: 'x'}})
|
||||
expect(onChange).toHaveBeenLastCalledWith('x')
|
||||
|
||||
fireEvent.keyUp(input, {key: 'Escape', code: 27})
|
||||
expect(onChange).toHaveBeenLastCalledWith('Half Asleep in Frog Pajamas')
|
||||
expect(onChangeMode).toHaveBeenLastCalledWith('view')
|
||||
})
|
||||
})
|
||||
|
|
|
@ -78,14 +78,15 @@ describe('EditableNumber', () => {
|
|||
expect(onChangeMode).toHaveBeenCalledWith('view')
|
||||
})
|
||||
|
||||
it('reverts to the old value on Escape', () => {
|
||||
it('reverts to the old value and exits edit mode on Escape', () => {
|
||||
const onChange = jest.fn()
|
||||
const onInputChange = jest.fn()
|
||||
const onChangeMode = jest.fn()
|
||||
const {getByDisplayValue} = render(
|
||||
<EditableNumber
|
||||
mode="edit"
|
||||
onChange={onChange}
|
||||
onChangeMode={() => {}}
|
||||
onChangeMode={onChangeMode}
|
||||
onInputChange={onInputChange}
|
||||
label="Pick a number"
|
||||
value="17"
|
||||
|
@ -93,10 +94,12 @@ describe('EditableNumber', () => {
|
|||
)
|
||||
|
||||
const input = getByDisplayValue('17')
|
||||
fireEvent.input(input, {target: {value: '2'}})
|
||||
fireEvent.keyDown(input, {key: 'Escape', code: 27})
|
||||
expect(onInputChange).toHaveBeenCalledWith('2')
|
||||
expect(onChange).toHaveBeenCalledWith('17')
|
||||
fireEvent.change(input, {target: {value: '2'}})
|
||||
expect(onInputChange).toHaveBeenLastCalledWith('2')
|
||||
|
||||
fireEvent.keyUp(input, {key: 'Escape', code: 27})
|
||||
expect(onChange).toHaveBeenLastCalledWith('17')
|
||||
expect(onChangeMode).toHaveBeenLastCalledWith('view')
|
||||
})
|
||||
|
||||
// I want to test that the input grows in width as the user
|
||||
|
|
|
@ -60,6 +60,7 @@ export default class Header extends React.Component {
|
|||
assignment: TeacherAssignmentShape.isRequired,
|
||||
onChangeAssignment: func.isRequired,
|
||||
onValidate: func.isRequired,
|
||||
invalidMessage: func.isRequired,
|
||||
onSetWorkstate: func.isRequired,
|
||||
onUnsubmittedClick: func,
|
||||
onPublishChange: func,
|
||||
|
@ -215,6 +216,7 @@ export default class Header extends React.Component {
|
|||
onChange={this.handleNameChange}
|
||||
onChangeMode={this.handleNameChangeMode}
|
||||
onValidate={this.props.onValidate}
|
||||
invalidMessage={this.props.invalidMessage}
|
||||
readOnly={this.props.readOnly}
|
||||
/>
|
||||
</View>
|
||||
|
|
|
@ -30,6 +30,8 @@ export default class EveryoneElse extends React.Component {
|
|||
static propTypes = {
|
||||
assignment: TeacherAssignmentShape.isRequired,
|
||||
onChangeAssignment: func.isRequired,
|
||||
onValidate: func.isRequired,
|
||||
invalidMessage: func.isRequired,
|
||||
readOnly: bool
|
||||
}
|
||||
|
||||
|
@ -42,9 +44,19 @@ export default class EveryoneElse extends React.Component {
|
|||
this.props.onChangeAssignment(path, value)
|
||||
}
|
||||
|
||||
// see
|
||||
// OverrideListPresenter.due_for calling
|
||||
// OverrideListPresenter.multiple_due_dates calling
|
||||
// assignment.has_active_overrides
|
||||
hasActiveOverrides() {
|
||||
return (
|
||||
this.props.assignment.assignmentOverrides.nodes &&
|
||||
this.props.assignment.assignmentOverrides.nodes.length
|
||||
)
|
||||
}
|
||||
|
||||
overrideFromAssignment(assignment) {
|
||||
const title =
|
||||
assignment.assignmentOverrides.nodes.length > 0 ? I18n.t('Everyone else') : I18n.t('Everyone')
|
||||
const title = this.hasActiveOverrides() ? I18n.t('Everyone else') : I18n.t('Everyone')
|
||||
|
||||
const fauxOverride = {
|
||||
gid: `assignment_${assignment.id}`,
|
||||
|
@ -70,6 +82,8 @@ export default class EveryoneElse extends React.Component {
|
|||
<Override
|
||||
override={fauxOverride}
|
||||
onChangeOverride={this.handleChangeOverride}
|
||||
onValidate={this.props.onValidate}
|
||||
invalidMessage={this.props.invalidMessage}
|
||||
index={-1}
|
||||
readOnly={this.props.readOnly}
|
||||
/>
|
||||
|
|
|
@ -25,34 +25,65 @@ import View from '@instructure/ui-layout/lib/components/View'
|
|||
import OverrideSummary from './OverrideSummary'
|
||||
import OverrideDetail from './OverrideDetail'
|
||||
|
||||
Override.propTypes = {
|
||||
override: OverrideShape.isRequired,
|
||||
onChangeOverride: func.isRequired,
|
||||
index: number.isRequired, // offset of this override in the assignment
|
||||
readOnly: bool
|
||||
}
|
||||
Override.defaultProps = {
|
||||
readOnly: false
|
||||
}
|
||||
export default class Override extends React.Component {
|
||||
static propTypes = {
|
||||
override: OverrideShape.isRequired,
|
||||
onChangeOverride: func.isRequired,
|
||||
onValidate: func.isRequired,
|
||||
invalidMessage: func.isRequired,
|
||||
index: number.isRequired, // offset of this override in the assignment
|
||||
readOnly: bool
|
||||
}
|
||||
|
||||
export default function Override(props) {
|
||||
return (
|
||||
<View as="div" margin="0 0 small 0" data-testid="Override">
|
||||
<ToggleGroup
|
||||
toggleLabel={I18n.t('Expand')}
|
||||
summary={<OverrideSummary override={props.override} />}
|
||||
background="default"
|
||||
>
|
||||
<OverrideDetail
|
||||
override={props.override}
|
||||
onChangeOverride={onChangeOverride}
|
||||
readOnly={props.readOnly}
|
||||
/>
|
||||
</ToggleGroup>
|
||||
</View>
|
||||
)
|
||||
static defaultProps = {
|
||||
readOnly: false
|
||||
}
|
||||
|
||||
function onChangeOverride(path, value) {
|
||||
props.onChangeOverride(props.index, path, value)
|
||||
constructor(props) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
expanded: false
|
||||
}
|
||||
}
|
||||
|
||||
handleChangeOverride = (path, value) => {
|
||||
return this.props.onChangeOverride(this.props.index, path, value)
|
||||
}
|
||||
|
||||
handleValidate = (path, value) => {
|
||||
return this.props.onValidate(this.props.index, path, value)
|
||||
}
|
||||
|
||||
invalidMessage = path => {
|
||||
return this.props.invalidMessage(this.props.index, path)
|
||||
}
|
||||
|
||||
handleToggle = (_event, expanded) => {
|
||||
this.setState({expanded})
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<View as="div" margin="0 0 small 0" data-testid="Override">
|
||||
<ToggleGroup
|
||||
expanded={this.state.expanded}
|
||||
onToggle={this.handleToggle}
|
||||
toggleLabel={
|
||||
this.state.expanded ? I18n.t('Click to hide details') : I18n.t('Click to show details')
|
||||
}
|
||||
summary={<OverrideSummary override={this.props.override} />}
|
||||
background="default"
|
||||
>
|
||||
<OverrideDetail
|
||||
override={this.props.override}
|
||||
onChangeOverride={this.handleChangeOverride}
|
||||
onValidate={this.handleValidate}
|
||||
invalidMessage={this.invalidMessage}
|
||||
readOnly={this.props.readOnly}
|
||||
/>
|
||||
</ToggleGroup>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,63 +16,145 @@
|
|||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import React from 'react'
|
||||
import {bool, string} from 'prop-types'
|
||||
import {bool, func, oneOf, string} from 'prop-types'
|
||||
import I18n from 'i18n!assignments_2'
|
||||
import FriendlyDatetime from '../../../../shared/FriendlyDatetime'
|
||||
import IconCalendarMonth from '@instructure/ui-icons/lib/Line/IconCalendarMonth'
|
||||
import FormField from '@instructure/ui-form-field/lib/components/FormField'
|
||||
import Flex, {FlexItem} from '@instructure/ui-layout/lib/components/Flex'
|
||||
import View from '@instructure/ui-layout/lib/components/View'
|
||||
import generateElementId from '@instructure/ui-utils/lib/dom/generateElementId'
|
||||
import {Flex, FlexItem} from '@instructure/ui-layout'
|
||||
import {FormFieldGroup} from '@instructure/ui-forms'
|
||||
import {ScreenReaderContent} from '@instructure/ui-a11y'
|
||||
|
||||
OverrideDates.propTypes = {
|
||||
dueAt: string,
|
||||
unlockAt: string,
|
||||
lockAt: string,
|
||||
readOnly: bool
|
||||
}
|
||||
import AssignmentDate from '../Editables/AssignmentDate'
|
||||
|
||||
OverrideDates.defaultProps = {
|
||||
readOnly: false
|
||||
}
|
||||
export default class OverrideDates extends React.Component {
|
||||
static propTypes = {
|
||||
mode: oneOf(['view', 'edit']), // TODO: needs to be isReqired from above
|
||||
onChange: func.isRequired,
|
||||
onValidate: func.isRequired,
|
||||
invalidMessage: func.isRequired,
|
||||
dueAt: string,
|
||||
unlockAt: string,
|
||||
lockAt: string,
|
||||
readOnly: bool
|
||||
}
|
||||
|
||||
export default function OverrideDates(props) {
|
||||
return (
|
||||
<Flex
|
||||
as="div"
|
||||
margin="small 0"
|
||||
padding="0"
|
||||
justifyItems="space-between"
|
||||
wrapItems
|
||||
data-testid="OverrideDates"
|
||||
>
|
||||
<FlexItem margin="0 x-small small 0" as="div" grow>
|
||||
{renderDate(I18n.t('Due:'), props.dueAt)}
|
||||
</FlexItem>
|
||||
<FlexItem margin="0 x-small small 0" as="div" grow>
|
||||
{renderDate(I18n.t('Available:'), props.unlockAt)}
|
||||
</FlexItem>
|
||||
<FlexItem margin="0 0 small 0" as="div" grow>
|
||||
{renderDate(I18n.t('Until:'), props.lockAt)}
|
||||
</FlexItem>
|
||||
</Flex>
|
||||
)
|
||||
}
|
||||
static defaultProps = {
|
||||
readOnly: false
|
||||
}
|
||||
|
||||
function renderDate(label, value) {
|
||||
const id = generateElementId('overidedate')
|
||||
return (
|
||||
<FormField id={id} label={label} layout="stacked">
|
||||
<View id={id} as="div" padding="x-small" borderWidth="small" borderRadius="medium">
|
||||
<Flex justifyItems="space-between">
|
||||
<FlexItem>
|
||||
{value && <FriendlyDatetime dateTime={value} format={I18n.t('#date.formats.full')} />}
|
||||
constructor(props) {
|
||||
super(props)
|
||||
|
||||
const mode = props.mode || 'view'
|
||||
this.state = {
|
||||
dueMode: mode,
|
||||
unlockMode: mode,
|
||||
lockMode: mode
|
||||
}
|
||||
}
|
||||
|
||||
onChangeDue = newValue => this.props.onChange('dueAt', newValue)
|
||||
|
||||
onChangeUnlock = newValue => this.props.onChange('unlockAt', newValue)
|
||||
|
||||
onChangeLock = newValue => this.props.onChange('lockAt', newValue)
|
||||
|
||||
onChangeDueMode = dueMode => this.setState({dueMode})
|
||||
|
||||
onChangeUnlockMode = unlockMode => this.setState({unlockMode})
|
||||
|
||||
onChangeLockMode = lockMode => this.setState({lockMode})
|
||||
|
||||
onValidateDue = value => this.props.onValidate('dueAt', value)
|
||||
|
||||
onValidateUnlock = value => this.props.onValidate('unlockAt', value)
|
||||
|
||||
onValidateLock = value => this.props.onValidate('lockAt', value)
|
||||
|
||||
invalidMessageDue = () => this.props.invalidMessage('dueAt')
|
||||
|
||||
invalidMessageUnlock = () => this.props.invalidMessage('unlockAt')
|
||||
|
||||
invalidMessageLock = () => this.props.invalidMessage('lockAt')
|
||||
|
||||
allDatesAreBeingViewed = () =>
|
||||
this.state.dueMode === 'view' &&
|
||||
this.state.unlockMode === 'view' &&
|
||||
this.state.lockMode === 'view'
|
||||
|
||||
renderDate(field, label, value, mode, onchange, onchangemode, onvalidate, invalidMessage) {
|
||||
return (
|
||||
<AssignmentDate
|
||||
mode={mode}
|
||||
onChange={onchange}
|
||||
onChangeMode={onchangemode}
|
||||
onValidate={onvalidate}
|
||||
invalidMessage={invalidMessage}
|
||||
field={field}
|
||||
value={value}
|
||||
label={label}
|
||||
readOnly={this.props.readOnly}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
render() {
|
||||
// show an error message only when all dates are in view
|
||||
const message =
|
||||
this.allDatesAreBeingViewed() &&
|
||||
(this.invalidMessageDue() || this.invalidMessageUnlock() || this.invalidMessageLock())
|
||||
return (
|
||||
<FormFieldGroup
|
||||
description={
|
||||
<ScreenReaderContent>{I18n.t('Due, available, and until dates')}</ScreenReaderContent>
|
||||
}
|
||||
messages={message ? [{type: 'error', text: message}] : null}
|
||||
>
|
||||
<Flex
|
||||
as="div"
|
||||
margin="small 0"
|
||||
padding="0"
|
||||
justifyItems="space-between"
|
||||
alignItems="start"
|
||||
wrapItems
|
||||
data-testid="OverrideDates"
|
||||
>
|
||||
<FlexItem margin="0 x-small 0 0" as="div" grow width="30%">
|
||||
{this.renderDate(
|
||||
'due_at',
|
||||
I18n.t('Due'),
|
||||
this.props.dueAt,
|
||||
this.state.dueMode,
|
||||
this.onChangeDue,
|
||||
this.onChangeDueMode,
|
||||
this.onValidateDue,
|
||||
this.invalidMessageDue
|
||||
)}
|
||||
</FlexItem>
|
||||
<FlexItem padding="0 0 xx-small x-small">
|
||||
<IconCalendarMonth />
|
||||
<FlexItem margin="0 x-small 0 0" as="div" grow width="30%">
|
||||
{this.renderDate(
|
||||
'unlock_at',
|
||||
I18n.t('Available'),
|
||||
this.props.unlockAt,
|
||||
this.state.unlockMode,
|
||||
this.onChangeUnlock,
|
||||
this.onChangeUnlockMode,
|
||||
this.onValidateUnlock,
|
||||
this.invalidMessageUnlock
|
||||
)}
|
||||
</FlexItem>
|
||||
<FlexItem margin="0 0 0 0" as="div" grow width="30%">
|
||||
{this.renderDate(
|
||||
'lock_at',
|
||||
I18n.t('Until'),
|
||||
this.props.lockAt,
|
||||
this.state.lockMode,
|
||||
this.onChangeLock,
|
||||
this.onChangeLockMode,
|
||||
this.onValidateLock,
|
||||
this.invalidMessageLock
|
||||
)}
|
||||
</FlexItem>
|
||||
</Flex>
|
||||
</View>
|
||||
</FormField>
|
||||
)
|
||||
</FormFieldGroup>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -28,6 +28,8 @@ export default class OverrideDetail extends React.Component {
|
|||
static propTypes = {
|
||||
override: OverrideShape.isRequired,
|
||||
onChangeOverride: func.isRequired,
|
||||
onValidate: func.isRequired,
|
||||
invalidMessage: func.isRequired,
|
||||
readOnly: bool
|
||||
}
|
||||
|
||||
|
@ -35,6 +37,10 @@ export default class OverrideDetail extends React.Component {
|
|||
readOnly: false
|
||||
}
|
||||
|
||||
handleChangeDate = (which, value) => {
|
||||
this.props.onChangeOverride(which, value)
|
||||
}
|
||||
|
||||
renderAssignedTo() {
|
||||
return <OverrideAssignTo override={this.props.override} variant="detail" />
|
||||
}
|
||||
|
@ -45,6 +51,10 @@ export default class OverrideDetail extends React.Component {
|
|||
dueAt={this.props.override.dueAt}
|
||||
unlockAt={this.props.override.unlockAt}
|
||||
lockAt={this.props.override.lockAt}
|
||||
onChange={this.handleChangeDate}
|
||||
onValidate={this.props.onValidate}
|
||||
invalidMessage={this.props.invalidMessage}
|
||||
readOnly={this.props.readOnly}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -52,23 +52,45 @@ export default class OverrideSummary extends React.Component {
|
|||
return (
|
||||
<Text>
|
||||
<OverrideSubmissionTypes variant="summary" override={override} />
|
||||
<Text> | </Text>
|
||||
<FriendlyDatetime
|
||||
prefix={I18n.t('Due: ')}
|
||||
dateTime={override.dueAt}
|
||||
format={I18n.t('#date.formats.full')}
|
||||
/>
|
||||
<Text>
|
||||
<div style={{display: 'inline-block', padding: '0 .5em'}}>|</div>
|
||||
{override.dueAt ? (
|
||||
<FriendlyDatetime
|
||||
prefix={I18n.t('Due: ')}
|
||||
dateTime={override.dueAt}
|
||||
format={I18n.t('#date.formats.full')}
|
||||
/>
|
||||
) : (
|
||||
I18n.t('No Due Date')
|
||||
)}
|
||||
</Text>
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
|
||||
// it's unfortunate but when both unlock and lock dates exist
|
||||
// AvailabilityDates only prefixes with "Available" if the formatStyle="long"
|
||||
// If I chnage it there, it will alter the Student view
|
||||
renderAvailability(override) {
|
||||
return (
|
||||
<Text>
|
||||
{I18n.t('Available ')}
|
||||
<AvailabilityDates assignment={override} formatStyle="short" />
|
||||
</Text>
|
||||
)
|
||||
// both dates exist, manually add Available prefix
|
||||
if (override.unlockAt && override.lockAt) {
|
||||
return (
|
||||
<Text>
|
||||
{I18n.t('Available ')}
|
||||
<AvailabilityDates assignment={override} formatStyle="short" />
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
// only one date exists, AvailabilityDates will include the Available prefix
|
||||
if (override.unlockAt || override.lockAt) {
|
||||
return (
|
||||
<Text>
|
||||
<AvailabilityDates assignment={override} formatStyle="short" />
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
// no dates exist, so the assignment is simply Available
|
||||
return <Text>{I18n.t('Available')}</Text>
|
||||
}
|
||||
|
||||
render() {
|
||||
|
|
|
@ -29,6 +29,8 @@ export default class Overrides extends React.Component {
|
|||
static propTypes = {
|
||||
assignment: TeacherAssignmentShape.isRequired,
|
||||
onChangeAssignment: func.isRequired,
|
||||
onValidate: func.isRequired,
|
||||
invalidMessage: func.isRequired,
|
||||
readOnly: bool
|
||||
}
|
||||
|
||||
|
@ -37,24 +39,34 @@ export default class Overrides extends React.Component {
|
|||
}
|
||||
|
||||
handleChangeOverride = (overrideIndex, path, value) => {
|
||||
if (path === 'allowedAttempts' || path === 'submissionTypes') {
|
||||
const hoistToAssignment = ['allowedAttempts', 'submissionTypes']
|
||||
if (hoistToAssignment.includes(path)) {
|
||||
this.props.onChangeAssignment(path, value)
|
||||
} else {
|
||||
this.props.onChangeAssignment(`assignmentOverrides.nodes.${overrideIndex}.${path}`, value)
|
||||
}
|
||||
}
|
||||
|
||||
handleValidateEveryoneElse = (_ignore, path, value) => this.props.onValidate(path, value)
|
||||
|
||||
handleValidateOverride = (overrideIndex, path, value) =>
|
||||
this.props.onValidate(`assignmentOverrides.nodes.${overrideIndex}.${path}`, value)
|
||||
|
||||
everyoneElseInvalidMessage = (_ignore, path) => this.props.invalidMessage(path)
|
||||
|
||||
invalidMessage = (overrideIndex, path) =>
|
||||
this.props.invalidMessage(`assignmentOverrides.nodes.${overrideIndex}.${path}`)
|
||||
|
||||
renderEveryoneElse() {
|
||||
if (this.props.assignment.dueAt !== null) {
|
||||
return (
|
||||
<EveryoneElse
|
||||
assignment={this.props.assignment}
|
||||
onChangeAssignment={this.props.onChangeAssignment}
|
||||
readOnly={this.props.readOnly}
|
||||
/>
|
||||
)
|
||||
}
|
||||
return null
|
||||
return (
|
||||
<EveryoneElse
|
||||
assignment={this.props.assignment}
|
||||
onChangeAssignment={this.props.onChangeAssignment}
|
||||
onValidate={this.handleValidateEveryoneElse}
|
||||
invalidMessage={this.everyoneElseInvalidMessage}
|
||||
readOnly={this.props.readOnly}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
renderOverrides() {
|
||||
|
@ -74,6 +86,8 @@ export default class Overrides extends React.Component {
|
|||
}}
|
||||
index={index}
|
||||
onChangeOverride={this.handleChangeOverride}
|
||||
onValidate={this.handleValidateOverride}
|
||||
invalidMessage={this.invalidMessage}
|
||||
readOnly={this.props.readOnly}
|
||||
/>
|
||||
))
|
||||
|
@ -85,7 +99,7 @@ export default class Overrides extends React.Component {
|
|||
return (
|
||||
<View as="div">
|
||||
{this.renderOverrides()}
|
||||
{this.renderEveryoneElse()}
|
||||
{this.props.assignment.onlyVisibleToOverrides ? null : this.renderEveryoneElse()}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -41,7 +41,14 @@ it("pulls everyone else's dates from the assignment", () => {
|
|||
}
|
||||
})
|
||||
|
||||
const {getByText} = render(<EveryoneElse assignment={assignment} onChangeAssignment={() => {}} />)
|
||||
const {getByText} = render(
|
||||
<EveryoneElse
|
||||
assignment={assignment}
|
||||
onChangeAssignment={() => {}}
|
||||
onValidate={() => true}
|
||||
invalidMessage={() => undefined}
|
||||
/>
|
||||
)
|
||||
expect(getByText('Everyone else')).toBeInTheDocument()
|
||||
|
||||
const due = `Due: ${tz.format(aDueAt, I18n.t('#date.formats.full'))}`
|
||||
|
|
|
@ -21,25 +21,36 @@ import {render, fireEvent, waitForElement} from 'react-testing-library'
|
|||
import {mockOverride} from '../../../test-utils'
|
||||
import Override from '../Override'
|
||||
|
||||
it('renders an override', () => {
|
||||
const override = mockOverride()
|
||||
const {getByTestId} = render(
|
||||
<Override override={override} onChangeOverride={() => {}} index={0} />
|
||||
function renderOverride(override, props = {}) {
|
||||
return render(
|
||||
<Override
|
||||
override={override}
|
||||
onChangeOverride={() => {}}
|
||||
index={0}
|
||||
onValidate={() => true}
|
||||
invalidMessage={() => undefined}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
expect(getByTestId('OverrideSummary')).toBeInTheDocument()
|
||||
})
|
||||
}
|
||||
|
||||
it('displays OverrideDetail on expanding toggle group', async () => {
|
||||
const override = mockOverride()
|
||||
const {getByText, getByTestId} = render(
|
||||
<Override override={override} onChangeOverride={() => {}} index={0} />
|
||||
)
|
||||
describe('Override', () => {
|
||||
it('renders an override', () => {
|
||||
const override = mockOverride()
|
||||
const {getByTestId} = renderOverride(override)
|
||||
expect(getByTestId('OverrideSummary')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
const expandButton = getByText('Expand')
|
||||
fireEvent.click(expandButton)
|
||||
// the detail is now rendered
|
||||
const detail = await waitForElement(() => getByTestId('OverrideDetail'))
|
||||
expect(detail).toBeInTheDocument()
|
||||
// and the summary's still there
|
||||
expect(getByTestId('OverrideSummary')).toBeInTheDocument()
|
||||
it('displays OverrideDetail on expanding toggle group', async () => {
|
||||
const override = mockOverride()
|
||||
const {getByText, getByTestId} = renderOverride(override)
|
||||
|
||||
const expandButton = getByText('Click to show details')
|
||||
fireEvent.click(expandButton)
|
||||
// the detail is now rendered
|
||||
const detail = await waitForElement(() => getByTestId('OverrideDetail'))
|
||||
expect(detail).toBeInTheDocument()
|
||||
// and the summary's still there
|
||||
expect(getByTestId('OverrideSummary')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
|
|
@ -17,27 +17,127 @@
|
|||
*/
|
||||
|
||||
import React from 'react'
|
||||
import {render} from 'react-testing-library'
|
||||
import {mockOverride} from '../../../test-utils'
|
||||
import {render, fireEvent, wait} from 'react-testing-library'
|
||||
import {toLocaleString, browserTimeZone} from '@instructure/ui-i18n/lib/DateTime'
|
||||
import {mockOverride, closest} from '../../../test-utils'
|
||||
import OverrideDates from '../OverrideDates'
|
||||
|
||||
import I18n from 'i18n!assignments_2'
|
||||
import tz from 'timezone'
|
||||
const locale = 'en'
|
||||
const timeZone = browserTimeZone()
|
||||
|
||||
it('renders readonly override dates', () => {
|
||||
const override = mockOverride()
|
||||
describe('OverrideDates', () => {
|
||||
it('renders override dates', () => {
|
||||
const override = mockOverride()
|
||||
|
||||
const {getByText} = render(
|
||||
<OverrideDates dueAt={override.dueAt} unlockAt={override.unlockAt} lockAt={override.lockAt} />
|
||||
)
|
||||
const {getByText, getAllByTestId} = render(
|
||||
<OverrideDates
|
||||
dueAt={override.dueAt}
|
||||
unlockAt={override.unlockAt}
|
||||
lockAt={override.lockAt}
|
||||
onChange={() => {}}
|
||||
onValidate={() => true}
|
||||
invalidMessage={() => undefined}
|
||||
/>
|
||||
)
|
||||
|
||||
const due = `${tz.format(override.dueAt, I18n.t('#date.formats.full'))}`
|
||||
const available = `${tz.format(override.unlockAt, I18n.t('#date.formats.full'))}`
|
||||
const until = `${tz.format(override.lockAt, I18n.t('#date.formats.full'))}`
|
||||
expect(getByText('Due:')).toBeInTheDocument()
|
||||
expect(getByText(due)).toBeInTheDocument()
|
||||
expect(getByText('Available:')).toBeInTheDocument()
|
||||
expect(getByText(available)).toBeInTheDocument()
|
||||
expect(getByText('Until:')).toBeInTheDocument()
|
||||
expect(getByText(until)).toBeInTheDocument()
|
||||
expect(getAllByTestId('EditableDateTime')).toHaveLength(3)
|
||||
expect(getByText('Due:')).toBeInTheDocument()
|
||||
expect(getByText('Available:')).toBeInTheDocument()
|
||||
expect(getByText('Until:')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('renders missing override dates', () => {
|
||||
const override = mockOverride({unlockAt: null, lockAt: null})
|
||||
|
||||
const {getByText, getAllByTestId} = render(
|
||||
<OverrideDates
|
||||
dueAt={override.dueAt}
|
||||
unlockAt={override.unlockAt}
|
||||
lockAt={null}
|
||||
onChange={() => {}}
|
||||
onValidate={() => true}
|
||||
invalidMessage={() => undefined}
|
||||
/>
|
||||
)
|
||||
|
||||
expect(getAllByTestId('EditableDateTime')).toHaveLength(3)
|
||||
expect(getByText('No Until Date')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
// to get 100% test coverage
|
||||
failADate('dueAt')
|
||||
failADate('unlockAt')
|
||||
failADate('lockAt')
|
||||
})
|
||||
|
||||
function failADate(whichDate) {
|
||||
const editButtonLabel = {
|
||||
dueAt: 'Edit Due',
|
||||
unlockAt: 'Edit Available',
|
||||
lockAt: 'Edit Until'
|
||||
}
|
||||
const errMessages = {}
|
||||
|
||||
it(`renders the error message when ${whichDate} date is invalid`, async () => {
|
||||
const override = mockOverride({
|
||||
dueAt: '2018-12-25T23:59:59-05:00',
|
||||
unlockAt: '2018-12-23T00:00:00-05:00',
|
||||
lockAt: '2018-12-29T23:59:00-05:00'
|
||||
})
|
||||
|
||||
// validate + invalidMessage mock the real deal
|
||||
function validate(which, value) {
|
||||
if (value < '2019-01-01T00:00:00-05:00') {
|
||||
errMessages[which] = `${which} be bad`
|
||||
return false
|
||||
}
|
||||
delete errMessages[which]
|
||||
}
|
||||
|
||||
function invalidMessage(which) {
|
||||
return errMessages[which]
|
||||
}
|
||||
const {container, getByText, getByDisplayValue, queryByTestId} = render(
|
||||
<div>
|
||||
<OverrideDates
|
||||
dueAt={override.dueAt}
|
||||
unlockAt={override.unlockAt}
|
||||
lockAt={override.lockAt}
|
||||
onChange={() => {}}
|
||||
onValidate={validate}
|
||||
invalidMessage={invalidMessage}
|
||||
/>
|
||||
<span id="focus-me" tabIndex="-1">
|
||||
focus me
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
|
||||
// click the edit button
|
||||
const editDueBtn = closest(getByText(editButtonLabel[whichDate]), 'button')
|
||||
editDueBtn.click()
|
||||
const dateDisplay = toLocaleString(override[whichDate], locale, timeZone, 'LL')
|
||||
let dinput
|
||||
// wait for the popup
|
||||
await wait(() => {
|
||||
dinput = getByDisplayValue(dateDisplay)
|
||||
})
|
||||
// focus the date input and change it's value to a date that fails validation
|
||||
dinput.focus()
|
||||
fireEvent.change(dinput, {target: {value: '2019-01-02T00:00:00-05:00'}})
|
||||
|
||||
// blur the DateTimeInput to flip me to view mode.
|
||||
container.querySelector('#focus-me').focus()
|
||||
// wait for the popup to close
|
||||
// (using test-utils' waitForNoElement, I get a
|
||||
// "Warning: Can't perform a React state update on an unmounted component."
|
||||
// though I cannot see the difference in the underlying logic between this
|
||||
// and waitForNoElement)
|
||||
await wait(() => {
|
||||
expect(queryByTestId('EditableDateTime-editor')).toBeNull()
|
||||
})
|
||||
|
||||
// the error message should be in the OverrideDates
|
||||
expect(getByText(`${whichDate} be bad`)).toBeInTheDocument()
|
||||
})
|
||||
}
|
||||
|
|
|
@ -21,50 +21,60 @@ import {render} from 'react-testing-library'
|
|||
import {mockOverride} from '../../../test-utils'
|
||||
import OverrideDetail from '../OverrideDetail'
|
||||
|
||||
it('renders readonly override details', () => {
|
||||
const override = mockOverride({
|
||||
submissionTypes: ['online_text_entry', 'online_url', 'media_recording', 'online_upload']
|
||||
function renderOD(override, props = {}) {
|
||||
return render(
|
||||
<OverrideDetail
|
||||
override={override}
|
||||
onChangeOverride={() => {}}
|
||||
onValidate={() => true}
|
||||
invalidMessage={() => undefined}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
describe('OverrideDetail', () => {
|
||||
it('renders readonly override details', () => {
|
||||
const override = mockOverride({
|
||||
submissionTypes: ['online_text_entry', 'online_url', 'media_recording', 'online_upload']
|
||||
})
|
||||
|
||||
const {getByText, getByTestId} = renderOD(override, {readOnly: true})
|
||||
|
||||
// the labels
|
||||
expect(getByText('Assign to:')).toBeInTheDocument()
|
||||
expect(getByText('Due:')).toBeInTheDocument()
|
||||
expect(getByText('Available:')).toBeInTheDocument()
|
||||
expect(getByText('Until:')).toBeInTheDocument()
|
||||
expect(getByText('Submission Type')).toBeInTheDocument()
|
||||
expect(getByText('Attempts Allowed')).toBeInTheDocument()
|
||||
expect(getByText('Score to keep')).toBeInTheDocument()
|
||||
// the sub-components
|
||||
expect(getByTestId('OverrideAssignTo')).toBeInTheDocument()
|
||||
expect(getByTestId('OverrideDates')).toBeInTheDocument()
|
||||
expect(getByTestId('OverrideSubmissionTypes')).toBeInTheDocument()
|
||||
expect(getByTestId('OverrideAttempts-Detail')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
const {getByText, getByTestId} = render(
|
||||
<OverrideDetail override={override} onChangeOverride={() => {}} readOnly />
|
||||
)
|
||||
it('renders editable override details', () => {
|
||||
const override = mockOverride({
|
||||
submissionTypes: ['online_text_entry', 'online_url', 'media_recording', 'online_upload']
|
||||
})
|
||||
|
||||
// the labels
|
||||
expect(getByText('Assign to:')).toBeInTheDocument()
|
||||
expect(getByText('Due:')).toBeInTheDocument()
|
||||
expect(getByText('Available:')).toBeInTheDocument()
|
||||
expect(getByText('Until:')).toBeInTheDocument()
|
||||
expect(getByText('Submission Type')).toBeInTheDocument()
|
||||
expect(getByText('Attempts Allowed')).toBeInTheDocument()
|
||||
expect(getByText('Score to keep')).toBeInTheDocument()
|
||||
// the sub-components
|
||||
expect(getByTestId('OverrideAssignTo')).toBeInTheDocument()
|
||||
expect(getByTestId('OverrideDates')).toBeInTheDocument()
|
||||
expect(getByTestId('OverrideSubmissionTypes')).toBeInTheDocument()
|
||||
expect(getByTestId('OverrideAttempts-Detail')).toBeInTheDocument()
|
||||
})
|
||||
const {getByText, getByTestId} = renderOD(override)
|
||||
|
||||
it('renders editable override details', () => {
|
||||
const override = mockOverride({
|
||||
submissionTypes: ['online_text_entry', 'online_url', 'media_recording', 'online_upload']
|
||||
// the labels
|
||||
expect(getByText('Assign to:')).toBeInTheDocument()
|
||||
expect(getByText('Due:')).toBeInTheDocument()
|
||||
expect(getByText('Available:')).toBeInTheDocument()
|
||||
expect(getByText('Until:')).toBeInTheDocument()
|
||||
expect(getByText('Submission Type')).toBeInTheDocument()
|
||||
expect(getByText('Attempts Allowed')).toBeInTheDocument()
|
||||
expect(getByText('Score to keep')).toBeInTheDocument()
|
||||
// the sub-components
|
||||
expect(getByTestId('OverrideAssignTo')).toBeInTheDocument()
|
||||
expect(getByTestId('OverrideDates')).toBeInTheDocument()
|
||||
expect(getByTestId('OverrideSubmissionTypes')).toBeInTheDocument()
|
||||
expect(getByTestId('OverrideAttempts-Detail')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
const {getByText, getByTestId} = render(
|
||||
<OverrideDetail override={override} onChangeOverride={() => {}} />
|
||||
)
|
||||
|
||||
// the labels
|
||||
expect(getByText('Assign to:')).toBeInTheDocument()
|
||||
expect(getByText('Due:')).toBeInTheDocument()
|
||||
expect(getByText('Available:')).toBeInTheDocument()
|
||||
expect(getByText('Until:')).toBeInTheDocument()
|
||||
expect(getByText('Submission Type')).toBeInTheDocument()
|
||||
expect(getByText('Attempts Allowed')).toBeInTheDocument()
|
||||
expect(getByText('Score to keep')).toBeInTheDocument()
|
||||
// the sub-components
|
||||
expect(getByTestId('OverrideAssignTo')).toBeInTheDocument()
|
||||
expect(getByTestId('OverrideDates')).toBeInTheDocument()
|
||||
expect(getByTestId('OverrideSubmissionTypes')).toBeInTheDocument()
|
||||
expect(getByTestId('OverrideAttempts-Detail')).toBeInTheDocument()
|
||||
})
|
||||
|
|
|
@ -24,29 +24,84 @@ import {render} from 'react-testing-library'
|
|||
import {mockOverride} from '../../../test-utils'
|
||||
import OverrideSummary from '../OverrideSummary'
|
||||
|
||||
it('renders an OverrideSummary', () => {
|
||||
const dueAt = '2018-11-27T13:00-0500'
|
||||
const unlockAt = '2018-11-26T13:00-0500'
|
||||
const lockAt = '2018-11-28T13:00-0500'
|
||||
const override = mockOverride({
|
||||
title: 'Section A',
|
||||
dueAt,
|
||||
unlockAt,
|
||||
lockAt,
|
||||
submissionTypes: ['online_upload', 'online_url'],
|
||||
allowedAttempts: 1
|
||||
describe('OverrideSummary', () => {
|
||||
it('renders with unlock and lock dates', () => {
|
||||
const dueAt = '2018-11-27T13:00-0500'
|
||||
const unlockAt = '2018-11-26T13:00-0500'
|
||||
const lockAt = '2018-11-28T13:00-0500'
|
||||
const override = mockOverride({
|
||||
title: 'Section A',
|
||||
dueAt,
|
||||
unlockAt,
|
||||
lockAt,
|
||||
submissionTypes: ['online_upload', 'online_url'],
|
||||
allowedAttempts: 1
|
||||
})
|
||||
const {getByText, getByTestId} = render(<OverrideSummary override={override} />)
|
||||
expect(getByTestId('OverrideAssignTo')).toBeInTheDocument()
|
||||
expect(getByTestId('OverrideSubmissionTypes')).toBeInTheDocument()
|
||||
|
||||
const due = `Due: ${tz.format(dueAt, I18n.t('#date.formats.full'))}`
|
||||
expect(getByText(due)).toBeInTheDocument()
|
||||
|
||||
const unlock = `${tz.format(unlockAt, I18n.t('#date.formats.short'))}`
|
||||
const lock = `to ${tz.format(lockAt, I18n.t('#date.formats.full'))}`
|
||||
expect(getByText(unlock)).toBeInTheDocument()
|
||||
expect(getByText(lock)).toBeInTheDocument()
|
||||
|
||||
expect(getByTestId('OverrideAttempts-Summary')).toBeInTheDocument()
|
||||
})
|
||||
const {getByText, getByTestId} = render(<OverrideSummary override={override} />)
|
||||
expect(getByTestId('OverrideAssignTo')).toBeInTheDocument()
|
||||
expect(getByTestId('OverrideSubmissionTypes')).toBeInTheDocument()
|
||||
|
||||
const due = `Due: ${tz.format(dueAt, I18n.t('#date.formats.full'))}`
|
||||
expect(getByText(due)).toBeInTheDocument()
|
||||
it('renders with neither unlock or lock dates', () => {
|
||||
const dueAt = '2018-11-27T13:00-0500'
|
||||
const unlockAt = null
|
||||
const lockAt = null
|
||||
const override = mockOverride({
|
||||
title: 'Section A',
|
||||
dueAt,
|
||||
unlockAt,
|
||||
lockAt,
|
||||
submissionTypes: ['online_upload', 'online_url'],
|
||||
allowedAttempts: 1
|
||||
})
|
||||
const {getByText} = render(<OverrideSummary override={override} />)
|
||||
|
||||
const unlock = `${tz.format(unlockAt, I18n.t('#date.formats.short'))}`
|
||||
const lock = `to ${tz.format(lockAt, I18n.t('#date.formats.full'))}`
|
||||
expect(getByText(unlock)).toBeInTheDocument()
|
||||
expect(getByText(lock)).toBeInTheDocument()
|
||||
expect(getByText('Available')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
expect(getByTestId('OverrideAttempts-Summary')).toBeInTheDocument()
|
||||
it('renders with only unlock date', () => {
|
||||
const dueAt = '2018-11-27T13:00-0500'
|
||||
const unlockAt = '2018-11-26T13:00-0500'
|
||||
const lockAt = null
|
||||
const override = mockOverride({
|
||||
title: 'Section A',
|
||||
dueAt,
|
||||
unlockAt,
|
||||
lockAt,
|
||||
submissionTypes: ['online_upload', 'online_url'],
|
||||
allowedAttempts: 1
|
||||
})
|
||||
const {getByText} = render(<OverrideSummary override={override} />)
|
||||
|
||||
const unlock = `${tz.format(unlockAt, I18n.t('#date.formats.full'))}`
|
||||
expect(getByText(`Available after ${unlock}`)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('renders with only lock date', () => {
|
||||
const dueAt = '2018-11-27T13:00-0500'
|
||||
const unlockAt = null
|
||||
const lockAt = '2018-11-28T13:00-0500'
|
||||
const override = mockOverride({
|
||||
title: 'Section A',
|
||||
dueAt,
|
||||
unlockAt,
|
||||
lockAt,
|
||||
submissionTypes: ['online_upload', 'online_url'],
|
||||
allowedAttempts: 1
|
||||
})
|
||||
const {getByText} = render(<OverrideSummary override={override} />)
|
||||
|
||||
const lock = `${tz.format(lockAt, I18n.t('#date.formats.full'))}`
|
||||
expect(getByText(`Available until ${lock}`)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
|
|
@ -26,6 +26,9 @@ import get from 'lodash/get'
|
|||
import set from 'lodash/set'
|
||||
|
||||
import {showFlashAlert} from 'jsx/shared/FlashAlert'
|
||||
import ErrorBoundary from 'jsx/shared/components/ErrorBoundary'
|
||||
import GenericErrorPage from 'jsx/shared/components/GenericErrorPage/index'
|
||||
import errorShipUrl from '../../student/SVG/ErrorShip.svg'
|
||||
|
||||
import {Alert} from '@instructure/ui-alerts'
|
||||
import {Mask} from '@instructure/ui-overlays'
|
||||
|
@ -41,7 +44,9 @@ import TeacherFooter from './TeacherFooter'
|
|||
import ConfirmDialog from './ConfirmDialog'
|
||||
import MessageStudentsWhoDialog from './MessageStudentsWhoDialog'
|
||||
import TeacherViewContext, {TeacherViewContextDefaults} from './TeacherViewContext'
|
||||
import {validate} from '../Validators'
|
||||
import AssignmentFieldValidator from '../AssignentFieldValidator'
|
||||
|
||||
const pathToOverrides = /assignmentOverrides\.nodes\.\d+/
|
||||
|
||||
export default class TeacherView extends React.Component {
|
||||
static propTypes = {
|
||||
|
@ -50,8 +55,7 @@ export default class TeacherView extends React.Component {
|
|||
}
|
||||
|
||||
static defaultProps = {
|
||||
// for now, put "#edit" in the URL and it will turn off readOnly
|
||||
readOnly: window.location.hash.indexOf('edit') < 0
|
||||
readOnly: window.location.hash.indexOf('readOnly') >= 0
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
|
@ -74,15 +78,26 @@ export default class TeacherView extends React.Component {
|
|||
locale: (window.ENV && window.ENV.MOMENT_LOCALE) || TeacherViewContextDefaults.locale,
|
||||
timeZone: (window.ENV && window.ENV.TIMEZONE) || TeacherViewContextDefaults.timeZone
|
||||
}
|
||||
|
||||
this.fieldValidator = new AssignmentFieldValidator()
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
window.addEventListener('beforeunload', this.handleBeforeUnload)
|
||||
}
|
||||
|
||||
handleBeforeUnload = event => {
|
||||
if (this.state.isDirty) {
|
||||
event.preventDefault()
|
||||
event.returnValue = ''
|
||||
}
|
||||
}
|
||||
|
||||
// @param value: the new value. may be a scalar, an array, or an object.
|
||||
// @param path: where w/in the assignment it should go. May be a string representing
|
||||
// the dot-separated path (e.g. 'assignmentOverrides.nodes.0`) or an array
|
||||
// of steps (e.g. ['assignmentOverrides', 'nodes', 0] (which could also be '0'))
|
||||
// @param path: where w/in the assignment it should go as a string representing
|
||||
// the dot-separated path (e.g. 'assignmentOverrides.nodes.0`) to the value
|
||||
updateWorkingAssignment(path, value) {
|
||||
const dottedpath = Array.isArray(path) ? path.join('.') : path
|
||||
const old = get(this.state.workingAssignment, dottedpath)
|
||||
const old = get(this.state.workingAssignment, path)
|
||||
if (old === value) {
|
||||
return this.state.workingAssignment
|
||||
}
|
||||
|
@ -95,19 +110,8 @@ export default class TeacherView extends React.Component {
|
|||
// add or remove path from the set of invalid fields based on the new value
|
||||
// returns the updated set
|
||||
updateInvalids(path, value) {
|
||||
const isValid = this.validate(path, value)
|
||||
if (isValid) {
|
||||
if (this.state.invalids[path]) {
|
||||
const invalids = {...this.state.invalids}
|
||||
delete invalids[path]
|
||||
return invalids
|
||||
}
|
||||
} else if (!this.state.invalids[path]) {
|
||||
const invalids = {...this.state.invalids}
|
||||
invalids[path] = true
|
||||
return invalids
|
||||
}
|
||||
return this.state.invalids
|
||||
this.validate(path, value)
|
||||
return this.fieldValidator.invalidFields()
|
||||
}
|
||||
|
||||
// if the new value is different from the existing value, update TeacherView's react state
|
||||
|
@ -125,11 +129,21 @@ export default class TeacherView extends React.Component {
|
|||
}
|
||||
|
||||
// validate the value at the path
|
||||
// if path points into an override, extract the override from the assignment
|
||||
// and pass it as the context in which to validate the value
|
||||
validate = (path, value) => {
|
||||
const isValid = validate(path, value)
|
||||
let context = this.state.workingAssignment
|
||||
const match = pathToOverrides.exec(path)
|
||||
if (match) {
|
||||
// extract the override
|
||||
context = get(this.state.workingAssignment, match[0])
|
||||
}
|
||||
const isValid = this.fieldValidator.validate(path, value, context)
|
||||
return isValid
|
||||
}
|
||||
|
||||
invalidMessage = path => this.fieldValidator.errorMessage(path)
|
||||
|
||||
isAssignmentValid = () => Object.keys(this.state.invalids).length === 0
|
||||
|
||||
handleUnsubmittedClick = () => {
|
||||
|
@ -175,7 +189,15 @@ export default class TeacherView extends React.Component {
|
|||
}
|
||||
|
||||
handleCancel = () => {
|
||||
this.setState({workingAssignment: this.props.assignment, isDirty: false, invalids: {}})
|
||||
this.setState((state, props) => {
|
||||
// revalidate the current invalid fields with the original values
|
||||
Object.keys(state.invalids).forEach(path => this.validate(path, get(props.assignment, path)))
|
||||
return {
|
||||
workingAssignment: this.props.assignment,
|
||||
isDirty: false,
|
||||
invalids: this.fieldValidator.invalidFields()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
handleSave(saveAssignment) {
|
||||
|
@ -188,8 +210,10 @@ export default class TeacherView extends React.Component {
|
|||
name: assignment.name,
|
||||
description: assignment.description,
|
||||
state: assignment.state,
|
||||
// ,dueAt: assignment.due_at,
|
||||
pointsPossible: parseFloat(assignment.pointsPossible)
|
||||
pointsPossible: parseFloat(assignment.pointsPossible),
|
||||
dueAt: assignment.dueAt,
|
||||
unlockAt: assignment.unlockAt,
|
||||
lockAt: assignment.lockAt
|
||||
}
|
||||
})
|
||||
})
|
||||
|
@ -327,52 +351,63 @@ export default class TeacherView extends React.Component {
|
|||
const assignment = this.state.workingAssignment
|
||||
const clazz = classnames('assignments-teacher', {dirty})
|
||||
return (
|
||||
<TeacherViewContext.Provider value={this.contextValue}>
|
||||
<div className={clazz}>
|
||||
{this.renderDeleteDialog()}
|
||||
<ScreenReaderContent>
|
||||
<h1>{assignment.name}</h1>
|
||||
</ScreenReaderContent>
|
||||
<Mutation
|
||||
mutation={SAVE_ASSIGNMENT}
|
||||
onCompleted={this.handleSaveSuccess}
|
||||
onError={this.handleSaveError}
|
||||
>
|
||||
{saveAssignment => (
|
||||
<React.Fragment>
|
||||
<Header
|
||||
assignment={assignment}
|
||||
onChangeAssignment={this.handleChangeAssignment}
|
||||
onValidate={this.validate}
|
||||
onSetWorkstate={newState => this.handleSetWorkstate(saveAssignment, newState)}
|
||||
onUnsubmittedClick={this.handleUnsubmittedClick}
|
||||
onDelete={this.handleDeleteButtonPressed}
|
||||
readOnly={this.props.readOnly}
|
||||
/>
|
||||
<ContentTabs
|
||||
assignment={assignment}
|
||||
onChangeAssignment={this.handleChangeAssignment}
|
||||
onValidate={this.validate}
|
||||
readOnly={this.props.readOnly}
|
||||
/>
|
||||
{this.renderMessageStudentsWhoDialog()}
|
||||
{dirty || !this.isAssignmentValid() ? (
|
||||
<TeacherFooter
|
||||
onCancel={this.handleCancel}
|
||||
onSave={() => this.handleSave(saveAssignment)}
|
||||
onPublish={() => this.handlePublish(saveAssignment)}
|
||||
<ErrorBoundary
|
||||
errorComponent={
|
||||
<GenericErrorPage
|
||||
imageUrl={errorShipUrl}
|
||||
errorCategory={I18n.t('Assignments 2 Teacher View Error Page')}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<TeacherViewContext.Provider value={this.contextValue}>
|
||||
<div className={clazz}>
|
||||
{this.renderDeleteDialog()}
|
||||
<ScreenReaderContent>
|
||||
<h1>{assignment.name}</h1>
|
||||
</ScreenReaderContent>
|
||||
<Mutation
|
||||
mutation={SAVE_ASSIGNMENT}
|
||||
onCompleted={this.handleSaveSuccess}
|
||||
onError={this.handleSaveError}
|
||||
>
|
||||
{saveAssignment => (
|
||||
<React.Fragment>
|
||||
<Header
|
||||
assignment={assignment}
|
||||
onChangeAssignment={this.handleChangeAssignment}
|
||||
onValidate={this.validate}
|
||||
invalidMessage={this.invalidMessage}
|
||||
onSetWorkstate={newState => this.handleSetWorkstate(saveAssignment, newState)}
|
||||
onUnsubmittedClick={this.handleUnsubmittedClick}
|
||||
onDelete={this.handleDeleteButtonPressed}
|
||||
readOnly={this.props.readOnly}
|
||||
/>
|
||||
) : null}
|
||||
</React.Fragment>
|
||||
)}
|
||||
</Mutation>
|
||||
<Portal open={this.state.isSaving}>
|
||||
<Mask fullscreen>
|
||||
<Spinner size="large" title={I18n.t('Saving assignment')} />
|
||||
</Mask>
|
||||
</Portal>
|
||||
</div>
|
||||
</TeacherViewContext.Provider>
|
||||
<ContentTabs
|
||||
assignment={assignment}
|
||||
onChangeAssignment={this.handleChangeAssignment}
|
||||
onValidate={this.validate}
|
||||
invalidMessage={this.invalidMessage}
|
||||
readOnly={this.props.readOnly}
|
||||
/>
|
||||
{this.renderMessageStudentsWhoDialog()}
|
||||
{dirty || !this.isAssignmentValid() ? (
|
||||
<TeacherFooter
|
||||
onCancel={this.handleCancel}
|
||||
onSave={() => this.handleSave(saveAssignment)}
|
||||
onPublish={() => this.handlePublish(saveAssignment)}
|
||||
/>
|
||||
) : null}
|
||||
</React.Fragment>
|
||||
)}
|
||||
</Mutation>
|
||||
<Portal open={this.state.isSaving}>
|
||||
<Mask fullscreen>
|
||||
<Spinner size="large" title={I18n.t('Saving assignment')} />
|
||||
</Mask>
|
||||
</Portal>
|
||||
</div>
|
||||
</TeacherViewContext.Provider>
|
||||
</ErrorBoundary>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -54,6 +54,7 @@ export default class Toolbox extends React.Component {
|
|||
assignment: TeacherAssignmentShape.isRequired,
|
||||
onChangeAssignment: func.isRequired,
|
||||
onValidate: func.isRequired,
|
||||
invalidMessage: func.isRequired,
|
||||
onSetWorkstate: func.isRequired,
|
||||
onUnsubmittedClick: func,
|
||||
onDelete: func,
|
||||
|
@ -166,6 +167,7 @@ export default class Toolbox extends React.Component {
|
|||
onChange={this.handlePointsChange}
|
||||
onChangeMode={this.handlePointsChangeMode}
|
||||
onValidate={this.props.onValidate}
|
||||
invalidMessage={this.props.invalidMessage}
|
||||
readOnly={this.props.readOnly}
|
||||
/>
|
||||
)
|
||||
|
|
|
@ -27,6 +27,7 @@ it('renders', () => {
|
|||
assignment={mockAssignment()}
|
||||
onChangeAssignment={() => {}}
|
||||
onValidate={() => true}
|
||||
invalidMessage={() => undefined}
|
||||
/>
|
||||
)
|
||||
expect(container.querySelectorAll('[role="tab"]')).toHaveLength(4)
|
||||
|
|
|
@ -30,6 +30,7 @@ function renderDetails(assignment, props = {}) {
|
|||
assignment={assignment}
|
||||
onChangeAssignment={() => {}}
|
||||
onValidate={() => true}
|
||||
invalidMessage={() => undefined}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
|
@ -67,7 +68,7 @@ describe('Assignent Details', () => {
|
|||
|
||||
expect(getByText('Section A')).toBeInTheDocument()
|
||||
expect(getByText('Section B')).toBeInTheDocument()
|
||||
expect(queryAllByText('Everyone', {exact: false})).toHaveLength(0)
|
||||
expect(queryAllByText('Everyone', {exact: false})).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('renders the Add Override button if !readOnly', () => {
|
||||
|
|
|
@ -32,6 +32,7 @@ describe('assignments 2 teacher view header', () => {
|
|||
onChangeAssignment={() => {}}
|
||||
onSetWorkstate={() => {}}
|
||||
onValidate={() => true}
|
||||
invalidMessage={() => undefined}
|
||||
/>
|
||||
</MockedProvider>
|
||||
)
|
||||
|
|
|
@ -30,6 +30,7 @@ function renderToolbox(assignment) {
|
|||
onChangeAssignment={() => {}}
|
||||
onSetWorkstate={() => {}}
|
||||
onValidate={() => true}
|
||||
invalidMessage={() => undefined}
|
||||
/>
|
||||
</MockedProvider>
|
||||
)
|
||||
|
|
|
@ -135,6 +135,7 @@ export function mockAssignment(overrides) {
|
|||
submissionTypes: ['online_text_entry'],
|
||||
allowedExtensions: [],
|
||||
allowedAttempts: null,
|
||||
onlyVisibleToOverrides: false,
|
||||
assignmentOverrides: {
|
||||
pageInfo: mockPageInfo(),
|
||||
nodes: []
|
||||
|
|
|
@ -84,7 +84,7 @@ class OverrideListPresenter
|
|||
# Returns an array of due date hashes.
|
||||
def visible_due_dates
|
||||
return [] unless assignment
|
||||
|
||||
|
||||
assignment.dates_hash_visible_to(user).each do |due_date|
|
||||
due_date[:raw] = due_date.dup
|
||||
due_date[:lock_at] = lock_at due_date
|
||||
|
|
Loading…
Reference in New Issue