From 2b1a89bca24f37c427ec5ecdc05caf6c7d0a2d3a Mon Sep 17 00:00:00 2001 From: Nathaniel Caza Date: Fri, 23 Feb 2024 08:42:15 -0600 Subject: [PATCH 1/6] update title --- .../ScheduleOnCallNotificationsListDest.stories.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/app/schedules/on-call-notifications/ScheduleOnCallNotificationsListDest.stories.tsx b/web/src/app/schedules/on-call-notifications/ScheduleOnCallNotificationsListDest.stories.tsx index b21e34b182..02c0377956 100644 --- a/web/src/app/schedules/on-call-notifications/ScheduleOnCallNotificationsListDest.stories.tsx +++ b/web/src/app/schedules/on-call-notifications/ScheduleOnCallNotificationsListDest.stories.tsx @@ -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: { From a633e3a58ae380cf5d75a7c66be284c999e3264a Mon Sep 17 00:00:00 2001 From: Nathaniel Caza Date: Fri, 23 Feb 2024 08:42:35 -0600 Subject: [PATCH 2/6] yarn cleanup --- yarn.lock | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/yarn.lock b/yarn.lock index c4d3aa4719..ebf31596dc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12552,14 +12552,7 @@ __metadata: languageName: node linkType: hard -"ip@npm:^2.0.0": - version: 2.0.1 - resolution: "ip@npm:2.0.1" - checksum: d765c9fd212b8a99023a4cde6a558a054c298d640fec1020567494d257afd78ca77e37126b1a3ef0e053646ced79a816bf50621d38d5e768cdde0431fa3b0d35 - languageName: node - linkType: hard - -"ip@npm:^2.0.1": +"ip@npm:^2.0.0, ip@npm:^2.0.1": version: 2.0.1 resolution: "ip@npm:2.0.1" checksum: d765c9fd212b8a99023a4cde6a558a054c298d640fec1020567494d257afd78ca77e37126b1a3ef0e053646ced79a816bf50621d38d5e768cdde0431fa3b0d35 From be9a55cb048925e0d7145633dd53f614e4881e78 Mon Sep 17 00:00:00 2001 From: Nathaniel Caza Date: Fri, 23 Feb 2024 08:42:46 -0600 Subject: [PATCH 3/6] add formdest --- .../ScheduleOnCallNotificationsFormDest.tsx | 203 ++++++++++++++++++ web/src/app/util/RequireConfig.tsx | 6 + 2 files changed, 209 insertions(+) create mode 100644 web/src/app/schedules/on-call-notifications/ScheduleOnCallNotificationsFormDest.tsx diff --git a/web/src/app/schedules/on-call-notifications/ScheduleOnCallNotificationsFormDest.tsx b/web/src/app/schedules/on-call-notifications/ScheduleOnCallNotificationsFormDest.tsx new file mode 100644 index 0000000000..fe4634067f --- /dev/null +++ b/web/src/app/schedules/on-call-notifications/ScheduleOnCallNotificationsFormDest.tsx @@ -0,0 +1,203 @@ +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 + 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): 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 ( + { + 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, + } + })} + > + + + + } + /> + } + /> + + + {props.value.time && ( + + + Times shown in schedule timezone ({zone}) + + + )} + + + + } + /> + + + + {days.map((day, i) => ( + + } + /> + ))} + + + + + + {destinationTypes.map((t) => + renderMenuItem({ + label: t.name, + value: t.type, + disabled: !t.enabled, + disabledMessage: t.enabled ? '' : t.disabledMessage, + }), + )} + + + + + + + + {currentType?.userDisclaimer} + + + + + ) +} diff --git a/web/src/app/util/RequireConfig.tsx b/web/src/app/util/RequireConfig.tsx index a2556d0f72..a1eddab159 100644 --- a/web/src/app/util/RequireConfig.tsx +++ b/web/src/app/util/RequireConfig.tsx @@ -217,6 +217,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) From 6af1343e962d1c9ac10d6034c1621211d1152f89 Mon Sep 17 00:00:00 2001 From: Nathaniel Caza Date: Fri, 23 Feb 2024 08:42:56 -0600 Subject: [PATCH 4/6] add formdest stories --- ...uleOnCallNotificationsFormDest.stories.tsx | 27 +++++++++++++++++++ web/src/app/storybook/defaultDestTypes.ts | 4 +-- 2 files changed, 29 insertions(+), 2 deletions(-) create mode 100644 web/src/app/schedules/on-call-notifications/ScheduleOnCallNotificationsFormDest.stories.tsx diff --git a/web/src/app/schedules/on-call-notifications/ScheduleOnCallNotificationsFormDest.stories.tsx b/web/src/app/schedules/on-call-notifications/ScheduleOnCallNotificationsFormDest.stories.tsx new file mode 100644 index 0000000000..9dd9262914 --- /dev/null +++ b/web/src/app/schedules/on-call-notifications/ScheduleOnCallNotificationsFormDest.stories.tsx @@ -0,0 +1,27 @@ +import type { Meta, StoryObj } from '@storybook/react' +import ScheduleOnCallNotificationsFormDest from './ScheduleOnCallNotificationsFormDest' + +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'], +} satisfies Meta + +export default meta +type Story = StoryObj + +export const Empty: Story = { + args: {}, +} diff --git a/web/src/app/storybook/defaultDestTypes.ts b/web/src/app/storybook/defaultDestTypes.ts index 77f086c97b..0e1f48a59b 100644 --- a/web/src/app/storybook/defaultDestTypes.ts +++ b/web/src/app/storybook/defaultDestTypes.ts @@ -9,7 +9,7 @@ export const destTypes: DestinationTypeInfo[] = [ userDisclaimer: '', isContactMethod: true, isEPTarget: false, - isSchedOnCallNotify: false, + isSchedOnCallNotify: true, iconURL: '', iconAltText: '', supportsStatusUpdates: false, @@ -37,7 +37,7 @@ export const destTypes: DestinationTypeInfo[] = [ userDisclaimer: '', isContactMethod: true, isEPTarget: false, - isSchedOnCallNotify: false, + isSchedOnCallNotify: true, iconURL: '', iconAltText: '', supportsStatusUpdates: true, From cb9a8954e5d0fa95dfb6a0def89c5ae28a2d799c Mon Sep 17 00:00:00 2001 From: Nathaniel Caza Date: Fri, 23 Feb 2024 08:55:43 -0600 Subject: [PATCH 5/6] fix labels --- .../ScheduleOnCallNotificationsFormDest.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/web/src/app/schedules/on-call-notifications/ScheduleOnCallNotificationsFormDest.tsx b/web/src/app/schedules/on-call-notifications/ScheduleOnCallNotificationsFormDest.tsx index fe4634067f..7dafc4fd1b 100644 --- a/web/src/app/schedules/on-call-notifications/ScheduleOnCallNotificationsFormDest.tsx +++ b/web/src/app/schedules/on-call-notifications/ScheduleOnCallNotificationsFormDest.tsx @@ -92,6 +92,7 @@ export default function ScheduleOnCallNotificationsFormDest( field, } })} + optionalLabels > From 21cf28726c6943ae6ff98655e9de2da5a0a9c22a Mon Sep 17 00:00:00 2001 From: Nathaniel Caza Date: Fri, 23 Feb 2024 08:55:53 -0600 Subject: [PATCH 6/6] add test to stories --- ...uleOnCallNotificationsFormDest.stories.tsx | 64 ++++++++++++++++++- 1 file changed, 63 insertions(+), 1 deletion(-) diff --git a/web/src/app/schedules/on-call-notifications/ScheduleOnCallNotificationsFormDest.stories.tsx b/web/src/app/schedules/on-call-notifications/ScheduleOnCallNotificationsFormDest.stories.tsx index 9dd9262914..aeba910f69 100644 --- a/web/src/app/schedules/on-call-notifications/ScheduleOnCallNotificationsFormDest.stories.tsx +++ b/web/src/app/schedules/on-call-notifications/ScheduleOnCallNotificationsFormDest.stories.tsx @@ -1,5 +1,10 @@ +import React from 'react' import type { Meta, StoryObj } from '@storybook/react' -import ScheduleOnCallNotificationsFormDest from './ScheduleOnCallNotificationsFormDest' +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', @@ -17,6 +22,14 @@ const meta = { }, }, tags: ['autodocs'], + render: function Component(args) { + const [, setArgs] = useArgs() + const onChange = (newValue: Value): void => { + if (args.onChange) args.onChange(newValue) + setArgs({ value: newValue }) + } + return + }, } satisfies Meta export default meta @@ -25,3 +38,52 @@ type Story = StoryObj 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() + }, +}