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/temporary schedules: design enhancements #1912

Merged
merged 42 commits into from
Sep 28, 2021
Merged
Show file tree
Hide file tree
Changes from 36 commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
44ffa66
reorganize temporary sched form into 1 step
Forfold Sep 16, 2021
a126e70
use outlined text fields in app
Forfold Sep 16, 2021
86627d3
use outlined accordion
Forfold Sep 16, 2021
c97246a
capitalize accordion header
Forfold Sep 16, 2021
9711a5f
use secondary text color to match button in accordion
Forfold Sep 16, 2021
4bf7af7
remove step logic
Forfold Sep 16, 2021
c5712b9
remove more step stuff
Forfold Sep 16, 2021
6950d49
simplify search select calls
Forfold Sep 20, 2021
017578b
update field names so cypress tests dont conflict
Forfold Sep 20, 2021
8547444
update tests for new design
Forfold Sep 20, 2021
817d5bf
use button style for accordion dropdown panel
Forfold Sep 20, 2021
ca519f8
set start times to m-f
Forfold Sep 20, 2021
feba47a
Merge branch 'master' of github.com:target/goalert into temp-sched-ux
Forfold Sep 21, 2021
d97d573
resolve some conflicts
Forfold Sep 21, 2021
cab56b2
dont show awkward start/end field errors from shifts list
Forfold Sep 21, 2021
bf3f240
cleanup/formatting and placement of elements in dialog
Forfold Sep 21, 2021
6be7aa8
cleanup
Forfold Sep 21, 2021
947de0c
fix for default formfield add shift values
Forfold Sep 21, 2021
7e8cf11
update props comment to correctly reflect how it works
Forfold Sep 22, 2021
9ee3673
remove redundant noError calls
Forfold Sep 22, 2021
e196d70
use better wording for start/end
Forfold Sep 22, 2021
3db6f3c
use boolean instead of count for submit attempts
Forfold Sep 22, 2021
aa64710
fix min times for schedule start/end
Forfold Sep 22, 2021
b5f2e59
remove step text from error message
Forfold Sep 22, 2021
b26ae9a
add info notices for out of bounds days
Forfold Sep 23, 2021
f87f715
show out of bounds info notices at top of each day
Forfold Sep 23, 2021
5876f72
rename to match other function names
Forfold Sep 24, 2021
3e73509
re-use strategy for getting lower/upper bound times instead of using …
Forfold Sep 24, 2021
54b82ab
add zone to fromiso calls
Forfold Sep 24, 2021
cbd0262
set zone on fromISO calls
Forfold Sep 27, 2021
ad5aef5
add zone to another fromISO call, set field disabled while loading zone
Forfold Sep 27, 2021
72e4e71
simplify logic getting out of bounds items
Forfold Sep 27, 2021
f14b56a
add TestConfig interface for unit tests
Forfold Sep 27, 2021
4d74756
add checks for out of bound items
Forfold Sep 27, 2021
cfdb4be
add comment for clarification
Forfold Sep 27, 2021
7cc6eae
fix cypress test for retry on submit
Forfold Sep 27, 2021
896dc38
fix attempt count increasing when clicking next on step forms
Forfold Sep 27, 2021
4721d48
remove nonSubmit prop
dctalbot Sep 27, 2021
ef2b754
remove min requirement on start field so form can be edited
Forfold Sep 28, 2021
0fe3247
Merge branch 'temp-sched-ux' of github.com:target/goalert into temp-s…
Forfold Sep 28, 2021
5b3a19e
disable start end fields when editing
Forfold Sep 28, 2021
e68ea44
Revert "remove min requirement on start field so form can be edited"
Forfold Sep 28, 2021
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
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ export function CreateAlertInfo() {
fullWidth
multiline
rows={7}
variant='outlined'
placeholder='Alert Details'
name='details'
component={TextField}
Expand Down
4 changes: 3 additions & 1 deletion web/src/app/dialogs/FormDialog.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ function FormDialog(props) {
const width = useWidth()
const isWideScreen = isWidthUp('md', width)
const [open, setOpen] = useState(true)
const [attemptCount, setAttemptCount] = useState(0)

const {
alert,
Expand Down Expand Up @@ -135,7 +136,8 @@ function FormDialog(props) {
</Button>
<LoadingButton
form='dialog-form'
attemptCount={errors.filter((e) => !e.nonSubmit).length ? 1 : 0}
onClick={() => setAttemptCount(attemptCount + 1)}
attemptCount={attemptCount}
buttonText={primaryActionLabel || (confirm ? 'Confirm' : submitText)}
color='primary'
loading={loading}
Expand Down
3 changes: 1 addition & 2 deletions web/src/app/forms/FormField.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,8 @@ export class FormField extends React.PureComponent {
// but the error name is different from graphql responses
errorName: p.string,

// label above form component
label: p.node,
formLabel: p.bool, // use formLabel instead of label if true
formLabel: p.bool, // if true, sets the label above the component instead. default off

// required indicates the field may not be left blank.
required: p.bool,
Expand Down
6 changes: 6 additions & 0 deletions web/src/app/mui.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,11 @@ export const theme = createTheme({
error: red,
},

props: {
MuiTextField: {
variant: 'outlined',
},
},

...testOverrides,
})
276 changes: 276 additions & 0 deletions web/src/app/schedules/temp-sched/TempSchedAddNewShift.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,276 @@
import React, { useEffect, useState } from 'react'
import { Button, Grid } from '@material-ui/core'
import Accordion from '@material-ui/core/Accordion'
import AccordionActions from '@material-ui/core/AccordionActions'
import AccordionSummary from '@material-ui/core/AccordionSummary'
import AccordionDetails from '@material-ui/core/AccordionDetails'
import Typography from '@material-ui/core/Typography'
import ExpandMoreIcon from '@material-ui/icons/ExpandMore'
import ToggleIcon from '@material-ui/icons/CompareArrows'
import _ from 'lodash'
import { fmtLocal, Shift, Value } from './sharedUtils'
import { FormContainer, FormField } from '../../forms'
import { DateTime, Interval } from 'luxon'
import { FieldError } from '../../util/errutil'
import { isISOAfter } from '../../util/shifts'
import { useScheduleTZ } from './hooks'
import { ISODateTimePicker } from '../../util/ISOPickers'
import { UserSelect } from '../../selection'
import ClickableText from '../../util/ClickableText'
import NumberField from '../../util/NumberField'

type AddShiftsStepProps = {
value: Value
onChange: (newValue: Shift[]) => void

scheduleID: string
edit?: boolean
}

type DTShift = {
userID: string
span: Interval
}

function shiftsToDT(shifts: Shift[]): DTShift[] {
return shifts.map((s) => ({
userID: s.userID,
span: Interval.fromDateTimes(
DateTime.fromISO(s.start),
DateTime.fromISO(s.end),
),
}))
}

function DTToShifts(shifts: DTShift[]): Shift[] {
return shifts.map((s) => ({
userID: s.userID,
start: s.span.start.toISO(),
end: s.span.end.toISO(),
}))
}

// mergeShifts will take the incoming shifts and merge them with
// the shifts stored in value. Using Luxon's Interval, overlaps
// and edge cases when merging are handled for us.
function mergeShifts(_shifts: Shift[]): Shift[] {
const byUser = _.groupBy(shiftsToDT(_shifts), 'userID')

return DTToShifts(
_.flatten(
_.values(
_.mapValues(byUser, (shifts, userID) => {
return Interval.merge(_.map(shifts, 'span')).map((span) => ({
userID,
span,
}))
}),
),
),
)
}

export default function TempSchedAddNewShift({
scheduleID,
onChange,
value,
edit,
}: AddShiftsStepProps): JSX.Element {
const [shift, setShift] = useState(null as Shift | null)
const [submitted, setSubmitted] = useState(false)

const [manualEntry, setManualEntry] = useState(false)
const [now] = useState(DateTime.utc().startOf('minute').toISO())
const { q, zone, isLocalZone } = useScheduleTZ(scheduleID)

// set start equal to the temporary schedule's start
// can't this do on mount since the step renderer puts everyone on the DOM at once
useEffect(() => {
if (zone === '') return

setShift({
start: value.start,
end: DateTime.fromISO(value.start, { zone }).plus({ hours: 8 }).toISO(),
userID: '',
})
}, [value.start, zone])

// fieldErrors handles errors manually through the client
// as this step form is nested inside the greater form
// that makes the network request.
function fieldErrors(s = submitted): FieldError[] {
const result: FieldError[] = []
const requiredMsg = 'this field is required'
const add = (field: string, message: string): void => {
result.push({ field, message } as FieldError)
}

if (!shift) return result
if (s) {
if (!shift.userID) add('userID', requiredMsg)
if (!shift.start) add('start', requiredMsg)
if (!shift.end) add('end', requiredMsg)
}

if (!isISOAfter(shift.end, shift.start)) {
add('end', 'must be after shift start time')
add('start', 'must be before shift end time')
}

return result
}

function handleAddShift(): void {
if (fieldErrors(true).length) {
setSubmitted(true)
return
}
if (!shift) return // ts sanity check

onChange(mergeShifts(value.shifts.concat(shift)))
const end = DateTime.fromISO(shift.end, { zone })
const diff = end.diff(DateTime.fromISO(shift.start, { zone }))
setShift({
userID: '',
start: shift.end,
end: end.plus(diff).toISO(),
})
setSubmitted(false)
}

return (
<FormContainer
errors={fieldErrors()}
value={shift}
onChange={(val: Shift) => setShift(val)}
>
<Accordion variant='outlined'>
<AccordionSummary
expandIcon={<ExpandMoreIcon />}
data-cy='add-shift-expander'
>
<Typography
color='textSecondary'
variant='button'
style={{ width: '100%' }}
>
ADD SHIFT
</Typography>
</AccordionSummary>
<AccordionDetails data-cy='add-shift-container'>
<Grid container spacing={2}>
<Grid item xs={12}>
<FormField
fullWidth
component={UserSelect}
label='Select a User'
name='userID'
/>
</Grid>
<Grid item xs={6}>
<FormField
fullWidth
component={ISODateTimePicker}
label='Shift Start'
name='shift-start'
fieldName='start'
min={edit ? value.start : now}
mapOnChangeValue={(value: string, formValue: Value) => {
if (!manualEntry) {
const diff = DateTime.fromISO(value, { zone }).diff(
DateTime.fromISO(formValue.start, { zone }),
)
formValue.end = DateTime.fromISO(formValue.end, { zone })
.plus(diff)
.toISO()
}
return value
}}
timeZone={zone}
disabled={q.loading}
hint={isLocalZone ? '' : fmtLocal(value?.start)}
/>
</Grid>
<Grid item xs={6}>
{manualEntry ? (
<FormField
fullWidth
component={ISODateTimePicker}
label='Shift End'
name='shift-end'
fieldName='end'
min={edit ? value.start : now}
hint={
<React.Fragment>
{!isLocalZone && fmtLocal(value?.end)}
<div>
<ClickableText
data-cy='toggle-duration-on'
onClick={() => setManualEntry(false)}
endIcon={<ToggleIcon />}
>
Configure as duration
</ClickableText>
</div>
</React.Fragment>
}
timeZone={zone}
disabled={q.loading}
/>
) : (
<FormField
fullWidth
component={NumberField}
label='Shift Duration (hours)'
name='shift-end'
fieldName='end'
float
// value held in form input
mapValue={(nextVal: string, formValue: Value) => {
const nextValDT = DateTime.fromISO(nextVal, { zone })
if (!formValue || !nextValDT.isValid) return ''
return nextValDT
.diff(
DateTime.fromISO(formValue.start, { zone }),
'hours',
)
.hours.toString()
}}
// value held in state
mapOnChangeValue={(nextVal: string, formValue: Value) => {
if (!nextVal) return ''
return DateTime.fromISO(formValue.start, { zone })
.plus({ hours: parseFloat(nextVal) })
.toISO()
}}
step='any'
min={0}
disabled={q.loading}
hint={
<ClickableText
data-cy='toggle-duration-off'
onClick={() => setManualEntry(true)}
endIcon={<ToggleIcon />}
>
Configure as date/time
</ClickableText>
}
/>
)}
</Grid>
</Grid>
</AccordionDetails>
<AccordionActions>
<Button
data-cy='add-shift'
color='secondary'
variant='contained'
onClick={handleAddShift}
>
Add
</Button>
</AccordionActions>
</Accordion>
</FormContainer>
)
}
Loading