From 3f712fd9f1870a092614292966207409e960a279 Mon Sep 17 00:00:00 2001 From: KatieMSB Date: Fri, 28 Feb 2025 15:38:29 -0600 Subject: [PATCH 1/3] convert wizard components to typescript --- .../wizard/{WizardForm.jsx => WizardForm.tsx} | 58 +++++++++++++----- .../{WizardRouter.jsx => WizardRouter.tsx} | 60 ++++++++++++------- web/src/app/wizard/propTypes.js | 27 --------- 3 files changed, 79 insertions(+), 66 deletions(-) rename web/src/app/wizard/{WizardForm.jsx => WizardForm.tsx} (84%) rename web/src/app/wizard/{WizardRouter.jsx => WizardRouter.tsx} (78%) delete mode 100644 web/src/app/wizard/propTypes.js diff --git a/web/src/app/wizard/WizardForm.jsx b/web/src/app/wizard/WizardForm.tsx similarity index 84% rename from web/src/app/wizard/WizardForm.jsx rename to web/src/app/wizard/WizardForm.tsx index f53b54a591..0435240549 100644 --- a/web/src/app/wizard/WizardForm.jsx +++ b/web/src/app/wizard/WizardForm.tsx @@ -1,5 +1,4 @@ import React from 'react' -import p from 'prop-types' import StepIcon from '@mui/material/StepIcon' import FormControl from '@mui/material/FormControl' import FormControlLabel from '@mui/material/FormControlLabel' @@ -11,12 +10,12 @@ import TextField from '@mui/material/TextField' import Typography from '@mui/material/Typography' import { FormContainer, FormField } from '../forms' import WizardScheduleForm from './WizardScheduleForm' -import { value as valuePropType } from './propTypes' import makeStyles from '@mui/styles/makeStyles' import * as _ from 'lodash' import { useIsWidthDown } from '../util/useWidth' import MaterialSelect from '../selection/MaterialSelect' import { useFeatures } from '../util/RequireConfig' +import { RotationType, User } from '../../schema' const useStyles = makeStyles({ fieldItem: { @@ -28,20 +27,57 @@ const useStyles = makeStyles({ }, }) -export default function WizardForm(props) { +export interface WizardFormRotation { + startDate?: string + favorite?: boolean + type?: RotationType | 'never' + enable?: string + users?: User[] + timeZone?: string | null +} + +export interface WizardFormSchedule { + timeZone: string | null + enable?: string + users: User[] + rotation: WizardFormRotation + followTheSunRotation: WizardFormRotation +} + +export interface WizardFormValue { + teamName: string + primarySchedule: WizardFormSchedule + secondarySchedule: WizardFormSchedule + delayMinutes: number + repeat: string + key: { + label: string + value: string + } | null +} + +interface WizardFormProps { + onChange: (value: WizardFormValue) => void + value: WizardFormValue + errors: Error[] +} + +export default function WizardForm(props: WizardFormProps): React.JSX.Element { const { onChange, value } = props const fullScreen = useIsWidthDown('md') const classes = useStyles() const keyTypes = useFeatures().integrationKeyTypes - const handleSecondaryScheduleToggle = (e) => { + const handleSecondaryScheduleToggle = ( + e: React.ChangeEvent, + ): void => { const newVal = _.cloneDeep(value) newVal.secondarySchedule.enable = e.target.value onChange(newVal) } - const sectionHeading = (text) => { + const sectionHeading = (text: string): React.JSX.Element => { return ( {text} @@ -49,7 +85,7 @@ export default function WizardForm(props) { ) } - const getDelayLabel = () => { + const getDelayLabel = (): string => { if (value.secondarySchedule.enable === 'yes') { return 'How long would you like to wait until escalating to your secondary schedule (in minutes)?' } @@ -180,13 +216,3 @@ export default function WizardForm(props) { ) } - -WizardForm.propTypes = { - onChange: p.func.isRequired, - value: valuePropType, - errors: p.arrayOf( - p.shape({ - message: p.string.isRequired, - }), - ), -} diff --git a/web/src/app/wizard/WizardRouter.jsx b/web/src/app/wizard/WizardRouter.tsx similarity index 78% rename from web/src/app/wizard/WizardRouter.jsx rename to web/src/app/wizard/WizardRouter.tsx index 9b58c5491c..a85d24df7a 100644 --- a/web/src/app/wizard/WizardRouter.jsx +++ b/web/src/app/wizard/WizardRouter.tsx @@ -10,9 +10,9 @@ import DialogActions from '@mui/material/DialogActions' import makeStyles from '@mui/styles/makeStyles' import { DateTime } from 'luxon' import { fieldErrors, nonFieldErrors } from '../util/errutil' -import WizardForm from './WizardForm' +import WizardForm, { WizardFormValue } from './WizardForm' import LoadingButton from '../loading/components/LoadingButton' -import { gql, useMutation } from 'urql' +import { gql, useMutation, UseMutationExecute } from 'urql' import { Form } from '../forms' import { getService, @@ -24,6 +24,7 @@ import DialogTitleWrapper from '../dialogs/components/DialogTitleWrapper' import DialogContentError from '../dialogs/components/DialogContentError' import { useIsWidthDown } from '../util/useWidth' import { Redirect } from 'wouter' +import { CreateEscalationPolicyStepInput } from '../../schema' const mutation = gql` mutation ($input: CreateServiceInput!) { @@ -42,19 +43,19 @@ const useStyles = makeStyles(() => ({ }, })) -export default function WizardRouter() { +export default function WizardRouter(): React.JSX.Element { const classes = useStyles() const fullScreen = useIsWidthDown('md') - const [errorMessage, setErrorMessage] = useState(null) + const [errorMessage, setErrorMessage] = useState(null) const [complete, setComplete] = useState(false) const [redirect, setRedirect] = useState(false) - const [value, setValue] = useState({ + const [value, setValue] = useState({ teamName: '', - delayMinutes: '', + delayMinutes: 0, repeat: '', key: null, primarySchedule: { - timeZone: null, + timeZone: '', users: [], rotation: { startDate: DateTime.local().startOf('day').toISO(), @@ -91,11 +92,11 @@ export default function WizardRouter() { * Handles not returning a second step if the secondary * schedule is not enabled in the form. */ - const getSteps = () => { + const getSteps = (): CreateEscalationPolicyStepInput[] => { const secondary = value.secondarySchedule.enable === 'yes' const steps = [] - const step = (key) => ({ + const step = (key: string): CreateEscalationPolicyStepInput => ({ delayMinutes: value.delayMinutes, newSchedule: { ...getSchedule(key, value, secondary), @@ -119,7 +120,11 @@ export default function WizardRouter() { * * e.g. createService: { newEscalationPolicy: {...} } */ - const submit = (e, isValid, commit) => { + const submit = ( + e: { preventDefault: () => void }, + isValid: boolean, + commit: UseMutationExecute, + ): void => { e.preventDefault() // prevents reloading if (!isValid) return @@ -135,7 +140,7 @@ export default function WizardRouter() { }, } } catch (err) { - setErrorMessage(err.message) + setErrorMessage((err as Error).message) } if (variables) { @@ -143,19 +148,29 @@ export default function WizardRouter() { if (result.error) { const generalErrors = nonFieldErrors(result.error) const graphqlErrors = fieldErrors(result.error).map((error) => { - const name = error.field - .split('.') - .pop() // get last occurrence - .replace(/([A-Z])/g, ' $1') // insert a space before all caps - .replace(/^./, (str) => str.toUpperCase()) // uppercase the first character - - return `${name}: ${error.message}` + const fieldError = error.field.split('.').pop() + if (fieldError) { + const name = fieldError + .replace(/([A-Z])/g, ' $1') // insert a space before all caps + .replace(/^./, (str) => str.toUpperCase()) // uppercase the first character + + return `${name}: ${error.message}` + } }) - const errors = generalErrors.concat(graphqlErrors) + const errors = [...generalErrors, ...graphqlErrors] if (errors.length) { - setErrorMessage(errors.map((e) => e.message || e).join('\n')) + setErrorMessage( + errors + .map((e) => { + if (e instanceof Error) { + return e.message + } + return e + }) + .join('\n'), + ) } } else { setComplete(true) @@ -164,7 +179,7 @@ export default function WizardRouter() { } } - const onDialogClose = (data) => { + const onDialogClose = (data: { createService: boolean }): void => { if (data && data.createService) { return setRedirect(true) } @@ -174,7 +189,7 @@ export default function WizardRouter() { window.scrollTo(0, 0) } - function renderSubmittedContent() { + function renderSubmittedContent(): React.JSX.Element | undefined { if (complete) { return ( @@ -204,7 +219,6 @@ export default function WizardRouter() { > setValue(value)} diff --git a/web/src/app/wizard/propTypes.js b/web/src/app/wizard/propTypes.js deleted file mode 100644 index c9d6d5bedd..0000000000 --- a/web/src/app/wizard/propTypes.js +++ /dev/null @@ -1,27 +0,0 @@ -import p from 'prop-types' - -const scheduleShape = p.shape({ - timeZone: p.string, - users: p.array, - rotation: p.shape({ - startDate: p.string, - type: p.string, - }), - followTheSunRotation: p.shape({ - enable: p.string, - users: p.array, - timeZone: p.string, - }), -}) - -export const value = p.shape({ - teamName: p.string, - primarySchedule: scheduleShape, - secondarySchedule: scheduleShape, - delayMinutes: p.string, - repeat: p.string, - key: p.shape({ - label: p.string, - value: p.string, - }), -}).isRequired From dbe015d96f3532f9c372b7539616151f62d96beb Mon Sep 17 00:00:00 2001 From: KatieMSB Date: Fri, 28 Feb 2025 16:14:29 -0600 Subject: [PATCH 2/3] convert rest of wizards --- web/src/app/wizard/WizardForm.tsx | 6 +- ...cheduleForm.jsx => WizardScheduleForm.tsx} | 82 ++++++++++++------- .../app/wizard/{util.test.js => util.test.ts} | 0 web/src/app/wizard/{util.js => util.ts} | 0 .../{utilTestData.js => utilTestData.ts} | 0 5 files changed, 56 insertions(+), 32 deletions(-) rename web/src/app/wizard/{WizardScheduleForm.jsx => WizardScheduleForm.tsx} (80%) rename web/src/app/wizard/{util.test.js => util.test.ts} (100%) rename web/src/app/wizard/{util.js => util.ts} (100%) rename web/src/app/wizard/{utilTestData.js => utilTestData.ts} (100%) diff --git a/web/src/app/wizard/WizardForm.tsx b/web/src/app/wizard/WizardForm.tsx index 0435240549..1a2dfd6613 100644 --- a/web/src/app/wizard/WizardForm.tsx +++ b/web/src/app/wizard/WizardForm.tsx @@ -15,7 +15,7 @@ import * as _ from 'lodash' import { useIsWidthDown } from '../util/useWidth' import MaterialSelect from '../selection/MaterialSelect' import { useFeatures } from '../util/RequireConfig' -import { RotationType, User } from '../../schema' +import { RotationType } from '../../schema' const useStyles = makeStyles({ fieldItem: { @@ -32,14 +32,14 @@ export interface WizardFormRotation { favorite?: boolean type?: RotationType | 'never' enable?: string - users?: User[] + users?: string[] timeZone?: string | null } export interface WizardFormSchedule { timeZone: string | null enable?: string - users: User[] + users: string[] rotation: WizardFormRotation followTheSunRotation: WizardFormRotation } diff --git a/web/src/app/wizard/WizardScheduleForm.jsx b/web/src/app/wizard/WizardScheduleForm.tsx similarity index 80% rename from web/src/app/wizard/WizardScheduleForm.jsx rename to web/src/app/wizard/WizardScheduleForm.tsx index 53f1c7b8b1..d43f63bf41 100644 --- a/web/src/app/wizard/WizardScheduleForm.jsx +++ b/web/src/app/wizard/WizardScheduleForm.tsx @@ -1,5 +1,4 @@ import React from 'react' -import p from 'prop-types' import FormControl from '@mui/material/FormControl' import FormControlLabel from '@mui/material/FormControlLabel' import FormHelperText from '@mui/material/FormHelperText' @@ -11,11 +10,12 @@ import Tooltip from '@mui/material/Tooltip' import InfoIcon from '@mui/icons-material/Info' import { TimeZoneSelect, UserSelect } from '../selection' import { FormField } from '../forms' -import { value as valuePropType } from './propTypes' import * as _ from 'lodash' import { ISODateTimePicker } from '../util/ISOPickers' import makeStyles from '@mui/styles/makeStyles' import { useIsWidthDown } from '../util/useWidth' +import { WizardFormValue } from './WizardForm' +import { RotationType } from '../../schema' const useStyles = makeStyles(() => ({ fieldItem: { @@ -30,16 +30,32 @@ const useStyles = makeStyles(() => ({ }, })) +interface WizardScheduleFormProps { + value: WizardFormValue + onChange: (value: WizardFormValue) => void + secondary?: boolean +} + /** * Renders the form fields to be used in the wizard that * can be used for creating a primary and secondary schedule. */ -export default function WizardScheduleForm({ value, onChange, secondary }) { +export default function WizardScheduleForm({ + value, + onChange, + secondary, +}: WizardScheduleFormProps): React.JSX.Element { const fullScreen = useIsWidthDown('md') const classes = useStyles() - function renderFollowTheSun(key, schedType) { - if (value[key].followTheSunRotation.enable === 'yes') { + function renderFollowTheSun( + key: string, + schedType: string, + ): React.JSX.Element | undefined { + const currentSchedule = secondary + ? value.secondarySchedule + : value.primarySchedule + if (currentSchedule.followTheSunRotation.enable === 'yes') { return ( @@ -56,7 +72,7 @@ export default function WizardScheduleForm({ value, onChange, secondary }) { formLabel fullWidth={fullScreen} required - value={value[key].followTheSunRotation.users} + value={currentSchedule.followTheSunRotation.users} /> @@ -79,27 +95,41 @@ export default function WizardScheduleForm({ value, onChange, secondary }) { } } - const getKey = () => { - return secondary ? 'secondarySchedule' : 'primarySchedule' - } - - const handleRotationTypeChange = (e) => { + const handleRotationTypeChange = ( + e: React.ChangeEvent, + ): void => { const newVal = _.cloneDeep(value) - newVal[getKey()].rotation.type = e.target.value + if (secondary) { + newVal.secondarySchedule.rotation.type = e.target.value as RotationType + } else { + newVal.primarySchedule.rotation.type = e.target.value as RotationType + } onChange(newVal) } - const handleFollowTheSunToggle = (e) => { + const handleFollowTheSunToggle = ( + e: React.ChangeEvent, + ): void => { const newVal = _.cloneDeep(value) - newVal[getKey()].followTheSunRotation.enable = e.target.value + if (secondary) { + newVal.secondarySchedule.followTheSunRotation.enable = e.target.value + } else { + newVal.primarySchedule.followTheSunRotation.enable = e.target.value + } onChange(newVal) } - function renderRotationFields(key, schedType) { + function renderRotationFields( + key: string, + schedType: string, + ): React.JSX.Element { + const currentSchedule = secondary + ? value.secondarySchedule + : value.primarySchedule const hideRotationFields = - value[key].users.length <= 1 || - value[key].rotation.type === 'never' || - !value[key].rotation.type + currentSchedule.users.length <= 1 || + currentSchedule.rotation.type === 'never' || + !currentSchedule.rotation.type return ( @@ -116,26 +146,26 @@ export default function WizardScheduleForm({ value, onChange, secondary }) { } label='Weekly' /> } label='Daily' /> } label='Never*' @@ -189,7 +219,7 @@ export default function WizardScheduleForm({ value, onChange, secondary }) { aria-label='secondary' name={`${key}.fts`} row - value={value[key].followTheSunRotation.enable} + value={currentSchedule.followTheSunRotation.enable} onChange={handleFollowTheSunToggle} > ) } - -WizardScheduleForm.propTypes = { - onChange: p.func.isRequired, - value: valuePropType, - secondary: p.bool, -} diff --git a/web/src/app/wizard/util.test.js b/web/src/app/wizard/util.test.ts similarity index 100% rename from web/src/app/wizard/util.test.js rename to web/src/app/wizard/util.test.ts diff --git a/web/src/app/wizard/util.js b/web/src/app/wizard/util.ts similarity index 100% rename from web/src/app/wizard/util.js rename to web/src/app/wizard/util.ts diff --git a/web/src/app/wizard/utilTestData.js b/web/src/app/wizard/utilTestData.ts similarity index 100% rename from web/src/app/wizard/utilTestData.js rename to web/src/app/wizard/utilTestData.ts From b922cc7aefb43132de97f2b4f2cc9a6061775a3a Mon Sep 17 00:00:00 2001 From: KatieMSB Date: Mon, 3 Mar 2025 09:36:56 -0600 Subject: [PATCH 3/3] update util.ts --- web/src/app/wizard/util.ts | 86 ++++++++++++++++++++++++++------------ 1 file changed, 60 insertions(+), 26 deletions(-) diff --git a/web/src/app/wizard/util.ts b/web/src/app/wizard/util.ts index 3663224930..e0a4b3cb9d 100644 --- a/web/src/app/wizard/util.ts +++ b/web/src/app/wizard/util.ts @@ -1,34 +1,50 @@ -import { DateTime } from 'luxon' +import { DateTime, DurationLike } from 'luxon' +import { WizardFormValue } from './WizardForm' +import { + CreateEscalationPolicyInput, + CreateRotationInput, + CreateScheduleInput, + CreateServiceInput, + IntegrationKeyType, + RotationType, + ScheduleTargetInput, +} from '../../schema' const DESC = 'Generated by Setup Wizard' const CHAR_LIMIT = 64 const SECONDARY_CHAR_COUNT = 10 const FTS_CHAR_COUNT = 4 -export function getService(value) { +export function getService(value: WizardFormValue): CreateServiceInput { return { name: value.teamName, description: DESC, newIntegrationKeys: [ { - type: value.key.value, - name: value.key.label + ' Integration Key', + type: value.key?.value as IntegrationKeyType, + name: value.key?.label + ' Integration Key', }, ], favorite: true, } } -export function getEscalationPolicy(value) { +export function getEscalationPolicy( + value: WizardFormValue, +): CreateEscalationPolicyInput { return { name: value.teamName, description: DESC, - repeat: value.repeat, + repeat: Number(value.repeat), } } -export function getSchedule(key, value, secondary) { - const s = value[key] +export function getSchedule( + key: string, + value: WizardFormValue, + secondary: boolean, +): CreateScheduleInput { + const s = secondary ? value.secondarySchedule : value.primarySchedule let name = value.teamName if (secondary) { @@ -38,7 +54,7 @@ export function getSchedule(key, value, secondary) { return { name, description: DESC, - timeZone: s.timeZone, + timeZone: s.timeZone || '', favorite: true, } } @@ -47,9 +63,13 @@ export function getSchedule(key, value, secondary) { * Generates the variables for the targets * to be used while creating a new schedule */ -export function getScheduleTargets(key, value, secondary) { - const s = value[key] - const targets = [] +export function getScheduleTargets( + key: string, + value: WizardFormValue, + secondary: boolean, +): ScheduleTargetInput[] { + const s = secondary ? value.secondarySchedule : value.primarySchedule + const targets = [] as ScheduleTargetInput[] const fts = s.followTheSunRotation.enable === 'yes' // return just the users as schedule targets if rotation type is set to "never" @@ -68,7 +88,7 @@ export function getScheduleTargets(key, value, secondary) { const duration = type === 'daily' ? { day: 1 } : type === 'weekly' ? { week: 1 } : null - const target = (isFTS) => { + const target = (isFTS: boolean): CreateRotationInput => { let tzText = isFTS ? s.followTheSunRotation.timeZone : '' if (isFTS && s.followTheSunRotation.timeZone === s.timeZone) { tzText = tzText + ' FTS' @@ -79,26 +99,40 @@ export function getScheduleTargets(key, value, secondary) { (secondary ? (key.includes('primary') ? ' Primary' : ' Secondary') : '') + (tzText ? ` ${tzText}` : '') + const getErrMsg = (): number => { + const timeZoneLength = isFTS + ? s.followTheSunRotation?.timeZone?.length + : 0 + const timeZoneMatch = + isFTS && s.followTheSunRotation?.timeZone === s.timeZone + ? FTS_CHAR_COUNT + : 0 + const secondaryCharCount = secondary ? SECONDARY_CHAR_COUNT : 0 + + const remainingChars = + CHAR_LIMIT - secondaryCharCount - (timeZoneLength || 0) - timeZoneMatch + + throw new Error(`cannot be more than ${remainingChars} characters`) + } + // name length validation if (name.length > CHAR_LIMIT) { - throw new Error( - `cannot be more than ${ - CHAR_LIMIT - - (secondary ? SECONDARY_CHAR_COUNT : 0) - - (isFTS ? s.followTheSunRotation.timeZone.length : 0) - - (isFTS && s.followTheSunRotation.timeZone === s.timeZone - ? FTS_CHAR_COUNT - : 0) - } characters`, - ) + throw getErrMsg() + } + + const getTimeZone = (): string => { + if (isFTS) return s.followTheSunRotation.timeZone as string + return s.timeZone as string } return { name: name.replace(/\//g, '-'), description: DESC, - timeZone: isFTS ? s.followTheSunRotation.timeZone : s.timeZone, - start: DateTime.fromISO(s.rotation.startDate).minus(duration).toISO(), - type: s.rotation.type, + timeZone: getTimeZone(), + start: DateTime.fromISO(s.rotation?.startDate as string) + .minus(duration as DurationLike) + .toISO(), + type: s.rotation.type as RotationType, userIDs: isFTS ? s.followTheSunRotation.users : s.users, } }