Skip to content

Commit

Permalink
dest: add sched notif form (#3704)
Browse files Browse the repository at this point in the history
* update title

* yarn cleanup

* add formdest

* add formdest stories

* fix labels

* add test to stories
  • Loading branch information
mastercactapus authored Feb 26, 2024
1 parent be306e7 commit 8a63b98
Show file tree
Hide file tree
Showing 5 changed files with 302 additions and 3 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import React from 'react'
import type { Meta, StoryObj } from '@storybook/react'
import ScheduleOnCallNotificationsFormDest, {
Value,
} from './ScheduleOnCallNotificationsFormDest'
import { useArgs } from '@storybook/preview-api'
import { expect, userEvent, within } from '@storybook/test'

const meta = {
title: 'schedules/on-call-notifications/FormDest',
component: ScheduleOnCallNotificationsFormDest,
argTypes: {},
args: {
scheduleID: '',
value: {
time: null,
weekdayFilter: [false, false, false, false, false, false, false],
dest: {
type: 'single-field',
values: [],
},
},
},
tags: ['autodocs'],
render: function Component(args) {
const [, setArgs] = useArgs()
const onChange = (newValue: Value): void => {
if (args.onChange) args.onChange(newValue)
setArgs({ value: newValue })
}
return <ScheduleOnCallNotificationsFormDest {...args} onChange={onChange} />
},
} satisfies Meta<typeof ScheduleOnCallNotificationsFormDest>

export default meta
type Story = StoryObj<typeof meta>

export const Empty: Story = {
args: {},
}

export const ValidationErrors: Story = {
args: {
errors: [
{
path: ['mutation', 'input', 'time'],
message: 'error with time',
extensions: {
code: 'INVALID_INPUT_VALUE',
},
},
{
path: ['mutation', 'input', 'dest'],
message: 'error with dest field',
extensions: {
code: 'INVALID_DEST_FIELD_VALUE',
fieldID: 'phone-number',
},
},
{
path: ['mutation', 'input', 'dest', 'type'],
message: 'error with dest type',
extensions: {
code: 'INVALID_INPUT_VALUE',
},
},
],
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement)

await userEvent.click(
await canvas.findByLabelText(
'Notify at a specific day and time every week',
),
)

await expect(await canvas.findByLabelText('Time')).toBeInvalid()
await expect(
// mui puts aria-invalid on the input, but not the combobox (which the label points to)
canvasElement.querySelector('input[name="dest.type"]'),
).toBeInvalid()
await expect(await canvas.findByLabelText('Phone Number')).toBeInvalid()

await expect(await canvas.findByText('Error with time')).toBeVisible()
await expect(await canvas.findByText('Error with dest field')).toBeVisible()
await expect(await canvas.findByText('Error with dest type')).toBeVisible()
},
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
import {
Checkbox,
FormControlLabel,
Grid,
Radio,
RadioGroup,
TextField,
Typography,
} from '@mui/material'
import makeStyles from '@mui/styles/makeStyles'
import { DateTime } from 'luxon'
import React from 'react'

import { FormContainer, FormField } from '../../forms'
import { renderMenuItem } from '../../selection/DisableableMenuItem'
import { ISOTimePicker } from '../../util/ISOPickers'
import { Time } from '../../util/Time'
import { useScheduleTZ } from '../useScheduleTZ'
import { EVERY_DAY, NO_DAY } from './util'
import { useSchedOnCallNotifyTypes } from '../../util/RequireConfig'
import { DestinationInput, WeekdayFilter } from '../../../schema'
import DestinationField from '../../selection/DestinationField'
import {
DestFieldValueError,
KnownError,
isDestFieldError,
isInputFieldError,
} from '../../util/errtypes'

const days = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']

export type Value = {
time: string | null
weekdayFilter: WeekdayFilter
dest: DestinationInput
}

interface ScheduleOnCallNotificationsFormProps {
scheduleID: string
value: Value
errors?: Array<KnownError | DestFieldValueError>
onChange: (val: Value) => void
disablePortal?: boolean
}

const useStyles = makeStyles({
margin0: { margin: 0 },
tzNote: { fontStyle: 'italic' },
})

export const errorPaths = (prefix = '*'): string[] => [
`${prefix}.time`,
`${prefix}.weedkayFilter`,
`${prefix}.dest.type`,
`${prefix}.dest`,
]

export default function ScheduleOnCallNotificationsFormDest(
props: ScheduleOnCallNotificationsFormProps,
): JSX.Element {
const { scheduleID, ...formProps } = props
const classes = useStyles()
const { zone } = useScheduleTZ(scheduleID)
const destinationTypes = useSchedOnCallNotifyTypes()
const currentType = destinationTypes.find(
(d) => d.type === props.value.dest.type,
)

if (!currentType) throw new Error('invalid destination type')

const handleRuleChange = (e: React.ChangeEvent<HTMLInputElement>): void => {
if (e.target.value === 'on-change') {
props.onChange({ ...formProps.value, time: null, weekdayFilter: NO_DAY })
return
}
props.onChange({
...props.value,
weekdayFilter: EVERY_DAY,
time: DateTime.fromObject({ hour: 9 }, { zone }).toISO(),
})
}

return (
<FormContainer
{...formProps}
errors={props.errors?.filter(isInputFieldError).map((e) => {
let field = e.path[e.path.length - 1].toString()
if (field === 'type') field = 'dest.type'
return {
// need to convert to FormContainer's error format
message: e.message,
field,
}
})}
optionalLabels
>
<Grid container spacing={2} direction='column'>
<Grid item>
<RadioGroup
name='ruleType'
value={formProps.value.time ? 'time-of-day' : 'on-change'}
onChange={handleRuleChange}
>
<FormControlLabel
data-cy='notify-on-change'
label='Notify when on-call changes'
value='on-change'
control={<Radio />}
/>
<FormControlLabel
data-cy='notify-at-time'
label='Notify at a specific day and time every week'
value='time-of-day'
control={<Radio />}
/>
</RadioGroup>
</Grid>
{props.value.time && (
<Grid item xs={12}>
<Typography color='textSecondary' className={classes.tzNote}>
Times shown in schedule timezone ({zone})
</Typography>
</Grid>
)}
<Grid item container spacing={2} alignItems='center'>
<Grid item xs={12} sm={5} md={4}>
<FormField
component={ISOTimePicker}
timeZone={zone}
fullWidth
name='time'
disabled={!props.value.time}
required={!!props.value.time}
hint={
<Time
format='clock'
time={props.value.time}
suffix=' in local time'
/>
}
/>
</Grid>
<Grid item xs={12} sm={7} md={8}>
<Grid container justifyContent='space-between'>
{days.map((day, i) => (
<FormControlLabel
key={i}
label={day}
labelPlacement='top'
classes={{ labelPlacementTop: classes.margin0 }}
control={
<FormField
noError
component={Checkbox}
checkbox
name={`weekdayFilter[${i}]`}
disabled={!props.value.time}
/>
}
/>
))}
</Grid>
</Grid>
</Grid>
<Grid item xs={12} sm={12} md={6}>
<FormField
fullWidth
name='dest.type'
label='Destination Type'
required
select
disablePortal={props.disablePortal}
component={TextField}
>
{destinationTypes.map((t) =>
renderMenuItem({
label: t.name,
value: t.type,
disabled: !t.enabled,
disabledMessage: t.enabled ? '' : t.disabledMessage,
}),
)}
</FormField>
</Grid>
<Grid item xs={12}>
<FormField
fullWidth
name='value'
fieldName='dest.values'
required
destType={props.value.dest.type}
component={DestinationField}
destFieldErrors={props.errors?.filter(isDestFieldError)}
/>
</Grid>
<Grid item xs={12}>
<Typography variant='caption'>
{currentType?.userDisclaimer}
</Typography>
</Grid>
</Grid>
</FormContainer>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ const errorScheduleID = '11111111-1111-1111-1111-111111111111'
const manyNotificationsScheduleID = '22222222-2222-2222-2222-222222222222'

const meta = {
title: 'schedules/on-call-notifications/ScheduleOnCallNotificationsListDest',
title: 'schedules/on-call-notifications/ListDest',
component: ScheduleOnCallNotificationsListDest,
argTypes: {},
parameters: {
Expand Down
4 changes: 2 additions & 2 deletions web/src/app/storybook/defaultDestTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export const destTypes: DestinationTypeInfo[] = [
userDisclaimer: '',
isContactMethod: true,
isEPTarget: false,
isSchedOnCallNotify: false,
isSchedOnCallNotify: true,
iconURL: '',
iconAltText: '',
supportsStatusUpdates: false,
Expand All @@ -36,7 +36,7 @@ export const destTypes: DestinationTypeInfo[] = [
userDisclaimer: '',
isContactMethod: true,
isEPTarget: false,
isSchedOnCallNotify: false,
isSchedOnCallNotify: true,
iconURL: '',
iconAltText: '',
supportsStatusUpdates: true,
Expand Down
6 changes: 6 additions & 0 deletions web/src/app/util/RequireConfig.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,12 @@ export function useContactMethodTypes(): DestinationTypeInfo[] {
return cfg.destTypes.filter((t) => t.isContactMethod)
}

/** useSchedOnCallNotifyTypes returns a list of schedule on-call notification destination types. */
export function useSchedOnCallNotifyTypes(): DestinationTypeInfo[] {
const cfg = React.useContext(ConfigContext)
return cfg.destTypes.filter((t) => t.isSchedOnCallNotify)
}

// useDestinationType returns information about the given destination type.
export function useDestinationType(type: DestinationType): DestinationTypeInfo {
const ctx = React.useContext(ConfigContext)
Expand Down

0 comments on commit 8a63b98

Please sign in to comment.