Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,10 @@ module.exports = {
'@typescript-eslint/no-empty-interface': 'off',
'@typescript-eslint/ban-ts-comment': 'off',
'@typescript-eslint/no-non-null-assertion': 'off',
'@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
'@typescript-eslint/no-unused-vars': [
'error',
{ argsIgnorePattern: '^_', varsIgnorePattern: '^_' },
],
eqeqeq: ['error', 'always', { null: 'ignore' }],
'import/no-default-export': 'error',
'import/no-unresolved': 'off', // plugin doesn't know anything
Expand Down
70 changes: 37 additions & 33 deletions app/components/form/fields/CheckboxField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,12 @@
*
* Copyright Oxide Computer Company
*/
import { Controller, type Control, type FieldPath, type FieldValues } from 'react-hook-form'
import {
useController,
type Control,
type FieldPath,
type FieldValues,
} from 'react-hook-form'

import { Checkbox, type CheckboxProps } from '~/ui/lib/Checkbox'

Expand All @@ -24,35 +29,34 @@ export const CheckboxField = <
control,
name,
...props
}: CheckboxFieldProps<TFieldValues, TName>) => (
<Controller
name={name}
control={control}
render={({ field: { onChange, value } }) => (
<Checkbox
{...props}
// If value is an array, we're dealing with a set of checkboxes that
// have the same name and different `value` attrs, and are therefore
// supposed to produce an array of the values that are checked. `value`
// is the value in form state, which can be a bool or array.
// `props.value` is the value string of the current checkbox, which is
// only relevant in the array case
onChange={(e) => {
if (Array.isArray(value) && props.value) {
// it's one of a set of checkboxes. if it was just checked, we're
// adding it to the array, otherwise we're removing it
const valueArray = value as string[]
const newValue = e.target.checked
? [...valueArray, props.value]
: valueArray.filter((x) => x !== props.value)
onChange(newValue)
} else {
// it's a single checkbox
onChange(e.target.checked)
}
}}
checked={Array.isArray(value) ? value.includes(props.value) : value}
/>
)}
/>
)
}: CheckboxFieldProps<TFieldValues, TName>) => {
const {
field: { onChange, value },
} = useController({ name, control })
return (
<Checkbox
{...props}
// If value is an array, we're dealing with a set of checkboxes that
// have the same name and different `value` attrs, and are therefore
// supposed to produce an array of the values that are checked. `value`
// is the value in form state, which can be a bool or array.
// `props.value` is the value string of the current checkbox, which is
// only relevant in the array case
onChange={(e) => {
if (Array.isArray(value) && props.value) {
// it's one of a set of checkboxes. if it was just checked, we're
// adding it to the array, otherwise we're removing it
const valueArray = value as string[]
const newValue = e.target.checked
? [...valueArray, props.value]
: valueArray.filter((x) => x !== props.value)
onChange(newValue)
} else {
// it's a single checkbox
onChange(e.target.checked)
}
}}
checked={Array.isArray(value) ? value.includes(props.value) : value}
/>
)
}
59 changes: 27 additions & 32 deletions app/components/form/fields/FileField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,12 @@
*
* Copyright Oxide Computer Company
*/
import { Controller, type Control, type FieldPath, type FieldValues } from 'react-hook-form'
import {
useController,
type Control,
type FieldPath,
type FieldValues,
} from 'react-hook-form'

import { FieldLabel } from '~/ui/lib/FieldLabel'
import { FileInput } from '~/ui/lib/FileInput'
Expand Down Expand Up @@ -37,37 +42,27 @@ export function FileField<
description?: string | React.ReactNode
disabled?: boolean
}) {
const {
field: { value: _, ...rest },
fieldState: { error },
} = useController({ name, control, rules: { required } })
return (
<Controller
name={name}
control={control}
rules={{ required }}
render={({ field: { value: _value, ...rest }, fieldState: { error } }) => (
<div>
<div className="mb-2">
<FieldLabel
id={`${id}-label`}
htmlFor={id}
tip={tooltipText}
optional={!required}
>
{label}
</FieldLabel>
{description && (
<TextInputHint id={`${id}-help-text`}>{description}</TextInputHint>
)}
</div>
<FileInput
id={id}
className="mt-2"
accept={accept}
disabled={disabled}
{...rest}
error={!!error}
/>
<ErrorMessage error={error} label={label} />
</div>
)}
/>
<div>
<div className="mb-2">
<FieldLabel id={`${id}-label`} htmlFor={id} tip={tooltipText} optional={!required}>
{label}
</FieldLabel>
{description && <TextInputHint id={`${id}-help-text`}>{description}</TextInputHint>}
</div>
<FileInput
id={id}
className="mt-2"
accept={accept}
disabled={disabled}
{...rest}
error={!!error}
/>
<ErrorMessage error={error} label={label} />
</div>
)
}
53 changes: 25 additions & 28 deletions app/components/form/fields/ListboxField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,12 @@
* Copyright Oxide Computer Company
*/
import cn from 'classnames'
import { Controller, type Control, type FieldPath, type FieldValues } from 'react-hook-form'
import {
useController,
type Control,
type FieldPath,
type FieldValues,
} from 'react-hook-form'

import { Listbox, type ListboxItem } from '~/ui/lib/Listbox'
import { capitalize } from '~/util/str'
Expand Down Expand Up @@ -50,37 +55,29 @@ export function ListboxField<
}: ListboxFieldProps<TFieldValues, TName>) {
// TODO: recreate this logic
// validate: (v) => (required && !v ? `${name} is required` : undefined),
const { field, fieldState } = useController({ name, control, rules: { required } })
return (
<div className={cn('max-w-lg', className)}>
<Controller
<Listbox
description={description}
label={label}
tooltipText={tooltipText}
required={required}
placeholder={placeholder}
selected={field.value || null}
items={items}
onChange={(value) => {
field.onChange(value)
onChange?.(value)
}}
// required to get required error to trigger on blur
// onBlur={field.onBlur}
disabled={disabled}
name={name}
rules={{ required }}
control={control}
render={({ field, fieldState: { error } }) => (
<>
<Listbox
description={description}
label={label}
tooltipText={tooltipText}
required={required}
placeholder={placeholder}
selected={field.value || null}
items={items}
onChange={(value) => {
field.onChange(value)
onChange?.(value)
}}
// required to get required error to trigger on blur
// onBlur={field.onBlur}
disabled={disabled}
name={name}
hasError={error !== undefined}
isLoading={isLoading}
/>
<ErrorMessage error={error} label={label} />
</>
)}
hasError={fieldState.error !== undefined}
isLoading={isLoading}
/>
<ErrorMessage error={fieldState.error} label={label} />
</div>
)
}
48 changes: 20 additions & 28 deletions app/components/form/fields/NumberField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
*/
import cn from 'classnames'
import { useId } from 'react'
import { Controller, type FieldPathByValue, type FieldValues } from 'react-hook-form'
import { useController, type FieldPathByValue, type FieldValues } from 'react-hook-form'

import { FieldLabel } from '~/ui/lib/FieldLabel'
import { NumberInput } from '~/ui/lib/NumberInput'
Expand Down Expand Up @@ -77,33 +77,25 @@ export const NumberFieldInner = <
const generatedId = useId()
const id = idProp || generatedId

const {
field,
fieldState: { error },
} = useController({ name, control, rules: { required, validate } })

return (
<Controller
name={name}
control={control}
rules={{ required, validate }}
render={({ field, fieldState: { error } }) => {
return (
<>
<NumberInput
id={id}
error={!!error}
aria-labelledby={cn(`${id}-label`, {
[`${id}-help-text`]: !!tooltipText,
})}
aria-describedby={tooltipText ? `${id}-label-tip` : undefined}
isDisabled={disabled}
maxValue={max ? Number(max) : undefined}
minValue={min !== undefined ? Number(min) : undefined}
{...field}
formatOptions={{
useGrouping: false,
}}
/>
<ErrorMessage error={error} label={label} />
</>
)
}}
/>
<>
<NumberInput
id={id}
error={!!error}
aria-labelledby={cn(`${id}-label`, !!tooltipText && `${id}-help-text`)}
aria-describedby={tooltipText ? `${id}-label-tip` : undefined}
isDisabled={disabled}
maxValue={max ? Number(max) : undefined}
minValue={min !== undefined ? Number(min) : undefined}
{...field}
formatOptions={{ useGrouping: false }}
/>
<ErrorMessage error={error} label={label} />
</>
)
}
Loading