From d1bb53478edf1776b7909e3d148698f982adf0ff Mon Sep 17 00:00:00 2001 From: Jethary Date: Mon, 5 Feb 2024 09:56:46 -0500 Subject: [PATCH] migrate some more compontns --- .../AppSettings/ManualIpHostnameForm.tsx | 37 +-- .../organisms/ConfigurePipette/ConfigForm.tsx | 212 ++++++++++-------- .../ConfigurePipette/ConfigFormGroup.tsx | 73 +++--- .../RenameRobotSlideout.tsx | 136 +++++++---- .../ConnectNetwork/ConnectModal/FormModal.tsx | 54 ++++- .../ConnectNetwork/ConnectModal/form-state.ts | 62 ++--- .../ConnectNetwork/ConnectModal/index.tsx | 56 +++-- .../RobotSettings/ConnectNetwork/types.ts | 5 +- app/src/pages/NameRobot/index.tsx | 124 ++++++---- yarn.lock | 2 +- 10 files changed, 466 insertions(+), 295 deletions(-) diff --git a/app/src/organisms/AppSettings/ManualIpHostnameForm.tsx b/app/src/organisms/AppSettings/ManualIpHostnameForm.tsx index e05123a3c147..1f99bf5709bf 100644 --- a/app/src/organisms/AppSettings/ManualIpHostnameForm.tsx +++ b/app/src/organisms/AppSettings/ManualIpHostnameForm.tsx @@ -1,7 +1,7 @@ import * as React from 'react' import { useDispatch } from 'react-redux' import { useTranslation } from 'react-i18next' -import { useForm } from 'react-hook-form' +import { FieldError, Resolver, useForm } from 'react-hook-form' import styled from 'styled-components' import { @@ -55,12 +55,6 @@ const StyledInput = styled.input` } ` -interface FormErrors { - ip?: { - type: string - message: string - } -} interface FormValues { ip: string } @@ -78,23 +72,36 @@ export function ManualIpHostnameForm({ dispatch(startDiscovery()) } - const validateForm = (data: FormValues): any => { - const errors: FormErrors = {} + const validateForm = ( + data: FormValues, + errors: Record + ): Record => { const ip = data.ip.trim() + let message: string | undefined // ToDo: kj 12/19/2022 for this, the best way is to use the regex because invisible unicode characters if (!ip) { - errors.ip = { type: 'required', message: t('add_ip_error') } + message = t('add_ip_error') + } + return { + ...errors, + ['ip']: { + type: 'error', + message: message, + }, } - return errors + } + + const resolver: Resolver = values => { + let errors = {} + errors = validateForm(values, errors) + return { values, errors } } const { formState, handleSubmit, register, reset } = useForm({ defaultValues: { ip: '', }, - resolver: data => { - return validateForm(data) - }, + resolver: resolver, }) const onSubmit = (data: FormValues): void => { @@ -139,7 +146,7 @@ export function ManualIpHostnameForm({ marginTop={SPACING.spacing4} color={COLORS.red50} > - {formState.errors.ip} + {formState.errors.ip.message} )} diff --git a/app/src/organisms/ConfigurePipette/ConfigForm.tsx b/app/src/organisms/ConfigurePipette/ConfigForm.tsx index 919d4224f7a1..3b77e8fc78a0 100644 --- a/app/src/organisms/ConfigurePipette/ConfigForm.tsx +++ b/app/src/organisms/ConfigurePipette/ConfigForm.tsx @@ -1,5 +1,4 @@ import * as React from 'react' -import { Formik, Form } from 'formik' import startCase from 'lodash/startCase' import mapValues from 'lodash/mapValues' @@ -15,13 +14,13 @@ import { ConfigQuirkGroup, } from './ConfigFormGroup' -import type { FormikProps } from 'formik' import type { FormValues } from './ConfigFormGroup' import type { PipetteSettingsField, PipetteSettingsFieldsMap, UpdatePipetteSettingsData, } from '@opentrons/api-client' +import { FieldError, Resolver, useForm } from 'react-hook-form' export interface DisplayFieldProps extends PipetteSettingsField { name: string @@ -47,11 +46,19 @@ const POWER_KEYS = ['plungerCurrent', 'pickUpCurrent', 'dropTipCurrent'] const TIP_KEYS = ['dropTipSpeed', 'pickUpDistance'] const QUIRK_KEY = 'quirks' -export class ConfigForm extends React.Component { - getFieldsByKey( +export const ConfigForm = (props: ConfigFormProps): JSX.Element => { + const { + updateInProgress, + formId, + settings, + updateSettings, + groupLabels, + } = props + + const getFieldsByKey = ( keys: string[], fields: PipetteSettingsFieldsMap - ): DisplayFieldProps[] { + ): DisplayFieldProps[] => { return keys.map(k => { const field = fields[k] const displayName = startCase(k) @@ -64,8 +71,8 @@ export class ConfigForm extends React.Component { }) } - getKnownQuirks = (): DisplayQuirkFieldProps[] => { - const quirks = this.props.settings[QUIRK_KEY] + const getKnownQuirks = (): DisplayQuirkFieldProps[] => { + const quirks = settings[QUIRK_KEY] if (!quirks) return [] const quirkKeys = Object.keys(quirks) return quirkKeys.map((name: string) => { @@ -79,22 +86,17 @@ export class ConfigForm extends React.Component { }) } - getVisibleFields: () => PipetteSettingsFieldsMap = () => { - return omit(this.props.settings, [QUIRK_KEY]) + const getVisibleFields = (): PipetteSettingsFieldsMap => { + return omit(settings, [QUIRK_KEY]) } - getUnknownKeys: () => string[] = () => { + const getUnknownKeys = (): string[] => { return keys( - omit(this.props.settings, [ - ...PLUNGER_KEYS, - ...POWER_KEYS, - ...TIP_KEYS, - QUIRK_KEY, - ]) + omit(settings, [...PLUNGER_KEYS, ...POWER_KEYS, ...TIP_KEYS, QUIRK_KEY]) ) } - handleSubmit: (values: FormValues) => void = values => { + const onSubmit: (values: FormValues) => void = values => { const fields = mapValues< FormValues, { value: PipetteSettingsField['value'] } | null @@ -104,29 +106,31 @@ export class ConfigForm extends React.Component { return { value: Number(v) } }) - this.props.updateSettings({ fields }) + updateSettings({ fields }) } - getFieldValue( + const getFieldValue = ( key: string, fields: DisplayFieldProps[], values: FormValues - ): number { + ): number => { const field = fields.find(f => f.name === key) const _default = field && field.default const value = values[key] || _default return Number(value) } - validate = (values: FormValues): {} => { - const errors = {} - const fields = this.getVisibleFields() - const plungerFields = this.getFieldsByKey(PLUNGER_KEYS, fields) + const validate = ( + values: FormValues, + errors: Record + ): Record => { + const fields = getVisibleFields() + const plungerFields = getFieldsByKey(PLUNGER_KEYS, fields) // validate all visible fields with min and max forOwn(fields, (field, name) => { // @ts-expect-error TODO: value needs to be of type string here, but technically that's not prover - const value = values[name].trim() + const value = values[name]?.trim() const { min, max } = field if (value !== '') { const parsed = Number(value) @@ -138,26 +142,38 @@ export class ConfigForm extends React.Component { // TODO(bc, 2021-05-18): this should probably be (parsed < min || parsed > max) so we're not accidentally comparing a string to a number (parsed < min || value > max) ) { - set(errors, name, `Min ${min} / Max ${max}`) + set(errors, name, { + type: 'numberError', + message: `Min ${min} / Max ${max}`, + }) } } }) const plungerGroupError = 'Please ensure the following: \n top > bottom > blowout > droptip' - const top = this.getFieldValue('top', plungerFields, values) - const bottom = this.getFieldValue('bottom', plungerFields, values) - const blowout = this.getFieldValue('blowout', plungerFields, values) - const dropTip = this.getFieldValue('dropTip', plungerFields, values) + const top = getFieldValue('top', plungerFields, values) + const bottom = getFieldValue('bottom', plungerFields, values) + const blowout = getFieldValue('blowout', plungerFields, values) + const dropTip = getFieldValue('dropTip', plungerFields, values) if (top <= bottom || bottom <= blowout || blowout <= dropTip) { - set(errors, 'plungerError', plungerGroupError) + set(errors, 'plungerError', { + type: 'plungerError', + message: plungerGroupError, + }) } return errors } - getInitialValues: () => FormValues = () => { - const fields = this.getVisibleFields() + const resolver: Resolver = values => { + let errors = {} + errors = validate(values, errors) + return { values, errors } + } + + const getInitialValues: () => FormValues = () => { + const fields = getVisibleFields() const initialFieldValues = mapValues< PipetteSettingsFieldsMap, string | boolean @@ -166,7 +182,7 @@ export class ConfigForm extends React.Component { // @ts-expect-error(sa, 2021-05-27): avoiding src code change, use optional chain to access f.value return f.value !== f.default ? f.value.toString() : '' }) - const initialQuirkValues = this.props.settings[QUIRK_KEY] + const initialQuirkValues = settings[QUIRK_KEY] const initialValues = Object.assign( {}, initialFieldValues, @@ -176,68 +192,70 @@ export class ConfigForm extends React.Component { return initialValues } - render(): JSX.Element { - const { updateInProgress, formId } = this.props - const fields = this.getVisibleFields() - const UNKNOWN_KEYS = this.getUnknownKeys() - const plungerFields = this.getFieldsByKey(PLUNGER_KEYS, fields) - const powerFields = this.getFieldsByKey(POWER_KEYS, fields) - const tipFields = this.getFieldsByKey(TIP_KEYS, fields) - const quirkFields = this.getKnownQuirks() - const quirksPresent = quirkFields.length > 0 - const unknownFields = this.getFieldsByKey(UNKNOWN_KEYS, fields) - const initialValues = this.getInitialValues() - - return ( - - {(formProps: FormikProps) => { - const { errors, values } = formProps - const handleReset = (): void => { - const newValues = mapValues(values, v => { - if (typeof v === 'boolean') { - // NOTE: checkbox fields don't have defaults from the API b/c they come in from `quirks` - // For now, we'll reset all checkboxes to true - return true - } - return '' - }) - formProps.resetForm({ values: newValues }) - } - return ( - -
- - - - - {quirksPresent && } - - - - - -
- ) - }} -
- ) + const fields = getVisibleFields() + const UNKNOWN_KEYS = getUnknownKeys() + const plungerFields = getFieldsByKey(PLUNGER_KEYS, fields) + const powerFields = getFieldsByKey(POWER_KEYS, fields) + const tipFields = getFieldsByKey(TIP_KEYS, fields) + const quirkFields = getKnownQuirks() + const quirksPresent = quirkFields.length > 0 + const unknownFields = getFieldsByKey(UNKNOWN_KEYS, fields) + const initialValues = getInitialValues() + + const { + handleSubmit, + reset, + getValues, + control, + formState: { errors }, + } = useForm({ + defaultValues: initialValues, + resolver: resolver, + }) + + const handleReset = (): void => { + const newValues = mapValues(getValues(), v => { + if (typeof v === 'boolean') { + // NOTE: checkbox fields don't have defaults from the API b/c they come in from `quirks` + // For now, we'll reset all checkboxes to true + return true + } + return '' + }) + reset(newValues) } + + return ( +
+ + + + + + {quirksPresent && ( + + )} + + + + + +
+ ) } diff --git a/app/src/organisms/ConfigurePipette/ConfigFormGroup.tsx b/app/src/organisms/ConfigurePipette/ConfigFormGroup.tsx index 919e4660d5de..95595cdd3252 100644 --- a/app/src/organisms/ConfigurePipette/ConfigFormGroup.tsx +++ b/app/src/organisms/ConfigurePipette/ConfigFormGroup.tsx @@ -1,5 +1,5 @@ import * as React from 'react' -import { Field } from 'formik' +import { Control, Controller } from 'react-hook-form' import { FormGroup, Flex, @@ -12,7 +12,6 @@ import { InputField } from '../../atoms/InputField' import { StyledText } from '../../atoms/text' import styles from './styles.css' -import type { FieldProps } from 'formik' import type { DisplayFieldProps, DisplayQuirkFieldProps } from './ConfigForm' export interface FormColumnProps { @@ -31,10 +30,11 @@ export interface ConfigFormGroupProps { groupLabel: string groupError?: string | null | undefined formFields: DisplayFieldProps[] + control: Control } export function ConfigFormGroup(props: ConfigFormGroupProps): JSX.Element { - const { groupLabel, groupError, formFields } = props + const { groupLabel, groupError, formFields, control } = props const formattedError = groupError && groupError.split('\n').map(function (item, key) { @@ -53,7 +53,9 @@ export function ConfigFormGroup(props: ConfigFormGroupProps): JSX.Element { > {groupError &&

{formattedError}

} {formFields.map((field, index) => { - return + return ( + + ) })} ) @@ -89,55 +91,61 @@ export function ConfigFormRow(props: ConfigFormRowProps): JSX.Element { } export interface ConfigInputProps { - field: DisplayFieldProps + displayField: DisplayFieldProps + control: Control } export function ConfigInput(props: ConfigInputProps): JSX.Element { - const { field } = props - const { name, units, displayName } = field - const id = makeId(field.name) - const _default = field.default?.toString() + const { displayField, control } = props + const { name, units, displayName } = displayField + const id = makeId(name) + const _default = displayField.default.toString() + return ( - - {(fieldProps: FieldProps) => ( + ( )} - + /> ) } export interface ConfigCheckboxProps { - field: DisplayQuirkFieldProps + displayQuirkField: DisplayQuirkFieldProps + control: Control } export function ConfigCheckbox(props: ConfigCheckboxProps): JSX.Element { - const { field } = props - const { name, displayName } = field + const { displayQuirkField, control } = props + const { name, displayName } = displayQuirkField const id = makeId(name) return ( - - {(fieldProps: FieldProps) => ( + ( )} - + /> {displayName} @@ -147,14 +155,21 @@ export function ConfigCheckbox(props: ConfigCheckboxProps): JSX.Element { export interface ConfigQuirkGroupProps { quirks: DisplayQuirkFieldProps[] + control: Control } export function ConfigQuirkGroup(props: ConfigQuirkGroupProps): JSX.Element { - const { quirks } = props + const { quirks, control } = props return ( {quirks.map((field, index) => { - return + return ( + + ) })} ) diff --git a/app/src/organisms/Devices/RobotSettings/AdvancedTab/AdvancedTabSlideouts/RenameRobotSlideout.tsx b/app/src/organisms/Devices/RobotSettings/AdvancedTab/AdvancedTabSlideouts/RenameRobotSlideout.tsx index 504f2eee46e2..0da142dee598 100644 --- a/app/src/organisms/Devices/RobotSettings/AdvancedTab/AdvancedTabSlideouts/RenameRobotSlideout.tsx +++ b/app/src/organisms/Devices/RobotSettings/AdvancedTab/AdvancedTabSlideouts/RenameRobotSlideout.tsx @@ -1,8 +1,8 @@ import * as React from 'react' import { useSelector, useDispatch } from 'react-redux' import { useHistory } from 'react-router-dom' +import { useForm, Resolver, Controller, FieldError } from 'react-hook-form' import { useTranslation } from 'react-i18next' -import { useFormik } from 'formik' import { Flex, DIRECTION_COLUMN, @@ -35,9 +35,8 @@ interface RenameRobotSlideoutProps { onCloseClick: () => void robotName: string } - -interface FormikErrors { - newRobotName?: string +interface FormValues { + newRobotName: string } /* max length is 17 and min length is 1 @@ -70,38 +69,69 @@ export function RenameRobotSlideout({ getUnreachableRobots(state) ) - const formik = useFormik({ - initialValues: { - newRobotName: '', - }, - onSubmit: (values, { resetForm }) => { - const newName = values.newRobotName - setPreviousRobotName(robotName) - const sameNameRobotInUnavailable = unreachableRobots.find( - robot => robot.name === newName + const validate = ( + data: FormValues, + errors: Record + ): Record => { + const newName = data.newRobotName + let message: string | undefined + if (!regexPattern.test(newName)) { + message = t('name_rule_error_name_length') + } + if ( + [...connectableRobots, ...reachableRobots].some( + robot => newName === robot.name ) - if (sameNameRobotInUnavailable != null) { - dispatch(removeRobot(sameNameRobotInUnavailable.name)) - } - updateRobotName(newName) - resetForm({ values: { newRobotName: '' } }) - }, - validate: values => { - const errors: FormikErrors = {} - const newName = values.newRobotName - if (!regexPattern.test(newName)) { - errors.newRobotName = t('name_rule_error_name_length') - } - if ( - [...connectableRobots, ...reachableRobots].some( - robot => newName === robot.name - ) - ) { - errors.newRobotName = t('name_rule_error_exist') - } - return errors + ) { + message = t('name_rule_error_exist') + } + + const updatedErrors = + message != null + ? { + ...errors, + newRobotName: { + type: 'error', + message: message, + }, + } + : errors + return updatedErrors + } + + const resolver: Resolver = values => { + let errors = {} + errors = validate(values, errors) + return { values, errors } + } + + const { + handleSubmit, + control, + formState: { isDirty, isValid, errors }, + reset, + watch, + trigger, + } = useForm({ + defaultValues: { + newRobotName: '', }, + resolver: resolver, }) + const newRobotName = watch('newRobotName') + + const onSubmit = (data: FormValues): void => { + const newName = data.newRobotName + setPreviousRobotName(robotName) + const sameNameRobotInUnavailable = unreachableRobots.find( + robot => robot.name === newName + ) + if (sameNameRobotInUnavailable != null) { + dispatch(removeRobot(sameNameRobotInUnavailable.name)) + } + updateRobotName(newName) + reset({ newRobotName: '' }) + } const { updateRobotName } = useUpdateRobotNameMutation({ onSuccess: (data: UpdatedRobotName) => { @@ -124,12 +154,14 @@ export function RenameRobotSlideout({ name: ANALYTICS_RENAME_ROBOT, properties: { previousRobotName, - newRobotName: formik.values.newRobotName, + newRobotName: newRobotName, }, }) - formik.handleSubmit() + handleSubmit(onSubmit) } - + console.log('isDirety', isDirty) + console.log('errors', errors) + console.log('isvalid', isValid) return ( {t('rename_robot')} @@ -161,27 +193,37 @@ export function RenameRobotSlideout({ > {t('robot_name')} - ( + { + field.onBlur() + trigger('newRobotName') + }} + /> + )} /> {t('characters_max')} - {formik.errors.newRobotName && ( + {errors.newRobotName != null ? ( - {formik.errors.newRobotName} + {errors.newRobotName.message} - )} + ) : null} ) diff --git a/app/src/organisms/Devices/RobotSettings/ConnectNetwork/ConnectModal/FormModal.tsx b/app/src/organisms/Devices/RobotSettings/ConnectNetwork/ConnectModal/FormModal.tsx index 129da41ce47d..9aac1e35c7ad 100644 --- a/app/src/organisms/Devices/RobotSettings/ConnectNetwork/ConnectModal/FormModal.tsx +++ b/app/src/organisms/Devices/RobotSettings/ConnectNetwork/ConnectModal/FormModal.tsx @@ -1,8 +1,12 @@ import * as React from 'react' -import { Form } from 'formik' +import { Control, Controller } from 'react-hook-form' import styled, { css } from 'styled-components' -import { FONT_SIZE_BODY_1, BUTTON_TYPE_SUBMIT } from '@opentrons/components' +import { + FONT_SIZE_BODY_1, + BUTTON_TYPE_SUBMIT, + Flex, +} from '@opentrons/components' import { ScrollableAlertModal } from '../../../../../molecules/modals' import { TextField } from './TextField' import { KeyFileField } from './KeyFileField' @@ -10,7 +14,7 @@ import { SecurityField } from './SecurityField' import { FIELD_TYPE_KEY_FILE, FIELD_TYPE_SECURITY } from '../constants' import * as Copy from '../i18n' -import type { ConnectFormField, WifiNetwork } from '../types' +import type { ConnectFormField, ConnectFormValues, WifiNetwork } from '../types' const fieldStyle = css` min-width: 12rem; @@ -19,7 +23,7 @@ const StyledCopy = styled.p` margin: 0 1rem 1rem; ` -const StyledForm = styled(Form)` +const StyledFlex = styled(Flex)` font-size: ${FONT_SIZE_BODY_1}; display: table; width: 80%; @@ -43,11 +47,12 @@ export interface FormModalProps { network: WifiNetwork | null fields: ConnectFormField[] isValid: boolean - onCancel: () => unknown + onCancel: () => void + control: Control } export const FormModal = (props: FormModalProps): JSX.Element => { - const { id, network, fields, isValid, onCancel } = props + const { id, network, fields, isValid, onCancel, control } = props const heading = network !== null @@ -76,26 +81,53 @@ export const FormModal = (props: FormModalProps): JSX.Element => { ]} > {body} - + {fields.map(fieldProps => { const { name } = fieldProps const fieldId = `${id}__${name}` if (fieldProps.type === FIELD_TYPE_SECURITY) { return ( - + ( + + )} + /> ) } if (fieldProps.type === FIELD_TYPE_KEY_FILE) { return ( - + ( + + )} + /> ) } - return + return ( + ( + + )} + /> + ) })} - + ) } diff --git a/app/src/organisms/Devices/RobotSettings/ConnectNetwork/ConnectModal/form-state.ts b/app/src/organisms/Devices/RobotSettings/ConnectNetwork/ConnectModal/form-state.ts index d3dff499a9b3..80d36326f82b 100644 --- a/app/src/organisms/Devices/RobotSettings/ConnectNetwork/ConnectModal/form-state.ts +++ b/app/src/organisms/Devices/RobotSettings/ConnectNetwork/ConnectModal/form-state.ts @@ -1,30 +1,32 @@ -import { useEffect } from 'react' -import { useFormikContext, useField } from 'formik' +import * as React from 'react' +import { useForm, useController } from 'react-hook-form' import { usePrevious } from '@opentrons/components' import type { ConnectFormValues, ConnectFormFieldProps } from '../types' export const useResetFormOnSecurityChange = (): void => { const { - values, - errors, - touched, - setValues, - setErrors, - setTouched, - } = useFormikContext() + control, + formState: { errors, touchedFields }, + setValue, + getValues, + trigger, + clearErrors, + } = useForm() - const ssid = values.ssid - const ssidTouched = touched.ssid + const ssid = getValues('ssid') + const ssidTouched = touchedFields.ssid const ssidError = errors.ssid - const securityType = values.securityType + const securityType = getValues('securityType') const prevSecurityType = usePrevious(securityType) - useEffect(() => { + React.useEffect(() => { if (prevSecurityType && securityType !== prevSecurityType) { - setErrors({ ssid: ssidError }) - setTouched({ ssid: ssidTouched, securityType: true }, false) - setValues({ ssid, securityType }) + clearErrors('ssid') + clearErrors('securityType') + setValue('ssid', ssid) + setValue('securityType', securityType) + trigger(['ssid', 'securityType']) } }, [ ssid, @@ -32,26 +34,26 @@ export const useResetFormOnSecurityChange = (): void => { ssidError, securityType, prevSecurityType, - setTouched, - setErrors, - setValues, + control, + setValue, + trigger ]) } export const useConnectFormField = (name: string): ConnectFormFieldProps => { - const [fieldProps, fieldMeta, fieldHelpers] = useField( - name - ) - const { value, onChange, onBlur } = fieldProps - const { setValue, setTouched } = fieldHelpers - const error = fieldMeta.touched ? fieldMeta.error : null + const { field, fieldState } = useController({ + name, + defaultValue: '', + }) + + const error = fieldState.error?.message return { - value: value ?? null, + value: field.value, error: error ?? null, - onChange, - onBlur, - setValue, - setTouched, + onChange: field.onChange, + onBlur: field.onBlur, + setValue: field.onChange, + setTouched: field.onBlur, } } diff --git a/app/src/organisms/Devices/RobotSettings/ConnectNetwork/ConnectModal/index.tsx b/app/src/organisms/Devices/RobotSettings/ConnectNetwork/ConnectModal/index.tsx index fa9f5f78a1ee..389ed60d367f 100644 --- a/app/src/organisms/Devices/RobotSettings/ConnectNetwork/ConnectModal/index.tsx +++ b/app/src/organisms/Devices/RobotSettings/ConnectNetwork/ConnectModal/index.tsx @@ -1,5 +1,4 @@ import * as React from 'react' -import { Formik, useFormikContext } from 'formik' import { useResetFormOnSecurityChange } from './form-state' import { @@ -17,47 +16,71 @@ import type { WifiKey, EapOption, } from '../types' +import { Control, useForm } from 'react-hook-form' export interface ConnectModalProps { robotName: string network: WifiNetwork | null wifiKeys: WifiKey[] eapOptions: EapOption[] - onConnect: (r: WifiConfigureRequest) => unknown - onCancel: () => unknown + isValid: boolean + onConnect: (r: WifiConfigureRequest) => void + onCancel: () => void + values: ConnectFormValues + control: Control } export const ConnectModal = (props: ConnectModalProps): JSX.Element => { const { network, eapOptions, onConnect } = props - const handleSubmit = (values: ConnectFormValues): void => { + const onSubmit = (values: ConnectFormValues): void => { const request = connectFormToConfigureRequest(network, values) if (request) onConnect(request) } const handleValidate = ( - values: ConnectFormValues + data: ConnectFormValues ): ReturnType => { - return validateConnectFormFields(network, eapOptions, values) + return validateConnectFormFields(network, eapOptions, data) } + const { + handleSubmit, + formState: { isValid }, + getValues, + control, + } = useForm({ + defaultValues: {}, + resolver: handleValidate, + }) + + const values = getValues() + return ( - - - +
+ + ) } export const ConnectModalComponent = ( props: ConnectModalProps ): JSX.Element => { - const { robotName, network, wifiKeys, eapOptions, onCancel } = props - const { values, isValid } = useFormikContext() + const { + robotName, + network, + wifiKeys, + eapOptions, + onCancel, + values, + isValid, + control, + } = props const id = `ConnectForm__${robotName}` const fields = getConnectFormFields( @@ -78,6 +101,7 @@ export const ConnectModalComponent = ( fields, isValid, onCancel, + control, }} /> ) diff --git a/app/src/organisms/Devices/RobotSettings/ConnectNetwork/types.ts b/app/src/organisms/Devices/RobotSettings/ConnectNetwork/types.ts index 588bfffe54e0..04ee7cc8f7f5 100644 --- a/app/src/organisms/Devices/RobotSettings/ConnectNetwork/types.ts +++ b/app/src/organisms/Devices/RobotSettings/ConnectNetwork/types.ts @@ -1,5 +1,4 @@ -import type { FormikErrors } from 'formik' - +import type { FieldError } from 'react-hook-form' import type { WifiNetwork, EapOption, @@ -46,7 +45,7 @@ export type ConnectFormValues = Partial<{ } }> -export type ConnectFormErrors = Partial> +export type ConnectFormErrors = Partial> interface ConnectFormFieldCommon { name: string diff --git a/app/src/pages/NameRobot/index.tsx b/app/src/pages/NameRobot/index.tsx index 2b65755b8fc3..f9e5c939c101 100644 --- a/app/src/pages/NameRobot/index.tsx +++ b/app/src/pages/NameRobot/index.tsx @@ -1,7 +1,7 @@ import * as React from 'react' +import { Controller, FieldError, Resolver, useForm } from 'react-hook-form' import { useTranslation } from 'react-i18next' import { useSelector, useDispatch } from 'react-redux' -import { useFormik } from 'formik' import { css } from 'styled-components' import { useHistory } from 'react-router-dom' @@ -50,8 +50,8 @@ const INPUT_FIELD_ODD_STYLE = css` text-align: center; ` -interface FormikErrors { - newRobotName?: string +interface FormValues { + newRobotName: string } export function NameRobot(): JSX.Element { @@ -81,39 +81,65 @@ export function NameRobot(): JSX.Element { getUnreachableRobots(state) ) - const formik = useFormik({ - initialValues: { - newRobotName: '', - }, - onSubmit: (values, { resetForm }) => { - const newName = values.newRobotName.concat(name) - const sameNameRobotInUnavailable = unreachableRobots.find( - robot => robot.name === newName + const validate = ( + data: FormValues, + errors: Record + ): Record => { + const newName = data.newRobotName.concat(name) + let message: string | undefined + // In ODD users cannot input letters and numbers from software keyboard + // so the app only checks the length of input string + if (newName.length < 1) { + message = t('name_rule_error_name_length') + } + if ( + [...connectableRobots, ...reachableRobots].some( + robot => newName === robot.name && robot.ip !== ipAddress ) - if (sameNameRobotInUnavailable != null) { - dispatch(removeRobot(sameNameRobotInUnavailable.name)) - } - updateRobotName(newName) - resetForm({ values: { newRobotName: '' } }) - }, - validate: values => { - const errors: FormikErrors = {} - const newName = values.newRobotName.concat(name) - // In ODD users cannot input letters and numbers from software keyboard - // so the app only checks the length of input string - if (newName.length < 1) { - errors.newRobotName = t('name_rule_error_name_length') - } - if ( - [...connectableRobots, ...reachableRobots].some( - robot => newName === robot.name && robot.ip !== ipAddress - ) - ) { - errors.newRobotName = t('name_rule_error_exist') - } - return errors + ) { + message = t('name_rule_error_exist') + } + return { + ...errors, + ['newRobotName']: { + type: 'error', + message: message, + }, + } + } + + const resolver: Resolver = values => { + let errors = {} + errors = validate(values, errors) + return { values, errors } + } + + const { + handleSubmit, + control, + formState: { isDirty, isValid, errors }, + reset, + watch, + trigger, + } = useForm({ + defaultValues: { + newRobotName: '', }, + resolver: resolver, }) + const newRobotName = watch('newRobotName') + + const onSubmit = (data: FormValues): void => { + const newName = data.newRobotName.concat(name) + const sameNameRobotInUnavailable = unreachableRobots.find( + robot => robot.name === newName + ) + if (sameNameRobotInUnavailable != null) { + dispatch(removeRobot(sameNameRobotInUnavailable.name)) + } + updateRobotName(newName) + reset({ newRobotName: '' }) + } const { updateRobotName, isLoading: isNaming } = useUpdateRobotNameMutation({ onSuccess: (data: UpdatedRobotName) => { @@ -140,10 +166,10 @@ export function NameRobot(): JSX.Element { name: ANALYTICS_RENAME_ROBOT, properties: { previousRobotName: previousName, - newRobotName: formik.values.newRobotName, + newRobotName: newRobotName, }, }) - formik.handleSubmit() + handleSubmit(onSubmit) } return ( @@ -230,15 +256,21 @@ export function NameRobot(): JSX.Element { {t('name_your_robot_description')} ) : null} - ( + + )} /> {t('name_rule_description')} - {formik.errors.newRobotName && ( + {errors.newRobotName != null ? ( - {formik.errors.newRobotName} + {errors.newRobotName.message} - )} + ) : null} diff --git a/yarn.lock b/yarn.lock index b218f7b6a12a..b37d5dff006c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2260,7 +2260,6 @@ date-fns "2.25.0" events "3.0.0" file-saver "2.0.1" - formik "2.1.4" history "4.7.2" i18next "^19.8.3" is-ip "3.1.0" @@ -2272,6 +2271,7 @@ react "18.2.0" react-dom "18.2.0" react-error-boundary "^4.0.10" + react-hook-form "7.49.3" react-i18next "13.5.0" react-intersection-observer "^8.33.1" react-redux "8.1.2"