diff --git a/packages/manager/.changeset/pr-11393-tech-stories-1733855306252.md b/packages/manager/.changeset/pr-11393-tech-stories-1733855306252.md new file mode 100644 index 00000000000..045c531f80c --- /dev/null +++ b/packages/manager/.changeset/pr-11393-tech-stories-1733855306252.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Tech Stories +--- + +Refactor VPCEditDrawer and SubnetEditDrawer to use `react-hook-form` instead of `formik` ([#11393](https://github.com/linode/manager/pull/11393)) diff --git a/packages/manager/src/features/VPCs/VPCDetail/SubnetEditDrawer.tsx b/packages/manager/src/features/VPCs/VPCDetail/SubnetEditDrawer.tsx index d4a58da251d..f07b04602b7 100644 --- a/packages/manager/src/features/VPCs/VPCDetail/SubnetEditDrawer.tsx +++ b/packages/manager/src/features/VPCs/VPCDetail/SubnetEditDrawer.tsx @@ -1,12 +1,13 @@ +import { yupResolver } from '@hookform/resolvers/yup'; import { Notice, TextField } from '@linode/ui'; -import { useFormik } from 'formik'; +import { modifySubnetSchema } from '@linode/validation'; import * as React from 'react'; +import { Controller, useForm } from 'react-hook-form'; import { ActionsPanel } from 'src/components/ActionsPanel/ActionsPanel'; import { Drawer } from 'src/components/Drawer'; import { useGrants, useProfile } from 'src/queries/profile/profile'; import { useUpdateSubnetMutation } from 'src/queries/vpcs/vpcs'; -import { getErrorMap } from 'src/utilities/errorUtils'; import type { ModifySubnetPayload, Subnet } from '@linode/api-v4'; @@ -24,29 +25,41 @@ export const SubnetEditDrawer = (props: Props) => { const { onClose, open, subnet, vpcId } = props; const { - error, isPending, mutateAsync: updateSubnet, - reset, + reset: resetMutation, } = useUpdateSubnetMutation(vpcId, subnet?.id ?? -1); - const form = useFormik({ - enableReinitialize: true, - initialValues: { + const { + control, + formState: { errors, isDirty, isSubmitting }, + handleSubmit, + reset: resetForm, + setError, + } = useForm({ + mode: 'onBlur', + resolver: yupResolver(modifySubnetSchema), + values: { label: subnet?.label ?? '', }, - async onSubmit(values) { - await updateSubnet(values); - onClose(); - }, }); - React.useEffect(() => { - if (open) { - form.resetForm(); - reset(); + const handleDrawerClose = () => { + onClose(); + resetForm(); + resetMutation(); + }; + + const onSubmit = async (values: ModifySubnetPayload) => { + try { + await updateSubnet(values); + handleDrawerClose(); + } catch (errors) { + for (const error of errors) { + setError(error?.field ?? 'root', { message: error.reason }); + } } - }, [open]); + }; const { data: profile } = useProfile(); const { data: grants } = useGrants(); @@ -59,11 +72,11 @@ export const SubnetEditDrawer = (props: Props) => { Boolean(profile?.restricted) && (vpcPermissions?.permissions === 'read_only' || grants?.vpc.length === 0); - const errorMap = getErrorMap(['label'], error); - return ( - - {errorMap.none && } + + {errors.root?.message && ( + + )} {readOnly && ( { variant="error" /> )} -
- + ( + + )} + control={control} name="label" - onChange={form.handleChange} - value={form.values.label} /> {
diff --git a/packages/manager/src/features/VPCs/VPCLanding/VPCEditDrawer.tsx b/packages/manager/src/features/VPCs/VPCLanding/VPCEditDrawer.tsx index 4254e5d38a5..9a2744b44ba 100644 --- a/packages/manager/src/features/VPCs/VPCLanding/VPCEditDrawer.tsx +++ b/packages/manager/src/features/VPCs/VPCLanding/VPCEditDrawer.tsx @@ -1,7 +1,8 @@ +import { yupResolver } from '@hookform/resolvers/yup'; import { Notice, TextField } from '@linode/ui'; -import { updateVPCSchema } from '@linode/validation/lib/vpcs.schema'; -import { useFormik } from 'formik'; +import { updateVPCSchema } from '@linode/validation'; import * as React from 'react'; +import { Controller, useForm } from 'react-hook-form'; import { ActionsPanel } from 'src/components/ActionsPanel/ActionsPanel'; import { Drawer } from 'src/components/Drawer'; @@ -9,9 +10,8 @@ import { RegionSelect } from 'src/components/RegionSelect/RegionSelect'; import { useGrants, useProfile } from 'src/queries/profile/profile'; import { useRegionsQuery } from 'src/queries/regions/regions'; import { useUpdateVPCMutation } from 'src/queries/vpcs/vpcs'; -import { getErrorMap } from 'src/utilities/errorUtils'; -import type { UpdateVPCPayload, VPC } from '@linode/api-v4/lib/vpcs/types'; +import type { UpdateVPCPayload, VPC } from '@linode/api-v4'; interface Props { onClose: () => void; @@ -36,60 +36,50 @@ export const VPCEditDrawer = (props: Props) => { (vpcPermissions?.permissions === 'read_only' || grants?.vpc.length === 0); const { - error, isPending, mutateAsync: updateVPC, - reset, + reset: resetMutation, } = useUpdateVPCMutation(vpc?.id ?? -1); - interface UpdateVPCPayloadWithNone extends UpdateVPCPayload { - none?: string; - } - - const form = useFormik({ - enableReinitialize: true, - initialValues: { + const { + control, + formState: { errors, isDirty, isSubmitting }, + handleSubmit, + reset: resetForm, + setError, + } = useForm({ + mode: 'onBlur', + resolver: yupResolver(updateVPCSchema), + values: { description: vpc?.description, label: vpc?.label, }, - async onSubmit(values) { - await updateVPC(values); - onClose(); - }, - validateOnChange: false, - validationSchema: updateVPCSchema, }); - const handleFieldChange = (field: string, value: string) => { - form.setFieldValue(field, value); - if (form.errors[field as keyof UpdateVPCPayloadWithNone]) { - form.setFieldError(field, undefined); - } + const handleDrawerClose = () => { + onClose(); + resetForm(); + resetMutation(); }; - React.useEffect(() => { - if (open) { - form.resetForm(); - reset(); - } - }, [open]); - - // If there's an error, sync it with formik - React.useEffect(() => { - if (error) { - const errorMap = getErrorMap(['label', 'description'], error); - for (const [field, reason] of Object.entries(errorMap)) { - form.setFieldError(field, reason); + const onSubmit = async (values: UpdateVPCPayload) => { + try { + await updateVPC(values); + handleDrawerClose(); + } catch (errors) { + for (const error of errors) { + setError(error?.field ?? 'root', { message: error.reason }); } } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [error]); + }; const { data: regionsData, error: regionsError } = useRegionsQuery(); return ( - - {form.errors.none && } + + {errors.root?.message && ( + + )} {readOnly && ( { variant="error" /> )} -
- + ( + + )} + control={control} name="label" - onChange={(e) => handleFieldChange('label', e.target.value)} - value={form.values.label} /> - handleFieldChange('description', e.target.value)} - rows={1} - value={form.values.description} + ( + + )} + control={control} + name="description" /> {regionsData && ( {
diff --git a/packages/validation/.changeset/pr-11393-changed-1733855352751.md b/packages/validation/.changeset/pr-11393-changed-1733855352751.md new file mode 100644 index 00000000000..afb76254075 --- /dev/null +++ b/packages/validation/.changeset/pr-11393-changed-1733855352751.md @@ -0,0 +1,5 @@ +--- +"@linode/validation": Changed +--- + +Update VPC label validation schema punctuation, fix label validation regex ([#11393](https://github.com/linode/manager/pull/11393)) diff --git a/packages/validation/src/vpcs.schema.ts b/packages/validation/src/vpcs.schema.ts index be92dd771e3..1196436186c 100644 --- a/packages/validation/src/vpcs.schema.ts +++ b/packages/validation/src/vpcs.schema.ts @@ -2,13 +2,13 @@ import ipaddr from 'ipaddr.js'; import { array, lazy, object, string } from 'yup'; const LABEL_MESSAGE = 'Label must be between 1 and 64 characters.'; -const LABEL_REQUIRED = 'Label is required'; +const LABEL_REQUIRED = 'Label is required.'; const LABEL_REQUIREMENTS = - 'Must include only ASCII letters, numbers, and dashes'; + 'Label must include only ASCII letters, numbers, and dashes.'; const labelTestDetails = { testName: 'no two dashes in a row', - testMessage: 'Must not contain two dashes in a row', + testMessage: 'Label must not contain two dashes in a row.', }; const IP_EITHER_BOTH_NOT_NEITHER = @@ -116,11 +116,11 @@ const labelValidation = string() ) .min(1, LABEL_MESSAGE) .max(64, LABEL_MESSAGE) - .matches(/[a-zA-Z0-9-]+/, LABEL_REQUIREMENTS); + .matches(/^[a-zA-Z0-9-]*$/, LABEL_REQUIREMENTS); export const updateVPCSchema = object({ - label: labelValidation.notRequired(), - description: string().notRequired(), + label: labelValidation, + description: string(), }); export const createSubnetSchema = object().shape(