Skip to content

Commit 6e30acb

Browse files
committed
fix the issue by using field-level validation
1 parent 36b1884 commit 6e30acb

5 files changed

Lines changed: 39 additions & 45 deletions

File tree

app/components/form/fields/NumberField.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ export const NumberFieldInner = <
6565
name,
6666
label = capitalize(name),
6767
validate,
68+
deps,
6869
control,
6970
required,
7071
id: idProp,
@@ -83,6 +84,7 @@ export const NumberFieldInner = <
8384
control,
8485
rules: {
8586
required,
87+
deps,
8688
// it seems we need special logic to enforce required on NaN
8789
validate(value, values) {
8890
if (required && Number.isNaN(value)) return `${label} is required`

app/components/form/fields/TextField.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
type FieldPath,
1313
type FieldPathValue,
1414
type FieldValues,
15+
type RegisterOptions,
1516
type Validate,
1617
} from 'react-hook-form'
1718

@@ -45,6 +46,7 @@ export interface TextFieldProps<
4546
placeholder?: string
4647
units?: string
4748
validate?: Validate<FieldPathValue<TFieldValues, TName>, TFieldValues>
49+
deps?: RegisterOptions<TFieldValues, TName>['deps']
4850
control: Control<TFieldValues>
4951
/** Alters the value of the input during the field's onChange event. */
5052
transform?: (value: string) => string
@@ -98,6 +100,7 @@ export const TextFieldInner = <
98100
type = 'text',
99101
label = capitalize(name),
100102
validate,
103+
deps,
101104
control,
102105
required,
103106
id: idProp,
@@ -109,7 +112,7 @@ export const TextFieldInner = <
109112
const {
110113
field: { onChange, ...fieldRest },
111114
fieldState: { error },
112-
} = useController({ name, control, rules: { required, validate } })
115+
} = useController({ name, control, rules: { required, validate, deps } })
113116
return (
114117
<>
115118
<UITextField

app/forms/subnet-pool-member-add.spec.ts

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,22 +7,24 @@
77
*/
88
import { describe, expect, it } from 'vitest'
99

10-
import { validateForm } from './subnet-pool-member-add'
10+
import { validateMember } from './subnet-pool-member-add'
1111

12-
const validate = (values: Parameters<typeof validateForm>[1]) => validateForm('v4', values)
13-
const validate6 = (values: Parameters<typeof validateForm>[1]) => validateForm('v6', values)
12+
const validate = (values: Parameters<typeof validateMember>[1]) =>
13+
validateMember('v4', values)
14+
const validate6 = (values: Parameters<typeof validateMember>[1]) =>
15+
validateMember('v6', values)
1416

1517
const valid = { subnet: '10.0.0.0/16', minPrefixLength: 20, maxPrefixLength: 28 }
1618

1719
type Field = 'subnet' | 'minPrefixLength' | 'maxPrefixLength'
1820

1921
function errMsg(result: ReturnType<typeof validate>, field: Field) {
20-
return result === true ? undefined : result[field]?.message
22+
return result[field]
2123
}
2224

23-
describe('validateForm', () => {
25+
describe('validateMember', () => {
2426
it('accepts valid v4 input', () => {
25-
expect(validate(valid)).toBe(true)
27+
expect(validate(valid)).toEqual({})
2628
})
2729

2830
it('accepts valid v6 input', () => {
@@ -31,7 +33,7 @@ describe('validateForm', () => {
3133
minPrefixLength: 48,
3234
maxPrefixLength: 64,
3335
})
34-
expect(result).toBe(true)
36+
expect(result).toEqual({})
3537
})
3638

3739
it('accepts omitted prefix lengths', () => {
@@ -40,7 +42,7 @@ describe('validateForm', () => {
4042
minPrefixLength: NaN,
4143
maxPrefixLength: NaN,
4244
})
43-
expect(result).toBe(true)
45+
expect(result).toEqual({})
4446
})
4547

4648
it('rejects invalid CIDR', () => {

app/forms/subnet-pool-member-add.tsx

Lines changed: 22 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -41,62 +41,43 @@ const defaultValues: MemberAddForm = {
4141
maxPrefixLength: NaN,
4242
}
4343

44-
// Uses form-level validate (RHF ≥7.72.0) so we can look at all three fields
45-
// together. Unlike `resolver`, this runs alongside field-level validation, so
46-
// `required` / `min` / `max` on the fields still apply.
47-
export function validateForm(poolVersion: IpVersion, values: MemberAddForm) {
44+
type ValidationErrors = Partial<Record<keyof MemberAddForm, string>>
45+
46+
export function validateMember(poolVersion: IpVersion, values: MemberAddForm) {
4847
const maxBound = poolVersion === 'v4' ? 32 : 128
4948
const parsed = parseIpNet(values.subnet)
5049
const { minPrefixLength: minPL, maxPrefixLength: maxPL } = values
5150
const subnetWidth = parsed.type !== 'error' ? parsed.width : undefined
5251
const inRange = (v: number) => !Number.isNaN(v) && v >= 0 && v <= maxBound
5352

54-
const errors: Partial<Record<keyof MemberAddForm, { type: string; message: string }>> = {}
53+
const errors: ValidationErrors = {}
5554

5655
if (parsed.type === 'error') {
57-
errors.subnet = { type: 'pattern', message: parsed.message }
56+
errors.subnet = parsed.message
5857
} else if (parsed.type !== poolVersion) {
59-
errors.subnet = {
60-
type: 'pattern',
61-
message: `IP${parsed.type} subnet not allowed in IP${poolVersion} pool`,
62-
}
58+
errors.subnet = `IP${parsed.type} subnet not allowed in IP${poolVersion} pool`
6359
}
6460

6561
// min and max prefix length are optional, and NaN is the value they have
6662
// when they're unset (matching NumberField)
6763

6864
// min prefix: bounds → ordering → subnet width
6965
if (!Number.isNaN(minPL) && !inRange(minPL)) {
70-
errors.minPrefixLength = {
71-
type: 'validate',
72-
message: `Must be between 0 and ${maxBound}`,
73-
}
66+
errors.minPrefixLength = `Must be between 0 and ${maxBound}`
7467
} else if (inRange(minPL) && inRange(maxPL) && minPL > maxPL) {
75-
errors.minPrefixLength = {
76-
type: 'validate',
77-
message: 'Min prefix length must be ≤ max prefix length',
78-
}
68+
errors.minPrefixLength = 'Min prefix length must be ≤ max prefix length'
7969
} else if (inRange(minPL) && subnetWidth !== undefined && minPL < subnetWidth) {
80-
errors.minPrefixLength = {
81-
type: 'validate',
82-
message: `Must be ≥ subnet prefix length (${subnetWidth})`,
83-
}
70+
errors.minPrefixLength = `Must be ≥ subnet prefix length (${subnetWidth})`
8471
}
8572

8673
// max prefix: bounds → subnet width
8774
if (!Number.isNaN(maxPL) && !inRange(maxPL)) {
88-
errors.maxPrefixLength = {
89-
type: 'validate',
90-
message: `Must be between 0 and ${maxBound}`,
91-
}
75+
errors.maxPrefixLength = `Must be between 0 and ${maxBound}`
9276
} else if (inRange(maxPL) && subnetWidth !== undefined && maxPL < subnetWidth) {
93-
errors.maxPrefixLength = {
94-
type: 'validate',
95-
message: `Must be ≥ subnet prefix length (${subnetWidth})`,
96-
}
77+
errors.maxPrefixLength = `Must be ≥ subnet prefix length (${subnetWidth})`
9778
}
9879

99-
return Object.keys(errors).length > 0 ? errors : true
80+
return errors
10081
}
10182

10283
export const handle = titleCrumb('Add Member')
@@ -120,10 +101,7 @@ export default function SubnetPoolMemberAdd() {
120101
},
121102
})
122103

123-
const form = useForm<MemberAddForm>({
124-
defaultValues,
125-
validate: ({ formValues }) => validateForm(poolData.ipVersion, formValues),
126-
})
104+
const form = useForm<MemberAddForm>({ defaultValues })
127105

128106
const maxBound = poolData.ipVersion === 'v4' ? 32 : 128
129107

@@ -157,6 +135,8 @@ export default function SubnetPoolMemberAdd() {
157135
description="CIDR notation (e.g., 10.0.0.0/16)"
158136
control={form.control}
159137
required
138+
validate={(_subnet, values) => validateMember(poolData.ipVersion, values).subnet}
139+
deps={['minPrefixLength', 'maxPrefixLength']}
160140
/>
161141
<NumberField
162142
name="minPrefixLength"
@@ -165,6 +145,9 @@ export default function SubnetPoolMemberAdd() {
165145
control={form.control}
166146
min={0}
167147
max={maxBound}
148+
validate={(_minPrefixLength, values) =>
149+
validateMember(poolData.ipVersion, values).minPrefixLength
150+
}
168151
/>
169152
<NumberField
170153
name="maxPrefixLength"
@@ -173,6 +156,10 @@ export default function SubnetPoolMemberAdd() {
173156
control={form.control}
174157
min={0}
175158
max={maxBound}
159+
validate={(_maxPrefixLength, values) =>
160+
validateMember(poolData.ipVersion, values).maxPrefixLength
161+
}
162+
deps="minPrefixLength"
176163
/>
177164
<SideModalFormDocs docs={[docLinks.subnetPools]} />
178165
</SideModalForm>

test/e2e/subnet-pools.e2e.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ test('Subnet pool add member shows prefix length validation errors', async ({ pa
122122

123123
await expect(dialog).toBeVisible()
124124
await expect(
125-
page.getByText('Min prefix length must be ≤ max prefix length')
125+
dialog.getByText('Min prefix length must be ≤ max prefix length')
126126
).toBeVisible()
127127
})
128128

0 commit comments

Comments
 (0)