Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ui/overrides: show errors on user conflict #182

Merged
merged 18 commits into from
Nov 25, 2019
Merged
Changes from 15 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
274 changes: 137 additions & 137 deletions web/src/app/schedules/ScheduleOverrideForm.js
Original file line number Diff line number Diff line change
@@ -1,24 +1,25 @@
import React from 'react'
import React, { useEffect, useState } from 'react'
import p from 'prop-types'
import { FormContainer, FormField } from '../forms'
import {
Grid,
InputAdornment,
IconButton,
Typography,
withStyles,
makeStyles,
} from '@material-ui/core'
import { useQuery } from 'react-apollo'
import { ScheduleTZFilter } from './ScheduleTZFilter'
import { connect } from 'react-redux'
import { useSelector } from 'react-redux'
import { urlParamSelector } from '../selectors'
import { DateRange, ChevronRight, ChevronLeft } from '@material-ui/icons'
import { DateRange } from '@material-ui/icons'
import { DateTimePicker } from '@material-ui/pickers'
import { DateTime } from 'luxon'
import { UserSelect } from '../selection'
import Query from '../util/Query'
import gql from 'graphql-tag'
import { mapOverrideUserError } from './util'
import DialogContentError from '../dialogs/components/DialogContentError'
import _ from 'lodash-es'

const query = gql`
query($id: ID!) {
Expand All @@ -38,163 +39,162 @@ const query = gql`
}
`

const styles = theme => ({
const useStyles = makeStyles({
tzNote: {
display: 'flex',
alignItems: 'center',
},
})

@connect(state => ({ zone: urlParamSelector(state)('tz', 'local') }))
@withStyles(styles)
export default class ScheduleOverrideForm extends React.PureComponent {
static propTypes = {
scheduleID: p.string.isRequired,
export default function ScheduleOverrideForm(props) {
const { add, remove, errors = [], scheduleID, value, ...formProps } = props

value: p.shape({
addUserID: p.string.isRequired,
removeUserID: p.string.isRequired,
start: p.string.isRequired,
end: p.string.isRequired,
}).isRequired,
const classes = useStyles()
const params = useSelector(urlParamSelector)
const zone = params('tz', 'local')
const [userConflictErrors, setUserConflictErrors] = useState([])

add: p.bool,
remove: p.bool,
const userConflictError = props.errors.find(e => e && e.field === 'userID')

errors: p.arrayOf(
p.shape({
field: p.oneOf(['addUserID', 'removeUserID', 'userID', 'start', 'end'])
.isRequired,
message: p.string.isRequired,
}),
),
// used to grab conflicting errors from pre-existing overrides
const { data } = useQuery(query, {
variables: {
id: _.get(userConflictError, 'details.CONFLICTING_ID', ''),
},
pollInterval: 0,
skip: !userConflictError,
})

onChange: p.func.isRequired,
}
useEffect(() => {
if (!data) return

render() {
const userError = this.props.errors.find(e => e.field === 'userID')
return (
<Query
query={query}
variables={{ id: userError ? userError.details.CONFLICTING_ID : '' }}
noPoll
skip={!userError}
noSpin
render={({ data }) => this.renderForm(data)}
/>
setUserConflictErrors(
errors
.filter(e => e.field !== 'userID')
.concat(
userConflictError
? mapOverrideUserError(data.userOverride, value, zone)
: [],
),
)
}

renderForm(data) {
const { add, remove, zone, errors, value, ...formProps } = this.props
const userError = errors.find(e => e.field === 'userID')
const formErrors = errors
.filter(e => e.field !== 'userID')
.concat(
userError ? mapOverrideUserError(data.userOverride, value, zone) : [],
)
}, [data])

return (
<FormContainer
optionalLabels
errors={formErrors}
value={value}
{...formProps}
>
<Grid container spacing={2}>
<Grid
item
xs={12}
sm={12}
md={6}
className={this.props.classes.tzNote}
return (
<FormContainer
optionalLabels
errors={errors.concat(userConflictErrors)}
value={value}
{...formProps}
>
<Grid container spacing={2}>
<Grid item xs={12} sm={12} md={6} className={classes.tzNote}>
<Typography
// variant='caption'
color='textSecondary'
style={{ fontStyle: 'italic' }}
>
<Typography
// variant='caption'
color='textSecondary'
style={{ fontStyle: 'italic' }}
>
Start and end time shown in{' '}
{zone === 'local' ? 'local time' : zone}.
</Typography>
</Grid>
<Grid item xs={12} sm={12} md={6}>
{/* Purposefully leaving out of form, as it's only used for converting display times. */}
<ScheduleTZFilter
label={tz => `Configure in ${tz}`}
scheduleID={this.props.scheduleID}
/>
</Grid>
{remove && (
<Grid item xs={12}>
<FormField
fullWidth
component={UserSelect}
name='removeUserID'
label={add && remove ? 'User to be Replaced' : 'User to Remove'}
required
/>
</Grid>
)}
{add && (
<Grid item xs={12}>
<FormField
fullWidth
component={UserSelect}
required
name='addUserID'
label='User to Add'
/>
</Grid>
)}
Start and end time shown in {zone === 'local' ? 'local time' : zone}
.
</Typography>
</Grid>
<Grid item xs={12} sm={12} md={6}>
{/* Purposefully leaving out of form, as it's only used for converting display times. */}
<ScheduleTZFilter
label={tz => `Configure in ${tz}`}
scheduleID={scheduleID}
/>
</Grid>
{remove && (
<Grid item xs={12}>
<FormField
fullWidth
component={DateTimePicker}
mapValue={value => DateTime.fromISO(value, { zone })}
mapOnChangeValue={value => value.toISO()}
showTodayButton
component={UserSelect}
name='removeUserID'
label={add && remove ? 'User to be Replaced' : 'User to Remove'}
required
name='start'
leftArrowIcon={<ChevronLeft />}
rightArrowIcon={<ChevronRight />}
InputProps={{
endAdornment: (
<InputAdornment position='end'>
<IconButton>
<DateRange />
</IconButton>
</InputAdornment>
),
}}
/>
</Grid>
)}
{add && (
<Grid item xs={12}>
<FormField
fullWidth
component={DateTimePicker}
mapValue={value => DateTime.fromISO(value, { zone })}
mapOnChangeValue={value => value.toISO()}
showTodayButton
name='end'
component={UserSelect}
required
leftArrowIcon={<ChevronLeft />}
rightArrowIcon={<ChevronRight />}
InputProps={{
endAdornment: (
<InputAdornment position='end'>
<IconButton>
<DateRange />
</IconButton>
</InputAdornment>
),
}}
name='addUserID'
label='User to Add'
/>
</Grid>
{userError && <DialogContentError error={userError.message} />}
)}
<Grid item xs={12}>
<FormField
fullWidth
component={DateTimePicker}
mapValue={value => DateTime.fromISO(value, { zone })}
mapOnChangeValue={value => value.toISO()}
showTodayButton
required
name='start'
InputProps={{
Forfold marked this conversation as resolved.
Show resolved Hide resolved
endAdornment: (
<InputAdornment position='end'>
<IconButton>
<DateRange />
</IconButton>
</InputAdornment>
),
}}
/>
</Grid>
</FormContainer>
)
}
<Grid item xs={12}>
<FormField
fullWidth
component={DateTimePicker}
mapValue={value => DateTime.fromISO(value, { zone })}
mapOnChangeValue={value => value.toISO()}
showTodayButton
name='end'
required
InputProps={{
endAdornment: (
<InputAdornment position='end'>
<IconButton>
<DateRange />
</IconButton>
</InputAdornment>
),
}}
/>
</Grid>
{userConflictError && (
<DialogContentError error={userConflictError.message} />
)}
</Grid>
</FormContainer>
)
}

ScheduleOverrideForm.propTypes = {
scheduleID: p.string.isRequired,

value: p.shape({
addUserID: p.string.isRequired,
removeUserID: p.string.isRequired,
start: p.string.isRequired,
end: p.string.isRequired,
}).isRequired,

add: p.bool,
remove: p.bool,

disabled: p.bool.isRequired,
errors: p.arrayOf(
p.shape({
field: p.oneOf(['addUserID', 'removeUserID', 'userID', 'start', 'end'])
.isRequired,
message: p.string.isRequired,
}),
),

onChange: p.func.isRequired,
}